Files
Hadi aa6496901f up
Signed-off-by: Hadi <hadi@example.fr>
2026-08-18 16:15:03 +02:00

5.2 KiB

drawer

A full-height panel flush against the left or right edge of an already-rendered background, on top of it dimmed - the sidebar/drawer equivalent of modal, which this package otherwise mirrors closely: same stack of panels triggered from anywhere via an exported tea.Msg (ShowMsg/Show) rather than a direct reference to the Model that ends up rendering it, same composite-over-an-already-rendered-string Render, no assumption about how the host builds that string.

Quick start

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

type model struct {
	d             drawer.Model
	width, height int
}

func newModel() model {
	return model{d: drawer.New()}
}

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

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		if msg.String() == "l" {
			return m, drawer.Show("Nav", drawer.Text("Home\nProjects\nSettings"))
		}
		if msg.String() == "esc" && m.d.Open() {
			return m, drawer.Close()
		}
	}

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

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

Any component in the same bubbletea program can trigger a drawer via drawer.Show, without holding a reference to the drawer.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.

Content is a model

A drawer's body is a tea.Model, not a string. While a drawer is on top of the stack it gets every message the drawer.Model receives, its Init runs when it opens, and its commands come back out - so it can hold a file list, a filter form, or a picker that reports its choice with a tea.Msg of its own, which the component that opened it listens for:

type pickedMsg struct{ file string }

// somewhere else
return m, drawer.Show("Files", newFileList(dir), drawer.WithSide(drawer.Right))

Only the topmost drawer is updated: everything beneath it is dimmed and frozen until the drawers above it close.

For a drawer with nothing to interact with, drawer.Text wraps a plain string:

return m, drawer.Show("Nav", drawer.Text("Home\nProjects\nSettings"))

The box shrinks to fit whatever the content draws (unless WithWidth fixes it), so a content that wants a specific size sets it on itself - the drawer only ever sees the rendered result.

Side and width

return m, drawer.Show("Nav", drawer.Text("Home\nProjects\nSettings"),
	drawer.WithSide(drawer.Right), drawer.WithWidth(24))

WithSide anchors the drawer to drawer.Left (the default) or drawer.Right. WithWidth fixes the drawer's total width instead of shrinking to fit its content, still capped by whatever actually fits the background - same unit as the Model-level WithMaxWidth. Either way, the drawer always spans the background's full height, flush top to bottom.

Showing and closing

return m, drawer.Show("Files", newFileList(dir), drawer.WithSide(drawer.Right))

return m, drawer.Close() // close the topmost drawer

The stack is a plain LIFO, with no identity: a drawer is closed by being on top, never by being named. There is nothing to tag a drawer with, and nothing that can target one in the middle of the stack - the topmost is both the only one that receives messages and the only one Close can reach, so the two rules never disagree.

  • drawer.Open() reports whether at least one drawer is currently shown - handy for a host that wants to route key presses to the drawer instead of its normal UI while one is open. Note that a host doing this also makes it impossible for a key to open a second drawer, which is what keeps the stack shallow without any bookkeeping.
  • A content model closes its own drawer by returning drawer.Close(), since it only ever runs while it is the topmost one.

Stacking

Drawers stack: showing a second one while the first is still open pushes it on top, dimming both the background and the first drawer to the same flat color, same as modal. Opening a left drawer and a right drawer at once still stacks - if you want both visible at full color simultaneously, render them as two ordinary panels via layout instead; this package is for transient sidebars, not permanent chrome.

Styling

d := drawer.New(drawer.WithMaxWidth(30), drawer.WithStyles(myStyles))

return m, drawer.Show("Nav", drawer.Text("content"), drawer.WithDrawerStyle(oneOffStyles))

WithMaxWidth caps how wide a drawer can grow (border and padding included) before wrapping; a drawer narrower than the cap shrinks to fit its content instead of padding out to it, unless shown with WithWidth. A drawer can also never overflow past the edge of whatever background it's rendered on. WithStyles sets the default look for every drawer shown by this Model; WithDrawerStyle (a Show option) overrides it for one drawer alone. DefaultStyles() builds from style.S, same palette roles as modal.DefaultStyles().

Examples

  • examples/drawer - a left nav drawer and a right inspector drawer, either one at a time.