feat: add generic event filter (#536)

`WithFilter` lets you supply an event filter that will be invoked
before Bubble Tea processes a `tea.Msg`. The event filter can return
any `tea.Msg` which will then get handled by Bubble Tea instead of
the original event. If the event filter returns nil, the event
will be ignored and Bubble Tea will not process it.

As an example, this could be used to prevent a program from
shutting down if there are unsaved changes.

Based on the fantastic work by @aschey and supersedes #521.

Resolves #472.
This commit is contained in:
Christian Muehlhaeuser
2023-04-17 22:02:55 +02:00
committed by GitHub
parent 8514d90b9e
commit c56884c0e2
5 changed files with 253 additions and 6 deletions

View File

@@ -0,0 +1,154 @@
package main
// A program demonstrating how to use the WithFilter option to intercept events.
import (
"fmt"
"log"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
choiceStyle = lipgloss.NewStyle().PaddingLeft(1).Foreground(lipgloss.Color("241"))
saveTextStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("170"))
quitViewStyle = lipgloss.NewStyle().Padding(1).Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color("170"))
)
func main() {
p := tea.NewProgram(initialModel(), tea.WithFilter(filter))
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
func filter(teaModel tea.Model, msg tea.Msg) tea.Msg {
if _, ok := msg.(tea.QuitMsg); !ok {
return msg
}
m := teaModel.(model)
if m.hasChanges {
return nil
}
return msg
}
type model struct {
textarea textarea.Model
help help.Model
keymap keymap
saveText string
hasChanges bool
quitting bool
}
type keymap struct {
save key.Binding
quit key.Binding
}
func initialModel() model {
ti := textarea.New()
ti.Placeholder = "Only the best words"
ti.Focus()
return model{
textarea: ti,
help: help.NewModel(),
keymap: keymap{
save: key.NewBinding(
key.WithKeys("ctrl+s"),
key.WithHelp("ctrl+s", "save"),
),
quit: key.NewBinding(
key.WithKeys("esc", "ctrl+c"),
key.WithHelp("esc", "quit"),
),
},
}
}
func (m model) Init() tea.Cmd {
return textarea.Blink
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.quitting {
return m.updatePromptView(msg)
}
return m.updateTextView(msg)
}
func (m model) updateTextView(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
m.saveText = ""
switch {
case key.Matches(msg, m.keymap.save):
m.saveText = "Changes saved!"
m.hasChanges = false
case key.Matches(msg, m.keymap.quit):
m.quitting = true
return m, tea.Quit
case msg.Type == tea.KeyRunes:
m.saveText = ""
m.hasChanges = true
fallthrough
default:
if !m.textarea.Focused() {
cmd = m.textarea.Focus()
cmds = append(cmds, cmd)
}
}
}
m.textarea, cmd = m.textarea.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m model) updatePromptView(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
// For simplicity's sake, we'll treat any key besides "y" as "no"
if key.Matches(msg, m.keymap.quit) || msg.String() == "y" {
m.hasChanges = false
return m, tea.Quit
}
m.quitting = false
}
return m, nil
}
func (m model) View() string {
if m.quitting {
if m.hasChanges {
text := lipgloss.JoinHorizontal(lipgloss.Top, "You have unsaved changes. Quit without saving?", choiceStyle.Render("[yn]"))
return quitViewStyle.Render(text)
}
return "Very important, thank you\n"
}
helpView := m.help.ShortHelpView([]key.Binding{
m.keymap.save,
m.keymap.quit,
})
return fmt.Sprintf(
"\nType some important things.\n\n%s\n\n %s\n %s",
m.textarea.View(),
saveTextStyle.Render(m.saveText),
helpView,
) + "\n\n"
}