diff --git a/examples/filterapi/main.go b/examples/filterapi/main.go new file mode 100644 index 0000000..66bd731 --- /dev/null +++ b/examples/filterapi/main.go @@ -0,0 +1,125 @@ +package main + +import ( + "github.com/charmbracelet/bubbles/textinput" + "log" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/evertras/bubble-table/table" +) + +const ( + columnKeyTitle = "title" + columnKeyAuthor = "author" + columnKeyDescription = "description" +) + +type Model struct { + table table.Model + filterTextInput textinput.Model +} + +func NewModel() Model { + columns := []table.Column{ + table.NewColumn(columnKeyTitle, "Title", 13).WithFiltered(true), + table.NewColumn(columnKeyAuthor, "Author", 13).WithFiltered(true), + table.NewColumn(columnKeyDescription, "Description", 50), + } + return Model{ + table: table. + New(columns). + Filtered(true). + Focused(true). + WithFooterVisibility(false). + WithPageSize(10). + WithRows([]table.Row{ + table.NewRow(table.RowData{ + columnKeyTitle: "Computer Systems : A Programmer's Perspective", + columnKeyAuthor: "Randal E. Bryant、David R. O'Hallaron / Prentice Hall ", + columnKeyDescription: "This book explains the important and enduring concepts underlying all computer...", + }), + table.NewRow(table.RowData{ + columnKeyTitle: "Effective Java : 3rd Edition", + columnKeyAuthor: "Joshua Bloch", + columnKeyDescription: "The Definitive Guide to Java Platform Best Practices—Updated for Java 9 Java ...", + }), + table.NewRow(table.RowData{ + columnKeyTitle: "Structure and Interpretation of Computer Programs - 2nd Edition (MIT)", + columnKeyAuthor: "Harold Abelson、Gerald Jay Sussman", + columnKeyDescription: "Structure and Interpretation of Computer Programs has had a dramatic impact on...", + }), + table.NewRow(table.RowData{ + columnKeyTitle: "Game Programming Patterns", + columnKeyAuthor: "Robert Nystrom / Genever Benning", + columnKeyDescription: "The biggest challenge facing many game programmers is completing their game. M...", + }), + }), + filterTextInput: textinput.New(), + } +} + +func (m Model) Init() tea.Cmd { + return nil +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var ( + cmd tea.Cmd + cmds []tea.Cmd + ) + + switch msg := msg.(type) { + case tea.KeyMsg: + // global + if msg.String() == "ctrl+c" { + cmds = append(cmds, tea.Quit) + return m, tea.Batch(cmds...) + } + // event to filter + if m.filterTextInput.Focused() { + if msg.String() == "enter" { + m.filterTextInput.Blur() + } else { + m.filterTextInput, cmd = m.filterTextInput.Update(msg) + } + m.table = m.table.WithFilterInput(m.filterTextInput) + return m, tea.Batch(cmds...) + } + + // others component + switch msg.String() { + case "/": + m.filterTextInput.Focus() + case "q": + cmds = append(cmds, tea.Quit) + return m, tea.Batch(cmds...) + default: + m.table, cmd = m.table.Update(msg) + cmds = append(cmds, cmd) + } + + } + + return m, tea.Batch(cmds...) +} + +func (m Model) View() string { + body := strings.Builder{} + + body.WriteString("A filtered simple default table\n" + + "Currently filter by Title and Author, press / + letters to start filtering, and escape to clear filter.\nPress q or ctrl+c to quit\n\n") + + body.WriteString(m.filterTextInput.View() + "\n") + body.WriteString(m.table.View()) + + return body.String() +} + +func main() { + p := tea.NewProgram(NewModel()) + + if err := p.Start(); err != nil { + log.Fatal(err) + } +} diff --git a/table/filter_test.go b/table/filter_test.go index 56cc772..3eb987b 100644 --- a/table/filter_test.go +++ b/table/filter_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/stretchr/testify/assert" ) @@ -179,3 +180,27 @@ func TestGetFilteredRowsRefocusAfterFilter(t *testing.T) { assert.Equal(t, 1, model.MaxPages()) assert.Equal(t, 0, model.TotalRows()) } + +func TestFilterWithExternalTextInput(t *testing.T) { + columns := []Column{NewColumn("title", "title", 10).WithFiltered(true)} + rows := []Row{ + NewRow(RowData{ + "title": "AAA", + "description": "", + }), + NewRow(RowData{ + "title": "BBB", + "description": "", + }), + // Empty + NewRow(RowData{}), + } + model := New(columns).WithRows(rows).Filtered(true) + input := textinput.New() + input.SetValue("AaA") + model = model.WithFilterInput(input) + + filteredRows := model.getFilteredRows(rows) + + assert.Len(t, filteredRows, 1) +} diff --git a/table/footer.go b/table/footer.go index 2af09ac..4fd5547 100644 --- a/table/footer.go +++ b/table/footer.go @@ -6,7 +6,7 @@ import ( ) func (m Model) hasFooter() bool { - return m.staticFooter != "" || m.pageSize != 0 || m.filtered + return m.footerVisible && (m.staticFooter != "" || m.pageSize != 0 || m.filtered) } func (m Model) renderFooter() string { @@ -25,7 +25,7 @@ func (m Model) renderFooter() string { sections := []string{} if m.filtered && (m.filterTextInput.Focused() || m.filterTextInput.Value() != "") { - sections = append(sections, fmt.Sprintf("/%s", m.filterTextInput.View())) + sections = append(sections, m.filterTextInput.View()) } // paged feature enabled diff --git a/table/model.go b/table/model.go index 6cfdd2c..5ef9e90 100644 --- a/table/model.go +++ b/table/model.go @@ -36,7 +36,8 @@ type Model struct { unselectedText string // Footers - staticFooter string + footerVisible bool + staticFooter string // Pagination pageSize int @@ -60,11 +61,12 @@ type Model struct { // New creates a new table ready for further modifications. func New(columns []Column) Model { filterInput := textinput.New() - filterInput.Prompt = "" + filterInput.Prompt = "/" model := Model{ columns: make([]Column, len(columns)), highlightStyle: defaultHighlightStyle.Copy(), border: borderDefault, + footerVisible: true, keyMap: DefaultKeyMap(), selectedText: "[x]", diff --git a/table/options.go b/table/options.go index d40a49e..a02b397 100644 --- a/table/options.go +++ b/table/options.go @@ -1,6 +1,7 @@ package table import ( + "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/lipgloss" ) @@ -249,3 +250,19 @@ func (m Model) WithColumns(columns []Column) Model { return m } + +// WithFilterInput makes the table use the provided text input bubble for +// filtering rather than using the built-in default. This allows for external +// text input controls to be used. +func (m Model) WithFilterInput(input textinput.Model) Model { + m.filterTextInput = input + + return m +} + +// WithFooterVisibility sets the visibility of the footer. +func (m Model) WithFooterVisibility(visibility bool) Model { + m.footerVisible = visibility + + return m +} diff --git a/table/options_test.go b/table/options_test.go index 1e173d9..a7b336b 100644 --- a/table/options_test.go +++ b/table/options_test.go @@ -129,4 +129,11 @@ func TestPageOptions(t *testing.T) { model = model.WithCurrentPage(6) assert.Equal(t, 6, model.CurrentPage()) assert.Equal(t, 26, model.rowCursorIndex) + + model = model.WithFooterVisibility(false) + assert.Equal(t, "", model.renderFooter()) + + model = model.WithFooterVisibility(true) + assert.Greater(t, len(model.renderFooter()), 10) + assert.Contains(t, model.renderFooter(), "6/6") }