bloom

Troubleshooting

Fixes for common problems - a blank screen, clicks that don't arrive or arrive twice, animations that don't move, text that doesn't wrap, and more.

See what the layout did

bloom.Debug lays out a tree like Solve and returns it as text: every node, how it's sized, its ID, and where it went. Nodes that were collapsed, scrolled away, or only partly on screen are marked. Write it somewhere you can read while the program runs, such as a log file:

func (m model) View() tea.View {
	if m.debug {
		log.Print(bloom.Debug(m.layout(), m.width, m.height))
	}
	return m.ui.View(m.width, m.height, m.layout())
}
Column 0,0 30x5
├─ Row 0,0 30x3
│  ├─ Fixed(10) id="sidebar" hidden collapsible=1
│  │  └─ Box hidden
│  │     └─ Text "Inbox" hidden
│  └─ Fill id="reader" 0,0 30x3 min=24
│     └─ main.reader 0,0 30x3
└─ Text "ready" 0,3 30x1 truncate

Rectangles are x,y then width x height. Debug is stateless, so layout animations show where they're headed, not where they are mid-flight, and effects that move a node, such as fx.Slide, aren't applied. The format is for reading, and may change between versions.

The screen is blank

m.ui.View draws nothing while the width is zero, which it is until Bubble Tea sends the first tea.WindowSizeMsg. Store the size from that message in your model and pass it to View:

case tea.WindowSizeMsg:
	m.width, m.height = msg.Width, msg.Height

Clicks don't arrive

Check each of these:

  • The view turns on mouse reporting: v.MouseMode = tea.MouseModeCellMotion (or tea.MouseModeAllMotion for hover).
  • The handler is on the node, or on something it's inside, with On(...).
  • A node inside it doesn't have a handler of the same kind. The innermost one takes the event; see which node gets the event.
  • A click runs when the button comes up over the node it went down on. Releasing it elsewhere cancels the click.
  • A modal layer isn't open, taking the events.
  • The view comes from m.ui.View(...), which installs the OnMouse handler that runs your handlers. If you build the tea.View yourself or replace its OnMouse, pass every message to m.ui.Update, which then runs them.

A handler runs, but nothing changes

Handlers are closures made in View, so they change whatever View captured. If that's a copy, the change is lost:

  • View has a value receiver, func (m model) View(). Use a pointer receiver, or have the handler send a message with on.Msg(msg) and change the state in Update.
  • The handler is built in a component kept by value in its parent, such as a Bubbles-style list updated with m.list, cmd = m.list.Update(msg). Have the component send a message with on.Msg and handle it in its own Update, or build the handler in the parent over &m.list.

See change state, or send a message.

Every click happens twice

Your handlers run, and the original tea.MouseClickMsg reaches Update too. If you also handle the raw mouse messages, don't act on an event a handler already handled.

The screen freezes after a click

A handler runs on the Bubble Tea event loop, the same as Update, so it must not block: no sleeping, waiting, or I/O in the handler itself. Return a command for that work instead, which runs in the background.

Animations don't move, or stop partway

  • Return the command from Show, Hide, To, Play, and Loop. Without it, the first frame is never scheduled.
  • Pass every message to every animation, with anim.Update(msg, &m.a, &m.b), and return its command. Passing only some message types drops the frame messages.
  • For layout animation (Animate), pass every message to m.ui.Update and return its command, and give the animated slot or layer an ID.
  • Animations in a slice need to be updated through pointers to the elements, as in stacking toasts.

Effects look wrong on a light terminal

Dim fades toward the terminal's background, and some effects use its foreground. Until the terminal reports its colors, bloom assumes white on black. Return m.ui.Init() from your model's Init and pass every message to m.ui.Update so the root hears the reply. For terminals that never answer, set defaults with bloom.WithColors.

Text doesn't wrap, or wraps when it shouldn't

  • bloom.Text(s) on its own is clipped, not wrapped. Add .Style(...), even an empty lipgloss.NewStyle(), to wrap it to its node's width.
  • A status bar or title that should stay on one line needs .Truncate().
  • A string you rendered yourself with Lip Gloss has a fixed size already. Pass the style to Text(...).Style(...) or Box(...) instead, so bloom renders it at the right size.

A panel disappears

A Collapsible slot is dropped when the minimum sizes in its row or column don't fit. Lower another slot's Min, raise the collapsible slot's priority, or make the terminal bigger. frame.Rect(id) reports false for a collapsed slot, which you can use to tell the user the panel is hidden.

A component has the wrong size

Components that size themselves internally, such as a Bubbles viewport, need to be told the size their slot has. Find it with bloom.Solve when the window size or layout changes; see Using Bubbles components. Remember that a slot's rectangle includes the border of a Box inside it.

Wide characters look misaligned

bloom measures text with grapheme clustering by default, as Lip Gloss does. If your terminal measures emoji or other wide characters differently, set the method to match with bloom.WithWidthMethod.

Still stuck?

Ask in GitHub Discussions, report a bug in issues, or chat on Discord.

Last updated on

On this page