diff --git a/cfg/config.go b/cfg/config.go index 2517263..c3a0a4e 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -1,5 +1,5 @@ /* -Copyright © 2022-2025 Thomas von Dein +Copyright © 2022-2026 Thomas von Dein This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -28,7 +28,7 @@ import ( ) const ( - Version = "v1.5.12" + Version = "v1.6.0" MAXPARTS = 2 ) @@ -102,15 +102,15 @@ type Config struct { SortByColumn string // 1,2 UseSortByColumn []int // []int{1,2} - TransposeColumns string // 1,2 - UseTransposeColumns []int // []int{1,2} - Transposers []string // []string{"/ /-/", "/foo/bar/"} - UseTransposers []Transposer // {Search: re, Replace: string} + TransposeColumns string // 1,2 + UseTransposeColumns []int // []int{1,2} + + Transposers []string // []string{"/ /-/", "/foo/bar/"} + UseTransposers []Transposer // {Search: re, Replace: string} + + Colorizers []string // []string{"/ /-/", "/foo/fg[:bg]/"} + UseColorizers []Transposer // {Search: re, Replace: color} - /* - FIXME: make configurable somehow, config file or ENV - see https://github.com/gookit/color. - */ ColorStyle color.Style HighlightStyle color.Style NoHighlightStyle color.Style @@ -357,6 +357,23 @@ func (conf *Config) PrepareTransposers() error { return nil } +func (conf *Config) PrepareColorizers() error { + for _, colorizer := range conf.Colorizers { + parts := strings.Split(colorizer, string(colorizer[0])) + if len(parts) != 4 { + return fmt.Errorf("colorizer function must have the format /regexp/foreground-color[:background-color]/") + } + + conf.UseColorizers = append(conf.UseColorizers, + Transposer{ + Search: *regexp.MustCompile(parts[1]), + Replace: parts[2]}, + ) + } + + return nil +} + func (conf *Config) CheckEnv() { // check for environment vars, command line flags have precedence, // NO_COLOR is being checked by the color module itself. diff --git a/cfg/config_test.go b/cfg/config_test.go index b4b1058..6b2bbb4 100644 --- a/cfg/config_test.go +++ b/cfg/config_test.go @@ -39,7 +39,6 @@ func TestPrepareModeFlags(t *testing.T) { {Modeflag{}, ASCII}, } - // FIXME: use a map for easier printing for _, testdata := range tests { testname := fmt.Sprintf("PrepareModeFlags-expect-%d", testdata.expect) t.Run(testname, func(t *testing.T) { diff --git a/cmd/root.go b/cmd/root.go index d9ceddf..404e5ea 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,5 +1,5 @@ /* -Copyright © 2022-2025 Thomas von Dein +Copyright © 2022-2026 Thomas von Dein This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -95,6 +95,7 @@ func Execute() { conf.PrepareCustomHeaders(headers) wrapE(conf.PrepareFilters()) + wrapE(conf.PrepareColorizers()) conf.DetermineColormode() conf.ApplyDefaults() @@ -191,6 +192,8 @@ func Execute() { "filter", "F", nil, "Filter by field (field=regexp || field!=regexp)") rootCmd.PersistentFlags().StringArrayVarP(&conf.Transposers, "regex-transposer", "R", nil, "apply /search/replace/ regexp to fields given in -T") + rootCmd.PersistentFlags().StringArrayVarP(&conf.Colorizers, + "regex-colorizer", "K", nil, "apply /search/color[:background]/ to the whole output") // input rootCmd.PersistentFlags().StringVarP(&conf.InputFile, "read-file", "r", "", diff --git a/cmd/shortusage.go b/cmd/shortusage.go index aa2f118..e4eb555 100644 --- a/cmd/shortusage.go +++ b/cmd/shortusage.go @@ -1,7 +1,7 @@ package cmd const shortusage = `tablizer [regex,...] [-r file] [flags] --c col,... show specified columns -L highlight matching lines +-c col,... show specified columns -L colorize rows -k col,... sort by specified columns -j read JSON input -F col=reg filter field with regexp -v invert match -T col,... transpose specified columns -n numberize columns @@ -13,6 +13,7 @@ const shortusage = `tablizer [regex,...] [-r file] [flags] -x col,... use custom headers -d debug -o char use char as output separator -g auto generate headers +-K /pattern/foreground[:background]/ colorize pattern of output -O org -C CSV -M md -X ext -S shell -Y yaml -J json -P template -a sort by age -i sort numerically -t sort by time -D sort descending order -m show manual -v show version --help show detailed help` diff --git a/cmd/tablizer.go b/cmd/tablizer.go index d27a71e..bbeb50e 100644 --- a/cmd/tablizer.go +++ b/cmd/tablizer.go @@ -20,6 +20,7 @@ SYNOPSIS -F, --filter Filter given field with regex, can be used multiple times -T, --transpose-columns string Transpose the speficied columns (separated by ,) -R, --regex-transposer Apply /search/replace/ regexp to fields given in -T + -K --regex-colorizer /from/color/ colorize pattern of output (color: fg[:bg]) -j, --json Read JSON input (must be array of hashes) -I, --interactive Interactively filter and select rows -g, --auto-headers Generate headers if there are none present in input @@ -467,9 +468,20 @@ CONFIGURATION AND COLORS lightGreen, lightMagenta, lightRed, lightWhite, lightYellow, magenta, red, white, yellow - The Variables FG and BG are being used to highlight matches. The other - *FG and *BG variables are for colored table output (enabled with the - "-L" parameter). + but you may also use HTML color codes without the hash sign. + + The Variables FG and BG are being used to highlight matching rows. The + other *FG and *BG variables are for colored table output (enabled with + the "-L" parameter). + + You can also use the option "-K" to colorize particular patterns, not + whole lines. The option can be given multiple times and expects the + following parameter: + + -K '/regex/foreground[:background]/ + + that is, background color is optional. This colorization will applied on + top of any previous colorizations, if any. Colorization can be turned off completely either by setting the parameter "-N" or the environment variable NO_COLOR to a true value. @@ -531,6 +543,7 @@ Operational Flags: -F, --filter Filter given field with regex, can be used multiple times -T, --transpose-columns string Transpose the speficied columns (separated by ,) -R, --regex-transposer Apply /search/replace/ regexp to fields given in -T + -K --regex-colorizer /from/color/ colorize pattern of output (color: fg[:bg]) -j, --json Read JSON input (must be array of hashes) -I, --interactive Interactively filter and select rows -g, --auto-headers Generate headers if there are none present in input diff --git a/lib/filter.go b/lib/filter.go index f603c8b..3d2f349 100644 --- a/lib/filter.go +++ b/lib/filter.go @@ -1,5 +1,5 @@ /* -Copyright © 2022-2025 Thomas von Dein +Copyright © 2022-2026 Thomas von Dein This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,8 +22,8 @@ import ( "io" "strings" - "github.com/lithammer/fuzzysearch/fuzzy" "codeberg.org/scip/tablizer/cfg" + "github.com/lithammer/fuzzysearch/fuzzy" ) /* diff --git a/lib/helpers.go b/lib/helpers.go index cd63cd8..bd9582e 100644 --- a/lib/helpers.go +++ b/lib/helpers.go @@ -1,5 +1,5 @@ /* -Copyright © 2022 Thomas von Dein +Copyright © 2022-2026 Thomas von Dein This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -26,8 +26,8 @@ import ( "strconv" "strings" - "github.com/gookit/color" "codeberg.org/scip/tablizer/cfg" + "github.com/gookit/color" ) func findindex(s []int, e int) (int, bool) { @@ -245,35 +245,33 @@ func reduceColumns(conf cfg.Config, data *Tabdata) { } } -// FIXME: refactor this beast! func colorizeData(conf cfg.Config, output string) string { + if !conf.NoColor && !color.IsConsole(os.Stdout) { + return output + } + switch { - case conf.UseHighlight && color.IsConsole(os.Stdout): + case conf.UseHighlight: highlight := true colorized := "" - first := true + style := color.Style{} - for _, line := range strings.Split(output, "\n") { - if highlight { - if first { - // we need to add two spaces to the header line - // because tablewriter omits them for some reason - // in pprint mode. This doesn't matter as long as - // we don't use colorization. But with colors the - // missing spaces can be seen. - if conf.OutputMode == cfg.ASCII { - line += " " - } - - line = conf.HighlightHdrStyle.Sprint(line) - first = false - } else { - line = conf.HighlightStyle.Sprint(line) - } - } else { - line = conf.NoHighlightStyle.Sprint(line) + for idx, line := range strings.Split(output, "\n") { + if idx == 0 { + style = conf.HighlightHdrStyle } + switch highlight { + case true: + if idx > 0 { + style = conf.HighlightStyle + } + case false: + style = conf.NoHighlightStyle + } + + line = style.Sprint(line) + highlight = !highlight colorized += line + "\n" @@ -281,7 +279,7 @@ func colorizeData(conf cfg.Config, output string) string { return colorized - case len(conf.Patterns) > 0 && !conf.NoColor && color.IsConsole(os.Stdout): + case len(conf.Patterns) > 0: out := output for _, re := range conf.Patterns { diff --git a/lib/printer.go b/lib/printer.go index 72ba39e..9bd9f1f 100644 --- a/lib/printer.go +++ b/lib/printer.go @@ -1,5 +1,5 @@ /* -Copyright © 2022-2025 Thomas von Dein +Copyright © 2022-2026 Thomas von Dein This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -23,6 +23,7 @@ import ( "fmt" "io" "log" + "os" "strconv" "strings" "text/template" @@ -75,8 +76,8 @@ func printData(writer io.Writer, conf cfg.Config, data *Tabdata) { } } -func output(writer io.Writer, str string) { - _, err := fmt.Fprint(writer, str) +func output(writer io.Writer, conf cfg.Config, str string) { + _, err := fmt.Fprint(writer, colorizeOutput(conf, str)) if err != nil { log.Fatalf("failed to print output: %s", err) } @@ -138,7 +139,7 @@ func printOrgmodeData(writer io.Writer, conf cfg.Config, data *Tabdata) { log.Fatalf("Failed to render table: %s", err) } - output(writer, color.Sprint(colorizeData(conf, tableString.String()))) + output(writer, conf, color.Sprint(colorizeData(conf, tableString.String()))) } /* @@ -197,7 +198,7 @@ func printMarkdownData(writer io.Writer, conf cfg.Config, data *Tabdata) { log.Fatalf("Failed to render table: %s", err) } - output(writer, color.Sprint(colorizeData(conf, tableString.String()))) + output(writer, conf, color.Sprint(colorizeData(conf, tableString.String()))) } /* @@ -254,7 +255,7 @@ func printASCIIData(writer io.Writer, conf cfg.Config, data *Tabdata) { log.Fatalf("Failed to render table: %s", err) } - output(writer, color.Sprint(colorizeData(conf, tableString.String()))) + output(writer, conf, color.Sprint(colorizeData(conf, tableString.String()))) } /* @@ -275,7 +276,7 @@ func printExtendedData(writer io.Writer, conf cfg.Config, data *Tabdata) { } } - output(writer, colorizeData(conf, out)) + output(writer, conf, colorizeData(conf, out)) } /* @@ -298,7 +299,7 @@ func printShellData(writer io.Writer, data *Tabdata) { } // no colorization here - output(writer, out) + output(writer, cfg.Config{}, out) } func printJsonData(writer io.Writer, data *Tabdata) { @@ -327,7 +328,7 @@ func printJsonData(writer io.Writer, data *Tabdata) { log.Fatal(err) } - output(writer, string(jsonstr)) + output(writer, cfg.Config{}, string(jsonstr)) } func printYamlData(writer io.Writer, data *Tabdata) { @@ -364,7 +365,7 @@ func printYamlData(writer io.Writer, data *Tabdata) { log.Fatal(err) } - output(writer, string(yamlstr)) + output(writer, cfg.Config{}, string(yamlstr)) } func printCSVData(writer io.Writer, conf cfg.Config, data *Tabdata) { @@ -414,3 +415,23 @@ func printTemplateData(writer io.Writer, conf cfg.Config, data *Tabdata) { log.Fatalf("failed to print output: %s", err) } } + +func colorizeOutput(conf cfg.Config, input string) string { + if len(conf.UseColorizers) > 0 && !conf.NoColor && color.IsConsole(os.Stdout) { + for _, colorizer := range conf.UseColorizers { + // colorize matching parts of the whole output with given color, if the terminal supports it + // color may contain fg:bg or just fg. Color definitions see https://github.com/gookit/color + input = colorizer.Search.ReplaceAllStringFunc(input, func(in string) string { + col := colorizer.Replace + if strings.Contains(col, ":") { + parts := strings.Split(col, ":") + return color.Sprintf("%s", parts[0], parts[1], in) + } + + return color.Sprintf("%s", col, in) + }) + } + } + + return input +} diff --git a/t/testtable6.csv b/t/testtable6.csv new file mode 100644 index 0000000..4c9c01b --- /dev/null +++ b/t/testtable6.csv @@ -0,0 +1,5 @@ +Date,Account Number,Subject,Amount +20250101,968723487,Dogs Medication Invoice 9919292,-450.00 +20250103,172747812,Tax return tax id HHD813D/12564H,+912.14 +20250105,987122711,Car repair order 020123,-299.45 +20250108,731217273,Rent - 12234 Sunset Blvd,-2960.00 diff --git a/tablizer.1 b/tablizer.1 index 87e4c00..e82b6f8 100644 --- a/tablizer.1 +++ b/tablizer.1 @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "TABLIZER 1" -.TH TABLIZER 1 "2025-12-08" "1" "User Commands" +.TH TABLIZER 1 "2026-01-19" "1" "User Commands" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -158,6 +158,7 @@ tablizer \- Manipulate tabular output of other programs \& \-F, \-\-filter Filter given field with regex, can be used multiple times \& \-T, \-\-transpose\-columns string Transpose the speficied columns (separated by ,) \& \-R, \-\-regex\-transposer Apply /search/replace/ regexp to fields given in \-T +\& \-K \-\-regex\-colorizer /from/color/ colorize pattern of output (color: fg[:bg]) \& \-j, \-\-json Read JSON input (must be array of hashes) \& \-I, \-\-interactive Interactively filter and select rows \& \-g, \-\-auto\-headers Generate headers if there are none present in input @@ -682,9 +683,22 @@ black, blue, cyan, darkGray, default, green, lightBlue, lightCyan, lightGreen, lightMagenta, lightRed, lightWhite, lightYellow, magenta, red, white, yellow .PP -The Variables \fB\s-1FG\s0\fR and \fB\s-1BG\s0\fR are being used to highlight matches. The -other *FG and *BG variables are for colored table output (enabled with -the \f(CW\*(C`\-L\*(C'\fR parameter). +but you may also use \s-1HTML\s0 color codes without the hash sign. +.PP +The Variables \fB\s-1FG\s0\fR and \fB\s-1BG\s0\fR are being used to highlight matching +rows. The other *FG and *BG variables are for colored table output +(enabled with the \f(CW\*(C`\-L\*(C'\fR parameter). +.PP +You can also use the option \f(CW\*(C`\-K\*(C'\fR to colorize particular patterns, not +whole lines. The option can be given multiple times and expects the +following parameter: +.PP +.Vb 1 +\& \-K \*(Aq/regex/foreground[:background]/ +.Ve +.PP +that is, background color is optional. This colorization will applied +on top of any previous colorizations, if any. .PP Colorization can be turned off completely either by setting the parameter \f(CW\*(C`\-N\*(C'\fR or the environment variable \fB\s-1NO_COLOR\s0\fR to a true value. diff --git a/tablizer.pod b/tablizer.pod index 740cdb0..4884981 100644 --- a/tablizer.pod +++ b/tablizer.pod @@ -19,6 +19,7 @@ tablizer - Manipulate tabular output of other programs -F, --filter Filter given field with regex, can be used multiple times -T, --transpose-columns string Transpose the speficied columns (separated by ,) -R, --regex-transposer Apply /search/replace/ regexp to fields given in -T + -K --regex-colorizer /from/color/ colorize pattern of output (color: fg[:bg]) -j, --json Read JSON input (must be array of hashes) -I, --interactive Interactively filter and select rows -g, --auto-headers Generate headers if there are none present in input @@ -514,9 +515,20 @@ black, blue, cyan, darkGray, default, green, lightBlue, lightCyan, lightGreen, lightMagenta, lightRed, lightWhite, lightYellow, magenta, red, white, yellow -The Variables B and B are being used to highlight matches. The -other *FG and *BG variables are for colored table output (enabled with -the C<-L> parameter). +but you may also use HTML color codes without the hash sign. + +The Variables B and B are being used to highlight matching +rows. The other *FG and *BG variables are for colored table output +(enabled with the C<-L> parameter). + +You can also use the option C<-K> to colorize particular patterns, not +whole lines. The option can be given multiple times and expects the +following parameter: + + -K '/regex/foreground[:background]/ + +that is, background color is optional. This colorization will applied +on top of any previous colorizations, if any. Colorization can be turned off completely either by setting the parameter C<-N> or the environment variable B to a true value.