diff --git a/README.md b/README.md
index 8fcf288..27b5adc 100644
--- a/README.md
+++ b/README.md
@@ -6,25 +6,29 @@
Tablizer can be used to re-format tabular output of other
programs. While you could do this using standard unix tools, in some
-cases it's a hard job.
+cases it's a hard job. With tablizer you can filter by column[s],
+ignore certain column[s] by regex, name or number. It can output the
+tabular data in a range of formats (see below). There's even an
+interactive filter/selection tool available.
Usage:
```default
Usage:
- tablizer [regex] [file, ...] [flags]
+ tablizer [regex,...] [file, ...] [flags]
Operational Flags:
-c, --columns string Only show the speficied columns (separated by ,)
-v, --invert-match select non-matching rows
- -n, --no-numbering Disable header numbering
+ -n, --numbering Enable header numbering
-N, --no-color Disable pattern highlighting
-H, --no-headers Disable headers display
-s, --separator string Custom field separator
- -k, --sort-by int Sort by column (default: 1)
+ -k, --sort-by int|name Sort by column (default: 1)
-z, --fuzzy Use fuzzy search [experimental]
-F, --filter field[!]=reg Filter given field with regex, can be used multiple times
-T, --transpose-columns string Transpose the speficied columns (separated by ,)
-R, --regex-transposer /from/to/ Apply /search/replace/ regexp to fields given in -T
+ -I, --interactive Interactively filter and select rows
Output Flags (mutually exclusive):
-X, --extended Enable extended output
diff --git a/cfg/config.go b/cfg/config.go
index 06634cc..04ad989 100644
--- a/cfg/config.go
+++ b/cfg/config.go
@@ -78,6 +78,7 @@ type Config struct {
Patterns []*Pattern
UseFuzzySearch bool
UseHighlight bool
+ Interactive bool
SortMode string
SortDescending bool
diff --git a/cmd/root.go b/cmd/root.go
index e21b47d..e3bf795 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -73,7 +73,7 @@ func Execute() {
}
if ShowManual {
- Pager("tablizer manual page", manpage)
+ lib.Pager("tablizer manual page", manpage)
return
}
@@ -130,6 +130,8 @@ func Execute() {
"Yank the speficied columns (separated by ,) to the clipboard")
rootCmd.PersistentFlags().StringVarP(&conf.TransposeColumns, "transpose-columns", "T", "",
"Transpose the speficied columns (separated by ,)")
+ rootCmd.PersistentFlags().BoolVarP(&conf.Interactive, "interactive", "I", false,
+ "interactive mode (experimental)")
// sort options
rootCmd.PersistentFlags().StringVarP(&conf.SortByColumn, "sort-by", "k", "",
diff --git a/cmd/tablizer.go b/cmd/tablizer.go
index d7d95da..9385733 100644
--- a/cmd/tablizer.go
+++ b/cmd/tablizer.go
@@ -20,6 +20,7 @@ SYNOPSIS
-F, --filter field[!]=reg Filter given field with regex, can be used multiple times
-T, --transpose-columns string Transpose the speficied columns (separated by ,)
-R, --regex-transposer /from/to/ Apply /search/replace/ regexp to fields given in -T
+ -I, --interactive Interactively filter and select rows
Output Flags (mutually exclusive):
-X, --extended Enable extended output
@@ -189,6 +190,20 @@ DESCRIPTION
If the option -v is specified, the filtering is inverted.
+ INTERACTIVE FILTERING
+ You can also use the interactive mode, enabled with "-I" to filter and
+ select rows. This mode is complementary, that is, other filter options
+ are still being respected.
+
+ To enter e filter, hit "/", enter a filter string and finish with
+ "ENTER". Use "SPACE" to select/deselect rows, use "a" to select all
+ (visible) rows.
+
+ Commit your selection with "q". The selected rows are being fed to the
+ requested output mode as usual. Abort with "CTRL-c", in which case the
+ results of the interactive mode are being ignored and all rows are being
+ fed to output.
+
COLUMNS
The parameter -c can be used to specify, which columns to display. By
default tablizer numerizes the header names and these numbers can be
@@ -416,6 +431,9 @@ LICENSE
Released under the MIT License, Copyright (c) 2006-2011 Kirill
Simonov
+ bubble-table (https://github.com/Evertras/bubble-table)
+ Released under the MIT License, Copyright (c) 2022 Brandon Fulljames
+
AUTHORS
Thomas von Dein tom AT vondein DOT org
@@ -437,6 +455,7 @@ Operational Flags:
-F, --filter field[!]=reg Filter given field with regex, can be used multiple times
-T, --transpose-columns string Transpose the speficied columns (separated by ,)
-R, --regex-transposer /from/to/ Apply /search/replace/ regexp to fields given in -T
+ -I, --interactive Interactively filter and select rows
Output Flags (mutually exclusive):
-X, --extended Enable extended output
diff --git a/go.mod b/go.mod
index 4f89944..b4c084f 100644
--- a/go.mod
+++ b/go.mod
@@ -10,6 +10,7 @@ require (
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.4
github.com/charmbracelet/lipgloss v1.1.0
+ github.com/evertras/bubble-table v0.17.2
github.com/gookit/color v1.5.4
github.com/hashicorp/hcl/v2 v2.24.0
github.com/lithammer/fuzzysearch v1.1.8
@@ -23,10 +24,11 @@ require (
require (
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
+ github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
- github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
+ github.com/charmbracelet/colorprofile v0.3.1 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
- github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
+ github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.18.0 // indirect
@@ -40,6 +42,7 @@ require (
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
@@ -48,6 +51,7 @@ require (
github.com/spf13/pflag v1.0.6 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/zclconf/go-cty v1.16.3 // indirect
+ golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
diff --git a/go.sum b/go.sum
index 7824f73..9324412 100644
--- a/go.sum
+++ b/go.sum
@@ -6,20 +6,22 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
-github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
-github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
+github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
+github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
-github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
-github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
+github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
+github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -28,6 +30,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
+github.com/evertras/bubble-table v0.17.2 h1:4MtLO888s2xb94OG3KqJCIEav6gE3V4ob56hmOammf0=
+github.com/evertras/bubble-table v0.17.2/go.mod h1:ifHujS1YxwnYSOgcR2+m3GnJ84f7CVU/4kUOxUCjEbQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
@@ -51,6 +55,7 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
@@ -59,6 +64,8 @@ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
@@ -98,8 +105,8 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
-golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
diff --git a/lib/io.go b/lib/io.go
index 3e39ef8..8c40079 100644
--- a/lib/io.go
+++ b/lib/io.go
@@ -58,6 +58,15 @@ func ProcessFiles(conf *cfg.Config, args []string) error {
return err
}
+ if conf.Interactive {
+ newdata, err := tableEditor(conf, &data)
+ if err != nil {
+ return err
+ }
+
+ data = *newdata
+ }
+
printData(os.Stdout, *conf, &data)
return nil
diff --git a/cmd/pager.go b/lib/pager.go
similarity index 91%
rename from cmd/pager.go
rename to lib/pager.go
index 5185d23..b7e2910 100644
--- a/cmd/pager.go
+++ b/lib/pager.go
@@ -1,4 +1,4 @@
-package cmd
+package lib
// pager setup using bubbletea
// file shamlelessly copied from:
@@ -28,18 +28,18 @@ var (
}()
)
-type model struct {
+type Doc struct {
content string
title string
ready bool
viewport viewport.Model
}
-func (m model) Init() tea.Cmd {
+func (m Doc) Init() tea.Cmd {
return nil
}
-func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m Doc) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
@@ -79,21 +79,21 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
}
-func (m model) View() string {
+func (m Doc) View() string {
if !m.ready {
return "\n Initializing..."
}
return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView())
}
-func (m model) headerView() string {
+func (m Doc) headerView() string {
// title := titleStyle.Render("RPN Help Overview")
title := titleStyle.Render(m.title)
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title)))
return lipgloss.JoinHorizontal(lipgloss.Center, title, line)
}
-func (m model) footerView() string {
+func (m Doc) footerView() string {
info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100))
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info)))
return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
@@ -108,7 +108,7 @@ func max(a, b int) int {
func Pager(title, message string) {
p := tea.NewProgram(
- model{content: message, title: title},
+ Doc{content: message, title: title},
tea.WithAltScreen(), // use the full size of the terminal in its "alternate screen buffer"
tea.WithMouseCellMotion(), // turn on mouse support so we can track the mouse wheel
)
diff --git a/lib/tableeditor.go b/lib/tableeditor.go
new file mode 100644
index 0000000..867ae52
--- /dev/null
+++ b/lib/tableeditor.go
@@ -0,0 +1,271 @@
+/*
+Copyright © 2025 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
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package lib
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/evertras/bubble-table/table"
+ "github.com/tlinden/tablizer/cfg"
+)
+
+type FilterTable struct {
+ Table table.Model
+
+ // Window dimensions
+ totalWidth int
+ totalHeight int
+
+ // Table dimensions
+ horizontalMargin int
+ verticalMargin int
+
+ Rows int
+
+ quitting bool
+ unchanged bool
+}
+
+const (
+ // Add a fixed margin to account for description & instructions
+ fixedVerticalMargin = 0
+
+ ExtraRows = 8
+
+ HELP = "/:filter esc:clear-filter q:commit c-c:abort space:select a:select-all | "
+)
+
+var (
+ customBorder = table.Border{
+ Top: "─",
+ Left: "│",
+ Right: "│",
+ Bottom: "─",
+
+ TopRight: "╮",
+ TopLeft: "╭",
+ BottomRight: "╯",
+ BottomLeft: "╰",
+
+ TopJunction: "┬",
+ LeftJunction: "├",
+ RightJunction: "┤",
+ BottomJunction: "┴",
+ InnerJunction: "┼",
+
+ InnerDivider: "│",
+ }
+)
+
+func NewModel(data *Tabdata) FilterTable {
+ columns := make([]table.Column, len(data.headers))
+ rows := make([]table.Row, len(data.entries))
+ lengths := make([]int, len(data.headers))
+
+ // give columns at least the header width
+ for idx, header := range data.headers {
+ lengths[idx] = len(header)
+ }
+
+ // determine max width per column
+ for _, entry := range data.entries {
+ for i, cell := range entry {
+ if len(cell) > lengths[i] {
+ lengths[i] = len(cell)
+ }
+ }
+ }
+
+ // setup column data
+ for idx, header := range data.headers {
+ columns[idx] = table.NewColumn(strings.ToLower(header), header, lengths[idx]+2).
+ WithFiltered(true)
+ }
+
+ // setup table data
+ for idx, entry := range data.entries {
+ rowdata := make(table.RowData, len(entry))
+
+ for i, cell := range entry {
+ rowdata[strings.ToLower(data.headers[i])] = cell + " "
+ }
+
+ rows[idx] = table.NewRow(rowdata)
+ }
+
+ keys := table.DefaultKeyMap()
+ keys.RowDown.SetKeys("j", "down", "s")
+ keys.RowUp.SetKeys("k", "up", "w")
+
+ // our final interactive table filled with our prepared data
+ return FilterTable{
+ Table: table.New(columns).
+ WithRows(rows).
+ WithKeyMap(keys).
+ Filtered(true).
+ Focused(true).
+ SelectableRows(true).
+ WithSelectedText(" ", "✓").
+ WithFooterVisibility(true).
+ WithHeaderVisibility(true).
+ Border(customBorder),
+ horizontalMargin: 10,
+ Rows: len(data.entries),
+ }
+}
+
+func (m FilterTable) ToggleSelected() {
+ rows := m.Table.GetVisibleRows()
+ selected := m.Table.SelectedRows()
+
+ if len(selected) > 0 {
+ for i, row := range selected {
+ rows[i] = row.Selected(false)
+ }
+ } else {
+ for i, row := range rows {
+ rows[i] = row.Selected(true)
+ }
+ }
+
+ m.Table.WithRows(rows)
+}
+
+func (m FilterTable) Init() tea.Cmd {
+ return nil
+}
+
+func (m FilterTable) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var (
+ cmd tea.Cmd
+ cmds []tea.Cmd
+ )
+
+ m.Table, cmd = m.Table.Update(msg)
+ cmds = append(cmds, cmd)
+
+ if !m.Table.GetIsFilterInputFocused() {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch msg.String() {
+ case "q":
+ m.quitting = true
+ m.unchanged = false
+ cmds = append(cmds, tea.Quit)
+
+ case "ctrl+c":
+ m.quitting = true
+ m.unchanged = true
+ cmds = append(cmds, tea.Quit)
+
+ case "a":
+ m.ToggleSelected()
+ }
+ case tea.WindowSizeMsg:
+ m.totalWidth = msg.Width
+ m.totalHeight = msg.Height
+
+ m.recalculateTable()
+ }
+ }
+ m.updateFooter()
+
+ return m, tea.Batch(cmds...)
+}
+
+func (m *FilterTable) updateFooter() {
+ selected := m.Table.SelectedRows()
+ footer := fmt.Sprintf("selected: %d", len(selected))
+
+ if m.Table.GetIsFilterInputFocused() {
+ footer = fmt.Sprintf("/%s %s", m.Table.GetCurrentFilter(), footer)
+ } else if m.Table.GetIsFilterActive() {
+ footer = fmt.Sprintf("Filter: %s %s", m.Table.GetCurrentFilter(), footer)
+ }
+
+ m.Table = m.Table.WithStaticFooter(HELP + footer)
+}
+
+func (m *FilterTable) recalculateTable() {
+ m.Table = m.Table.
+ WithTargetWidth(m.calculateWidth()).
+ WithMinimumHeight(m.calculateHeight()).
+ WithPageSize(m.calculateHeight() - ExtraRows)
+}
+
+func (m FilterTable) calculateWidth() int {
+ return m.totalWidth - m.horizontalMargin
+}
+
+func (m FilterTable) calculateHeight() int {
+ if m.Rows+ExtraRows < m.totalHeight {
+ // FIXME: avoid full screen somehow
+ return m.Rows + ExtraRows
+ }
+
+ return m.totalHeight - m.verticalMargin - fixedVerticalMargin
+}
+
+func (m FilterTable) View() string {
+ body := strings.Builder{}
+
+ if !m.quitting {
+ body.WriteString(m.Table.View())
+ }
+
+ return body.String()
+}
+
+func tableEditor(conf *cfg.Config, data *Tabdata) (*Tabdata, error) {
+ // we render to STDERR to avoid dead lock when the user redirects STDOUT
+ // see https://github.com/charmbracelet/bubbletea/issues/860
+ lipgloss.SetDefaultRenderer(lipgloss.NewRenderer(os.Stderr))
+
+ program := tea.NewProgram(
+ NewModel(data),
+ tea.WithOutput(os.Stderr),
+ tea.WithAltScreen())
+
+ m, err := program.Run()
+
+ if err != nil {
+ return nil, err
+ }
+
+ if m.(FilterTable).unchanged {
+ return data, err
+ }
+
+ table := m.(FilterTable).Table
+ data.entries = make([][]string, len(table.SelectedRows()))
+
+ for pos, row := range m.(FilterTable).Table.SelectedRows() {
+ entry := make([]string, len(data.headers))
+ for idx, field := range data.headers {
+ entry[idx] = row.Data[strings.ToLower(field)].(string)
+ }
+
+ data.entries[pos] = entry
+ }
+
+ return data, err
+}
diff --git a/tablizer.1 b/tablizer.1
index 0268132..415e68e 100644
--- a/tablizer.1
+++ b/tablizer.1
@@ -133,7 +133,7 @@
.\" ========================================================================
.\"
.IX Title "TABLIZER 1"
-.TH TABLIZER 1 "2025-03-06" "1" "User Commands"
+.TH TABLIZER 1 "2025-08-28" "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 field[!]=reg Filter given field with regex, can be used multiple times
\& \-T, \-\-transpose\-columns string Transpose the speficied columns (separated by ,)
\& \-R, \-\-regex\-transposer /from/to/ Apply /search/replace/ regexp to fields given in \-T
+\& \-I, \-\-interactive Interactively filter and select rows
\&
\& Output Flags (mutually exclusive):
\& \-X, \-\-extended Enable extended output
@@ -349,6 +350,20 @@ These field filters can also be negated:
.Ve
.PP
If the option \fB\-v\fR is specified, the filtering is inverted.
+.SS "\s-1INTERACTIVE FILTERING\s0"
+.IX Subsection "INTERACTIVE FILTERING"
+You can also use the interactive mode, enabled with \f(CW\*(C`\-I\*(C'\fR to filter
+and select rows. This mode is complementary, that is, other filter
+options are still being respected.
+.PP
+To enter e filter, hit \f(CW\*(C`/\*(C'\fR, enter a filter string and finish with
+\&\f(CW\*(C`ENTER\*(C'\fR. Use \f(CW\*(C`SPACE\*(C'\fR to select/deselect rows, use \f(CW\*(C`a\*(C'\fR to select all
+(visible) rows.
+.PP
+Commit your selection with \f(CW\*(C`q\*(C'\fR. The selected rows are being fed to
+the requested output mode as usual. Abort with \f(CW\*(C`CTRL\-c\*(C'\fR, in which
+case the results of the interactive mode are being ignored and all
+rows are being fed to output.
.SS "\s-1COLUMNS\s0"
.IX Subsection "COLUMNS"
The parameter \fB\-c\fR can be used to specify, which columns to
@@ -615,6 +630,9 @@ Released under the \s-1MIT\s0 License, Copyright (c) 201 by Oleku Konko
.IP "yaml (gopkg.in/yaml.v3)" 4
.IX Item "yaml (gopkg.in/yaml.v3)"
Released under the \s-1MIT\s0 License, Copyright (c) 2006\-2011 Kirill Simonov
+.IP "bubble-table (https://github.com/Evertras/bubble\-table)" 4
+.IX Item "bubble-table (https://github.com/Evertras/bubble-table)"
+Released under the \s-1MIT\s0 License, Copyright (c) 2022 Brandon Fulljames
.SH "AUTHORS"
.IX Header "AUTHORS"
Thomas von Dein \fBtom \s-1AT\s0 vondein \s-1DOT\s0 org\fR
diff --git a/tablizer.pod b/tablizer.pod
index ec86063..5dbd166 100644
--- a/tablizer.pod
+++ b/tablizer.pod
@@ -19,6 +19,7 @@ tablizer - Manipulate tabular output of other programs
-F, --filter field[!]=reg Filter given field with regex, can be used multiple times
-T, --transpose-columns string Transpose the speficied columns (separated by ,)
-R, --regex-transposer /from/to/ Apply /search/replace/ regexp to fields given in -T
+ -I, --interactive Interactively filter and select rows
Output Flags (mutually exclusive):
-X, --extended Enable extended output
@@ -202,6 +203,20 @@ These field filters can also be negated:
If the option B<-v> is specified, the filtering is inverted.
+=head2 INTERACTIVE FILTERING
+
+You can also use the interactive mode, enabled with C<-I> to filter
+and select rows. This mode is complementary, that is, other filter
+options are still being respected.
+
+To enter e filter, hit C>, enter a filter string and finish with
+C. Use C to select/deselect rows, use C to select all
+(visible) rows.
+
+Commit your selection with C. The selected rows are being fed to
+the requested output mode as usual. Abort with C, in which
+case the results of the interactive mode are being ignored and all
+rows are being fed to output.
=head2 COLUMNS
@@ -465,6 +480,10 @@ Released under the MIT License, Copyright (c) 201 by Oleku Konko
Released under the MIT License, Copyright (c) 2006-2011 Kirill Simonov
+=item bubble-table (https://github.com/Evertras/bubble-table)
+
+Released under the MIT License, Copyright (c) 2022 Brandon Fulljames
+
=back
=head1 AUTHORS