diff --git a/go.mod b/go.mod index b4330af6..877c34df 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df github.com/go-playground/validator/v10 v10.28.0 github.com/hashicorp/nomad/api v0.0.0-20250812204832-62b195aaa535 // v1.10.4 - github.com/jedib0t/go-pretty/v6 v6.6.8 + github.com/jedib0t/go-pretty/v6 v6.7.7 github.com/matcornic/hermes/v2 v2.1.0 github.com/microcosm-cc/bluemonday v1.0.27 github.com/moby/buildkit v0.25.2 diff --git a/go.sum b/go.sum index f258db6c..3efb7de6 100644 --- a/go.sum +++ b/go.sum @@ -191,8 +191,8 @@ github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ= github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= -github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc= -github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jedib0t/go-pretty/v6 v6.7.7 h1:Y1Id3lJ3k4UB8uwWWy3l8EVFnUlx5chR5+VbsofPNX0= +github.com/jedib0t/go-pretty/v6 v6.7.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/EXAMPLES.md b/vendor/github.com/jedib0t/go-pretty/v6/table/EXAMPLES.md new file mode 100644 index 00000000..8845187a --- /dev/null +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/EXAMPLES.md @@ -0,0 +1,650 @@ +# Examples + +All the examples below are going to start with the following block, although +nothing except a single Row is mandatory for the `Render()` function to render +something: +```golang +package main + +import ( + "os" + + "github.com/jedib0t/go-pretty/v6/table" +) + +func main() { + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"}) + t.AppendRows([]table.Row{ + {1, "Arya", "Stark", 3000}, + {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}, + }) + t.AppendSeparator() + t.AppendRow([]interface{}{300, "Tyrion", "Lannister", 5000}) + t.AppendFooter(table.Row{"", "", "Total", 10000}) + t.Render() +} +``` +Running the above will result in: +``` ++-----+------------+-----------+--------+-----------------------------+ +| # | FIRST NAME | LAST NAME | SALARY | | ++-----+------------+-----------+--------+-----------------------------+ +| 1 | Arya | Stark | 3000 | | +| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | ++-----+------------+-----------+--------+-----------------------------+ +| 300 | Tyrion | Lannister | 5000 | | ++-----+------------+-----------+--------+-----------------------------+ +| | | TOTAL | 10000 | | ++-----+------------+-----------+--------+-----------------------------+ +``` + +--- + +
+Ready-to-use Styles + +Table comes with a bunch of ready-to-use Styles that make the table look really +good. Set or Change the style using: +```golang + t.SetStyle(table.StyleLight) + t.Render() +``` +to get: +``` +┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ +│ # │ FIRST NAME │ LAST NAME │ SALARY │ │ +├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ +│ 1 │ Arya │ Stark │ 3000 │ │ +│ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ +├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ +│ 300 │ Tyrion │ Lannister │ 5000 │ │ +├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ +│ │ │ TOTAL │ 10000 │ │ +└─────┴────────────┴───────────┴────────┴─────────────────────────────┘ +``` + +Or if you want to use a full-color mode, and don't care for boxes, use: +```golang + t.SetStyle(table.StyleColoredBright) + t.Render() +``` +to get: + +Colored Table + +
+ +--- + +
+Roll your own Style + +You can also roll your own style: +```golang + t.SetStyle(table.Style{ + Name: "myNewStyle", + Box: table.BoxStyle{ + BottomLeft: "\\", + BottomRight: "/", + BottomSeparator: "v", + Left: "[", + LeftSeparator: "{", + MiddleHorizontal: "-", + MiddleSeparator: "+", + MiddleVertical: "|", + PaddingLeft: "<", + PaddingRight: ">", + Right: "]", + RightSeparator: "}", + TopLeft: "(", + TopRight: ")", + TopSeparator: "^", + UnfinishedRow: " ~~~", + }, + Color: table.ColorOptions{ + IndexColumn: text.Colors{text.BgCyan, text.FgBlack}, + Footer: text.Colors{text.BgCyan, text.FgBlack}, + Header: text.Colors{text.BgHiCyan, text.FgBlack}, + Row: text.Colors{text.BgHiWhite, text.FgBlack}, + RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, + }, + Format: table.FormatOptions{ + Footer: text.FormatUpper, + Header: text.FormatUpper, + Row: text.FormatDefault, + }, + Options: table.Options{ + DrawBorder: true, + SeparateColumns: true, + SeparateFooter: true, + SeparateHeader: true, + SeparateRows: false, + }, + }) +``` + +Or you can use one of the ready-to-use Styles, and just make a few tweaks: +```golang + t.SetStyle(table.StyleLight) + t.Style().Color.Header = text.Colors{text.BgHiCyan, text.FgBlack} + t.Style().Color.IndexColumn = text.Colors{text.BgHiCyan, text.FgBlack} + t.Style().Format.Footer = text.FormatLower + t.Style().Options.DrawBorder = false +``` + +
+ +--- + +
+Customize Horizontal Separators + +For more granular control over horizontal lines in your table, you can use `BoxStyleHorizontal` to customize different separator types independently. This allows you to have different horizontal line styles for titles, headers, rows, and footers. + +The `BoxStyleHorizontal` struct provides 10 customizable separator types: +- `TitleTop` / `TitleBottom` - Lines above/below the title +- `HeaderTop` / `HeaderMiddle` / `HeaderBottom` - Lines in the header section +- `RowTop` / `RowMiddle` / `RowBottom` - Lines in the data rows section +- `FooterTop` / `FooterMiddle` / `FooterBottom` - Lines in the footer section + +You can customize each separator type using: + +```golang + tw := table.NewWriter() + tw.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"}) + tw.AppendRows([]table.Row{ + {1, "Arya", "Stark", 3000}, + {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}, + {300, "Tyrion", "Lannister", 5000}, + }) + tw.AppendFooter(table.Row{"", "", "Total", 10000}) + tw.SetStyle(table.StyleDefault) + tw.Style().Box.Horizontal = &table.BoxStyleHorizontal{ + HeaderTop: "=", // Thicker line above header + HeaderMiddle: "-", + HeaderBottom: "~", // Thicker line below header + RowTop: "-", + RowMiddle: "- ", + RowBottom: "-", + FooterTop: "~", // Thicker line above footer + FooterMiddle: "-", + FooterBottom: "=", // Thicker line below footer + } + tw.Style().Options.SeparateRows = true + fmt.Println(tw.Render()) +``` +to get something like: +``` ++=====+============+===========+========+=============================+ +| # | FIRST NAME | LAST NAME | SALARY | | ++~~~~~+~~~~~~~~~~~~+~~~~~~~~~~~+~~~~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ +| 1 | Arya | Stark | 3000 | | ++- - -+- - - - - - +- - - - - -+- - - - +- - - - - - - - - - - - - - -+ +| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | ++- - -+- - - - - - +- - - - - -+- - - - +- - - - - - - - - - - - - - -+ +| 300 | Tyrion | Lannister | 5000 | | ++~~~~~+~~~~~~~~~~~~+~~~~~~~~~~~+~~~~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ +| | | TOTAL | 10000 | | ++=====+============+===========+========+=============================+ +``` + +When `BoxStyle.Horizontal` is set to a non-nil value, it overrides the `MiddleHorizontal` string for all horizontal separators. If `Horizontal` is nil, the table will automatically use `MiddleHorizontal` for all separator types. + +
+ +--- + +
+Auto-Merge + +You can auto-merge cells horizontally and vertically, but you have request for +it specifically for each row/column using `RowConfig` or `ColumnConfig`. + +```golang + rowConfigAutoMerge := table.RowConfig{AutoMerge: true} + + t := table.NewWriter() + t.AppendHeader(table.Row{"Node IP", "Pods", "Namespace", "Container", "RCE", "RCE"}, rowConfigAutoMerge) + t.AppendHeader(table.Row{"Node IP", "Pods", "Namespace", "Container", "EXE", "RUN"}) + t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 1", "Y", "Y"}, rowConfigAutoMerge) + t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 2", "Y", "N"}, rowConfigAutoMerge) + t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1B", "C 3", "N", "N"}, rowConfigAutoMerge) + t.AppendRow(table.Row{"1.1.1.1", "Pod 1B", "NS 2", "C 4", "N", "N"}, rowConfigAutoMerge) + t.AppendRow(table.Row{"1.1.1.1", "Pod 1B", "NS 2", "C 5", "Y", "N"}, rowConfigAutoMerge) + t.AppendRow(table.Row{"2.2.2.2", "Pod 2", "NS 3", "C 6", "Y", "Y"}, rowConfigAutoMerge) + t.AppendRow(table.Row{"2.2.2.2", "Pod 2", "NS 3", "C 7", "Y", "Y"}, rowConfigAutoMerge) + t.AppendFooter(table.Row{"", "", "", 7, 5, 3}) + t.SetAutoIndex(true) + t.SetColumnConfigs([]table.ColumnConfig{ + {Number: 1, AutoMerge: true}, + {Number: 2, AutoMerge: true}, + {Number: 3, AutoMerge: true}, + {Number: 4, AutoMerge: true}, + {Number: 5, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter}, + {Number: 6, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter}, + }) + t.SetStyle(table.StyleLight) + t.Style().Options.SeparateRows = true + fmt.Println(t.Render()) +``` +to get: +``` +┌───┬─────────┬────────┬───────────┬───────────┬───────────┐ +│ │ NODE IP │ PODS │ NAMESPACE │ CONTAINER │ RCE │ +│ │ │ │ │ ├─────┬─────┤ +│ │ │ │ │ │ EXE │ RUN │ +├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┤ +│ 1 │ 1.1.1.1 │ Pod 1A │ NS 1A │ C 1 │ Y │ +├───┤ │ │ ├───────────┼─────┬─────┤ +│ 2 │ │ │ │ C 2 │ Y │ N │ +├───┤ │ ├───────────┼───────────┼─────┴─────┤ +│ 3 │ │ │ NS 1B │ C 3 │ N │ +├───┤ ├────────┼───────────┼───────────┼───────────┤ +│ 4 │ │ Pod 1B │ NS 2 │ C 4 │ N │ +├───┤ │ │ ├───────────┼─────┬─────┤ +│ 5 │ │ │ │ C 5 │ Y │ N │ +├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┤ +│ 6 │ 2.2.2.2 │ Pod 2 │ NS 3 │ C 6 │ Y │ +├───┤ │ │ ├───────────┼───────────┤ +│ 7 │ │ │ │ C 7 │ Y │ +├───┼─────────┼────────┼───────────┼───────────┼─────┬─────┤ +│ │ │ │ │ 7 │ 5 │ 3 │ +└───┴─────────┴────────┴───────────┴───────────┴─────┴─────┘ +``` + +
+ +--- + +
+Paging + +You can limit the number of lines rendered in a single "Page". This logic +can handle rows with multiple lines too. The recommended way is to use the +`Pager()` interface: + +```golang + pager := t.Pager(PageSize(1)) + pager.Render() // Render first page + pager.Next() // Move to next page and render + pager.Prev() // Move to previous page and render + pager.GoTo(3) // Jump to page 3 + pager.Location() // Get current page number +``` + +Or use the deprecated `SetPageSize()` method for simple cases: +```golang + t.SetPageSize(1) + t.Render() +``` +to get: +``` ++-----+------------+-----------+--------+-----------------------------+ +| # | FIRST NAME | LAST NAME | SALARY | | ++-----+------------+-----------+--------+-----------------------------+ +| 1 | Arya | Stark | 3000 | | ++-----+------------+-----------+--------+-----------------------------+ +| | | TOTAL | 10000 | | ++-----+------------+-----------+--------+-----------------------------+ + ++-----+------------+-----------+--------+-----------------------------+ +| # | FIRST NAME | LAST NAME | SALARY | | ++-----+------------+-----------+--------+-----------------------------+ +| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | ++-----+------------+-----------+--------+-----------------------------+ +| | | TOTAL | 10000 | | ++-----+------------+-----------+--------+-----------------------------+ + ++-----+------------+-----------+--------+-----------------------------+ +| # | FIRST NAME | LAST NAME | SALARY | | ++-----+------------+-----------+--------+-----------------------------+ +| 300 | Tyrion | Lannister | 5000 | | ++-----+------------+-----------+--------+-----------------------------+ +| | | TOTAL | 10000 | | ++-----+------------+-----------+--------+-----------------------------+ +``` + +
+ +--- + +
+Filtering + +Filtering can be done on one or more columns. All filters are applied with AND logic (all must match). +Filters are applied before sorting. + +```golang + t.FilterBy([]table.FilterBy{ + {Name: "Salary", Operator: table.GreaterThan, Value: 2000}, + {Name: "First Name", Operator: table.Contains, Value: "on"}, + }) +``` + +The `Operator` field in `FilterBy` supports various filtering operators: +- `Equal` / `NotEqual` - Exact match +- `GreaterThan` / `GreaterThanOrEqual` - Numeric comparisons +- `LessThan` / `LessThanOrEqual` - Numeric comparisons +- `Contains` / `NotContains` - String search +- `StartsWith` / `EndsWith` - String prefix/suffix matching +- `RegexMatch` / `RegexNotMatch` - Regular expression matching + +You can make string comparisons case-insensitive by setting `IgnoreCase: true`: +```golang + t.FilterBy([]table.FilterBy{ + {Name: "First Name", Operator: table.Equal, Value: "JON", IgnoreCase: true}, + }) +``` + +For advanced filtering requirements, you can provide a custom filter function: +```golang + t.FilterBy([]table.FilterBy{ + { + Number: 2, + CustomFilter: func(cellValue string) bool { + // Custom logic: include rows where first name length > 3 + return len(cellValue) > 3 + }, + }, + }) +``` + +Example: Filter by salary and name +```golang + t := table.NewWriter() + t.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"}) + t.AppendRows([]table.Row{ + {1, "Arya", "Stark", 3000}, + {20, "Jon", "Snow", 2000}, + {300, "Tyrion", "Lannister", 5000}, + {400, "Sansa", "Stark", 2500}, + }) + t.FilterBy([]table.FilterBy{ + {Number: 4, Operator: table.GreaterThan, Value: 2000}, + {Number: 3, Operator: table.Contains, Value: "Stark"}, + }) + t.Render() +``` +to get: +``` ++-----+------------+-----------+--------+ +| # | FIRST NAME | LAST NAME | SALARY | ++-----+------------+-----------+--------+ +| 1 | Arya | Stark | 3000 | +| 400 | Sansa | Stark | 2500 | ++-----+------------+-----------+--------+ +``` + +
+ +--- + +
+Sorting + +Sorting can be done on one or more columns. The following code will make the +rows be sorted first by "First Name" and then by "Last Name" (in case of similar +"First Name" entries). +```golang + t.SortBy([]table.SortBy{ + {Name: "First Name", Mode: table.Asc}, + {Name: "Last Name", Mode: table.Asc}, + }) +``` + +The `Mode` field in `SortBy` supports various sorting modes: +- `Asc` / `Dsc` - Alphabetical ascending/descending +- `AscNumeric` / `DscNumeric` - Numerical ascending/descending +- `AscAlphaNumeric` / `DscAlphaNumeric` - Alphabetical first, then numerical +- `AscNumericAlpha` / `DscNumericAlpha` - Numerical first, then alphabetical + +You can also make sorting case-insensitive by setting `IgnoreCase: true`: +```golang + t.SortBy([]table.SortBy{ + {Name: "First Name", Mode: table.Asc, IgnoreCase: true}, + }) +``` + +
+ +--- + +
+Sorting Customization + +For advanced sorting requirements, you can provide a custom comparison function +using `CustomLess`. This function overrides the `Mode` and `IgnoreCase` settings +and gives you full control over the sorting logic. + +The `CustomLess` function receives two string values (the cell contents converted +to strings) and must return: +- `-1` when the first value should come before the second +- `0` when the values are considered equal (sorting continues to the next column) +- `1` when the first value should come after the second + +Example: Custom numeric sorting that handles string numbers correctly. +```golang + t.SortBy([]table.SortBy{ + { + Number: 1, + CustomLess: func(iStr string, jStr string) int { + iNum, iErr := strconv.Atoi(iStr) + jNum, jErr := strconv.Atoi(jStr) + if iErr != nil || jErr != nil { + // Fallback to string comparison if not numeric + if iStr < jStr { + return -1 + } + if iStr > jStr { + return 1 + } + return 0 + } + if iNum < jNum { + return -1 + } + if iNum > jNum { + return 1 + } + return 0 + }, + }, + }) +``` + +Example: Combining custom sorting with default sorting modes. +```golang + t.SortBy([]table.SortBy{ + { + Number: 1, + CustomLess: func(iStr string, jStr string) int { + // Custom logic: "same" values come first + if iStr == "same" && jStr != "same" { + return -1 + } + if iStr != "same" && jStr == "same" { + return 1 + } + return 0 // Equal, continue to next column + }, + }, + {Number: 2, Mode: table.Asc}, // Default alphabetical sort + {Number: 3, Mode: table.AscNumeric}, // Default numeric sort + }) +``` +
+ +--- + +
+Wrapping (or) Row/Column Width restrictions + +You can restrict the maximum (text) width for a Row: +```golang + t.SetAllowedRowLength(50) + t.Render() +``` +to get: +``` ++-----+------------+-----------+--------+------- ~ +| # | FIRST NAME | LAST NAME | SALARY | ~ ++-----+------------+-----------+--------+------- ~ +| 1 | Arya | Stark | 3000 | ~ +| 20 | Jon | Snow | 2000 | You kn ~ ++-----+------------+-----------+--------+------- ~ +| 300 | Tyrion | Lannister | 5000 | ~ ++-----+------------+-----------+--------+------- ~ +| | | TOTAL | 10000 | ~ ++-----+------------+-----------+--------+------- ~ +``` + +
+ +--- + +
+Column Control - Alignment, Colors, Width and more + +You can control a lot of things about individual cells/columns which overrides +global properties/styles using the `SetColumnConfig()` interface: +- Alignment (horizontal & vertical) +- Colorization +- Transform individual cells based on the content +- Visibility +- Width (minimum & maximum) + +```golang + nameTransformer := text.Transformer(func(val interface{}) string { + return text.Bold.Sprint(val) + }) + + t.SetColumnConfigs([]ColumnConfig{ + { + Name: "First Name", + Align: text.AlignLeft, + AlignFooter: text.AlignLeft, + AlignHeader: text.AlignLeft, + Colors: text.Colors{text.BgBlack, text.FgRed}, + ColorsHeader: text.Colors{text.BgRed, text.FgBlack, text.Bold}, + ColorsFooter: text.Colors{text.BgRed, text.FgBlack}, + Hidden: false, + Transformer: nameTransformer, + TransformerFooter: nameTransformer, + TransformerHeader: nameTransformer, + VAlign: text.VAlignMiddle, + VAlignFooter: text.VAlignTop, + VAlignHeader: text.VAlignBottom, + WidthMin: 6, + WidthMax: 64, + } + }) +``` + +
+ +--- + +
+CSV + +```golang + t.RenderCSV() +``` +to get: +``` +,First Name,Last Name,Salary, +1,Arya,Stark,3000, +20,Jon,Snow,2000,"You know nothing\, Jon Snow!" +300,Tyrion,Lannister,5000, +,,Total,10000, +``` + +
+ +--- + +
+HTML Table + +```golang + t.Style().HTML = table.HTMLOptions{ + CSSClass: "game-of-thrones", + EmptyColumn: " ", + EscapeText: true, + Newline: "
", + ConvertColorsToSpans: true, // Convert ANSI escape sequences to HTML tags with CSS classes + } + t.RenderHTML() +``` +to get: +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#First NameLast NameSalary 
1AryaStark3000 
20JonSnow2000You know nothing, Jon Snow!
300TyrionLannister5000 
  Total10000 
+``` + +
+ +--- + +
+Markdown Table + +```golang + t.RenderMarkdown() +``` +to get: +```markdown +| # | First Name | Last Name | Salary | | +| ---:| --- | --- | ---:| --- | +| 1 | Arya | Stark | 3000 | | +| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | +| 300 | Tyrion | Lannister | 5000 | | +| | | Total | 10000 | | +``` + +
+ +--- diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/README.md b/vendor/github.com/jedib0t/go-pretty/v6/table/README.md index 3375bd3c..778041d2 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/table/README.md +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/README.md @@ -3,42 +3,7 @@ Pretty-print tables into ASCII/Unicode strings. - - Add Rows one-by-one or as a group (`AppendRow`/`AppendRows`) - - Add Header(s) and Footer(s) (`AppendHeader`/`AppendFooter`) - - Add a Separator manually after any Row (`AppendSeparator`) - - Auto Index Rows (1, 2, 3 ...) and Columns (A, B, C, ...) (`SetAutoIndex`) - - Auto Merge (_not supported in CSV/Markdown/TSV modes_) - - Cells in a Row (`RowConfig.AutoMerge`) - - Columns (`ColumnConfig.AutoMerge`) (_not supported in HTML mode_) - - Limit the length of - - Rows (`SetAllowedRowLength`) - - Columns (`ColumnConfig.Width*`) - - Auto-size Rows (`Style().Size.WidthMin` and `Style().Size.WidthMax`) - - Page results by a specified number of Lines (`SetPageSize`) - - Alignment - Horizontal & Vertical - - Auto (horizontal) Align (numeric columns aligned Right) - - Custom (horizontal) Align per column (`ColumnConfig.Align*`) - - Custom (vertical) VAlign per column with multi-line cell support (`ColumnConfig.VAlign*`) - - Mirror output to an `io.Writer` (ex. `os.StdOut`) (`SetOutputMirror`) - - Sort by one or more Columns (`SortBy`) - - Suppress/hide columns with no content (`SuppressEmptyColumns`) - - Suppress trailing spaces in the last column (`SupressTrailingSpaces`) - - Customizable Cell rendering per Column (`ColumnConfig.Transformer*`) - - Hide any columns that you don't want displayed (`ColumnConfig.Hidden`) - - Reset Headers/Rows/Footers at will to reuse the same Table Writer (`Reset*`) - - Completely customizable styles (`SetStyle`/`Style`) - - Many ready-to-use styles: [style.go](style.go) - - Colorize Headers/Body/Footers using [../text/color.go](../text/color.go) - - Custom text-case for Headers/Body/Footers - - Enable separators between each row - - Render table without a Border - - and a lot more... - - Render as: - - (ASCII/Unicode) Table - - CSV - - HTML Table (with custom CSS Class) - - Markdown Table - - TSV +## Sample Table Rendering ``` +---------------------------------------------------------------------+ @@ -57,396 +22,120 @@ Pretty-print tables into ASCII/Unicode strings. A demonstration of all the capabilities can be found here: [../cmd/demo-table](../cmd/demo-table) -If you want very specific examples, read ahead. +If you want very specific examples, look at the [EXAMPLES.md](EXAMPLES.md) file. -**Hint**: I've tried to ensure that almost all supported use-cases are covered -by unit-tests and that they print the table rendered. Run -`go test -v github.com/jedib0t/go-pretty/v6/table` to see the test outputs and -help you figure out how to do something. +## Features -# Examples +### Core Table Building -All the examples below are going to start with the following block, although -nothing except a single Row is mandatory for the `Render()` function to render -something: -```golang -package main + - Add Rows one-by-one or as a group (`AppendRow`/`AppendRows`) + - Add Header(s) and Footer(s) (`AppendHeader`/`AppendFooter`) + - Add a Separator manually after any Row (`AppendSeparator`) + - Add Title above the table (`SetTitle`) + - Add Caption below the table (`SetCaption`) + - Import 1D or 2D arrays/grids as rows (`ImportGrid`) + - Reset Headers/Rows/Footers at will to reuse the same Table Writer (`Reset*`) -import ( - "os" +### Indexing & Navigation - "github.com/jedib0t/go-pretty/v6/table" -) + - Auto Index Rows (1, 2, 3 ...) and Columns (A, B, C, ...) (`SetAutoIndex`) + - Set which column is the index column (`SetIndexColumn`) + - Pager interface for navigating through paged output (`Pager()`) + - `GoTo(pageNum)` - Jump to specific page + - `Next()` - Move to next page + - `Prev()` - Move to previous page + - `Location()` - Get current page number + - `Render()` - Render current page + - `SetOutputMirror()` - Mirror output to io.Writer -func main() { - t := table.NewWriter() - t.SetOutputMirror(os.Stdout) - t.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"}) - t.AppendRows([]table.Row{ - {1, "Arya", "Stark", 3000}, - {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}, - }) - t.AppendSeparator() - t.AppendRow([]interface{}{300, "Tyrion", "Lannister", 5000}) - t.AppendFooter(table.Row{"", "", "Total", 10000}) - t.Render() -} -``` -Running the above will result in: -``` -+-----+------------+-----------+--------+-----------------------------+ -| # | FIRST NAME | LAST NAME | SALARY | | -+-----+------------+-----------+--------+-----------------------------+ -| 1 | Arya | Stark | 3000 | | -| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | -+-----+------------+-----------+--------+-----------------------------+ -| 300 | Tyrion | Lannister | 5000 | | -+-----+------------+-----------+--------+-----------------------------+ -| | | TOTAL | 10000 | | -+-----+------------+-----------+--------+-----------------------------+ -``` +### Auto Merge -## Styles + - Auto Merge cells (_not supported in CSV/Markdown/TSV modes_) + - Cells in a Row (`RowConfig.AutoMerge`) + - Columns (`ColumnConfig.AutoMerge`) (_not supported in HTML mode_) + - Custom alignment for merged cells (`RowConfig.AutoMergeAlign`) -You can customize almost every single thing about the table above. The previous -example just defaulted to `StyleDefault` during `Render()`. You can use a -ready-to-use style (as in [style.go](style.go)) or customize it as you want. +### Size & Width Control -### Ready-to-use Styles + - Limit the length of Rows (`SetAllowedRowLength` or `Style().Size.WidthMax`) + - Auto-size Rows (`Style().Size.WidthMin` and `Style().Size.WidthMax`) + - Column width control (`ColumnConfig.WidthMin` and `ColumnConfig.WidthMax`) + - Custom width enforcement functions (`ColumnConfig.WidthMaxEnforcer`) + - Default: `text.WrapText` + - Options: `text.WrapSoft`, `text.WrapHard`, `text.Trim`, or custom function -Table comes with a bunch of ready-to-use Styles that make the table look really -good. Set or Change the style using: -```golang - t.SetStyle(table.StyleLight) - t.Render() -``` -to get: -``` -┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ -│ # │ FIRST NAME │ LAST NAME │ SALARY │ │ -├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ -│ 1 │ Arya │ Stark │ 3000 │ │ -│ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ -├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ -│ 300 │ Tyrion │ Lannister │ 5000 │ │ -├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ -│ │ │ TOTAL │ 10000 │ │ -└─────┴────────────┴───────────┴────────┴─────────────────────────────┘ -``` +### Alignment -Or if you want to use a full-color mode, and don't care for boxes, use: -```golang - t.SetStyle(table.StyleColoredBright) - t.Render() -``` -to get: + - **Horizontal Alignment** + - Auto (numeric columns aligned Right, text aligned Left) + - Custom per column (`ColumnConfig.Align`, `AlignHeader`, `AlignFooter`) + - Options: Left, Center, Right, Justify, Auto + - **Vertical Alignment** + - Custom per column with multi-line cell support (`ColumnConfig.VAlign`, `VAlignHeader`, `VAlignFooter`) + - Options: Top, Middle, Bottom -Colored Table +### Sorting & Filtering -### Roll your own Style + - **Sorting** + - Sort by one or more Columns (`SortBy`) + - Multiple column sorting support + - Various sort modes: Alphabetical, Numeric, Alpha-numeric, Numeric-alpha + - Case-insensitive sorting option (`IgnoreCase`) + - Custom sorting functions (`CustomLess`) for advanced sorting logic + - **Filtering** + - Filter by one or more Columns (`FilterBy`) + - Multiple filters with AND logic (all must match) + - Various filter operators: + - Equality: Equal, NotEqual + - Numeric: GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual + - String: Contains, NotContains, StartsWith, EndsWith + - Regex: RegexMatch, RegexNotMatch + - Case-insensitive filtering option (`IgnoreCase`) + - Custom filter functions (`CustomFilter`) for advanced filtering logic + - Filters are applied before sorting + - Suppress/hide columns with no content (`SuppressEmptyColumns`) + - Hide specific columns (`ColumnConfig.Hidden`) + - Suppress trailing spaces in the last column (`SuppressTrailingSpaces`) -You can also roll your own style: -```golang - t.SetStyle(table.Style{ - Name: "myNewStyle", - Box: table.BoxStyle{ - BottomLeft: "\\", - BottomRight: "/", - BottomSeparator: "v", - Left: "[", - LeftSeparator: "{", - MiddleHorizontal: "-", - MiddleSeparator: "+", - MiddleVertical: "|", - PaddingLeft: "<", - PaddingRight: ">", - Right: "]", - RightSeparator: "}", - TopLeft: "(", - TopRight: ")", - TopSeparator: "^", - UnfinishedRow: " ~~~", - }, - Color: table.ColorOptions{ - IndexColumn: text.Colors{text.BgCyan, text.FgBlack}, - Footer: text.Colors{text.BgCyan, text.FgBlack}, - Header: text.Colors{text.BgHiCyan, text.FgBlack}, - Row: text.Colors{text.BgHiWhite, text.FgBlack}, - RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, - }, - Format: table.FormatOptions{ - Footer: text.FormatUpper, - Header: text.FormatUpper, - Row: text.FormatDefault, - }, - Options: table.Options{ - DrawBorder: true, - SeparateColumns: true, - SeparateFooter: true, - SeparateHeader: true, - SeparateRows: false, - }, - }) -``` +### Customization & Styling -Or you can use one of the ready-to-use Styles, and just make a few tweaks: -```golang - t.SetStyle(table.StyleLight) - t.Style().Color.Header = text.Colors{text.BgHiCyan, text.FgBlack} - t.Style().Color.IndexColumn = text.Colors{text.BgHiCyan, text.FgBlack} - t.Style().Format.Footer = text.FormatLower - t.Style().Options.DrawBorder = false -``` + - **Row Coloring** + - Custom row painter function (`SetRowPainter`) + - Row painter with attributes (`RowPainterWithAttributes`) + - Access to row number and sorted position + - **Cell Transformation** + - Customizable Cell rendering per Column (`ColumnConfig.Transformer`, `TransformerHeader`, `TransformerFooter`) + - Use built-in transformers from `text` package (Number, JSON, Time, URL, etc.) + - **Column Styling** + - Per-column colors (`ColumnConfig.Colors`, `ColorsHeader`, `ColorsFooter`) + - Per-column alignment (horizontal and vertical) + - Per-column width constraints + - **Completely customizable styles** (`SetStyle`/`Style`) + - Many ready-to-use styles: [style.go](style.go) + - `StyleDefault` - Classic ASCII borders + - `StyleLight` - Light box-drawing characters + - `StyleBold` - Bold box-drawing characters + - `StyleDouble` - Double box-drawing characters + - `StyleRounded` - Rounded box-drawing characters + - `StyleColoredBright` - Bright colors, no borders + - `StyleColoredDark` - Dark colors, no borders + - Many more colored variants (Blue, Cyan, Green, Magenta, Red, Yellow) + - Colorize Headers/Body/Footers using [../text/color.go](../text/color.go) + - Custom text-case for Headers/Body/Footers + - Enable/disable separators between rows + - Render table with or without borders + - Customize box-drawing characters + - Horizontal separators per section (title, header, rows, footer) using `BoxStyleHorizontal` + - Title and caption styling options + - HTML rendering options (CSS class, escaping, newlines, color conversion) + - Bidirectional text support (`Style().Format.Direction`) -## Auto-Merge +### Output Formats -You can auto-merge cells horizontally and vertically, but you have request for -it specifically for each row/column using `RowConfig` or `ColumnConfig`. - -```golang - rowConfigAutoMerge := table.RowConfig{AutoMerge: true} - - t := table.NewWriter() - t.AppendHeader(table.Row{"Node IP", "Pods", "Namespace", "Container", "RCE", "RCE"}, rowConfigAutoMerge) - t.AppendHeader(table.Row{"Node IP", "Pods", "Namespace", "Container", "EXE", "RUN"}) - t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 1", "Y", "Y"}, rowConfigAutoMerge) - t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 2", "Y", "N"}, rowConfigAutoMerge) - t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1B", "C 3", "N", "N"}, rowConfigAutoMerge) - t.AppendRow(table.Row{"1.1.1.1", "Pod 1B", "NS 2", "C 4", "N", "N"}, rowConfigAutoMerge) - t.AppendRow(table.Row{"1.1.1.1", "Pod 1B", "NS 2", "C 5", "Y", "N"}, rowConfigAutoMerge) - t.AppendRow(table.Row{"2.2.2.2", "Pod 2", "NS 3", "C 6", "Y", "Y"}, rowConfigAutoMerge) - t.AppendRow(table.Row{"2.2.2.2", "Pod 2", "NS 3", "C 7", "Y", "Y"}, rowConfigAutoMerge) - t.AppendFooter(table.Row{"", "", "", 7, 5, 3}) - t.SetAutoIndex(true) - t.SetColumnConfigs([]table.ColumnConfig{ - {Number: 1, AutoMerge: true}, - {Number: 2, AutoMerge: true}, - {Number: 3, AutoMerge: true}, - {Number: 4, AutoMerge: true}, - {Number: 5, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter}, - {Number: 6, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter}, - }) - t.SetStyle(table.StyleLight) - t.Style().Options.SeparateRows = true - fmt.Println(t.Render()) -``` -to get: -``` -┌───┬─────────┬────────┬───────────┬───────────┬───────────┐ -│ │ NODE IP │ PODS │ NAMESPACE │ CONTAINER │ RCE │ -│ │ │ │ │ ├─────┬─────┤ -│ │ │ │ │ │ EXE │ RUN │ -├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┤ -│ 1 │ 1.1.1.1 │ Pod 1A │ NS 1A │ C 1 │ Y │ -├───┤ │ │ ├───────────┼─────┬─────┤ -│ 2 │ │ │ │ C 2 │ Y │ N │ -├───┤ │ ├───────────┼───────────┼─────┴─────┤ -│ 3 │ │ │ NS 1B │ C 3 │ N │ -├───┤ ├────────┼───────────┼───────────┼───────────┤ -│ 4 │ │ Pod 1B │ NS 2 │ C 4 │ N │ -├───┤ │ │ ├───────────┼─────┬─────┤ -│ 5 │ │ │ │ C 5 │ Y │ N │ -├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┤ -│ 6 │ 2.2.2.2 │ Pod 2 │ NS 3 │ C 6 │ Y │ -├───┤ │ │ ├───────────┼───────────┤ -│ 7 │ │ │ │ C 7 │ Y │ -├───┼─────────┼────────┼───────────┼───────────┼─────┬─────┤ -│ │ │ │ │ 7 │ 5 │ 3 │ -└───┴─────────┴────────┴───────────┴───────────┴─────┴─────┘ -``` - -## Paging - -You can limit then number of lines rendered in a single "Page". This logic -can handle rows with multiple lines too. Here is a simple example: -```golang - t.SetPageSize(1) - t.Render() -``` -to get: -``` -+-----+------------+-----------+--------+-----------------------------+ -| # | FIRST NAME | LAST NAME | SALARY | | -+-----+------------+-----------+--------+-----------------------------+ -| 1 | Arya | Stark | 3000 | | -+-----+------------+-----------+--------+-----------------------------+ -| | | TOTAL | 10000 | | -+-----+------------+-----------+--------+-----------------------------+ - -+-----+------------+-----------+--------+-----------------------------+ -| # | FIRST NAME | LAST NAME | SALARY | | -+-----+------------+-----------+--------+-----------------------------+ -| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | -+-----+------------+-----------+--------+-----------------------------+ -| | | TOTAL | 10000 | | -+-----+------------+-----------+--------+-----------------------------+ - -+-----+------------+-----------+--------+-----------------------------+ -| # | FIRST NAME | LAST NAME | SALARY | | -+-----+------------+-----------+--------+-----------------------------+ -| 300 | Tyrion | Lannister | 5000 | | -+-----+------------+-----------+--------+-----------------------------+ -| | | TOTAL | 10000 | | -+-----+------------+-----------+--------+-----------------------------+ -``` - -## Sorting - -Sorting can be done on one or more columns. The following code will make the -rows be sorted first by "First Name" and then by "Last Name" (in case of similar -"First Name" entries). -```golang - t.SortBy([]table.SortBy{ - {Name: "First Name", Mode: table.Asc}, - {Name: "Last Name", Mode: table.Asc}, - }) -``` - -## Wrapping (or) Row/Column Width restrictions - -You can restrict the maximum (text) width for a Row: -```golang - t.SetAllowedRowLength(50) - t.Render() -``` -to get: -``` -+-----+------------+-----------+--------+------- ~ -| # | FIRST NAME | LAST NAME | SALARY | ~ -+-----+------------+-----------+--------+------- ~ -| 1 | Arya | Stark | 3000 | ~ -| 20 | Jon | Snow | 2000 | You kn ~ -+-----+------------+-----------+--------+------- ~ -| 300 | Tyrion | Lannister | 5000 | ~ -+-----+------------+-----------+--------+------- ~ -| | | TOTAL | 10000 | ~ -+-----+------------+-----------+--------+------- ~ -``` - -## Column Control - Alignment, Colors, Width and more - -You can control a lot of things about individual cells/columns which overrides -global properties/styles using the `SetColumnConfig()` interface: -- Alignment (horizontal & vertical) -- Colorization -- Transform individual cells based on the content -- Visibility -- Width (minimum & maximum) - -```golang - nameTransformer := text.Transformer(func(val interface{}) string { - return text.Bold.Sprint(val) - }) - - t.SetColumnConfigs([]ColumnConfig{ - { - Name: "First Name", - Align: text.AlignLeft, - AlignFooter: text.AlignLeft, - AlignHeader: text.AlignLeft, - Colors: text.Colors{text.BgBlack, text.FgRed}, - ColorsHeader: text.Colors{text.BgRed, text.FgBlack, text.Bold}, - ColorsFooter: text.Colors{text.BgRed, text.FgBlack}, - Hidden: false, - Transformer: nameTransformer, - TransformerFooter: nameTransformer, - TransformerHeader: nameTransformer, - VAlign: text.VAlignMiddle, - VAlignFooter: text.VAlignTop, - VAlignHeader: text.VAlignBottom, - WidthMin: 6, - WidthMax: 64, - } - }) -``` - -## Render As ... - -Tables can be rendered in other common formats such as: - -### ... CSV - -```golang - t.RenderCSV() -``` -to get: -``` -,First Name,Last Name,Salary, -1,Arya,Stark,3000, -20,Jon,Snow,2000,"You know nothing\, Jon Snow!" -300,Tyrion,Lannister,5000, -,,Total,10000, -``` - -### ... HTML Table - -```golang - t.Style().HTML = table.HTMLOptions{ - CSSClass: "game-of-thrones", - EmptyColumn: " ", - EscapeText: true, - Newline: "
", - } - t.RenderHTML() -``` -to get: -```html - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#First NameLast NameSalary 
1AryaStark3000 
20JonSnow2000You know nothing, Jon Snow!
300TyrionLannister5000 
  Total10000 
-``` - -### ... Markdown Table - -```golang - t.RenderMarkdown() -``` -to get: -```markdown -| # | First Name | Last Name | Salary | | -| ---:| --- | --- | ---:| --- | -| 1 | Arya | Stark | 3000 | | -| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | -| 300 | Tyrion | Lannister | 5000 | | -| | | Total | 10000 | | -``` + - **Render as:** + - (ASCII/Unicode) Table - Human-readable pretty format + - CSV - Comma-separated values + - HTML Table - With custom CSS Class and options + - Markdown Table - Markdown-compatible format + - TSV - Tab-separated values + - Mirror output to an `io.Writer` (ex. `os.StdOut`) (`SetOutputMirror`) diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/filter.go b/vendor/github.com/jedib0t/go-pretty/v6/table/filter.go new file mode 100644 index 00000000..7462cfc8 --- /dev/null +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/filter.go @@ -0,0 +1,250 @@ +package table + +import ( + "fmt" + "regexp" + "strconv" + "strings" +) + +// FilterBy defines what to filter (Column Name or Number), how to filter (Operator), +// and the value to compare against. +type FilterBy struct { + // Name is the name of the Column as it appears in the first Header row. + // If a Header is not provided, or the name is not found in the header, this + // will not work. + Name string + // Number is the Column # from left. When specified, it overrides the Name + // property. If you know the exact Column number, use this instead of Name. + Number int + + // Operator defines how to compare the column value against the Value. + Operator FilterOperator + + // Value is the value to compare against. The type should match the expected + // comparison type (string for string operations, numeric for numeric operations). + // For Contains, StartsWith, EndsWith, and RegexMatch, Value should be a string. + // For numeric comparisons (Equal, NotEqual, GreaterThan, etc.), Value can be + // a number (int, float64) or a string representation of a number. + Value interface{} + + // IgnoreCase makes string comparisons case-insensitive (only applies to + // string-based operators). + IgnoreCase bool + + // CustomFilter is a function that can be used to filter rows in a custom + // manner. Note that: + // * This overrides and ignores the Operator, Value, and IgnoreCase settings + // * This is called after the column contents are converted to string form + // * This function is expected to return: + // * true => include the row + // * false => exclude the row + // + // Use this when the default filtering logic is not sufficient. + CustomFilter func(cellValue string) bool +} + +// FilterOperator defines how to filter. +type FilterOperator int + +const ( + // Equal filters rows where the column value equals the Value. + Equal FilterOperator = iota + // NotEqual filters rows where the column value does not equal the Value. + NotEqual + // GreaterThan filters rows where the column value is greater than the Value. + GreaterThan + // GreaterThanOrEqual filters rows where the column value is greater than or equal to the Value. + GreaterThanOrEqual + // LessThan filters rows where the column value is less than the Value. + LessThan + // LessThanOrEqual filters rows where the column value is less than or equal to the Value. + LessThanOrEqual + // Contains filters rows where the column value contains the Value (string search). + Contains + // NotContains filters rows where the column value does not contain the Value (string search). + NotContains + // StartsWith filters rows where the column value starts with the Value. + StartsWith + // EndsWith filters rows where the column value ends with the Value. + EndsWith + // RegexMatch filters rows where the column value matches the Value as a regular expression. + RegexMatch + // RegexNotMatch filters rows where the column value does not match the Value as a regular expression. + RegexNotMatch +) + +func (t *Table) parseFilterBy(filterBy []FilterBy) []FilterBy { + var resFilterBy []FilterBy + for _, filter := range filterBy { + colNum := 0 + if filter.Number > 0 && filter.Number <= t.numColumns { + colNum = filter.Number + } else if filter.Name != "" && len(t.rowsHeaderRaw) > 0 { + // Parse from raw header rows + for idx, colName := range t.rowsHeaderRaw[0] { + if fmt.Sprint(colName) == filter.Name { + colNum = idx + 1 + break + } + } + } + if colNum > 0 { + resFilterBy = append(resFilterBy, FilterBy{ + Name: filter.Name, + Number: colNum, + Operator: filter.Operator, + Value: filter.Value, + IgnoreCase: filter.IgnoreCase, + CustomFilter: filter.CustomFilter, + }) + } + } + return resFilterBy +} + +func (t *Table) matchesFiltersRaw(row Row, filters []FilterBy) bool { + // All filters must match (AND logic) + for _, filter := range filters { + if !t.matchesFilterRaw(row, filter) { + return false + } + } + return true +} + +func (t *Table) matchesFilterRaw(row Row, filter FilterBy) bool { + colIdx := filter.Number - 1 + if colIdx < 0 || colIdx >= len(row) { + return false + } + + cellValue := row[colIdx] + cellValueStr := fmt.Sprint(cellValue) + + // Use custom filter if provided + if filter.CustomFilter != nil { + return filter.CustomFilter(cellValueStr) + } + + // Use operator-based filtering + return t.matchesOperator(cellValueStr, filter) +} + +func (t *Table) matchesOperator(cellValue string, filter FilterBy) bool { + switch filter.Operator { + case Equal: + return t.compareEqual(cellValue, filter.Value, filter.IgnoreCase) + case NotEqual: + return !t.compareEqual(cellValue, filter.Value, filter.IgnoreCase) + case GreaterThan: + return t.compareNumeric(cellValue, filter.Value, func(a, b float64) bool { return a > b }) + case GreaterThanOrEqual: + return t.compareNumeric(cellValue, filter.Value, func(a, b float64) bool { return a >= b }) + case LessThan: + return t.compareNumeric(cellValue, filter.Value, func(a, b float64) bool { return a < b }) + case LessThanOrEqual: + return t.compareNumeric(cellValue, filter.Value, func(a, b float64) bool { return a <= b }) + case Contains: + return t.compareContains(cellValue, filter.Value, filter.IgnoreCase) + case NotContains: + return !t.compareContains(cellValue, filter.Value, filter.IgnoreCase) + case StartsWith: + return t.compareStartsWith(cellValue, filter.Value, filter.IgnoreCase) + case EndsWith: + return t.compareEndsWith(cellValue, filter.Value, filter.IgnoreCase) + case RegexMatch: + return t.compareRegexMatch(cellValue, filter.Value, filter.IgnoreCase) + case RegexNotMatch: + return !t.compareRegexMatch(cellValue, filter.Value, filter.IgnoreCase) + default: + return false + } +} + +func (t *Table) compareEqual(cellValue string, filterValue interface{}, ignoreCase bool) bool { + filterStr := fmt.Sprint(filterValue) + if ignoreCase { + return strings.EqualFold(cellValue, filterStr) + } + return cellValue == filterStr +} + +func (t *Table) compareNumeric(cellValue string, filterValue interface{}, compareFunc func(float64, float64) bool) bool { + cellNum, cellErr := strconv.ParseFloat(cellValue, 64) + if cellErr != nil { + return false + } + + var filterNum float64 + switch v := filterValue.(type) { + case int: + filterNum = float64(v) + case int64: + filterNum = float64(v) + case float64: + filterNum = v + case float32: + filterNum = float64(v) + case string: + var err error + filterNum, err = strconv.ParseFloat(v, 64) + if err != nil { + return false + } + default: + // Try to convert to string and parse + filterStr := fmt.Sprint(filterValue) + var err error + filterNum, err = strconv.ParseFloat(filterStr, 64) + if err != nil { + return false + } + } + + return compareFunc(cellNum, filterNum) +} + +func (t *Table) compareContains(cellValue string, filterValue interface{}, ignoreCase bool) bool { + filterStr := fmt.Sprint(filterValue) + if ignoreCase { + return strings.Contains(strings.ToLower(cellValue), strings.ToLower(filterStr)) + } + return strings.Contains(cellValue, filterStr) +} + +func (t *Table) compareStartsWith(cellValue string, filterValue interface{}, ignoreCase bool) bool { + filterStr := fmt.Sprint(filterValue) + if ignoreCase { + return strings.HasPrefix(strings.ToLower(cellValue), strings.ToLower(filterStr)) + } + return strings.HasPrefix(cellValue, filterStr) +} + +func (t *Table) compareEndsWith(cellValue string, filterValue interface{}, ignoreCase bool) bool { + filterStr := fmt.Sprint(filterValue) + if ignoreCase { + return strings.HasSuffix(strings.ToLower(cellValue), strings.ToLower(filterStr)) + } + return strings.HasSuffix(cellValue, filterStr) +} + +func (t *Table) compareRegexMatch(cellValue string, filterValue interface{}, ignoreCase bool) bool { + filterStr := fmt.Sprint(filterValue) + + // Compile the regex pattern + var pattern *regexp.Regexp + var err error + if ignoreCase { + pattern, err = regexp.Compile("(?i)" + filterStr) + } else { + pattern, err = regexp.Compile(filterStr) + } + + if err != nil { + // If regex compilation fails, fall back to simple string matching + return t.compareEqual(cellValue, filterValue, ignoreCase) + } + + return pattern.MatchString(cellValue) +} diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/render.go b/vendor/github.com/jedib0t/go-pretty/v6/table/render.go index 4ef68f99..a1b6cfe9 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/table/render.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/render.go @@ -20,7 +20,7 @@ import ( // │ │ │ TOTAL │ 10000 │ │ // └─────┴────────────┴───────────┴────────┴─────────────────────────────┘ func (t *Table) Render() string { - t.initForRender() + t.initForRender(renderModeDefault) var out strings.Builder if t.numColumns > 0 { @@ -50,6 +50,7 @@ func (t *Table) Render() string { return t.render(&out) } +//gocyclo:ignore func (t *Table) renderColumn(out *strings.Builder, row rowStr, colIdx int, maxColumnLength int, hint renderHint) int { numColumnsRendered := 1 @@ -93,11 +94,10 @@ func (t *Table) renderColumn(out *strings.Builder, row rowStr, colIdx int, maxCo numColumnsRendered++ } } - colStr = align.Apply(colStr, maxColumnLength) // pad both sides of the column if !hint.isSeparatorRow || (hint.isSeparatorRow && mergeVertically) { - colStr = t.style.Box.PaddingLeft + colStr + t.style.Box.PaddingRight + colStr = t.style.Box.PaddingLeft + align.Apply(colStr, maxColumnLength) + t.style.Box.PaddingRight } t.renderColumnColorized(out, colIdx, colStr, hint) @@ -112,9 +112,9 @@ func (t *Table) renderColumnAutoIndex(out *strings.Builder, hint renderHint) { if hint.isSeparatorRow { numChars := t.autoIndexVIndexMaxLength + utf8.RuneCountInString(t.style.Box.PaddingLeft) + utf8.RuneCountInString(t.style.Box.PaddingRight) - chars := t.style.Box.MiddleHorizontal + chars := t.style.Box.middleHorizontal(hint.separatorType) if hint.isAutoIndexColumn && hint.isHeaderOrFooterSeparator() { - chars = text.RepeatAndTrim(" ", len(t.style.Box.MiddleHorizontal)) + chars = text.RepeatAndTrim(" ", len(chars)) } outAutoIndex.WriteString(text.RepeatAndTrim(chars, numChars)) } else { @@ -239,7 +239,7 @@ func (t *Table) renderLineMergeOutputs(out *strings.Builder, outLine *strings.Bu } func (t *Table) renderMarginLeft(out *strings.Builder, hint renderHint) { - out.WriteString(t.style.Format.Direction.Modifier()) + out.WriteString(t.directionModifier) if t.style.Options.DrawBorder { border := t.getBorderLeft(hint) colors := t.getBorderColors(hint) @@ -304,8 +304,10 @@ func (t *Table) renderRowSeparator(out *strings.Builder, hint renderHint) { } else if hint.isFooterRow && !t.style.Options.SeparateFooter { return } + hint.isSeparatorRow = true - t.renderLine(out, t.rowSeparator, hint) + separator := t.rowSeparatorStrings[hint.separatorType] + t.renderLine(out, t.rowSeparators[separator], hint) } func (t *Table) renderRows(out *strings.Builder, rows []rowStr, hint renderHint) { @@ -316,8 +318,17 @@ func (t *Table) renderRows(out *strings.Builder, rows []rowStr, hint renderHint) t.renderRow(out, row, hint) if t.shouldSeparateRows(rowIdx, len(rows)) { - hint.isFirstRow = false - t.renderRowSeparator(out, hint) + hintSep := hint + hintSep.isFirstRow = false + hintSep.isSeparatorRow = true + if hintSep.isHeaderRow { + hintSep.separatorType = separatorTypeHeaderMiddle + } else if hintSep.isFooterRow { + hintSep.separatorType = separatorTypeFooterMiddle + } else { + hintSep.separatorType = separatorTypeRowMiddle + } + t.renderRowSeparator(out, hintSep) } } } @@ -328,46 +339,69 @@ func (t *Table) renderRowsBorderBottom(out *strings.Builder) { isBorderBottom: true, isFooterRow: true, rowNumber: len(t.rowsFooter), + separatorType: separatorTypeFooterBottom, }) } else { t.renderRowSeparator(out, renderHint{ isBorderBottom: true, isFooterRow: false, rowNumber: len(t.rows), + separatorType: separatorTypeRowBottom, }) } } func (t *Table) renderRowsBorderTop(out *strings.Builder) { + st := separatorTypeHeaderTop + if t.title != "" { + st = separatorTypeTitleBottom + } else if len(t.rowsHeader) == 0 && !t.autoIndex { + st = separatorTypeRowTop + } + if len(t.rowsHeader) > 0 || t.autoIndex { t.renderRowSeparator(out, renderHint{ - isBorderTop: true, - isHeaderRow: true, - rowNumber: 0, + isBorderTop: true, + isHeaderRow: true, + isSeparatorRow: true, + rowNumber: 0, + separatorType: st, }) } else { t.renderRowSeparator(out, renderHint{ - isBorderTop: true, - isHeaderRow: false, - rowNumber: 0, + isBorderTop: true, + isHeaderRow: false, + isSeparatorRow: true, + rowNumber: 0, + separatorType: st, }) } } func (t *Table) renderRowsFooter(out *strings.Builder) { if len(t.rowsFooter) > 0 { - t.renderRowSeparator(out, renderHint{ - isFooterRow: true, - isFirstRow: true, - isSeparatorRow: true, - }) + // Only add separator before footer if there are data rows. + // Otherwise, renderRowsHeader already added one. + if len(t.rows) > 0 { + t.renderRowSeparator(out, renderHint{ + isFooterRow: true, + isFirstRow: true, + isSeparatorRow: true, + separatorType: separatorTypeFooterTop, + }) + } t.renderRows(out, t.rowsFooter, renderHint{isFooterRow: true}) } } func (t *Table) renderRowsHeader(out *strings.Builder) { if len(t.rowsHeader) > 0 || t.autoIndex { - hintSeparator := renderHint{isHeaderRow: true, isLastRow: true, isSeparatorRow: true} + hintSeparator := renderHint{ + isHeaderRow: true, + isLastRow: true, + isSeparatorRow: true, + separatorType: separatorTypeHeaderMiddle, + } if len(t.rowsHeader) > 0 { t.renderRows(out, t.rowsHeader, renderHint{isHeaderRow: true}) @@ -376,7 +410,13 @@ func (t *Table) renderRowsHeader(out *strings.Builder) { t.renderRow(out, t.getAutoIndexColumnIDs(), renderHint{isAutoIndexRow: true, isHeaderRow: true}) hintSeparator.rowNumber = 1 } - t.renderRowSeparator(out, hintSeparator) + + // Only add separator after header if there are data rows or footer rows. + // Otherwise, the bottom border is rendered directly. + if len(t.rows) > 0 || len(t.rowsFooter) > 0 || !t.style.Options.DoNotRenderSeparatorWhenEmpty { + hintSeparator.separatorType = separatorTypeHeaderBottom + t.renderRowSeparator(out, hintSeparator) + } } } @@ -393,8 +433,9 @@ func (t *Table) renderTitle(out *strings.Builder) { } if t.style.Options.DrawBorder { lenBorder := rowLength - text.StringWidthWithoutEscSequences(t.style.Box.TopLeft+t.style.Box.TopRight) + middleHorizontal := t.style.Box.middleHorizontal(separatorTypeTitleTop) out.WriteString(colorsBorder.Sprint(t.style.Box.TopLeft)) - out.WriteString(colorsBorder.Sprint(text.RepeatAndTrim(t.style.Box.MiddleHorizontal, lenBorder))) + out.WriteString(colorsBorder.Sprint(text.RepeatAndTrim(middleHorizontal, lenBorder))) out.WriteString(colorsBorder.Sprint(t.style.Box.TopRight)) } diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/render_csv.go b/vendor/github.com/jedib0t/go-pretty/v6/table/render_csv.go index 831194ef..e9ea5ef8 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/table/render_csv.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/render_csv.go @@ -14,7 +14,7 @@ import ( // 300,Tyrion,Lannister,5000, // ,,Total,10000, func (t *Table) RenderCSV() string { - t.initForRender() + t.initForRender(renderModeCSV) var out strings.Builder if t.numColumns > 0 { diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/render_hint.go b/vendor/github.com/jedib0t/go-pretty/v6/table/render_hint.go index e46cdc84..167cf88d 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/table/render_hint.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/render_hint.go @@ -15,6 +15,7 @@ type renderHint struct { isTitleRow bool // title row? rowLineNumber int // the line number for a multi-line row rowNumber int // the row number/index + separatorType separatorType } func (h *renderHint) isBorderOrSeparator() bool { @@ -37,3 +38,13 @@ func (h *renderHint) isHeaderOrFooterSeparator() bool { func (h *renderHint) isLastLineOfLastRow() bool { return h.isLastLineOfRow && h.isLastRow } + +type renderMode string + +const ( + renderModeDefault renderMode = "default" + renderModeCSV renderMode = "csv" + renderModeMarkdown renderMode = "markdown" + renderModeTSV renderMode = "tsv" + renderModeHTML renderMode = "html" +) diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/render_html.go b/vendor/github.com/jedib0t/go-pretty/v6/table/render_html.go index 729f95ee..fec68f00 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/table/render_html.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/render_html.go @@ -60,7 +60,7 @@ const ( // // func (t *Table) RenderHTML() string { - t.initForRender() + t.initForRender(renderModeHTML) var out strings.Builder if t.numColumns > 0 { @@ -106,11 +106,15 @@ func (t *Table) htmlRenderCaption(out *strings.Builder) { } func (t *Table) htmlRenderColumn(out *strings.Builder, colStr string) { - if t.style.HTML.EscapeText { + // convertEscSequencesToSpans already escapes text content, so skip + // EscapeText if ConvertColorsToSpans is true + if t.style.HTML.ConvertColorsToSpans { + colStr = convertEscSequencesToSpans(colStr) + } else if t.style.HTML.EscapeText { colStr = html.EscapeString(colStr) } if t.style.HTML.Newline != "\n" { - colStr = strings.Replace(colStr, "\n", t.style.HTML.Newline, -1) + colStr = strings.ReplaceAll(colStr, "\n", t.style.HTML.Newline) } out.WriteString(colStr) } diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/render_init.go b/vendor/github.com/jedib0t/go-pretty/v6/table/render_init.go index 333c3988..d79aa0f8 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/table/render_init.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/render_init.go @@ -43,11 +43,15 @@ func (t *Table) analyzeAndStringifyColumn(colIdx int, col interface{}, hint rend } else if colStrVal, ok := col.(string); ok { colStr = colStrVal } else { - colStr = fmt.Sprint(col) + colStr = convertValueToString(col) } colStr = strings.ReplaceAll(colStr, "\t", " ") colStr = text.ProcessCRLF(colStr) - return fmt.Sprintf("%s%s", t.style.Format.Direction.Modifier(), colStr) + // Avoid fmt.Sprintf when direction modifier is empty (most common case) + if t.directionModifier == "" { + return colStr + } + return t.directionModifier + colStr } func (t *Table) extractMaxColumnLengths(rows []rowStr, hint renderHint) { @@ -149,13 +153,18 @@ func (t *Table) reBalanceMaxMergedColumnLengths() { } } -func (t *Table) initForRender() { +func (t *Table) initForRender(mode renderMode) { + t.renderMode = mode + // pick a default style if none was set until now t.Style() // reset rendering state t.reset() + // cache the direction modifier to avoid repeated calls + t.directionModifier = t.style.Format.Direction.Modifier() + // initialize the column configs and normalize them t.initForRenderColumnConfigs() @@ -280,11 +289,15 @@ func (t *Table) initForRenderPaddedColumns() { } func (t *Table) initForRenderRows() { - // auto-index: calc the index column's max length - t.autoIndexVIndexMaxLength = len(fmt.Sprint(len(t.rowsRaw))) + // filter the rows as requested (before stringification and sorting) + t.initForRenderFilterRows() - // stringify all the rows to make it easy to render - t.rows = t.initForRenderRowsStringify(t.rowsRaw, renderHint{}) + // auto-index: calc the index column's max length + t.autoIndexVIndexMaxLength = len(fmt.Sprint(len(t.rowsRawFiltered))) + + // stringify the filtered rows + t.numColumns = 0 + t.rows = t.initForRenderRowsStringify(t.rowsRawFiltered, renderHint{}) t.rowsFooter = t.initForRenderRowsStringify(t.rowsFooterRaw, renderHint{isFooterRow: true}) t.rowsHeader = t.initForRenderRowsStringify(t.rowsHeaderRaw, renderHint{isHeaderRow: true}) @@ -301,6 +314,60 @@ func (t *Table) initForRenderRows() { t.initForRenderHideColumns() } +// initForRenderFilterRows filters the raw rows by removing non-matching rows from t.rowsRawFiltered. +func (t *Table) initForRenderFilterRows() { + // Restore original rows before filtering (in case of multiple renders with different filters) + if len(t.rowsRaw) > 0 { + t.rowsRawFiltered = make([]Row, len(t.rowsRaw)) + for i, row := range t.rowsRaw { + rowCopy := make(Row, len(row)) + copy(rowCopy, row) + t.rowsRawFiltered[i] = rowCopy + } + } + + if len(t.filterBy) == 0 { + // No filters, nothing to do + return + } + + // Store original separators and track which rows are kept + originalSeparators := make(map[int]bool) + for k, v := range t.separators { + originalSeparators[k] = v + } + + // Calculate numColumns from raw rows/headers for filter parsing + t.calculateNumColumnsFromRaw() + parsedFilterBy := t.parseFilterBy(t.filterBy) + if len(parsedFilterBy) == 0 { + // No valid filters, nothing to do + return + } + + // Filter rows in place and track which original rows were kept + filteredRows := t.rowsRawFiltered[:0] + keptIndices := make([]int, 0, len(t.rowsRawFiltered)) + for origIdx, row := range t.rowsRawFiltered { + if t.matchesFiltersRaw(row, parsedFilterBy) { + filteredRows = append(filteredRows, row) + keptIndices = append(keptIndices, origIdx) + } + } + t.rowsRawFiltered = filteredRows + + // Update separators map to reflect filtered rows + if len(originalSeparators) > 0 { + newSeparators := make(map[int]bool) + for newIdx, origIdx := range keptIndices { + if originalSeparators[origIdx] { + newSeparators[newIdx] = true + } + } + t.separators = newSeparators + } +} + func (t *Table) initForRenderRowsStringify(rows []Row, hint renderHint) []rowStr { rowsStr := make([]rowStr, len(rows)) for idx, row := range rows { @@ -315,36 +382,92 @@ func (t *Table) initForRenderRowPainterColors() { return } - // generate the colors - t.rowsColors = make([]text.Colors, len(t.rowsRaw)) - for idx, row := range t.rowsRaw { - idxColors := idx + // generate the colors for the final rows (after filtering and sorting) + // rowsColors will be indexed by the final position in t.rows + t.rowsColors = make([]text.Colors, len(t.rows)) + + // For each final position, find the row index in t.rowsRawFiltered (which is already filtered) + for finalPos := range t.rows { + var rowIdx int + if len(t.sortedRowIndices) > 0 { - // override with the sorted row index - for j := 0; j < len(t.sortedRowIndices); j++ { - if t.sortedRowIndices[j] == idx { - idxColors = j - break - } - } + // Rows were sorted: finalPos -> sortedRowIndices[finalPos] -> rowIdx in t.rowsRawFiltered + rowIdx = t.sortedRowIndices[finalPos] + } else { + // No sorting: finalPos -> rowIdx in t.rowsRawFiltered + rowIdx = finalPos } - if t.rowPainter != nil { - t.rowsColors[idxColors] = t.rowPainter(row) - } else if t.rowPainterWithAttributes != nil { - t.rowsColors[idxColors] = t.rowPainterWithAttributes(row, RowAttributes{ - Number: idx + 1, - NumberSorted: idxColors + 1, - }) + if rowIdx >= 0 && rowIdx < len(t.rowsRawFiltered) { + row := t.rowsRawFiltered[rowIdx] + if t.rowPainter != nil { + t.rowsColors[finalPos] = t.rowPainter(row) + } else if t.rowPainterWithAttributes != nil { + t.rowsColors[finalPos] = t.rowPainterWithAttributes(row, RowAttributes{ + Number: rowIdx + 1, + NumberSorted: finalPos + 1, + }) + } } } } func (t *Table) initForRenderRowSeparator() { - t.rowSeparator = make(rowStr, t.numColumns) - for colIdx, maxColumnLength := range t.maxColumnLengths { - maxColumnLength += text.StringWidthWithoutEscSequences(t.style.Box.PaddingLeft + t.style.Box.PaddingRight) - t.rowSeparator[colIdx] = text.RepeatAndTrim(t.style.Box.MiddleHorizontal, maxColumnLength) + // this is needed only for default render mode + if t.renderMode != renderModeDefault { + return + } + + // init the separatorType -> separator-string map + t.initForRenderRowSeparatorStrings() + + // init the separator-string -> separator-row map + t.rowSeparators = make(map[string]rowStr, len(t.rowSeparatorStrings)) + paddingLength := text.StringWidthWithoutEscSequences(t.style.Box.PaddingLeft + t.style.Box.PaddingRight) + for _, separator := range t.rowSeparatorStrings { + t.rowSeparators[separator] = make(rowStr, t.numColumns) + for colIdx, maxColumnLength := range t.maxColumnLengths { + t.rowSeparators[separator][colIdx] = text.RepeatAndTrim(separator, maxColumnLength+paddingLength) + } + } +} + +func (t *Table) initForRenderRowSeparatorStrings() { + // allocate and init only the separators that are needed + t.rowSeparatorStrings = make(map[separatorType]string) + addSeparatorType := func(st separatorType) { + t.rowSeparatorStrings[st] = t.style.Box.middleHorizontal(st) + } + + // for other render modes, we need all the separators + if t.title != "" { + addSeparatorType(separatorTypeTitleTop) + addSeparatorType(separatorTypeTitleBottom) + } + if len(t.rowsHeader) > 0 || t.autoIndex { + addSeparatorType(separatorTypeHeaderTop) + addSeparatorType(separatorTypeHeaderBottom) + if len(t.rowsHeader) > 1 { + addSeparatorType(separatorTypeHeaderMiddle) + } + } + if len(t.rows) > 0 { + addSeparatorType(separatorTypeRowTop) + addSeparatorType(separatorTypeRowBottom) + if len(t.rows) > 1 { + addSeparatorType(separatorTypeRowMiddle) + } + } else if len(t.rowsHeader) > 0 || t.autoIndex { + // When there are headers but no data rows, we still need separatorTypeRowBottom + // for the bottom border. + addSeparatorType(separatorTypeRowBottom) + } + if len(t.rowsFooter) > 0 || t.autoIndex { + addSeparatorType(separatorTypeFooterTop) + addSeparatorType(separatorTypeFooterBottom) + if len(t.rowsFooter) > 1 { + addSeparatorType(separatorTypeFooterMiddle) + } } } @@ -402,9 +525,10 @@ func (t *Table) reset() { t.maxRowLength = 0 t.numColumns = 0 t.numLinesRendered = 0 - t.rowSeparator = nil + t.rowSeparators = nil t.rows = nil t.rowsColors = nil t.rowsFooter = nil t.rowsHeader = nil + t.sortedRowIndices = nil } diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/render_markdown.go b/vendor/github.com/jedib0t/go-pretty/v6/table/render_markdown.go index adf573fc..80cdcc07 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/table/render_markdown.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/render_markdown.go @@ -14,7 +14,7 @@ import ( // | 300 | Tyrion | Lannister | 5000 | | // | | | Total | 10000 | | func (t *Table) RenderMarkdown() string { - t.initForRender() + t.initForRender(renderModeMarkdown) var out strings.Builder if t.numColumns > 0 { @@ -47,19 +47,15 @@ func (t *Table) markdownRenderRow(out *strings.Builder, row rowStr, hint renderH for colIdx := 0; colIdx < t.numColumns; colIdx++ { t.markdownRenderRowAutoIndex(out, colIdx, hint) - if hint.isSeparatorRow { - out.WriteString(t.getAlign(colIdx, hint).MarkdownProperty()) - } else { - var colStr string - if colIdx < len(row) { - colStr = row[colIdx] - } - out.WriteRune(' ') - colStr = strings.ReplaceAll(colStr, "|", "\\|") - colStr = strings.ReplaceAll(colStr, "\n", "
") - out.WriteString(colStr) - out.WriteRune(' ') + var colStr string + if colIdx < len(row) { + colStr = row[colIdx] } + out.WriteRune(' ') + colStr = strings.ReplaceAll(colStr, "|", "\\|") + colStr = strings.ReplaceAll(colStr, "\n", "
") + out.WriteString(colStr) + out.WriteRune(' ') out.WriteRune('|') } } @@ -83,7 +79,7 @@ func (t *Table) markdownRenderRows(out *strings.Builder, rows []rowStr, hint ren t.markdownRenderRow(out, row, hint) if idx == len(rows)-1 && hint.isHeaderRow { - t.markdownRenderRow(out, t.rowSeparator, renderHint{isSeparatorRow: true}) + t.markdownRenderSeparator(out, renderHint{isSeparatorRow: true}) } } } @@ -101,6 +97,21 @@ func (t *Table) markdownRenderRowsHeader(out *strings.Builder) { } } +func (t *Table) markdownRenderSeparator(out *strings.Builder, hint renderHint) { + // when working on line number 2 or more, insert a newline first + if out.Len() > 0 { + out.WriteRune('\n') + } + + out.WriteRune('|') + for colIdx := 0; colIdx < t.numColumns; colIdx++ { + t.markdownRenderRowAutoIndex(out, colIdx, hint) + + out.WriteString(t.getAlign(colIdx, hint).MarkdownProperty()) + out.WriteRune('|') + } +} + func (t *Table) markdownRenderTitle(out *strings.Builder) { if t.title != "" { out.WriteString("# ") diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/render_tsv.go b/vendor/github.com/jedib0t/go-pretty/v6/table/render_tsv.go index 67f1e7f7..312f365d 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/table/render_tsv.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/render_tsv.go @@ -6,7 +6,7 @@ import ( ) func (t *Table) RenderTSV() string { - t.initForRender() + t.initForRender(renderModeTSV) var out strings.Builder diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/sort.go b/vendor/github.com/jedib0t/go-pretty/v6/table/sort.go index 7a47765a..dd1d2175 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/table/sort.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/sort.go @@ -21,6 +21,18 @@ type SortBy struct { // IgnoreCase makes sorting case-insensitive IgnoreCase bool + + // CustomLess is a function that can be used to sort the column in a custom + // manner. Note that: + // * This overrides and ignores the Mode and IgnoreCase settings + // * This is called after the column contents are converted to string form + // * This function is expected to return: + // * -1 => when iStr comes before jStr + // * 0 => when iStr and jStr are considered equal + // * 1 => when iStr comes after jStr + // + // Use this when the default sorting logic is not sufficient. + CustomLess func(iStr string, jStr string) int } // SortMode defines How to sort. @@ -49,12 +61,6 @@ const ( DscNumericAlpha ) -type rowsSorter struct { - rows []rowStr - sortBy []SortBy - sortedIndices []int -} - // getSortedRowIndices sorts and returns the row indices in Sorted order as // directed by Table.sortBy which can be set using Table.SortBy(...) func (t *Table) getSortedRowIndices() []int { @@ -63,11 +69,31 @@ func (t *Table) getSortedRowIndices() []int { sortedIndices[idx] = idx } - if t.sortBy != nil && len(t.sortBy) > 0 { - sort.Sort(rowsSorter{ - rows: t.rows, - sortBy: t.parseSortBy(t.sortBy), - sortedIndices: sortedIndices, + if len(t.sortBy) > 0 { + parsedSortBy := t.parseSortBy(t.sortBy) + sort.Slice(sortedIndices, func(i, j int) bool { + isEqual, isLess := false, false + realI, realJ := sortedIndices[i], sortedIndices[j] + for _, sortBy := range parsedSortBy { + // extract the values/cells from the rows for comparison + rowI, rowJ, colIdx := t.rows[realI], t.rows[realJ], sortBy.Number-1 + iVal, jVal := "", "" + if colIdx < len(rowI) { + iVal = rowI[colIdx] + } + if colIdx < len(rowJ) { + jVal = rowJ[colIdx] + } + + // compare and choose whether to continue + isEqual, isLess = less(iVal, jVal, sortBy) + // if the values are not equal, return the result immediately + if !isEqual { + return isLess + } + // if the values are equal, continue to the next column + } + return isLess }) } @@ -94,48 +120,32 @@ func (t *Table) parseSortBy(sortBy []SortBy) []SortBy { Number: colNum, Mode: col.Mode, IgnoreCase: col.IgnoreCase, + CustomLess: col.CustomLess, }) } } return resSortBy } -func (rs rowsSorter) Len() int { - return len(rs.rows) -} - -func (rs rowsSorter) Swap(i, j int) { - rs.sortedIndices[i], rs.sortedIndices[j] = rs.sortedIndices[j], rs.sortedIndices[i] -} - -func (rs rowsSorter) Less(i, j int) bool { - shouldContinue, returnValue := false, false - realI, realJ := rs.sortedIndices[i], rs.sortedIndices[j] - for _, sortBy := range rs.sortBy { - // extract the values/cells from the rows for comparison - rowI, rowJ, colIdx := rs.rows[realI], rs.rows[realJ], sortBy.Number-1 - iVal, jVal := "", "" - if colIdx < len(rowI) { - iVal = rowI[colIdx] - } - if colIdx < len(rowJ) { - jVal = rowJ[colIdx] - } - - // compare and choose whether to continue - shouldContinue, returnValue = less(iVal, jVal, sortBy) - if !shouldContinue { - break +func less(iVal string, jVal string, sb SortBy) (bool, bool) { + if sb.CustomLess != nil { + // use the custom less function to compare the values + rc := sb.CustomLess(iVal, jVal) + if rc < 0 { + return false, true + } else if rc > 0 { + return false, false + } else { // rc == 0 + return true, false } } - return returnValue -} -func less(iVal string, jVal string, sb SortBy) (bool, bool) { + // if the values are equal, return fast to continue to next column if iVal == jVal { return true, false } + // otherwise, use the default sorting logic defined by Mode and IgnoreCase switch sb.Mode { case Asc, Dsc: return lessAlphabetic(iVal, jVal, sb) @@ -168,37 +178,27 @@ func lessAlphabetic(iVal string, jVal string, sb SortBy) (bool, bool) { } } -func lessAlphaNumericI(sb SortBy) (bool, bool) { - // i == "abc"; j == 5 - switch sb.Mode { - case AscAlphaNumeric, DscAlphaNumeric: - return false, true - default: // AscNumericAlpha, DscNumericAlpha - return false, false - } -} - -func lessAlphaNumericJ(sb SortBy) (bool, bool) { - // i == 5; j == "abc" - switch sb.Mode { - case AscAlphaNumeric, DscAlphaNumeric: - return false, false - default: // AscNumericAlpha, DscNumericAlpha: - return false, true - } -} - func lessMixedMode(iVal string, jVal string, sb SortBy) (bool, bool) { iNumVal, iErr := strconv.ParseFloat(iVal, 64) jNumVal, jErr := strconv.ParseFloat(jVal, 64) if iErr != nil && jErr != nil { // both are alphanumeric return lessAlphabetic(iVal, jVal, sb) } - if iErr != nil { // iVal is alphabetic, jVal is numeric - return lessAlphaNumericI(sb) + if iErr != nil { // iVal == "abc"; jVal == 5 + switch sb.Mode { + case AscAlphaNumeric, DscAlphaNumeric: + return false, true + default: // AscNumericAlpha, DscNumericAlpha + return false, false + } } - if jErr != nil { // iVal is numeric, jVal is alphabetic - return lessAlphaNumericJ(sb) + if jErr != nil { // iVal == 5; jVal == "abc" + switch sb.Mode { + case AscAlphaNumeric, DscAlphaNumeric: + return false, false + default: // AscNumericAlpha, DscNumericAlpha: + return false, true + } } // both values numeric return lessNumericVal(iNumVal, jNumVal, sb) diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/style.go b/vendor/github.com/jedib0t/go-pretty/v6/table/style.go index aaed2c27..b5f58493 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/table/style.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/style.go @@ -1,9 +1,5 @@ package table -import ( - "github.com/jedib0t/go-pretty/v6/text" -) - // Style declares how to render the Table and provides very fine-grained control // on how the Table gets rendered on the Console. type Style struct { @@ -340,591 +336,3 @@ var ( Title: TitleOptionsDefault, } ) - -// BoxStyle defines the characters/strings to use to render the borders and -// separators for the Table. -type BoxStyle struct { - BottomLeft string - BottomRight string - BottomSeparator string - EmptySeparator string - Left string - LeftSeparator string - MiddleHorizontal string - MiddleSeparator string - MiddleVertical string - PaddingLeft string - PaddingRight string - PageSeparator string - Right string - RightSeparator string - TopLeft string - TopRight string - TopSeparator string - UnfinishedRow string -} - -var ( - // StyleBoxDefault defines a Boxed-Table like below: - // +-----+------------+-----------+--------+-----------------------------+ - // | # | FIRST NAME | LAST NAME | SALARY | | - // +-----+------------+-----------+--------+-----------------------------+ - // | 1 | Arya | Stark | 3000 | | - // | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | - // | 300 | Tyrion | Lannister | 5000 | | - // +-----+------------+-----------+--------+-----------------------------+ - // | | | TOTAL | 10000 | | - // +-----+------------+-----------+--------+-----------------------------+ - StyleBoxDefault = BoxStyle{ - BottomLeft: "+", - BottomRight: "+", - BottomSeparator: "+", - EmptySeparator: " ", - Left: "|", - LeftSeparator: "+", - MiddleHorizontal: "-", - MiddleSeparator: "+", - MiddleVertical: "|", - PaddingLeft: " ", - PaddingRight: " ", - PageSeparator: "\n", - Right: "|", - RightSeparator: "+", - TopLeft: "+", - TopRight: "+", - TopSeparator: "+", - UnfinishedRow: " ~", - } - - // StyleBoxBold defines a Boxed-Table like below: - // ┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - // ┃ # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ┃ - // ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ - // ┃ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ ┃ - // ┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! ┃ - // ┃ 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ┃ - // ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ - // ┃ ┃ ┃ TOTAL ┃ 10000 ┃ ┃ - // ┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - StyleBoxBold = BoxStyle{ - BottomLeft: "┗", - BottomRight: "┛", - BottomSeparator: "┻", - EmptySeparator: " ", - Left: "┃", - LeftSeparator: "┣", - MiddleHorizontal: "━", - MiddleSeparator: "╋", - MiddleVertical: "┃", - PaddingLeft: " ", - PaddingRight: " ", - PageSeparator: "\n", - Right: "┃", - RightSeparator: "┫", - TopLeft: "┏", - TopRight: "┓", - TopSeparator: "┳", - UnfinishedRow: " ≈", - } - - // StyleBoxDouble defines a Boxed-Table like below: - // ╔═════╦════════════╦═══════════╦════════╦═════════════════════════════╗ - // ║ # ║ FIRST NAME ║ LAST NAME ║ SALARY ║ ║ - // ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣ - // ║ 1 ║ Arya ║ Stark ║ 3000 ║ ║ - // ║ 20 ║ Jon ║ Snow ║ 2000 ║ You know nothing, Jon Snow! ║ - // ║ 300 ║ Tyrion ║ Lannister ║ 5000 ║ ║ - // ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣ - // ║ ║ ║ TOTAL ║ 10000 ║ ║ - // ╚═════╩════════════╩═══════════╩════════╩═════════════════════════════╝ - StyleBoxDouble = BoxStyle{ - BottomLeft: "╚", - BottomRight: "╝", - BottomSeparator: "╩", - EmptySeparator: " ", - Left: "║", - LeftSeparator: "╠", - MiddleHorizontal: "═", - MiddleSeparator: "╬", - MiddleVertical: "║", - PaddingLeft: " ", - PaddingRight: " ", - PageSeparator: "\n", - Right: "║", - RightSeparator: "╣", - TopLeft: "╔", - TopRight: "╗", - TopSeparator: "╦", - UnfinishedRow: " ≈", - } - - // StyleBoxLight defines a Boxed-Table like below: - // ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ - // │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ - // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ - // │ 1 │ Arya │ Stark │ 3000 │ │ - // │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ - // │ 300 │ Tyrion │ Lannister │ 5000 │ │ - // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ - // │ │ │ TOTAL │ 10000 │ │ - // └─────┴────────────┴───────────┴────────┴─────────────────────────────┘ - StyleBoxLight = BoxStyle{ - BottomLeft: "└", - BottomRight: "┘", - BottomSeparator: "┴", - EmptySeparator: " ", - Left: "│", - LeftSeparator: "├", - MiddleHorizontal: "─", - MiddleSeparator: "┼", - MiddleVertical: "│", - PaddingLeft: " ", - PaddingRight: " ", - PageSeparator: "\n", - Right: "│", - RightSeparator: "┤", - TopLeft: "┌", - TopRight: "┐", - TopSeparator: "┬", - UnfinishedRow: " ≈", - } - - // StyleBoxRounded defines a Boxed-Table like below: - // ╭─────┬────────────┬───────────┬────────┬─────────────────────────────╮ - // │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ - // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ - // │ 1 │ Arya │ Stark │ 3000 │ │ - // │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ - // │ 300 │ Tyrion │ Lannister │ 5000 │ │ - // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ - // │ │ │ TOTAL │ 10000 │ │ - // ╰─────┴────────────┴───────────┴────────┴─────────────────────────────╯ - StyleBoxRounded = BoxStyle{ - BottomLeft: "╰", - BottomRight: "╯", - BottomSeparator: "┴", - EmptySeparator: " ", - Left: "│", - LeftSeparator: "├", - MiddleHorizontal: "─", - MiddleSeparator: "┼", - MiddleVertical: "│", - PaddingLeft: " ", - PaddingRight: " ", - PageSeparator: "\n", - Right: "│", - RightSeparator: "┤", - TopLeft: "╭", - TopRight: "╮", - TopSeparator: "┬", - UnfinishedRow: " ≈", - } - - // styleBoxTest defines a Boxed-Table like below: - // (-----^------------^-----------^--------^-----------------------------) - // [< #>||||< >] - // {-----+------------+-----------+--------+-----------------------------} - // [< 1>|||< 3000>|< >] - // [< 20>|||< 2000>|] - // [<300>|||< 5000>|< >] - // {-----+------------+-----------+--------+-----------------------------} - // [< >|< >||< 10000>|< >] - // \-----v------------v-----------v--------v-----------------------------/ - styleBoxTest = BoxStyle{ - BottomLeft: "\\", - BottomRight: "/", - BottomSeparator: "v", - EmptySeparator: " ", - Left: "[", - LeftSeparator: "{", - MiddleHorizontal: "--", - MiddleSeparator: "+", - MiddleVertical: "|", - PaddingLeft: "<", - PaddingRight: ">", - PageSeparator: "\n", - Right: "]", - RightSeparator: "}", - TopLeft: "(", - TopRight: ")", - TopSeparator: "^", - UnfinishedRow: " ~~~", - } -) - -// ColorOptions defines the ANSI colors to use for parts of the Table. -type ColorOptions struct { - Border text.Colors // borders (if nil, uses one of the below) - Footer text.Colors // footer row(s) colors - Header text.Colors // header row(s) colors - IndexColumn text.Colors // index-column colors (row #, etc.) - Row text.Colors // regular row(s) colors - RowAlternate text.Colors // regular row(s) colors for the even-numbered rows - Separator text.Colors // separators (if nil, uses one of the above) -} - -var ( - // ColorOptionsDefault defines sensible ANSI color options - basically NONE. - ColorOptionsDefault = ColorOptions{} - - // ColorOptionsBright renders dark text on bright background. - ColorOptionsBright = ColorOptionsBlackOnCyanWhite - - // ColorOptionsDark renders bright text on dark background. - ColorOptionsDark = ColorOptionsCyanWhiteOnBlack - - // ColorOptionsBlackOnBlueWhite renders Black text on Blue/White background. - ColorOptionsBlackOnBlueWhite = ColorOptions{ - Footer: text.Colors{text.BgBlue, text.FgBlack}, - Header: text.Colors{text.BgHiBlue, text.FgBlack}, - IndexColumn: text.Colors{text.BgHiBlue, text.FgBlack}, - Row: text.Colors{text.BgHiWhite, text.FgBlack}, - RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, - } - - // ColorOptionsBlackOnCyanWhite renders Black text on Cyan/White background. - ColorOptionsBlackOnCyanWhite = ColorOptions{ - Footer: text.Colors{text.BgCyan, text.FgBlack}, - Header: text.Colors{text.BgHiCyan, text.FgBlack}, - IndexColumn: text.Colors{text.BgHiCyan, text.FgBlack}, - Row: text.Colors{text.BgHiWhite, text.FgBlack}, - RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, - } - - // ColorOptionsBlackOnGreenWhite renders Black text on Green/White - // background. - ColorOptionsBlackOnGreenWhite = ColorOptions{ - Footer: text.Colors{text.BgGreen, text.FgBlack}, - Header: text.Colors{text.BgHiGreen, text.FgBlack}, - IndexColumn: text.Colors{text.BgHiGreen, text.FgBlack}, - Row: text.Colors{text.BgHiWhite, text.FgBlack}, - RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, - } - - // ColorOptionsBlackOnMagentaWhite renders Black text on Magenta/White - // background. - ColorOptionsBlackOnMagentaWhite = ColorOptions{ - Footer: text.Colors{text.BgMagenta, text.FgBlack}, - Header: text.Colors{text.BgHiMagenta, text.FgBlack}, - IndexColumn: text.Colors{text.BgHiMagenta, text.FgBlack}, - Row: text.Colors{text.BgHiWhite, text.FgBlack}, - RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, - } - - // ColorOptionsBlackOnRedWhite renders Black text on Red/White background. - ColorOptionsBlackOnRedWhite = ColorOptions{ - Footer: text.Colors{text.BgRed, text.FgBlack}, - Header: text.Colors{text.BgHiRed, text.FgBlack}, - IndexColumn: text.Colors{text.BgHiRed, text.FgBlack}, - Row: text.Colors{text.BgHiWhite, text.FgBlack}, - RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, - } - - // ColorOptionsBlackOnYellowWhite renders Black text on Yellow/White - // background. - ColorOptionsBlackOnYellowWhite = ColorOptions{ - Footer: text.Colors{text.BgYellow, text.FgBlack}, - Header: text.Colors{text.BgHiYellow, text.FgBlack}, - IndexColumn: text.Colors{text.BgHiYellow, text.FgBlack}, - Row: text.Colors{text.BgHiWhite, text.FgBlack}, - RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, - } - - // ColorOptionsBlueWhiteOnBlack renders Blue/White text on Black background. - ColorOptionsBlueWhiteOnBlack = ColorOptions{ - Footer: text.Colors{text.FgBlue, text.BgHiBlack}, - Header: text.Colors{text.FgHiBlue, text.BgHiBlack}, - IndexColumn: text.Colors{text.FgHiBlue, text.BgHiBlack}, - Row: text.Colors{text.FgHiWhite, text.BgBlack}, - RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, - } - - // ColorOptionsCyanWhiteOnBlack renders Cyan/White text on Black background. - ColorOptionsCyanWhiteOnBlack = ColorOptions{ - Footer: text.Colors{text.FgCyan, text.BgHiBlack}, - Header: text.Colors{text.FgHiCyan, text.BgHiBlack}, - IndexColumn: text.Colors{text.FgHiCyan, text.BgHiBlack}, - Row: text.Colors{text.FgHiWhite, text.BgBlack}, - RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, - } - - // ColorOptionsGreenWhiteOnBlack renders Green/White text on Black - // background. - ColorOptionsGreenWhiteOnBlack = ColorOptions{ - Footer: text.Colors{text.FgGreen, text.BgHiBlack}, - Header: text.Colors{text.FgHiGreen, text.BgHiBlack}, - IndexColumn: text.Colors{text.FgHiGreen, text.BgHiBlack}, - Row: text.Colors{text.FgHiWhite, text.BgBlack}, - RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, - } - - // ColorOptionsMagentaWhiteOnBlack renders Magenta/White text on Black - // background. - ColorOptionsMagentaWhiteOnBlack = ColorOptions{ - Footer: text.Colors{text.FgMagenta, text.BgHiBlack}, - Header: text.Colors{text.FgHiMagenta, text.BgHiBlack}, - IndexColumn: text.Colors{text.FgHiMagenta, text.BgHiBlack}, - Row: text.Colors{text.FgHiWhite, text.BgBlack}, - RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, - } - - // ColorOptionsRedWhiteOnBlack renders Red/White text on Black background. - ColorOptionsRedWhiteOnBlack = ColorOptions{ - Footer: text.Colors{text.FgRed, text.BgHiBlack}, - Header: text.Colors{text.FgHiRed, text.BgHiBlack}, - IndexColumn: text.Colors{text.FgHiRed, text.BgHiBlack}, - Row: text.Colors{text.FgHiWhite, text.BgBlack}, - RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, - } - - // ColorOptionsYellowWhiteOnBlack renders Yellow/White text on Black - // background. - ColorOptionsYellowWhiteOnBlack = ColorOptions{ - Footer: text.Colors{text.FgYellow, text.BgHiBlack}, - Header: text.Colors{text.FgHiYellow, text.BgHiBlack}, - IndexColumn: text.Colors{text.FgHiYellow, text.BgHiBlack}, - Row: text.Colors{text.FgHiWhite, text.BgBlack}, - RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, - } -) - -// FormatOptions defines the text-formatting to perform on parts of the Table. -type FormatOptions struct { - Direction text.Direction // (forced) BiDi direction for each Column - Footer text.Format // default text format - FooterAlign text.Align // default horizontal align - FooterVAlign text.VAlign // default vertical align - Header text.Format // default text format - HeaderAlign text.Align // default horizontal align - HeaderVAlign text.VAlign // default vertical align - Row text.Format // default text format - RowAlign text.Align // default horizontal align - RowVAlign text.VAlign // default vertical align -} - -// FormatOptionsDefault defines sensible formatting options. -var FormatOptionsDefault = FormatOptions{ - Footer: text.FormatUpper, - FooterAlign: text.AlignDefault, - FooterVAlign: text.VAlignDefault, - Header: text.FormatUpper, - HeaderAlign: text.AlignDefault, - HeaderVAlign: text.VAlignDefault, - Row: text.FormatDefault, - RowAlign: text.AlignDefault, - RowVAlign: text.VAlignDefault, -} - -// HTMLOptions defines the global options to control HTML rendering. -type HTMLOptions struct { - CSSClass string // CSS class to set on the overall tag - EmptyColumn string // string to replace "" columns with (entire content being "") - EscapeText bool // escape text into HTML-safe content? - Newline string // string to replace "\n" characters with -} - -// DefaultHTMLOptions defines sensible HTML rendering defaults. -var DefaultHTMLOptions = HTMLOptions{ - CSSClass: DefaultHTMLCSSClass, - EmptyColumn: " ", - EscapeText: true, - Newline: "
", -} - -// Options defines the global options that determine how the Table is -// rendered. -type Options struct { - // DoNotColorBordersAndSeparators disables coloring all the borders and row - // or column separators. - DoNotColorBordersAndSeparators bool - - // DrawBorder enables or disables drawing the border around the Table. - // Example of a table where it is disabled: - // # │ FIRST NAME │ LAST NAME │ SALARY │ - // ─────┼────────────┼───────────┼────────┼───────────────────────────── - // 1 │ Arya │ Stark │ 3000 │ - // 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! - // 300 │ Tyrion │ Lannister │ 5000 │ - // ─────┼────────────┼───────────┼────────┼───────────────────────────── - // │ │ TOTAL │ 10000 │ - DrawBorder bool - - // SeparateColumns enables or disable drawing border between columns. - // Example of a table where it is disabled: - // ┌─────────────────────────────────────────────────────────────────┐ - // │ # FIRST NAME LAST NAME SALARY │ - // ├─────────────────────────────────────────────────────────────────┤ - // │ 1 Arya Stark 3000 │ - // │ 20 Jon Snow 2000 You know nothing, Jon Snow! │ - // │ 300 Tyrion Lannister 5000 │ - // │ TOTAL 10000 │ - // └─────────────────────────────────────────────────────────────────┘ - SeparateColumns bool - - // SeparateFooter enables or disable drawing border between the footer and - // the rows. Example of a table where it is disabled: - // ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ - // │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ - // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ - // │ 1 │ Arya │ Stark │ 3000 │ │ - // │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ - // │ 300 │ Tyrion │ Lannister │ 5000 │ │ - // │ │ │ TOTAL │ 10000 │ │ - // └─────┴────────────┴───────────┴────────┴─────────────────────────────┘ - SeparateFooter bool - - // SeparateHeader enables or disable drawing border between the header and - // the rows. Example of a table where it is disabled: - // ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ - // │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ - // │ 1 │ Arya │ Stark │ 3000 │ │ - // │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ - // │ 300 │ Tyrion │ Lannister │ 5000 │ │ - // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ - // │ │ │ TOTAL │ 10000 │ │ - // └─────┴────────────┴───────────┴────────┴─────────────────────────────┘ - SeparateHeader bool - - // SeparateRows enables or disables drawing separators between each row. - // Example of a table where it is enabled: - // ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ - // │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ - // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ - // │ 1 │ Arya │ Stark │ 3000 │ │ - // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ - // │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ - // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ - // │ 300 │ Tyrion │ Lannister │ 5000 │ │ - // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ - // │ │ │ TOTAL │ 10000 │ │ - // └─────┴────────────┴───────────┴────────┴─────────────────────────────┘ - SeparateRows bool -} - -var ( - // OptionsDefault defines sensible global options. - OptionsDefault = Options{ - DrawBorder: true, - SeparateColumns: true, - SeparateFooter: true, - SeparateHeader: true, - SeparateRows: false, - } - - // OptionsNoBorders sets up a table without any borders. - OptionsNoBorders = Options{ - DrawBorder: false, - SeparateColumns: true, - SeparateFooter: true, - SeparateHeader: true, - SeparateRows: false, - } - - // OptionsNoBordersAndSeparators sets up a table without any borders or - // separators. - OptionsNoBordersAndSeparators = Options{ - DrawBorder: false, - SeparateColumns: false, - SeparateFooter: false, - SeparateHeader: false, - SeparateRows: false, - } -) - -// SizeOptions defines the way to control the width of the table output. -type SizeOptions struct { - // WidthMax is the maximum allotted width for the full row; - // any content beyond this will be truncated using the text - // in Style.Box.UnfinishedRow - WidthMax int - // WidthMin is the minimum allotted width for the full row; - // columns will be auto-expanded until the overall width - // is met - WidthMin int -} - -var ( - // SizeOptionsDefault defines sensible size options - basically NONE. - SizeOptionsDefault = SizeOptions{ - WidthMax: 0, - WidthMin: 0, - } -) - -// TitleOptions defines the way the title text is to be rendered. -type TitleOptions struct { - Align text.Align - Colors text.Colors - Format text.Format -} - -var ( - // TitleOptionsDefault defines sensible title options - basically NONE. - TitleOptionsDefault = TitleOptions{} - - // TitleOptionsBright renders Bright Bold text on Dark background. - TitleOptionsBright = TitleOptionsBlackOnCyan - - // TitleOptionsDark renders Dark Bold text on Bright background. - TitleOptionsDark = TitleOptionsCyanOnBlack - - // TitleOptionsBlackOnBlue renders Black text on Blue background. - TitleOptionsBlackOnBlue = TitleOptions{ - Colors: append(ColorOptionsBlackOnBlueWhite.Header, text.Bold), - } - - // TitleOptionsBlackOnCyan renders Black Bold text on Cyan background. - TitleOptionsBlackOnCyan = TitleOptions{ - Colors: append(ColorOptionsBlackOnCyanWhite.Header, text.Bold), - } - - // TitleOptionsBlackOnGreen renders Black Bold text onGreen background. - TitleOptionsBlackOnGreen = TitleOptions{ - Colors: append(ColorOptionsBlackOnGreenWhite.Header, text.Bold), - } - - // TitleOptionsBlackOnMagenta renders Black Bold text on Magenta background. - TitleOptionsBlackOnMagenta = TitleOptions{ - Colors: append(ColorOptionsBlackOnMagentaWhite.Header, text.Bold), - } - - // TitleOptionsBlackOnRed renders Black Bold text on Red background. - TitleOptionsBlackOnRed = TitleOptions{ - Colors: append(ColorOptionsBlackOnRedWhite.Header, text.Bold), - } - - // TitleOptionsBlackOnYellow renders Black Bold text on Yellow background. - TitleOptionsBlackOnYellow = TitleOptions{ - Colors: append(ColorOptionsBlackOnYellowWhite.Header, text.Bold), - } - - // TitleOptionsBlueOnBlack renders Blue Bold text on Black background. - TitleOptionsBlueOnBlack = TitleOptions{ - Colors: append(ColorOptionsBlueWhiteOnBlack.Header, text.Bold), - } - - // TitleOptionsCyanOnBlack renders Cyan Bold text on Black background. - TitleOptionsCyanOnBlack = TitleOptions{ - Colors: append(ColorOptionsCyanWhiteOnBlack.Header, text.Bold), - } - - // TitleOptionsGreenOnBlack renders Green Bold text on Black background. - TitleOptionsGreenOnBlack = TitleOptions{ - Colors: append(ColorOptionsGreenWhiteOnBlack.Header, text.Bold), - } - - // TitleOptionsMagentaOnBlack renders Magenta Bold text on Black background. - TitleOptionsMagentaOnBlack = TitleOptions{ - Colors: append(ColorOptionsMagentaWhiteOnBlack.Header, text.Bold), - } - - // TitleOptionsRedOnBlack renders Red Bold text on Black background. - TitleOptionsRedOnBlack = TitleOptions{ - Colors: append(ColorOptionsRedWhiteOnBlack.Header, text.Bold), - } - - // TitleOptionsYellowOnBlack renders Yellow Bold text on Black background. - TitleOptionsYellowOnBlack = TitleOptions{ - Colors: append(ColorOptionsYellowWhiteOnBlack.Header, text.Bold), - } -) diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/style_box.go b/vendor/github.com/jedib0t/go-pretty/v6/table/style_box.go new file mode 100644 index 00000000..e54f3c62 --- /dev/null +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/style_box.go @@ -0,0 +1,303 @@ +package table + +// BoxStyle defines the characters/strings to use to render the borders and +// separators for the Table. +type BoxStyle struct { + BottomLeft string + BottomRight string + BottomSeparator string + EmptySeparator string + Left string + LeftSeparator string + MiddleHorizontal string + MiddleSeparator string + MiddleVertical string + PaddingLeft string + PaddingRight string + PageSeparator string + Right string + RightSeparator string + TopLeft string + TopRight string + TopSeparator string + UnfinishedRow string + + // Horizontal lets you customize the horizontal lines for the Table + // in a more granular way than the MiddleHorizontal string. Setting + // this to a non-nil value will override MiddleHorizontal. + Horizontal *BoxStyleHorizontal +} + +// BoxStyleHorizontal defines the characters/strings to use to render the +// horizontal lines for the Table. +type BoxStyleHorizontal struct { + TitleTop string + TitleBottom string // overrides HeaderTop/RowTop + HeaderTop string + HeaderMiddle string + HeaderBottom string // overrides RowTop + RowTop string + RowMiddle string + RowBottom string + FooterTop string // overrides RowBottom + FooterMiddle string + FooterBottom string +} + +// NewBoxStyleHorizontal creates a new BoxStyleHorizontal with the given +// horizontal string. +func NewBoxStyleHorizontal(horizontal string) *BoxStyleHorizontal { + return &BoxStyleHorizontal{ + TitleTop: horizontal, + TitleBottom: horizontal, + HeaderTop: horizontal, + HeaderMiddle: horizontal, + HeaderBottom: horizontal, + RowTop: horizontal, + RowMiddle: horizontal, + RowBottom: horizontal, + FooterTop: horizontal, + FooterMiddle: horizontal, + FooterBottom: horizontal, + } +} + +var ( + // StyleBoxDefault defines a Boxed-Table like below: + // +-----+------------+-----------+--------+-----------------------------+ + // | # | FIRST NAME | LAST NAME | SALARY | | + // +-----+------------+-----------+--------+-----------------------------+ + // | 1 | Arya | Stark | 3000 | | + // | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | + // | 300 | Tyrion | Lannister | 5000 | | + // +-----+------------+-----------+--------+-----------------------------+ + // | | | TOTAL | 10000 | | + // +-----+------------+-----------+--------+-----------------------------+ + StyleBoxDefault = BoxStyle{ + BottomLeft: "+", + BottomRight: "+", + BottomSeparator: "+", + EmptySeparator: " ", + Left: "|", + LeftSeparator: "+", + MiddleHorizontal: "-", + MiddleSeparator: "+", + MiddleVertical: "|", + PaddingLeft: " ", + PaddingRight: " ", + PageSeparator: "\n", + Right: "|", + RightSeparator: "+", + TopLeft: "+", + TopRight: "+", + TopSeparator: "+", + UnfinishedRow: " ~", + } + + // StyleBoxBold defines a Boxed-Table like below: + // ┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ┃ + // ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ + // ┃ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ ┃ + // ┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! ┃ + // ┃ 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ┃ + // ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ + // ┃ ┃ ┃ TOTAL ┃ 10000 ┃ ┃ + // ┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + StyleBoxBold = BoxStyle{ + BottomLeft: "┗", + BottomRight: "┛", + BottomSeparator: "┻", + EmptySeparator: " ", + Left: "┃", + LeftSeparator: "┣", + MiddleHorizontal: "━", + MiddleSeparator: "╋", + MiddleVertical: "┃", + PaddingLeft: " ", + PaddingRight: " ", + PageSeparator: "\n", + Right: "┃", + RightSeparator: "┫", + TopLeft: "┏", + TopRight: "┓", + TopSeparator: "┳", + UnfinishedRow: " ≈", + } + + // StyleBoxDouble defines a Boxed-Table like below: + // ╔═════╦════════════╦═══════════╦════════╦═════════════════════════════╗ + // ║ # ║ FIRST NAME ║ LAST NAME ║ SALARY ║ ║ + // ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣ + // ║ 1 ║ Arya ║ Stark ║ 3000 ║ ║ + // ║ 20 ║ Jon ║ Snow ║ 2000 ║ You know nothing, Jon Snow! ║ + // ║ 300 ║ Tyrion ║ Lannister ║ 5000 ║ ║ + // ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣ + // ║ ║ ║ TOTAL ║ 10000 ║ ║ + // ╚═════╩════════════╩═══════════╩════════╩═════════════════════════════╝ + StyleBoxDouble = BoxStyle{ + BottomLeft: "╚", + BottomRight: "╝", + BottomSeparator: "╩", + EmptySeparator: " ", + Left: "║", + LeftSeparator: "╠", + MiddleHorizontal: "═", + MiddleSeparator: "╬", + MiddleVertical: "║", + PaddingLeft: " ", + PaddingRight: " ", + PageSeparator: "\n", + Right: "║", + RightSeparator: "╣", + TopLeft: "╔", + TopRight: "╗", + TopSeparator: "╦", + UnfinishedRow: " ≈", + } + + // StyleBoxLight defines a Boxed-Table like below: + // ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ + // │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ + // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ + // │ 1 │ Arya │ Stark │ 3000 │ │ + // │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ + // │ 300 │ Tyrion │ Lannister │ 5000 │ │ + // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ + // │ │ │ TOTAL │ 10000 │ │ + // └─────┴────────────┴───────────┴────────┴─────────────────────────────┘ + StyleBoxLight = BoxStyle{ + BottomLeft: "└", + BottomRight: "┘", + BottomSeparator: "┴", + EmptySeparator: " ", + Left: "│", + LeftSeparator: "├", + MiddleHorizontal: "─", + MiddleSeparator: "┼", + MiddleVertical: "│", + PaddingLeft: " ", + PaddingRight: " ", + PageSeparator: "\n", + Right: "│", + RightSeparator: "┤", + TopLeft: "┌", + TopRight: "┐", + TopSeparator: "┬", + UnfinishedRow: " ≈", + } + + // StyleBoxRounded defines a Boxed-Table like below: + // ╭─────┬────────────┬───────────┬────────┬─────────────────────────────╮ + // │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ + // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ + // │ 1 │ Arya │ Stark │ 3000 │ │ + // │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ + // │ 300 │ Tyrion │ Lannister │ 5000 │ │ + // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ + // │ │ │ TOTAL │ 10000 │ │ + // ╰─────┴────────────┴───────────┴────────┴─────────────────────────────╯ + StyleBoxRounded = BoxStyle{ + BottomLeft: "╰", + BottomRight: "╯", + BottomSeparator: "┴", + EmptySeparator: " ", + Left: "│", + LeftSeparator: "├", + MiddleHorizontal: "─", + MiddleSeparator: "┼", + MiddleVertical: "│", + PaddingLeft: " ", + PaddingRight: " ", + PageSeparator: "\n", + Right: "│", + RightSeparator: "┤", + TopLeft: "╭", + TopRight: "╮", + TopSeparator: "┬", + UnfinishedRow: " ≈", + } + + // styleBoxTest defines a Boxed-Table like below: + // (-----^------------^-----------^--------^-----------------------------) + // [< #>||||< >] + // {-----+------------+-----------+--------+-----------------------------} + // [< 1>|||< 3000>|< >] + // [< 20>|||< 2000>|] + // [<300>|||< 5000>|< >] + // {-----+------------+-----------+--------+-----------------------------} + // [< >|< >||< 10000>|< >] + // \-----v------------v-----------v--------v-----------------------------/ + styleBoxTest = BoxStyle{ + BottomLeft: "\\", + BottomRight: "/", + BottomSeparator: "v", + EmptySeparator: " ", + Left: "[", + LeftSeparator: "{", + MiddleHorizontal: "--", + MiddleSeparator: "+", + MiddleVertical: "|", + PaddingLeft: "<", + PaddingRight: ">", + PageSeparator: "\n", + Right: "]", + RightSeparator: "}", + TopLeft: "(", + TopRight: ")", + TopSeparator: "^", + UnfinishedRow: " ~~~", + } +) + +type separatorType int + +const ( + separatorTypeTitleTop separatorType = iota + separatorTypeTitleBottom + separatorTypeHeaderTop + separatorTypeHeaderMiddle + separatorTypeHeaderBottom + separatorTypeRowTop + separatorTypeRowMiddle + separatorTypeRowBottom + separatorTypeFooterTop + separatorTypeFooterMiddle + separatorTypeFooterBottom + separatorTypeCount // this should be the last value +) + +func (bs *BoxStyle) ensureHorizontalInitialized() { + if bs.Horizontal == nil { + bs.Horizontal = NewBoxStyleHorizontal(bs.MiddleHorizontal) + } +} + +func (bs *BoxStyle) middleHorizontal(st separatorType) string { + bs.ensureHorizontalInitialized() + + switch st { + case separatorTypeTitleTop: + return bs.Horizontal.TitleTop + case separatorTypeTitleBottom: + return bs.Horizontal.TitleBottom + case separatorTypeHeaderTop: + return bs.Horizontal.HeaderTop + case separatorTypeHeaderMiddle: + return bs.Horizontal.HeaderMiddle + case separatorTypeHeaderBottom: + return bs.Horizontal.HeaderBottom + case separatorTypeRowTop: + return bs.Horizontal.RowTop + case separatorTypeRowBottom: + return bs.Horizontal.RowBottom + case separatorTypeFooterTop: + return bs.Horizontal.FooterTop + case separatorTypeFooterMiddle: + return bs.Horizontal.FooterMiddle + case separatorTypeFooterBottom: + return bs.Horizontal.FooterBottom + default: + return bs.Horizontal.RowMiddle + } +} diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/style_color.go b/vendor/github.com/jedib0t/go-pretty/v6/table/style_color.go new file mode 100644 index 00000000..9f3ed2f2 --- /dev/null +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/style_color.go @@ -0,0 +1,139 @@ +package table + +import "github.com/jedib0t/go-pretty/v6/text" + +// ColorOptions defines the ANSI colors to use for parts of the Table. +type ColorOptions struct { + Border text.Colors // borders (if nil, uses one of the below) + Footer text.Colors // footer row(s) colors + Header text.Colors // header row(s) colors + IndexColumn text.Colors // index-column colors (row #, etc.) + Row text.Colors // regular row(s) colors + RowAlternate text.Colors // regular row(s) colors for the even-numbered rows + Separator text.Colors // separators (if nil, uses one of the above) +} + +var ( + // ColorOptionsDefault defines sensible ANSI color options - basically NONE. + ColorOptionsDefault = ColorOptions{} + + // ColorOptionsBright renders dark text on bright background. + ColorOptionsBright = ColorOptionsBlackOnCyanWhite + + // ColorOptionsDark renders bright text on dark background. + ColorOptionsDark = ColorOptionsCyanWhiteOnBlack + + // ColorOptionsBlackOnBlueWhite renders Black text on Blue/White background. + ColorOptionsBlackOnBlueWhite = ColorOptions{ + Footer: text.Colors{text.BgBlue, text.FgBlack}, + Header: text.Colors{text.BgHiBlue, text.FgBlack}, + IndexColumn: text.Colors{text.BgHiBlue, text.FgBlack}, + Row: text.Colors{text.BgHiWhite, text.FgBlack}, + RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, + } + + // ColorOptionsBlackOnCyanWhite renders Black text on Cyan/White background. + ColorOptionsBlackOnCyanWhite = ColorOptions{ + Footer: text.Colors{text.BgCyan, text.FgBlack}, + Header: text.Colors{text.BgHiCyan, text.FgBlack}, + IndexColumn: text.Colors{text.BgHiCyan, text.FgBlack}, + Row: text.Colors{text.BgHiWhite, text.FgBlack}, + RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, + } + + // ColorOptionsBlackOnGreenWhite renders Black text on Green/White + // background. + ColorOptionsBlackOnGreenWhite = ColorOptions{ + Footer: text.Colors{text.BgGreen, text.FgBlack}, + Header: text.Colors{text.BgHiGreen, text.FgBlack}, + IndexColumn: text.Colors{text.BgHiGreen, text.FgBlack}, + Row: text.Colors{text.BgHiWhite, text.FgBlack}, + RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, + } + + // ColorOptionsBlackOnMagentaWhite renders Black text on Magenta/White + // background. + ColorOptionsBlackOnMagentaWhite = ColorOptions{ + Footer: text.Colors{text.BgMagenta, text.FgBlack}, + Header: text.Colors{text.BgHiMagenta, text.FgBlack}, + IndexColumn: text.Colors{text.BgHiMagenta, text.FgBlack}, + Row: text.Colors{text.BgHiWhite, text.FgBlack}, + RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, + } + + // ColorOptionsBlackOnRedWhite renders Black text on Red/White background. + ColorOptionsBlackOnRedWhite = ColorOptions{ + Footer: text.Colors{text.BgRed, text.FgBlack}, + Header: text.Colors{text.BgHiRed, text.FgBlack}, + IndexColumn: text.Colors{text.BgHiRed, text.FgBlack}, + Row: text.Colors{text.BgHiWhite, text.FgBlack}, + RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, + } + + // ColorOptionsBlackOnYellowWhite renders Black text on Yellow/White + // background. + ColorOptionsBlackOnYellowWhite = ColorOptions{ + Footer: text.Colors{text.BgYellow, text.FgBlack}, + Header: text.Colors{text.BgHiYellow, text.FgBlack}, + IndexColumn: text.Colors{text.BgHiYellow, text.FgBlack}, + Row: text.Colors{text.BgHiWhite, text.FgBlack}, + RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, + } + + // ColorOptionsBlueWhiteOnBlack renders Blue/White text on Black background. + ColorOptionsBlueWhiteOnBlack = ColorOptions{ + Footer: text.Colors{text.FgBlue, text.BgHiBlack}, + Header: text.Colors{text.FgHiBlue, text.BgHiBlack}, + IndexColumn: text.Colors{text.FgHiBlue, text.BgHiBlack}, + Row: text.Colors{text.FgHiWhite, text.BgBlack}, + RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, + } + + // ColorOptionsCyanWhiteOnBlack renders Cyan/White text on Black background. + ColorOptionsCyanWhiteOnBlack = ColorOptions{ + Footer: text.Colors{text.FgCyan, text.BgHiBlack}, + Header: text.Colors{text.FgHiCyan, text.BgHiBlack}, + IndexColumn: text.Colors{text.FgHiCyan, text.BgHiBlack}, + Row: text.Colors{text.FgHiWhite, text.BgBlack}, + RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, + } + + // ColorOptionsGreenWhiteOnBlack renders Green/White text on Black + // background. + ColorOptionsGreenWhiteOnBlack = ColorOptions{ + Footer: text.Colors{text.FgGreen, text.BgHiBlack}, + Header: text.Colors{text.FgHiGreen, text.BgHiBlack}, + IndexColumn: text.Colors{text.FgHiGreen, text.BgHiBlack}, + Row: text.Colors{text.FgHiWhite, text.BgBlack}, + RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, + } + + // ColorOptionsMagentaWhiteOnBlack renders Magenta/White text on Black + // background. + ColorOptionsMagentaWhiteOnBlack = ColorOptions{ + Footer: text.Colors{text.FgMagenta, text.BgHiBlack}, + Header: text.Colors{text.FgHiMagenta, text.BgHiBlack}, + IndexColumn: text.Colors{text.FgHiMagenta, text.BgHiBlack}, + Row: text.Colors{text.FgHiWhite, text.BgBlack}, + RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, + } + + // ColorOptionsRedWhiteOnBlack renders Red/White text on Black background. + ColorOptionsRedWhiteOnBlack = ColorOptions{ + Footer: text.Colors{text.FgRed, text.BgHiBlack}, + Header: text.Colors{text.FgHiRed, text.BgHiBlack}, + IndexColumn: text.Colors{text.FgHiRed, text.BgHiBlack}, + Row: text.Colors{text.FgHiWhite, text.BgBlack}, + RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, + } + + // ColorOptionsYellowWhiteOnBlack renders Yellow/White text on Black + // background. + ColorOptionsYellowWhiteOnBlack = ColorOptions{ + Footer: text.Colors{text.FgYellow, text.BgHiBlack}, + Header: text.Colors{text.FgHiYellow, text.BgHiBlack}, + IndexColumn: text.Colors{text.FgHiYellow, text.BgHiBlack}, + Row: text.Colors{text.FgHiWhite, text.BgBlack}, + RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, + } +) diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/style_format.go b/vendor/github.com/jedib0t/go-pretty/v6/table/style_format.go new file mode 100644 index 00000000..aa3ba08a --- /dev/null +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/style_format.go @@ -0,0 +1,32 @@ +package table + +import "github.com/jedib0t/go-pretty/v6/text" + +// FormatOptions defines the text-formatting to perform on parts of the Table. +type FormatOptions struct { + Direction text.Direction // (forced) BiDi direction for each Column + Footer text.Format // default text format + FooterAlign text.Align // default horizontal align + FooterVAlign text.VAlign // default vertical align + Header text.Format // default text format + HeaderAlign text.Align // default horizontal align + HeaderVAlign text.VAlign // default vertical align + Row text.Format // default text format + RowAlign text.Align // default horizontal align + RowVAlign text.VAlign // default vertical align +} + +var ( + // FormatOptionsDefault defines sensible formatting options. + FormatOptionsDefault = FormatOptions{ + Footer: text.FormatUpper, + FooterAlign: text.AlignDefault, + FooterVAlign: text.VAlignDefault, + Header: text.FormatUpper, + HeaderAlign: text.AlignDefault, + HeaderVAlign: text.VAlignDefault, + Row: text.FormatDefault, + RowAlign: text.AlignDefault, + RowVAlign: text.VAlignDefault, + } +) diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/style_html.go b/vendor/github.com/jedib0t/go-pretty/v6/table/style_html.go new file mode 100644 index 00000000..a91fc73a --- /dev/null +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/style_html.go @@ -0,0 +1,21 @@ +package table + +// HTMLOptions defines the global options to control HTML rendering. +type HTMLOptions struct { + ConvertColorsToSpans bool // convert ANSI escape sequences to HTML tags with CSS classes? EscapeText will be true if this is true. + CSSClass string // CSS class to set on the overall
tag + EmptyColumn string // string to replace "" columns with (entire content being "") + EscapeText bool // escape text into HTML-safe content? + Newline string // string to replace "\n" characters with +} + +var ( + // DefaultHTMLOptions defines sensible HTML rendering defaults. + DefaultHTMLOptions = HTMLOptions{ + ConvertColorsToSpans: true, + CSSClass: DefaultHTMLCSSClass, + EmptyColumn: " ", + EscapeText: true, + Newline: "
", + } +) diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/style_options.go b/vendor/github.com/jedib0t/go-pretty/v6/table/style_options.go new file mode 100644 index 00000000..7e7a27c8 --- /dev/null +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/style_options.go @@ -0,0 +1,109 @@ +package table + +// Options defines the global options that determine how the Table is +// rendered. +type Options struct { + // DoNotColorBordersAndSeparators disables coloring all the borders and row + // or column separators. + DoNotColorBordersAndSeparators bool + + // DoNotRenderSeparatorWhenEmpty disables rendering the separator row after + // headers when there are no data rows (for example when only headers and/or + // footers are present). + DoNotRenderSeparatorWhenEmpty bool + + // DrawBorder enables or disables drawing the border around the Table. + // Example of a table where it is disabled: + // # │ FIRST NAME │ LAST NAME │ SALARY │ + // ─────┼────────────┼───────────┼────────┼───────────────────────────── + // 1 │ Arya │ Stark │ 3000 │ + // 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! + // 300 │ Tyrion │ Lannister │ 5000 │ + // ─────┼────────────┼───────────┼────────┼───────────────────────────── + // │ │ TOTAL │ 10000 │ + DrawBorder bool + + // SeparateColumns enables or disable drawing border between columns. + // Example of a table where it is disabled: + // ┌─────────────────────────────────────────────────────────────────┐ + // │ # FIRST NAME LAST NAME SALARY │ + // ├─────────────────────────────────────────────────────────────────┤ + // │ 1 Arya Stark 3000 │ + // │ 20 Jon Snow 2000 You know nothing, Jon Snow! │ + // │ 300 Tyrion Lannister 5000 │ + // │ TOTAL 10000 │ + // └─────────────────────────────────────────────────────────────────┘ + SeparateColumns bool + + // SeparateFooter enables or disable drawing border between the footer and + // the rows. Example of a table where it is disabled: + // ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ + // │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ + // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ + // │ 1 │ Arya │ Stark │ 3000 │ │ + // │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ + // │ 300 │ Tyrion │ Lannister │ 5000 │ │ + // │ │ │ TOTAL │ 10000 │ │ + // └─────┴────────────┴───────────┴────────┴─────────────────────────────┘ + SeparateFooter bool + + // SeparateHeader enables or disable drawing border between the header and + // the rows. Example of a table where it is disabled: + // ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ + // │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ + // │ 1 │ Arya │ Stark │ 3000 │ │ + // │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ + // │ 300 │ Tyrion │ Lannister │ 5000 │ │ + // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ + // │ │ │ TOTAL │ 10000 │ │ + // └─────┴────────────┴───────────┴────────┴─────────────────────────────┘ + SeparateHeader bool + + // SeparateRows enables or disables drawing separators between each row. + // Example of a table where it is enabled: + // ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ + // │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ + // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ + // │ 1 │ Arya │ Stark │ 3000 │ │ + // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ + // │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ + // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ + // │ 300 │ Tyrion │ Lannister │ 5000 │ │ + // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ + // │ │ │ TOTAL │ 10000 │ │ + // └─────┴────────────┴───────────┴────────┴─────────────────────────────┘ + SeparateRows bool +} + +var ( + // OptionsDefault defines sensible global options. + OptionsDefault = Options{ + DoNotColorBordersAndSeparators: false, + DrawBorder: true, + SeparateColumns: true, + SeparateFooter: true, + SeparateHeader: true, + SeparateRows: false, + } + + // OptionsNoBorders sets up a table without any borders. + OptionsNoBorders = Options{ + DoNotColorBordersAndSeparators: false, + DrawBorder: false, + SeparateColumns: true, + SeparateFooter: true, + SeparateHeader: true, + SeparateRows: false, + } + + // OptionsNoBordersAndSeparators sets up a table without any borders or + // separators. + OptionsNoBordersAndSeparators = Options{ + DoNotColorBordersAndSeparators: false, + DrawBorder: false, + SeparateColumns: false, + SeparateFooter: false, + SeparateHeader: false, + SeparateRows: false, + } +) diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/style_size.go b/vendor/github.com/jedib0t/go-pretty/v6/table/style_size.go new file mode 100644 index 00000000..b3806948 --- /dev/null +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/style_size.go @@ -0,0 +1,21 @@ +package table + +// SizeOptions defines the way to control the width of the table output. +type SizeOptions struct { + // WidthMax is the maximum allotted width for the full row; + // any content beyond this will be truncated using the text + // in Style.Box.UnfinishedRow + WidthMax int + // WidthMin is the minimum allotted width for the full row; + // columns will be auto-expanded until the overall width + // is met + WidthMin int +} + +var ( + // SizeOptionsDefault defines sensible size options - basically NONE. + SizeOptionsDefault = SizeOptions{ + WidthMax: 0, + WidthMin: 0, + } +) diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/styles_title.go b/vendor/github.com/jedib0t/go-pretty/v6/table/styles_title.go new file mode 100644 index 00000000..1a22a872 --- /dev/null +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/styles_title.go @@ -0,0 +1,81 @@ +package table + +import "github.com/jedib0t/go-pretty/v6/text" + +// TitleOptions defines the way the title text is to be rendered. +type TitleOptions struct { + Align text.Align + Colors text.Colors + Format text.Format +} + +var ( + // TitleOptionsDefault defines sensible title options - basically NONE. + TitleOptionsDefault = TitleOptions{} + + // TitleOptionsBright renders Bright Bold text on Dark background. + TitleOptionsBright = TitleOptionsBlackOnCyan + + // TitleOptionsDark renders Dark Bold text on Bright background. + TitleOptionsDark = TitleOptionsCyanOnBlack + + // TitleOptionsBlackOnBlue renders Black text on Blue background. + TitleOptionsBlackOnBlue = TitleOptions{ + Colors: append(ColorOptionsBlackOnBlueWhite.Header, text.Bold), + } + + // TitleOptionsBlackOnCyan renders Black Bold text on Cyan background. + TitleOptionsBlackOnCyan = TitleOptions{ + Colors: append(ColorOptionsBlackOnCyanWhite.Header, text.Bold), + } + + // TitleOptionsBlackOnGreen renders Black Bold text onGreen background. + TitleOptionsBlackOnGreen = TitleOptions{ + Colors: append(ColorOptionsBlackOnGreenWhite.Header, text.Bold), + } + + // TitleOptionsBlackOnMagenta renders Black Bold text on Magenta background. + TitleOptionsBlackOnMagenta = TitleOptions{ + Colors: append(ColorOptionsBlackOnMagentaWhite.Header, text.Bold), + } + + // TitleOptionsBlackOnRed renders Black Bold text on Red background. + TitleOptionsBlackOnRed = TitleOptions{ + Colors: append(ColorOptionsBlackOnRedWhite.Header, text.Bold), + } + + // TitleOptionsBlackOnYellow renders Black Bold text on Yellow background. + TitleOptionsBlackOnYellow = TitleOptions{ + Colors: append(ColorOptionsBlackOnYellowWhite.Header, text.Bold), + } + + // TitleOptionsBlueOnBlack renders Blue Bold text on Black background. + TitleOptionsBlueOnBlack = TitleOptions{ + Colors: append(ColorOptionsBlueWhiteOnBlack.Header, text.Bold), + } + + // TitleOptionsCyanOnBlack renders Cyan Bold text on Black background. + TitleOptionsCyanOnBlack = TitleOptions{ + Colors: append(ColorOptionsCyanWhiteOnBlack.Header, text.Bold), + } + + // TitleOptionsGreenOnBlack renders Green Bold text on Black background. + TitleOptionsGreenOnBlack = TitleOptions{ + Colors: append(ColorOptionsGreenWhiteOnBlack.Header, text.Bold), + } + + // TitleOptionsMagentaOnBlack renders Magenta Bold text on Black background. + TitleOptionsMagentaOnBlack = TitleOptions{ + Colors: append(ColorOptionsMagentaWhiteOnBlack.Header, text.Bold), + } + + // TitleOptionsRedOnBlack renders Red Bold text on Black background. + TitleOptionsRedOnBlack = TitleOptions{ + Colors: append(ColorOptionsRedWhiteOnBlack.Header, text.Bold), + } + + // TitleOptionsYellowOnBlack renders Yellow Bold text on Black background. + TitleOptionsYellowOnBlack = TitleOptions{ + Colors: append(ColorOptionsYellowWhiteOnBlack.Header, text.Bold), + } +) diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/table.go b/vendor/github.com/jedib0t/go-pretty/v6/table/table.go index 191dd771..02d40926 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/table/table.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/table.go @@ -28,6 +28,8 @@ type Table struct { // columnConfigMap stores the custom-configuration by column // number and is generated before rendering columnConfigMap map[int]ColumnConfig + // directionModifier caches the direction modifier string to avoid repeated calls + directionModifier string // firstRowOfPage tells if the renderer is on the first row of a page? firstRowOfPage bool // htmlCSSClass stores the HTML CSS Class to use on the
node @@ -50,6 +52,8 @@ type Table struct { outputMirror io.Writer // pager controls how the output is separated into pages pager pager + // renderMode contains the type of table to render + renderMode renderMode // rows stores the rows that make up the body (in string form) rows []rowStr // rowsColors stores the text.Colors over-rides for each row as defined by @@ -59,6 +63,8 @@ type Table struct { rowsConfigMap map[int]RowConfig // rowsRaw stores the rows that make up the body rowsRaw []Row + // rowsRawFiltered is the filtered version of rowsRaw + rowsRawFiltered []Row // rowsFooter stores the rows that make up the footer (in string form) rowsFooter []rowStr // rowsFooterConfigs stores RowConfig for each footer row @@ -76,9 +82,11 @@ type Table struct { rowPainter RowPainter // rowPainterWithAttributes is same as rowPainter, but with attributes rowPainterWithAttributes RowPainterWithAttributes - // rowSeparator is a dummy row that contains the separator columns (dashes - // that make up the separator between header/body/footer - rowSeparator rowStr + // rowSeparators contains the separator columns (dashes that make up the + // separators between title/header/body/footer + rowSeparators map[string]rowStr + // rowSeparatorStrings contains the separator strings for each separator type + rowSeparatorStrings map[separatorType]string // separators is used to keep track of all rowIndices after which a // separator has to be rendered separators map[int]bool @@ -86,6 +94,8 @@ type Table struct { sortBy []SortBy // sortedRowIndices is the output of sorting sortedRowIndices []int + // filterBy stores the filter criteria + filterBy []FilterBy // style contains all the strings used to draw the table, and more style *Style // suppressEmptyColumns hides columns which have no content on all regular @@ -127,12 +137,16 @@ func (t *Table) AppendHeader(row Row, config ...RowConfig) { // // Only the first item in the "config" will be tagged against this row. func (t *Table) AppendRow(row Row, config ...RowConfig) { - t.rowsRaw = append(t.rowsRaw, row) + t.rowsRawFiltered = append(t.rowsRawFiltered, row) + // Keep original rows in sync for filtering + rowCopy := make(Row, len(row)) + copy(rowCopy, row) + t.rowsRaw = append(t.rowsRaw, rowCopy) if len(config) > 0 { if t.rowsConfigMap == nil { t.rowsConfigMap = make(map[int]RowConfig) } - t.rowsConfigMap[len(t.rowsRaw)-1] = config[0] + t.rowsConfigMap[len(t.rowsRawFiltered)-1] = config[0] } } @@ -164,11 +178,17 @@ func (t *Table) AppendSeparator() { if t.separators == nil { t.separators = make(map[int]bool) } - if len(t.rowsRaw) > 0 { - t.separators[len(t.rowsRaw)-1] = true + if len(t.rowsRawFiltered) > 0 { + t.separators[len(t.rowsRawFiltered)-1] = true } } +// FilterBy sets the rules for filtering the Rows. All filters are applied with +// AND logic (all must match). Filters are applied before sorting. +func (t *Table) FilterBy(filterBy []FilterBy) { + t.filterBy = filterBy +} + // ImportGrid helps import 1d or 2d arrays as rows. func (t *Table) ImportGrid(grid interface{}) bool { rows := objAsSlice(grid) @@ -190,7 +210,7 @@ func (t *Table) ImportGrid(grid interface{}) bool { // Length returns the number of rows to be rendered. func (t *Table) Length() int { - return len(t.rowsRaw) + return len(t.rowsRawFiltered) } // Pager returns an object that splits the table output into pages and @@ -231,6 +251,7 @@ func (t *Table) ResetHeaders() { // ResetRows resets and clears all the rows appended earlier. func (t *Table) ResetRows() { + t.rowsRawFiltered = nil t.rowsRaw = nil t.separators = nil } @@ -305,12 +326,12 @@ func (t *Table) SetRowPainter(painter interface{}) { t.rowPainterWithAttributes = nil // if called as SetRowPainter(RowPainter(func...)) - switch painter.(type) { + switch p := painter.(type) { case RowPainter: - t.rowPainter = painter.(RowPainter) + t.rowPainter = p return case RowPainterWithAttributes: - t.rowPainterWithAttributes = painter.(RowPainterWithAttributes) + t.rowPainterWithAttributes = p return } @@ -367,6 +388,31 @@ func (t *Table) SuppressTrailingSpaces() { t.suppressTrailingSpaces = true } +// calculateNumColumnsFromRaw calculates the number of columns from raw rows and headers +func (t *Table) calculateNumColumnsFromRaw() { + t.numColumns = 0 + // Check headers first + if len(t.rowsHeaderRaw) > 0 { + for _, headerRow := range t.rowsHeaderRaw { + if len(headerRow) > t.numColumns { + t.numColumns = len(headerRow) + } + } + } + // Check data rows + for _, row := range t.rowsRawFiltered { + if len(row) > t.numColumns { + t.numColumns = len(row) + } + } + // Check footer rows + for _, footerRow := range t.rowsFooterRaw { + if len(footerRow) > t.numColumns { + t.numColumns = len(footerRow) + } + } +} + func (t *Table) getAlign(colIdx int, hint renderHint) text.Align { align := text.AlignDefault if cfg, ok := t.columnConfigMap[colIdx]; ok { @@ -505,13 +551,13 @@ func (t *Table) getColumnSeparator(row rowStr, colIdx int, hint renderHint) stri if hint.isSeparatorRow { if hint.isBorderTop { if t.shouldMergeCellsHorizontallyBelow(row, colIdx, hint) { - separator = t.style.Box.MiddleHorizontal + separator = t.style.Box.middleHorizontal(hint.separatorType) } else { separator = t.style.Box.TopSeparator } } else if hint.isBorderBottom { if t.shouldMergeCellsHorizontallyAbove(row, colIdx, hint) { - separator = t.style.Box.MiddleHorizontal + separator = t.style.Box.middleHorizontal(hint.separatorType) } else { separator = t.style.Box.BottomSeparator } @@ -531,7 +577,7 @@ func (t *Table) getColumnSeparatorNonBorder(mergeCellsAbove bool, mergeCellsBelo } mergeCurrCol := t.shouldMergeCellsVerticallyAbove(colIdx-1, hint) - return t.getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove, mergeCellsBelow, mergeCurrCol, mergeNextCol) + return t.getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove, mergeCellsBelow, mergeCurrCol, mergeNextCol, hint) } func (t *Table) getColumnSeparatorNonBorderAutoIndex(mergeNextCol bool, hint renderHint) string { @@ -546,11 +592,11 @@ func (t *Table) getColumnSeparatorNonBorderAutoIndex(mergeNextCol bool, hint ren return t.style.Box.MiddleSeparator } -func (t *Table) getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove bool, mergeCellsBelow bool, mergeCurrCol bool, mergeNextCol bool) string { +func (t *Table) getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove bool, mergeCellsBelow bool, mergeCurrCol bool, mergeNextCol bool, hint renderHint) string { if mergeCellsAbove && mergeCellsBelow && mergeCurrCol && mergeNextCol { return t.style.Box.EmptySeparator } else if mergeCellsAbove && mergeCellsBelow { - return t.style.Box.MiddleHorizontal + return t.style.Box.middleHorizontal(hint.separatorType) } else if mergeCellsAbove { return t.style.Box.TopSeparator } else if mergeCellsBelow { diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/util.go b/vendor/github.com/jedib0t/go-pretty/v6/table/util.go index 4636e881..b4179a20 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/table/util.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/util.go @@ -1,8 +1,10 @@ package table import ( + "fmt" "reflect" "sort" + "strconv" ) // AutoIndexColumnID returns a unique Column ID/Name for the given Column Number. @@ -26,6 +28,48 @@ func widthEnforcerNone(col string, _ int) string { return col } +// convertValueToString converts a value to string using fast type assertions +// for common numeric types before falling back to fmt.Sprint. +// +//gocyclo:ignore +func convertValueToString(v interface{}) string { + switch val := v.(type) { + case int: + return strconv.FormatInt(int64(val), 10) + case int8: + return strconv.FormatInt(int64(val), 10) + case int16: + return strconv.FormatInt(int64(val), 10) + case int32: + return strconv.FormatInt(int64(val), 10) + case int64: + return strconv.FormatInt(val, 10) + case uint: + return strconv.FormatUint(uint64(val), 10) + case uint8: + return strconv.FormatUint(uint64(val), 10) + case uint16: + return strconv.FormatUint(uint64(val), 10) + case uint32: + return strconv.FormatUint(uint64(val), 10) + case uint64: + return strconv.FormatUint(val, 10) + case float32: + return strconv.FormatFloat(float64(val), 'g', -1, 32) + case float64: + return strconv.FormatFloat(val, 'g', -1, 64) + case bool: + if val { + return "true" + } + return "false" + case string: + return val + default: + return fmt.Sprint(v) + } +} + // isNumber returns true if the argument is a numeric type; false otherwise. func isNumber(x interface{}) bool { if x == nil { diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/util_html.go b/vendor/github.com/jedib0t/go-pretty/v6/table/util_html.go new file mode 100644 index 00000000..97e2c521 --- /dev/null +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/util_html.go @@ -0,0 +1,151 @@ +package table + +import ( + "strings" + + "github.com/jedib0t/go-pretty/v6/text" +) + +// convertEscSequencesToSpans converts ANSI escape sequences to HTML tags with CSS classes. +func convertEscSequencesToSpans(str string) string { + converter := newEscSeqToSpanConverter() + return converter.Convert(str) +} + +// escSeqToSpanConverter converts ANSI escape sequences to HTML tags with CSS classes. +type escSeqToSpanConverter struct { + result strings.Builder + esp text.EscSeqParser + currentColors map[int]bool +} + +// newEscSeqToSpanConverter creates a new escape sequence to span converter. +func newEscSeqToSpanConverter() *escSeqToSpanConverter { + return &escSeqToSpanConverter{ + currentColors: make(map[int]bool), + } +} + +// Convert converts ANSI escape sequences in the string to HTML tags with CSS classes. +func (c *escSeqToSpanConverter) Convert(str string) string { + c.reset() + + // Process the string character by character + for _, char := range str { + wasInSequence := c.esp.InSequence() + c.esp.Consume(char) + + if c.esp.InSequence() { + // We're inside an escape sequence, skip it (don't write to result) + continue + } + + if wasInSequence { + // We just finished an escape sequence, update colors + newColors := make(map[int]bool) + for _, code := range c.esp.Codes() { + newColors[code] = true + } + c.updateSpan(newColors) + } else { + // Regular character, escape it for HTML safety and write it + // (will be inside current span if colors are active) + c.writeEscapedRune(char) + } + } + + // Close any open span + if len(c.currentColors) > 0 { + c.result.WriteString("") + } + + return c.result.String() +} + +// clearColors clears the current color tracking. +func (c *escSeqToSpanConverter) clearColors() { + c.currentColors = make(map[int]bool) +} + +// closeSpan closes the current span if one is open. +func (c *escSeqToSpanConverter) closeSpan() { + if len(c.currentColors) > 0 { + c.result.WriteString("") + } +} + +// colorsChanged checks if the color set has changed. +func (c *escSeqToSpanConverter) colorsChanged(newColors map[int]bool) bool { + // we never set the map values to false, so a simple size compare is enough + return len(c.currentColors) != len(newColors) +} + +// cssClasses converts color codes to CSS class names. +func (c *escSeqToSpanConverter) cssClasses(codes map[int]bool) string { + var colors text.Colors + for code := range codes { + colors = append(colors, text.Color(code)) + } + return colors.CSSClasses() +} + +// openSpan opens a new span with the given CSS class and tracks the colors. +func (c *escSeqToSpanConverter) openSpan(class string, newColors map[int]bool) { + c.result.WriteString("") + // Track colors since we opened a span + c.currentColors = make(map[int]bool) + for code := range newColors { + c.currentColors[code] = true + } +} + +// reset initializes the converter state for a new conversion. +func (c *escSeqToSpanConverter) reset() { + c.result.Reset() + c.esp = text.EscSeqParser{} + c.currentColors = make(map[int]bool) +} + +// updateSpan updates span tags when colors change. +func (c *escSeqToSpanConverter) updateSpan(newColors map[int]bool) { + if !c.colorsChanged(newColors) { + return + } + + c.closeSpan() + + // Open new span if there are colors with valid CSS classes + if len(newColors) > 0 { + class := c.cssClasses(newColors) + if class != "" { + c.openSpan(class, newColors) + } else { + // No CSS classes, so don't track these colors + c.clearColors() + } + } else { + // No colors, clear tracking + c.clearColors() + } +} + +// writeEscapedRune writes a rune to the result, escaping it if necessary for HTML safety. +func (c *escSeqToSpanConverter) writeEscapedRune(char rune) { + switch char { + case '<': + c.result.WriteString("<") + case '>': + c.result.WriteString(">") + case '&': + c.result.WriteString("&") + case '"': + c.result.WriteString(""") + case '\'': + c.result.WriteString("'") + default: + // Most characters don't need escaping, write directly + c.result.WriteRune(char) + } +} diff --git a/vendor/github.com/jedib0t/go-pretty/v6/table/writer.go b/vendor/github.com/jedib0t/go-pretty/v6/table/writer.go index 51eee8c7..15a96cf4 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/table/writer.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/table/writer.go @@ -11,6 +11,7 @@ type Writer interface { AppendRow(row Row, configs ...RowConfig) AppendRows(rows []Row, configs ...RowConfig) AppendSeparator() + FilterBy(filterBy []FilterBy) ImportGrid(grid interface{}) bool Length() int Pager(opts ...PagerOption) Pager diff --git a/vendor/github.com/jedib0t/go-pretty/v6/text/README.md b/vendor/github.com/jedib0t/go-pretty/v6/text/README.md index afd163af..0e8308a4 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/text/README.md +++ b/vendor/github.com/jedib0t/go-pretty/v6/text/README.md @@ -1,8 +1,141 @@ -# text +# Text +[![Go Reference](https://pkg.go.dev/badge/github.com/jedib0t/go-pretty/v6/text.svg)](https://pkg.go.dev/github.com/jedib0t/go-pretty/v6/text) -[![Go Reference](https://pkg.go.dev/badge/github.com/jedib0t/go-pretty/v6.svg)](https://pkg.go.dev/github.com/jedib0t/go-pretty/v6/text) - -Package with utility functions to manipulate strings/text. +Package with utility functions to manipulate strings/text with full support for +ANSI escape sequences (colors, formatting, etc.). Used heavily in the other packages in this repo ([list](../list), -[progress](../progress), and [table](../table)). \ No newline at end of file +[progress](../progress), and [table](../table)). + +## Features + +### Colors & Formatting + + - **ANSI Color Support** - Full support for terminal colors and formatting + - Foreground colors (Black, Red, Green, Yellow, Blue, Magenta, Cyan, White) + - Background colors (matching foreground set) + - Hi-intensity variants for both foreground and background + - **256-color palette support** - Extended color support for terminals + - Standard 16 colors (0-15) + - RGB cube colors (16-231) - 216 colors organized in a 6x6x6 cube + - Grayscale colors (232-255) - 24 shades of gray + - Helper functions: `Fg256Color(index)`, `Bg256Color(index)`, `Fg256RGB(r, g, b)`, `Bg256RGB(r, g, b)` + - Text attributes (Bold, Faint, Italic, Underline, Blink, Reverse, Concealed, CrossedOut) + - Automatic color detection based on environment variables (`NO_COLOR`, `FORCE_COLOR`, `TERM`) + - Global enable/disable functions for colors + - Cached escape sequences for performance + - **Text Formatting** - Transform text while preserving escape sequences + - `FormatDefault` - No transformation + - `FormatLower` - Convert to lowercase + - `FormatTitle` - Convert to title case + - `FormatUpper` - Convert to uppercase + - **HTML Support** - Generate HTML class attributes for colors + - **Color Combinations** - Combine multiple colors and attributes + +### Alignment + + - **Horizontal Alignment** + - `AlignDefault` / `AlignLeft` - Left-align text + - `AlignCenter` - Center-align text + - `AlignRight` - Right-align text + - `AlignJustify` - Justify text (distribute spaces between words) + - `AlignAuto` - Auto-detect: right-align numbers, left-align text + - HTML and Markdown property generation for alignment + - **Vertical Alignment** + - `VAlignTop` - Align to top + - `VAlignMiddle` - Align to middle + - `VAlignBottom` - Align to bottom + - Works with both string arrays and multi-line strings + - HTML property generation for vertical alignment + +### Text Wrapping + + - **WrapHard** - Hard wrap at specified length, breaks words if needed + - Handles ANSI escape sequences without breaking formatting + - Preserves paragraph breaks + - **WrapSoft** - Soft wrap at specified length, tries to keep words intact + - Handles ANSI escape sequences without breaking formatting + - Preserves paragraph breaks + - **WrapText** - Similar to WrapHard but also respects line breaks + - Handles ANSI escape sequences without breaking formatting + +### String Utilities + + - **Width Calculation** + - `StringWidth` - Calculate display width of string (including escape sequences) + - `StringWidthWithoutEscSequences` - Calculate display width ignoring escape sequences + - `RuneWidth` - Calculate display width of a single rune (handles East Asian characters) + - `LongestLineLen` - Find the longest line in a multi-line string + - **String Manipulation** + - `Trim` - Trim string to specified length while preserving escape sequences + - `Pad` - Pad string to specified length with a character + - `Snip` - Snip string to specified length with an indicator (e.g., "~") + - `RepeatAndTrim` - Repeat string until it reaches specified length + - `InsertEveryN` - Insert a character every N characters + - `ProcessCRLF` - Process carriage returns and line feeds correctly + - `Widen` - Convert half-width characters to full-width + - **Escape Sequence Handling** + - All functions properly handle ANSI escape sequences + - Escape sequences are preserved during transformations + - Width calculations ignore escape sequences + - `EscSeqParser` - Parser for advanced escape sequence parsing and tracking + - Supports both CSI (Control Sequence Introducer) and OSI (Operating System Command) formats + - Tracks active formatting codes and can generate consolidated escape sequences + - Full support for 256-color escape sequences (`\x1b[38;5;n`m` and `\x1b[48;5;n`m`) + +### Cursor Control + + - Move cursor in all directions + - `CursorUp` - Move cursor up N lines + - `CursorDown` - Move cursor down N lines + - `CursorLeft` - Move cursor left N characters + - `CursorRight` - Move cursor right N characters + - `EraseLine` - Erase all characters to the right of cursor + - Generate ANSI escape sequences for terminal cursor manipulation + +### Hyperlinks + + - **Terminal Hyperlinks** - Create clickable hyperlinks in supported terminals + - Uses OSC 8 escape sequences + - Format: `Hyperlink(url, text)` + - Falls back to plain text in unsupported terminals + +### Transformers + + - **Number Transformer** - Format numbers with colors + - Positive numbers colored green + - Negative numbers colored red + - Custom format string support (e.g., `%.2f`) + - Supports all numeric types (int, uint, float) + - **JSON Transformer** - Pretty-print JSON strings or objects + - Customizable indentation (prefix and indent string) + - Validates JSON before formatting + - **Time Transformer** - Format time.Time objects + - Custom layout support (e.g., `time.RFC3339`) + - Timezone localization support + - Auto-detects common time formats from strings + - **Unix Time Transformer** - Format Unix timestamps + - Handles seconds, milliseconds, microseconds, and nanoseconds + - Auto-detects timestamp unit based on value + - Timezone localization support + - **URL Transformer** - Format URLs with styling + - Underlined and colored blue by default + - Custom color support + +### Text Direction + + - **Bidirectional Text Support** + - `LeftToRight` - Force left-to-right text direction + - `RightToLeft` - Force right-to-left text direction + - Uses Unicode directional markers + +### Filtering + + - **String Filtering** - Filter string slices with custom functions + - `Filter(slice, predicate)` - Returns filtered slice + +### East Asian Character Support + + - Proper width calculation for East Asian characters (full-width, half-width) + - Configurable East Asian width handling via `OverrideRuneWidthEastAsianWidth()` + - Handles mixed character sets correctly \ No newline at end of file diff --git a/vendor/github.com/jedib0t/go-pretty/v6/text/ansi_unix.go b/vendor/github.com/jedib0t/go-pretty/v6/text/ansi_unix.go index 635be79e..e873a7b6 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/text/ansi_unix.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/text/ansi_unix.go @@ -3,6 +3,12 @@ package text +import "os" + func areANSICodesSupported() bool { - return true + // On Unix systems, ANSI codes are generally supported unless TERM is "dumb" + // This is a basic check; 256-color sequences are ANSI sequences and will + // be handled by terminals that support them (or ignored by those that don't) + term := os.Getenv("TERM") + return term != "dumb" } diff --git a/vendor/github.com/jedib0t/go-pretty/v6/text/color.go b/vendor/github.com/jedib0t/go-pretty/v6/text/color.go index 4154111f..32f6e5e2 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/text/color.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/text/color.go @@ -9,6 +9,7 @@ import ( "sync" ) +// colorsEnabled is true if colors are enabled and supported by the terminal. var colorsEnabled = areColorsOnInTheEnv() && areANSICodesSupported() // DisableColors (forcefully) disables color coding globally. @@ -21,13 +22,24 @@ func EnableColors() { colorsEnabled = true } -// areColorsOnInTheEnv returns true is colors are not disable using +// areColorsOnInTheEnv returns true if colors are not disabled using // well known environment variables. func areColorsOnInTheEnv() bool { - if os.Getenv("FORCE_COLOR") == "1" { + // FORCE_COLOR takes precedence - if set to a truthy value, enable colors + forceColor := os.Getenv("FORCE_COLOR") + if forceColor != "" && forceColor != "0" && forceColor != "false" { return true } - return os.Getenv("NO_COLOR") == "" || os.Getenv("NO_COLOR") == "0" + + // NO_COLOR: if set to any non-empty value (except "0"), disable colors + // Note: "0" is treated as "not set" to allow explicit enabling via NO_COLOR=0 + noColor := os.Getenv("NO_COLOR") + if noColor != "" && noColor != "0" { + return false + } + + // Default: check TERM - if not "dumb", assume colors are supported + return os.Getenv("TERM") != "dumb" } // The logic here is inspired from github.com/fatih/color; the following is @@ -103,18 +115,62 @@ const ( BgHiWhite ) +// 256-color support +// Internal encoding for 256-color codes (used by escape_seq_parser.go): +// Foreground 256-color: fg256Start + colorIndex (1000-1255) +// Background 256-color: bg256Start + colorIndex (2000-2255) +const ( + // fg256Start is the base value for 256-color foreground colors. + // Use Fg256Color(index) to create a 256-color foreground color. + fg256Start Color = 1000 + // bg256Start is the base value for 256-color background colors. + // Use Bg256Color(index) to create a 256-color background color. + bg256Start Color = 2000 +) + +// CSSClasses returns the CSS class names for the color. +func (c Color) CSSClasses() string { + // Check for 256-color and convert to RGB-based class + if c >= fg256Start && c < fg256Start+256 { + colorIndex := int(c - fg256Start) + r, g, b := color256ToRGB(colorIndex) + return fmt.Sprintf("fg-256-%d-%d-%d", r, g, b) + } + if c >= bg256Start && c < bg256Start+256 { + colorIndex := int(c - bg256Start) + r, g, b := color256ToRGB(colorIndex) + return fmt.Sprintf("bg-256-%d-%d-%d", r, g, b) + } + // Existing behavior for standard colors + if class, ok := colorCSSClassMap[c]; ok { + return class + } + return "" +} + // EscapeSeq returns the ANSI escape sequence for the color. func (c Color) EscapeSeq() string { + // Check if it's a 256-color foreground (1000-1255) + if c >= fg256Start && c < fg256Start+256 { + colorIndex := int(c - fg256Start) + return fmt.Sprintf("%s38;5;%d%s", EscapeStart, colorIndex, EscapeStop) + } + // Check if it's a 256-color background (2000-2255) + if c >= bg256Start && c < bg256Start+256 { + colorIndex := int(c - bg256Start) + return fmt.Sprintf("%s48;5;%d%s", EscapeStart, colorIndex, EscapeStop) + } + // Regular color (existing behavior) return EscapeStart + strconv.Itoa(int(c)) + EscapeStop } // HTMLProperty returns the "class" attribute for the color. func (c Color) HTMLProperty() string { - out := "" - if class, ok := colorCSSClassMap[c]; ok { - out = fmt.Sprintf("class=\"%s\"", class) + classes := c.CSSClasses() + if classes == "" { + return "" } - return out + return fmt.Sprintf("class=\"%s\"", classes) } // Sprint colorizes and prints the given string(s). @@ -134,6 +190,25 @@ type Colors []Color // colorsSeqMap caches the escape sequence for a set of colors var colorsSeqMap = sync.Map{} +// CSSClasses returns the CSS class names for the colors. +func (c Colors) CSSClasses() string { + if len(c) == 0 { + return "" + } + + var classes []string + for _, color := range c { + class := color.CSSClasses() + if class != "" { + classes = append(classes, class) + } + } + if len(classes) > 1 { + sort.Strings(classes) + } + return strings.Join(classes, " ") +} + // EscapeSeq returns the ANSI escape sequence for the colors set. func (c Colors) EscapeSeq() string { if len(c) == 0 { @@ -143,32 +218,39 @@ func (c Colors) EscapeSeq() string { colorsKey := fmt.Sprintf("%#v", c) escapeSeq, ok := colorsSeqMap.Load(colorsKey) if !ok || escapeSeq == "" { - colorNums := make([]string, len(c)) - for idx, color := range c { - colorNums[idx] = strconv.Itoa(int(color)) + codes := make([]string, 0, len(c)) + for _, color := range c { + codes = append(codes, c.colorToCode(color)) } - escapeSeq = EscapeStart + strings.Join(colorNums, ";") + EscapeStop + escapeSeq = EscapeStart + strings.Join(codes, ";") + EscapeStop colorsSeqMap.Store(colorsKey, escapeSeq) } return escapeSeq.(string) } +// colorToCode converts a Color to its escape sequence code string. +func (c Colors) colorToCode(color Color) string { + // Check if it's a 256-color foreground (1000-1255) + if color >= fg256Start && color < fg256Start+256 { + colorIndex := int(color - fg256Start) + return fmt.Sprintf("38;5;%d", colorIndex) + } + // Check if it's a 256-color background (2000-2255) + if color >= bg256Start && color < bg256Start+256 { + colorIndex := int(color - bg256Start) + return fmt.Sprintf("48;5;%d", colorIndex) + } + // Regular color + return strconv.Itoa(int(color)) +} + // HTMLProperty returns the "class" attribute for the colors. func (c Colors) HTMLProperty() string { - if len(c) == 0 { + classes := c.CSSClasses() + if classes == "" { return "" } - - var classes []string - for _, color := range c { - if class, ok := colorCSSClassMap[color]; ok { - classes = append(classes, class) - } - } - if len(classes) > 1 { - sort.Strings(classes) - } - return fmt.Sprintf("class=\"%s\"", strings.Join(classes, " ")) + return fmt.Sprintf("class=\"%s\"", classes) } // Sprint colorizes and prints the given string(s). @@ -187,3 +269,81 @@ func colorize(s string, escapeSeq string) string { } return Escape(s, escapeSeq) } + +// Fg256Color returns a foreground 256-color Color value. +// The index must be in the range 0-255. +func Fg256Color(index int) Color { + if index < 0 || index > 255 { + return Reset + } + return fg256Start + Color(index) +} + +// Bg256Color returns a background 256-color Color value. +// The index must be in the range 0-255. +func Bg256Color(index int) Color { + if index < 0 || index > 255 { + return Reset + } + return bg256Start + Color(index) +} + +// Fg256RGB returns a foreground 256-color from RGB values in the 6x6x6 color cube. +// Each RGB component must be in the range 0-5. +// The resulting color index will be in the range 16-231. +func Fg256RGB(r, g, b int) Color { + if r < 0 || r > 5 || g < 0 || g > 5 || b < 0 || b > 5 { + return Reset + } + index := 16 + (r*36 + g*6 + b) + return Fg256Color(index) +} + +// Bg256RGB returns a background 256-color from RGB values in the 6x6x6 color cube. +// Each RGB component must be in the range 0-5. +// The resulting color index will be in the range 16-231. +func Bg256RGB(r, g, b int) Color { + if r < 0 || r > 5 || g < 0 || g > 5 || b < 0 || b > 5 { + return Reset + } + index := 16 + (r*36 + g*6 + b) + return Bg256Color(index) +} + +// color256ToRGB converts a 256-color index to RGB values. +// Returns (r, g, b) values in the range 0-255. +func color256ToRGB(index int) (r, g, b int) { + if index < 16 { + // Standard 16 colors - map to predefined RGB values + standardColors := [16][3]int{ + {0, 0, 0}, // 0: black + {128, 0, 0}, // 1: red + {0, 128, 0}, // 2: green + {128, 128, 0}, // 3: yellow + {0, 0, 128}, // 4: blue + {128, 0, 128}, // 5: magenta + {0, 128, 128}, // 6: cyan + {192, 192, 192}, // 7: light gray + {128, 128, 128}, // 8: dark gray + {255, 0, 0}, // 9: bright red + {0, 255, 0}, // 10: bright green + {255, 255, 0}, // 11: bright yellow + {0, 0, 255}, // 12: bright blue + {255, 0, 255}, // 13: bright magenta + {0, 255, 255}, // 14: bright cyan + {255, 255, 255}, // 15: white + } + return standardColors[index][0], standardColors[index][1], standardColors[index][2] + } else if index < 232 { + // 216-color RGB cube (16-231) + index -= 16 + r = (index / 36) * 51 + g = ((index / 6) % 6) * 51 + b = (index % 6) * 51 + } else { + // 24 grayscale colors (232-255) + gray := 8 + (index-232)*10 + r, g, b = gray, gray, gray + } + return +} diff --git a/vendor/github.com/jedib0t/go-pretty/v6/text/escape_seq_parser.go b/vendor/github.com/jedib0t/go-pretty/v6/text/escape_seq_parser.go index c6ffa437..66edae69 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/text/escape_seq_parser.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/text/escape_seq_parser.go @@ -42,73 +42,7 @@ const ( escSeqKindOSI ) -type escSeqParser struct { - codes map[int]bool - - // consume specific - inEscSeq bool - escSeqKind escSeqKind - escapeSeq string -} - -func (s *escSeqParser) Codes() []int { - codes := make([]int, 0) - for code, val := range s.codes { - if val { - codes = append(codes, code) - } - } - sort.Ints(codes) - return codes -} - -func (s *escSeqParser) Consume(char rune) { - if !s.inEscSeq && char == EscapeStartRune { - s.inEscSeq = true - s.escSeqKind = escSeqKindUnknown - s.escapeSeq = "" - } else if s.inEscSeq && s.escSeqKind == escSeqKindUnknown { - if char == EscapeStartRuneCSI { - s.escSeqKind = escSeqKindCSI - } else if char == EscapeStartRuneOSI { - s.escSeqKind = escSeqKindOSI - } - } - - if s.inEscSeq { - s.escapeSeq += string(char) - - // --- FIX for OSC 8 hyperlinks (e.g. \x1b]8;;url\x07label\x1b]8;;\x07) - if s.escSeqKind == escSeqKindOSI && - strings.HasPrefix(s.escapeSeq, escapeStartConcealOSI) && - char == '\a' { // BEL - - s.ParseSeq(s.escapeSeq, s.escSeqKind) - s.Reset() - return - } - - if s.isEscapeStopRune(char) { - s.ParseSeq(s.escapeSeq, s.escSeqKind) - s.Reset() - } - } -} - -func (s *escSeqParser) InSequence() bool { - return s.inEscSeq -} - -func (s *escSeqParser) IsOpen() bool { - return len(s.codes) > 0 -} - -func (s *escSeqParser) Reset() { - s.inEscSeq = false - s.escSeqKind = escSeqKindUnknown - s.escapeSeq = "" -} - +// private constants const ( escCodeResetAll = 0 escCodeResetIntensity = 22 @@ -126,50 +60,132 @@ const ( escCodeReverse = 7 escCodeConceal = 8 escCodeCrossedOut = 9 + + // conceal OSI sequences + escapeStartConcealOSI = "\x1b]8;" + escapeStopConcealOSI = "\x1b\\" ) -func (s *escSeqParser) ParseSeq(seq string, seqKind escSeqKind) { - if s.codes == nil { - s.codes = make(map[int]bool) +// 256-color codes +const ( + escCode256FgStart = 38 + escCode256BgStart = 48 + escCode256Color = 5 + escCodeResetFg = 39 + escCodeResetBg = 49 + escCode256Max = 255 +) + +// Internal encoding for 256-color codes uses fg256Start and bg256Start from color.go +// Private constants initialized from private constants to avoid repeated casting in hot paths +// Foreground 256-color: fg256Start + colorIndex (1000-1255) +// Background 256-color: bg256Start + colorIndex (2000-2255) +const ( + escCode256FgBase = int(fg256Start) // 1000 + escCode256BgBase = int(bg256Start) // 2000 +) + +// Standard color code ranges +const ( + // Standard foreground colors (30-37) + escCodeFgStdStart = 30 + escCodeFgStdEnd = 37 + // Bright foreground colors (90-97) + escCodeFgBrightStart = 90 + escCodeFgBrightEnd = 97 + // Standard background colors (40-47) + escCodeBgStdStart = 40 + escCodeBgStdEnd = 47 + // Bright background colors (100-107) + escCodeBgBrightStart = 100 + escCodeBgBrightEnd = 107 +) + +// Special characters +const ( + escRuneBEL = '\a' // BEL character (ASCII 7) +) + +// EscSeqParser parses ANSI escape sequences from text and tracks active formatting codes. +// It supports both CSI (Control Sequence Introducer) and OSI (Operating System Command) +// escape sequence formats. +type EscSeqParser struct { + // codes tracks active escape sequence codes (e.g., 1 for bold, 3 for italic). + codes map[int]bool + + // inEscSeq indicates whether the parser is currently inside an escape sequence. + inEscSeq bool + // escSeqKind identifies the type of escape sequence being parsed (CSI or OSI). + escSeqKind escSeqKind + // escapeSeq accumulates the current escape sequence being parsed. + escapeSeq string +} + +func (s *EscSeqParser) Codes() []int { + codes := make([]int, 0) + for code, val := range s.codes { + if val { + codes = append(codes, code) + } + } + sort.Ints(codes) + return codes +} + +func (s *EscSeqParser) Consume(char rune) { + if !s.inEscSeq && char == EscapeStartRune { + s.inEscSeq = true + s.escSeqKind = escSeqKindUnknown + s.escapeSeq = "" + } else if s.inEscSeq && s.escSeqKind == escSeqKindUnknown { + switch char { + case EscapeStartRuneCSI: + s.escSeqKind = escSeqKindCSI + case EscapeStartRuneOSI: + s.escSeqKind = escSeqKindOSI + } } - if seqKind == escSeqKindOSI { - seq = strings.Replace(seq, EscapeStartOSI, "", 1) - seq = strings.Replace(seq, EscapeStopOSI, "", 1) - } else { // escSeqKindCSI - seq = strings.Replace(seq, EscapeStartCSI, "", 1) - seq = strings.Replace(seq, EscapeStopCSI, "", 1) - } + if s.inEscSeq { + s.escapeSeq += string(char) - codes := strings.Split(seq, ";") - for _, code := range codes { - code = strings.TrimSpace(code) - if codeNum, err := strconv.Atoi(code); err == nil { - switch codeNum { - case escCodeResetAll: - s.codes = make(map[int]bool) // clear everything - case escCodeResetIntensity: - delete(s.codes, escCodeBold) - delete(s.codes, escCodeDim) - case escCodeResetItalic: - delete(s.codes, escCodeItalic) - case escCodeResetUnderline: - delete(s.codes, escCodeUnderline) - case escCodeResetBlink: - delete(s.codes, escCodeBlinkSlow) - delete(s.codes, escCodeBlinkRapid) - case escCodeResetReverse: - delete(s.codes, escCodeReverse) - case escCodeResetCrossedOut: - delete(s.codes, escCodeCrossedOut) - default: - s.codes[codeNum] = true - } + // --- FIX for OSC 8 hyperlinks (e.g. \x1b]8;;url\x07label\x1b]8;;\x07) + if s.escSeqKind == escSeqKindOSI && + strings.HasPrefix(s.escapeSeq, escapeStartConcealOSI) && + char == escRuneBEL { // BEL + + s.ParseSeq(s.escapeSeq, s.escSeqKind) + s.Reset() + return + } + + if s.isEscapeStopRune(char) { + s.ParseSeq(s.escapeSeq, s.escSeqKind) + s.Reset() } } } -func (s *escSeqParser) ParseString(str string) string { +func (s *EscSeqParser) InSequence() bool { + return s.inEscSeq +} + +func (s *EscSeqParser) IsOpen() bool { + return len(s.codes) > 0 +} + +func (s *EscSeqParser) ParseSeq(seq string, seqKind escSeqKind) { + if s.codes == nil { + s.codes = make(map[int]bool) + } + + seq = s.stripEscapeSequence(seq, seqKind) + codes := s.splitAndTrimCodes(seq) + processed256ColorIndices := s.process256ColorSequences(codes) + s.processRegularCodes(codes, processed256ColorIndices) +} + +func (s *EscSeqParser) ParseString(str string) string { s.escapeSeq, s.inEscSeq, s.escSeqKind = "", false, escSeqKindUnknown for _, char := range str { s.Consume(char) @@ -177,15 +193,33 @@ func (s *escSeqParser) ParseString(str string) string { return s.Sequence() } -func (s *escSeqParser) Sequence() string { +func (s *EscSeqParser) Reset() { + s.inEscSeq = false + s.escSeqKind = escSeqKindUnknown + s.escapeSeq = "" +} + +func (s *EscSeqParser) Sequence() string { out := strings.Builder{} if s.IsOpen() { out.WriteString(EscapeStart) - for idx, code := range s.Codes() { + codes := s.Codes() + for idx, code := range codes { if idx > 0 { out.WriteRune(';') } - out.WriteString(fmt.Sprint(code)) + // Check if this is a 256-color foreground code (1000-1255) + if code >= escCode256FgBase && code <= escCode256FgBase+escCode256Max { + colorIndex := code - escCode256FgBase + out.WriteString(fmt.Sprintf("%d;%d;%d", escCode256FgStart, escCode256Color, colorIndex)) + } else if code >= escCode256BgBase && code <= escCode256BgBase+escCode256Max { + // 256-color background code (2000-2255) + colorIndex := code - escCode256BgBase + out.WriteString(fmt.Sprintf("%d;%d;%d", escCode256BgStart, escCode256Color, colorIndex)) + } else { + // Regular code + out.WriteString(fmt.Sprint(code)) + } } out.WriteString(EscapeStop) } @@ -193,12 +227,54 @@ func (s *escSeqParser) Sequence() string { return out.String() } -const ( - escapeStartConcealOSI = "\x1b]8;" - escapeStopConcealOSI = "\x1b\\" -) +// clearAllBackgroundColors clears all background color codes. +func (s *EscSeqParser) clearAllBackgroundColors() { + for code := escCodeBgStdStart; code <= escCodeBgStdEnd; code++ { + delete(s.codes, code) + } + for code := escCodeBgBrightStart; code <= escCodeBgBrightEnd; code++ { + delete(s.codes, code) + } + for code := escCode256BgBase; code <= escCode256BgBase+escCode256Max; code++ { + delete(s.codes, code) + } +} -func (s *escSeqParser) isEscapeStopRune(char rune) bool { +// clearAllForegroundColors clears all foreground color codes. +func (s *EscSeqParser) clearAllForegroundColors() { + for code := escCodeFgStdStart; code <= escCodeFgStdEnd; code++ { + delete(s.codes, code) + } + for code := escCodeFgBrightStart; code <= escCodeFgBrightEnd; code++ { + delete(s.codes, code) + } + for code := escCode256FgBase; code <= escCode256FgBase+escCode256Max; code++ { + delete(s.codes, code) + } +} + +// clearColorRange clears standard foreground or background colors. +func (s *EscSeqParser) clearColorRange(isForeground bool) { + if isForeground { + // Clear standard foreground colors (30-37, 90-97) + for code := escCodeFgStdStart; code <= escCodeFgStdEnd; code++ { + delete(s.codes, code) + } + for code := escCodeFgBrightStart; code <= escCodeFgBrightEnd; code++ { + delete(s.codes, code) + } + } else { + // Clear standard background colors (40-47, 100-107) + for code := escCodeBgStdStart; code <= escCodeBgStdEnd; code++ { + delete(s.codes, code) + } + for code := escCodeBgBrightStart; code <= escCodeBgBrightEnd; code++ { + delete(s.codes, code) + } + } +} + +func (s *EscSeqParser) isEscapeStopRune(char rune) bool { if strings.HasPrefix(s.escapeSeq, escapeStartConcealOSI) { if strings.HasSuffix(s.escapeSeq, escapeStopConcealOSI) { return true @@ -209,3 +285,140 @@ func (s *escSeqParser) isEscapeStopRune(char rune) bool { } return false } + +// isRegularCode checks if a code is a regular code (not a 256-color encoded value). +func (s *EscSeqParser) isRegularCode(codeNum int) bool { + return codeNum < escCode256FgBase || codeNum > escCode256BgBase+escCode256Max +} + +// parse256ColorSequence attempts to parse a 256-color sequence starting at index i. +// Returns (colorIndex, base, true) if valid, or (0, 0, false) if not. +func (s *EscSeqParser) parse256ColorSequence(codes []string, i int) (colorIndex int, base int, ok bool) { + if i+2 >= len(codes) { + return 0, 0, false + } + + codeNum, err := strconv.Atoi(codes[i]) + if err != nil { + return 0, 0, false + } + + var expectedBase int + switch codeNum { + case escCode256FgStart: + expectedBase = escCode256FgBase + case escCode256BgStart: + expectedBase = escCode256BgBase + default: + return 0, 0, false + } + + nextCode, err := strconv.Atoi(codes[i+1]) + if err != nil || nextCode != escCode256Color { + return 0, 0, false + } + + colorIndex, err = strconv.Atoi(codes[i+2]) + if err != nil || colorIndex < 0 || colorIndex > escCode256Max { + return 0, 0, false + } + + return colorIndex, expectedBase, true +} + +// process256ColorSequences processes 256-color sequences (38;5;n or 48;5;n) and returns +// a map of indices that were part of valid 256-color sequences. +func (s *EscSeqParser) process256ColorSequences(codes []string) map[int]bool { + processedIndices := make(map[int]bool) + for i := 0; i < len(codes); i++ { + if colorIndex, base, ok := s.parse256ColorSequence(codes, i); ok { + s.set256Color(base, colorIndex) + s.clearColorRange(base == escCode256FgBase) + processedIndices[i] = true + processedIndices[i+1] = true + processedIndices[i+2] = true + i += 2 // Skip i+1 and i+2 (loop will increment to i+3) + } + } + return processedIndices +} + +// processCode handles a single escape code. +func (s *EscSeqParser) processCode(codeNum int) { + switch codeNum { + case escCodeResetAll: + s.codes = make(map[int]bool) + case escCodeResetIntensity: + delete(s.codes, escCodeBold) + delete(s.codes, escCodeDim) + case escCodeResetItalic: + delete(s.codes, escCodeItalic) + case escCodeResetUnderline: + delete(s.codes, escCodeUnderline) + case escCodeResetBlink: + delete(s.codes, escCodeBlinkSlow) + delete(s.codes, escCodeBlinkRapid) + case escCodeResetReverse: + delete(s.codes, escCodeReverse) + case escCodeResetCrossedOut: + delete(s.codes, escCodeCrossedOut) + case escCodeResetFg: + s.clearAllForegroundColors() + case escCodeResetBg: + s.clearAllBackgroundColors() + default: + if s.isRegularCode(codeNum) { + s.codes[codeNum] = true + } + } +} + +// processRegularCodes processes regular escape codes and reset codes. +func (s *EscSeqParser) processRegularCodes(codes []string, processedIndices map[int]bool) { + for i, code := range codes { + if processedIndices[i] { + continue + } + + codeNum, err := strconv.Atoi(code) + if err != nil { + continue + } + + s.processCode(codeNum) + } +} + +// set256Color sets a 256-color code and clears conflicting colors. +func (s *EscSeqParser) set256Color(base int, colorIndex int) { + encodedValue := base + colorIndex + s.codes[encodedValue] = true + + // Clear other colors in the same range + for code := base; code <= base+escCode256Max; code++ { + if code != encodedValue { + delete(s.codes, code) + } + } +} + +// splitAndTrimCodes splits the sequence by semicolons and trims whitespace. +func (s *EscSeqParser) splitAndTrimCodes(seq string) []string { + codes := strings.Split(seq, ";") + for i := range codes { + codes[i] = strings.TrimSpace(codes[i]) + } + return codes +} + +// stripEscapeSequence removes escape sequence markers from the input string. +func (s *EscSeqParser) stripEscapeSequence(seq string, seqKind escSeqKind) string { + if seqKind == escSeqKindOSI { + seq = strings.Replace(seq, EscapeStartOSI, "", 1) + seq = strings.Replace(seq, EscapeStopOSI, "", 1) + } else { + seq = strings.Replace(seq, EscapeStartCSI, "", 1) + seq = strings.Replace(seq, EscapeStopCSI, "", 1) + } + return seq +} diff --git a/vendor/github.com/jedib0t/go-pretty/v6/text/string.go b/vendor/github.com/jedib0t/go-pretty/v6/text/string.go index fa28a4c0..fa3e4f1d 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/text/string.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/text/string.go @@ -28,7 +28,7 @@ func InsertEveryN(str string, runeToInsert rune, n int) string { sLen := StringWidthWithoutEscSequences(str) var out strings.Builder out.Grow(sLen + (sLen / n)) - outLen, esp := 0, escSeqParser{} + outLen, esp := 0, EscSeqParser{} for idx, c := range str { if esp.InSequence() { esp.Consume(c) @@ -52,7 +52,7 @@ func InsertEveryN(str string, runeToInsert rune, n int) string { // // LongestLineLen("Ghost!\nCome back here!\nRight now!") == 15 func LongestLineLen(str string) int { - maxLength, currLength, esp := 0, 0, escSeqParser{} + maxLength, currLength, esp := 0, 0, EscSeqParser{} //fmt.Println(str) for _, c := range str { //fmt.Printf("%03d | %03d | %c | %5v | %v | %#v\n", idx, c, c, esp.inEscSeq, esp.Codes(), esp.escapeSeq) @@ -76,17 +76,20 @@ func LongestLineLen(str string) int { return maxLength } -// OverrideRuneWidthEastAsianWidth can *probably* help with alignment, and -// length calculation issues when dealing with Unicode character-set and a -// non-English language set in the LANG variable. +// OverrideRuneWidthEastAsianWidth overrides the East Asian width detection in +// the runewidth library. This is primarily for advanced use cases. // -// Set this to 'false' to force the "runewidth" library to pretend to deal with -// English character-set. Be warned that if the text/content you are dealing -// with contains East Asian character-set, this may result in unexpected -// behavior. +// Box drawing (U+2500-U+257F) and block element (U+2580-U+259F) characters +// are automatically handled and always reported as width 1, regardless of +// this setting, fixing alignment issues that previously required setting this +// to false. // -// References: -// * https://github.com/mattn/go-runewidth/issues/64#issuecomment-1221642154 +// Setting this to false forces runewidth to treat all characters as if in an +// English locale. Warning: this may cause East Asian characters (Chinese, +// Japanese, Korean) to be incorrectly reported as width 1 instead of 2. +// +// See: +// * https://github.com/mattn/go-runewidth/issues/64 // * https://github.com/jedib0t/go-pretty/issues/220 // * https://github.com/jedib0t/go-pretty/issues/204 func OverrideRuneWidthEastAsianWidth(val bool) { @@ -184,16 +187,28 @@ func RuneCount(str string) int { return StringWidthWithoutEscSequences(str) } -// RuneWidth returns the mostly accurate character-width of the rune. This is -// not 100% accurate as the character width is usually dependent on the -// typeface (font) used in the console/terminal. For ex.: +// RuneWidth returns the display width of a rune. Width accuracy depends on +// the terminal font, as character width is font-dependent. Examples: // // RuneWidth('A') == 1 // RuneWidth('ツ') == 2 // RuneWidth('⊙') == 1 // RuneWidth('︿') == 2 // RuneWidth(0x27) == 0 +// +// Box drawing (U+2500-U+257F) and block element (U+2580-U+259F) characters +// are always treated as width 1, regardless of locale, to ensure proper +// alignment in tables and progress indicators. This fixes incorrect width 2 +// reporting in East Asian locales (e.g., LANG=zh_CN.UTF-8). +// +// See: +// * https://github.com/mattn/go-runewidth/issues/64 +// * https://github.com/jedib0t/go-pretty/issues/220 +// * https://github.com/jedib0t/go-pretty/issues/204 func RuneWidth(r rune) int { + if (r >= 0x2500 && r <= 0x257F) || (r >= 0x2580 && r <= 0x259F) { + return 1 + } return rwCondition.RuneWidth(r) } @@ -248,7 +263,7 @@ func StringWidth(str string) int { // StringWidthWithoutEscSequences("Ghost 生命"): 10 // StringWidthWithoutEscSequences("\x1b[33mGhost 生命\x1b[0m"): 10 func StringWidthWithoutEscSequences(str string) int { - count, esp := 0, escSeqParser{} + count, esp := 0, EscSeqParser{} for _, c := range str { if esp.InSequence() { esp.Consume(c) @@ -277,7 +292,7 @@ func Trim(str string, maxLen int) string { var out strings.Builder out.Grow(maxLen) - outLen, esp := 0, escSeqParser{} + outLen, esp := 0, EscSeqParser{} for _, sChr := range str { if esp.InSequence() { esp.Consume(sChr) @@ -306,7 +321,7 @@ func Widen(str string) string { sb := strings.Builder{} sb.Grow(len(str)) - esp := escSeqParser{} + esp := EscSeqParser{} for _, c := range str { if esp.InSequence() { sb.WriteRune(c) diff --git a/vendor/github.com/jedib0t/go-pretty/v6/text/transformer.go b/vendor/github.com/jedib0t/go-pretty/v6/text/transformer.go index 193a721c..ef122272 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/text/transformer.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/text/transformer.go @@ -11,9 +11,15 @@ import ( // Transformer related constants const ( - unixTimeMinMilliseconds = int64(10000000000) - unixTimeMinMicroseconds = unixTimeMinMilliseconds * 1000 - unixTimeMinNanoSeconds = unixTimeMinMicroseconds * 1000 + // Pre-computed time conversion constants to avoid repeated calculations + nanosPerSecond = int64(time.Second) + microsPerSecond = nanosPerSecond / 1000 + millisPerSecond = nanosPerSecond / 1000000 + + // Thresholds for detecting unix timestamp units (10 seconds worth in each unit) + unixTimeMinMilliseconds = 10 * nanosPerSecond + unixTimeMinMicroseconds = 10 * nanosPerSecond * 1000 + unixTimeMinNanoSeconds = 10 * nanosPerSecond * 1000000 ) // Transformer related variables @@ -40,103 +46,84 @@ type Transformer func(val interface{}) string // - transforms the number as directed by 'format' (ex.: %.2f) // - colors negative values Red // - colors positive values Green +// +//gocyclo:ignore func NewNumberTransformer(format string) Transformer { + // Pre-compute negative format string to avoid repeated allocations + negFormat := "-" + format + + transformInt64 := func(val int64) string { + if val < 0 { + return colorsNumberNegative.Sprintf(negFormat, -val) + } + if val > 0 { + return colorsNumberPositive.Sprintf(format, val) + } + return colorsNumberZero.Sprintf(format, val) + } + + transformUint64 := func(val uint64) string { + if val > 0 { + return colorsNumberPositive.Sprintf(format, val) + } + return colorsNumberZero.Sprintf(format, val) + } + + transformFloat64 := func(val float64) string { + if val < 0 { + return colorsNumberNegative.Sprintf(negFormat, -val) + } + if val > 0 { + return colorsNumberPositive.Sprintf(format, val) + } + return colorsNumberZero.Sprintf(format, val) + } + + // Use type switch for O(1) type checking instead of sequential type assertions return func(val interface{}) string { - if valStr := transformInt(format, val); valStr != "" { - return valStr + switch v := val.(type) { + case int: + return transformInt64(int64(v)) + case int8: + return transformInt64(int64(v)) + case int16: + return transformInt64(int64(v)) + case int32: + return transformInt64(int64(v)) + case int64: + return transformInt64(v) + case uint: + return transformUint64(uint64(v)) + case uint8: + return transformUint64(uint64(v)) + case uint16: + return transformUint64(uint64(v)) + case uint32: + return transformUint64(uint64(v)) + case uint64: + return transformUint64(v) + case float32: + return transformFloat64(float64(v)) + case float64: + return transformFloat64(v) + default: + return fmt.Sprint(val) } - if valStr := transformUint(format, val); valStr != "" { - return valStr - } - if valStr := transformFloat(format, val); valStr != "" { - return valStr - } - return fmt.Sprint(val) } } -func transformInt(format string, val interface{}) string { - transform := func(val int64) string { - if val < 0 { - return colorsNumberNegative.Sprintf("-"+format, -val) - } - if val > 0 { - return colorsNumberPositive.Sprintf(format, val) - } - return colorsNumberZero.Sprintf(format, val) - } - - if number, ok := val.(int); ok { - return transform(int64(number)) - } - if number, ok := val.(int8); ok { - return transform(int64(number)) - } - if number, ok := val.(int16); ok { - return transform(int64(number)) - } - if number, ok := val.(int32); ok { - return transform(int64(number)) - } - if number, ok := val.(int64); ok { - return transform(number) - } - return "" -} - -func transformUint(format string, val interface{}) string { - transform := func(val uint64) string { - if val > 0 { - return colorsNumberPositive.Sprintf(format, val) - } - return colorsNumberZero.Sprintf(format, val) - } - - if number, ok := val.(uint); ok { - return transform(uint64(number)) - } - if number, ok := val.(uint8); ok { - return transform(uint64(number)) - } - if number, ok := val.(uint16); ok { - return transform(uint64(number)) - } - if number, ok := val.(uint32); ok { - return transform(uint64(number)) - } - if number, ok := val.(uint64); ok { - return transform(number) - } - return "" -} - -func transformFloat(format string, val interface{}) string { - transform := func(val float64) string { - if val < 0 { - return colorsNumberNegative.Sprintf("-"+format, -val) - } - if val > 0 { - return colorsNumberPositive.Sprintf(format, val) - } - return colorsNumberZero.Sprintf(format, val) - } - - if number, ok := val.(float32); ok { - return transform(float64(number)) - } - if number, ok := val.(float64); ok { - return transform(number) - } - return "" -} - // NewJSONTransformer returns a Transformer that can format a JSON string or an // object into pretty-indented JSON-strings. func NewJSONTransformer(prefix string, indent string) Transformer { return func(val interface{}) string { if valStr, ok := val.(string); ok { + valStr = strings.TrimSpace(valStr) + // Validate JSON before attempting to indent to avoid unnecessary processing + if !json.Valid([]byte(valStr)) { + return fmt.Sprintf("%#v", valStr) + } var b bytes.Buffer - if err := json.Indent(&b, []byte(strings.TrimSpace(valStr)), prefix, indent); err == nil { + if err := json.Indent(&b, []byte(valStr), prefix, indent); err == nil { return b.String() } } else if b, err := json.MarshalIndent(val, prefix, indent); err == nil { @@ -154,17 +141,17 @@ func NewJSONTransformer(prefix string, indent string) Transformer { // location (use time.Local to get localized timestamps). func NewTimeTransformer(layout string, location *time.Location) Transformer { return func(val interface{}) string { - rsp := fmt.Sprint(val) + // Check for time.Time first to avoid unnecessary fmt.Sprint conversion if valTime, ok := val.(time.Time); ok { - rsp = formatTime(valTime, layout, location) - } else { - // cycle through some supported layouts to see if the string form - // of the object matches any of these layouts - for _, possibleTimeLayout := range possibleTimeLayouts { - if valTime, err := time.Parse(possibleTimeLayout, rsp); err == nil { - rsp = formatTime(valTime, layout, location) - break - } + return formatTime(valTime, layout, location) + } + // Only convert to string if not already time.Time + rsp := fmt.Sprint(val) + // Cycle through some supported layouts to see if the string form + // of the object matches any of these layouts + for _, possibleTimeLayout := range possibleTimeLayouts { + if valTime, err := time.Parse(possibleTimeLayout, rsp); err == nil { + return formatTime(valTime, layout, location) } } return rsp @@ -217,12 +204,13 @@ func formatTime(t time.Time, layout string, location *time.Location) string { } func formatTimeUnix(unixTime int64, timeTransformer Transformer) string { + // Use pre-computed constants instead of repeated time.Second.Nanoseconds() calls if unixTime >= unixTimeMinNanoSeconds { - unixTime = unixTime / time.Second.Nanoseconds() + unixTime = unixTime / nanosPerSecond } else if unixTime >= unixTimeMinMicroseconds { - unixTime = unixTime / (time.Second.Nanoseconds() / 1000) + unixTime = unixTime / microsPerSecond } else if unixTime >= unixTimeMinMilliseconds { - unixTime = unixTime / (time.Second.Nanoseconds() / 1000000) + unixTime = unixTime / millisPerSecond } return timeTransformer(time.Unix(unixTime, 0)) } diff --git a/vendor/github.com/jedib0t/go-pretty/v6/text/wrap.go b/vendor/github.com/jedib0t/go-pretty/v6/text/wrap.go index 8ad84855..8a9e803d 100644 --- a/vendor/github.com/jedib0t/go-pretty/v6/text/wrap.go +++ b/vendor/github.com/jedib0t/go-pretty/v6/text/wrap.go @@ -157,7 +157,7 @@ func terminateOutput(lastSeenEscSeq string, out *strings.Builder) { } func wrapHard(paragraph string, wrapLen int, out *strings.Builder) { - esp := escSeqParser{} + esp := EscSeqParser{} lineLen, lastSeenEscSeq := 0, "" words := strings.Fields(paragraph) for wordIdx, word := range words { @@ -186,7 +186,7 @@ func wrapHard(paragraph string, wrapLen int, out *strings.Builder) { } func wrapSoft(paragraph string, wrapLen int, out *strings.Builder) { - esp := escSeqParser{} + esp := EscSeqParser{} lineLen, lastSeenEscSeq := 0, "" words := strings.Fields(paragraph) for wordIdx, word := range words { diff --git a/vendor/modules.txt b/vendor/modules.txt index b901b603..0800d91d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -254,7 +254,7 @@ github.com/imdario/mergo # github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 ## explicit github.com/jaytaylor/html2text -# github.com/jedib0t/go-pretty/v6 v6.6.8 +# github.com/jedib0t/go-pretty/v6 v6.7.7 ## explicit; go 1.18 github.com/jedib0t/go-pretty/v6/table github.com/jedib0t/go-pretty/v6/text