Files
ilovetui/modal

modal

A centered popup box on top of a dimmed background, triggered from anywhere in a bubbletea program via an exported tea.Msg (ShowMsg/Show) rather than a direct reference to the Model that ends up rendering it - standard Elm architecture, no IPC between processes.

It composites over an already-rendered string, so it has no dependency on github.com/anotherhadi/ilovetui/layout: the same Model works whether the host uses layout for its main content or not.

Quick start

import (
	"github.com/anotherhadi/ilovetui/modal"
)

type model struct {
	m             modal.Model
	width, height int
}

func newModel() model {
	return model{m: modal.New()}
}

func (m model) Init() tea.Cmd { return m.m.Init() }

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		if msg.String() == "d" {
			return m, modal.Show("Delete file?", "This can't be undone.\n\ny: confirm  esc: cancel")
		}
		if msg.String() == "esc" && m.m.Open() {
			return m, modal.Close()
		}
	}

	var cmd tea.Cmd
	m.m, cmd = m.m.Update(msg)
	return m, cmd
}

func (m model) View() tea.View {
	background := renderYourUI(m.width, m.height)
	view := tea.NewView(m.m.Render(background))
	view.AltScreen = true
	return view
}

Any component in the same bubbletea program can trigger a modal via modal.Show, without holding a reference to the modal.Model that will actually render it - that Model just needs to see every tea.Msg the program produces (i.e. get its Update called from the top-level Update), same as any other child model.

Showing and dismissing

return m, modal.Show("Delete file?", "This can't be undone.", modal.WithID("confirm"))

return m, modal.Dismiss("confirm") // close a specific modal by id
return m, modal.Close()            // close whichever modal is on top, whatever its id
  • modal.Open() reports whether at least one modal is currently shown - handy for a host that wants to route key presses to the modal (e.g. esc to dismiss, enter to confirm) instead of its normal UI while one is open.
  • modal.TopID() returns the id of the topmost (currently interactive) modal, or "" if none is open - useful to tell which modal a generic key like enter should act on.
  • A modal shown without WithID gets an auto-generated id that's never returned to the caller - only a modal shown with WithID can be targeted by Dismiss later. Showing again with the same id replaces it in place instead of stacking a duplicate.

Stacking

Modals stack: showing a second one while the first is still open pushes it on top, dimming both the background and the first modal to the same flat color. Dismissing the top one reveals the one beneath, still in full color. This is what lets a "delete?" confirmation open a nested "really sure?" modal without any special-casing.

Styling

m := modal.New(modal.WithMaxWidth(60), modal.WithMaxHeight(20), modal.WithStyles(myStyles))

return m, modal.Show("Title", "Message", modal.WithModalStyle(oneOffStyles))

WithMaxWidth/WithMaxHeight cap how large a modal box can grow before wrapping/truncating; a modal narrower than the cap shrinks to fit its content instead of padding out to it. A modal can also never overflow past the edge of whatever background it's rendered on, regardless of these caps. WithStyles sets the default look for every modal shown by this Model; WithModalStyle (a Show option) overrides it for one modal alone. DefaultStyles() builds from style.S: the box borrows PanelFocused's border (the modal is what has focus while it's open), the dim color reuses Subtle.

Examples

  • examples/modal - open/dismiss, nested modals, styled from theme colors.