An advanced terminal viewport component for Bubble Tea terminal UI (TUI) applications.
go get github.com/robinovitch61/viewportCore viewport:
- Toggleable text wrapping
- Horizontal panning for unwrapped lines
- ANSI escape code and Unicode support
- Individual item selection
- Customizable styling
- Sticky top/bottom scrolling (auto-follow new content)
- Configurable sticky header
- Highlight ranges with custom styles
- Save viewport content to file
- Efficient item concatenation (e.g. prefixing line numbers via
MultiItem)
The filterableviewport package wraps the core viewport and adds:
- Customizable filter modes (exact, regex, case-insensitive built in; custom modes supported)
- Match highlighting with focused/unfocused styles
- Next/previous match navigation
- Matches-only view (hide non-matching items)
- Configurable match limit for large content
- Search history (up/down arrow while editing)
Implement the Object interface on your type:
import (
"github.com/robinovitch61/viewport/viewport"
"github.com/robinovitch61/viewport/viewport/item"
)
type myObject struct {
item item.Item
}
func (o myObject) GetItem() item.Item {
return o.item
}Create a viewport and set content:
vp := viewport.New[myObject](
width, height,
viewport.WithSelectionEnabled[myObject](true),
viewport.WithWrapText[myObject](true),
)
objects := []myObject{
{item: item.NewItem("first line")},
{item: item.NewItem("second line")},
}
vp.SetObjects(objects)Wire it into your Bubble Tea model's Update and View:
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
m.viewport, cmd = m.viewport.Update(msg)
return m, cmd
}
func (m model) View() string {
return m.viewport.View()
}Wrap an existing viewport to add filtering:
import "github.com/robinovitch61/viewport/filterableviewport"
fvp := filterableviewport.New[myObject](
vp,
filterableviewport.WithPrefixText[myObject]("Filter:"),
filterableviewport.WithEmptyText[myObject]("No Current Filter"),
filterableviewport.WithMatchingItemsOnly[myObject](false),
filterableviewport.WithCanToggleMatchingItemsOnly[myObject](true),
)
fvp.SetObjects(objects)Use the built-in filter mode constructors with your own key bindings:
import "charm.land/bubbles/v2/key"
fvp := filterableviewport.New[myObject](
vp,
filterableviewport.WithFilterModes[myObject]([]filterableviewport.FilterMode{
filterableviewport.ExactFilterMode(
key.NewBinding(key.WithKeys("f"), key.WithHelp("f", "filter")),
),
filterableviewport.RegexFilterMode(
key.NewBinding(key.WithKeys("ctrl+r"), key.WithHelp("ctrl+r", "regex")),
),
filterableviewport.CaseInsensitiveFilterMode(
key.NewBinding(key.WithKeys("ctrl+i"), key.WithHelp("ctrl+i", "case insensitive")),
),
}),
)Define entirely custom filter logic. A FilterMode provides a name, key binding, label
shown in the filter line, and a GetMatchFunc that returns a MatchFunc for scanning items:
import (
"strings"
"charm.land/bubbles/v2/key"
"github.com/robinovitch61/viewport/filterableviewport"
"github.com/robinovitch61/viewport/viewport/item"
)
// Define a name for your custom filter mode.
const FilterPrefix filterableviewport.FilterModeName = "prefix"
// A filter that only matches lines starting with the filter text.
prefixMode := filterableviewport.FilterMode{
Name: FilterPrefix,
Key: key.NewBinding(key.WithKeys("p"), key.WithHelp("p", "prefix filter")),
Label: "[prefix]",
GetMatchFunc: func(filterText string) (filterableviewport.MatchFunc, error) {
return func(content string) []item.ByteRange {
if strings.HasPrefix(content, filterText) {
return []item.ByteRange{{Start: 0, End: len(filterText)}}
}
return nil
}, nil
},
}
fvp := filterableviewport.New[myObject](
vp,
filterableviewport.WithFilterModes[myObject]([]filterableviewport.FilterMode{
prefixMode,
filterableviewport.RegexFilterMode(
key.NewBinding(key.WithKeys("r"), key.WithHelp("r", "regex")),
),
}),
)Set and query the active filter using typed constants:
// Set an exact filter programmatically
fvp.SetFilter("error", filterableviewport.FilterExact)
// Set a regex filter
fvp.SetFilter("err(or|ing)", filterableviewport.FilterRegex)
// Clear the filter
fvp.SetFilter("", "")
// Check what mode is active
if mode := fvp.GetActiveFilterMode(); mode != nil {
fmt.Printf("Filtering with %s mode\n", mode.Name)
}Built-in filter mode names: FilterExact, FilterRegex, FilterCaseInsensitive, FilterFuzzy.
| Key | Action |
|---|---|
j / down |
Scroll down |
k / up |
Scroll up |
f / pgdown / ctrl+f |
Page down |
b / pgup / ctrl+b |
Page up |
d / ctrl+d |
Half page down |
u / ctrl+u |
Half page up |
g / ctrl+g |
Jump to top |
G |
Jump to bottom |
left / right |
Horizontal pan |
| Key | Action |
|---|---|
/ |
Start exact filter |
r |
Start regex filter |
i |
Start case-insensitive filter |
enter |
Apply filter |
esc |
Cancel/clear filter |
n |
Next match |
N (shift+n) |
Previous match |
o |
Toggle matches-only view |
up / down |
Browse search history (while editing) |
Filter mode keys (/, r, i) are defined on each FilterMode, not in the KeyMap.
All other key bindings are configurable via WithKeyMap.
See the examples directory for runnable programs:
- viewport -- core viewport with wrapping and selection toggles
- filterableviewport -- viewport with filtering, match navigation, and matches-only mode
go run ./examples/viewport
go run ./examples/filterableviewport