SkillAgentSearch skills...

Bubblezone

helper utility for BubbleTea, allowing easy mouse event tracking

Install / Use

/learn @lrstanley/Bubblezone
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<!-- template:define:options { "nodescription": true } --> <img title="Logo" src="./_examples/_images/logo.png" width="961"> <!-- template:begin:header --> <!-- do not edit anything in this "template" block, its auto-generated --> <p align="center"> <a href="https://github.com/lrstanley/bubblezone/tags"> <img title="Latest Semver Tag" src="https://img.shields.io/github/v/tag/lrstanley/bubblezone?style=flat-square"> </a> <a href="https://github.com/lrstanley/bubblezone/commits/master"> <img title="Last commit" src="https://img.shields.io/github/last-commit/lrstanley/bubblezone?style=flat-square"> </a> <a href="https://github.com/lrstanley/bubblezone/actions?query=workflow%3Atest+event%3Apush"> <img title="GitHub Workflow Status (test @ master)" src="https://img.shields.io/github/actions/workflow/status/lrstanley/bubblezone/test.yml?branch=master&label=test&style=flat-square"> </a> <a href="https://codecov.io/gh/lrstanley/bubblezone"> <img title="Code Coverage" src="https://img.shields.io/codecov/c/github/lrstanley/bubblezone/master?style=flat-square"> </a> <a href="https://pkg.go.dev/github.com/lrstanley/bubblezone/v2"> <img title="Go Documentation" src="https://pkg.go.dev/badge/github.com/lrstanley/bubblezone/v2?style=flat-square"> </a> <a href="https://goreportcard.com/report/github.com/lrstanley/bubblezone/v2"> <img title="Go Report Card" src="https://goreportcard.com/badge/github.com/lrstanley/bubblezone/v2?style=flat-square"> </a> </p> <p align="center"> <a href="https://github.com/lrstanley/bubblezone/issues?q=is:open+is:issue+label:bug"> <img title="Bug reports" src="https://img.shields.io/github/issues/lrstanley/bubblezone/bug?label=issues&style=flat-square"> </a> <a href="https://github.com/lrstanley/bubblezone/issues?q=is:open+is:issue+label:enhancement"> <img title="Feature requests" src="https://img.shields.io/github/issues/lrstanley/bubblezone/enhancement?label=feature%20requests&style=flat-square"> </a> <a href="https://github.com/lrstanley/bubblezone/pulls"> <img title="Open Pull Requests" src="https://img.shields.io/github/issues-pr/lrstanley/bubblezone?label=prs&style=flat-square"> </a> <a href="https://github.com/lrstanley/bubblezone/discussions/new?category=q-a"> <img title="Ask a Question" src="https://img.shields.io/badge/support-ask_a_question!-blue?style=flat-square"> </a> <a href="https://liam.sh/chat"><img src="https://img.shields.io/badge/discord-bytecord-blue.svg?style=flat-square" title="Discord Chat"></a> </p> <!-- template:end:header --> <!-- template:begin:toc --> <!-- do not edit anything in this "template" block, its auto-generated -->

:link: Table of Contents

<!-- template:end:toc -->

:x: Problem

BubbleTea and lipgloss allow you to build extremely fast terminal interfaces, in a semantic and scalable way. Through abstracting layout, colors, events, and more, it's very easy to build a user-friendly application. BubbleTea also supports mouse events, either through the "basic" mouse events, like MouseButtonLeft, MouseButtonRight, MouseButtonWheelUp and MouseButtonWheelDown (and more), or through full motion tracking, allowing hover and mouse movement tracking.

This works great for a single-component application, where the state is managed in one location. However, when you start expanding your application, where components have various children, and those children have children, calculating mouse events like MouseButtonLeft and MouseButtonRight and determining which component was clicked becomes complicated, and rather tedious.

:heavy_check_mark: Solution

BubbleZone is one solution to this problem. BubbleZone allows you to wrap your components in zero-printable-width (to not impact lipgloss.Width() calculations) identifiers. Additionally, there is a scan method that wraps the entire application, stores the offsets of those identifiers as zones, and then removes them from the resulting output.

Any time there is a mouse event, pass it down to all children, thus allowing you to easily check if the event is within the bounds of the components zone. This makes it very simple to do things like focusing on various components, clicking "buttons", and more. Take a look at this example, where I didn't have to calculate where the mouse was being clicked, and which component was under the mouse:

bubblezone example

:sparkles: Features

  • :heavy_check_mark: It's fast -- given it has to process this information for every render, I tried to focus on performance where possible. If you see where improvements can be made, let me know!
  • :heavy_check_mark: It doesn't impact width calculations when using lipgloss.Width() (if you're using len() it will).
  • :heavy_check_mark: It's simple -- easily determine offset or if an event was within the bounds of a zone.
  • :heavy_check_mark: Want the mouse event position relative to the component? Easy!
  • :heavy_check_mark: Provides an optional global manager, when you have full access to all components, so you don't have to inject it as a dependency to all components.

:gear: Usage

go get -u github.com/lrstanley/bubblezone/v2@latest

BubbleZone supports either a global zone manager (initialized via NewGlobal()), or non-global (via New()). Using the global zone manager, simply use zone.<method>. The below examples will use the global manager.

Initialize the zone manager:

package main

import (
	// [...]
	zone "github.com/lrstanley/bubblezone/v2"
)


func main() {
	// [...]
	zone.NewGlobal()
	// If the UI will be closed at some point and the application will still run,
	// use zone.Close() to stop all background workers:
	// defer zone.Close()
	//
	// [...]
	//
	// Initialize your application here.
}

In your root model, wrap your View() output in zone.Scan(), which will register and monitor all zones, including stripping the ANSI sequences injected by zone.Mark().

func (r app) View() tea.View {
    var view tea.View
    // Ensure that alt-screen is enabled, as bubblezone will only work in alt-screen mode.
    view.AltScreen = true
    // Enable mouse motion tracking.
    view.MouseMode = tea.MouseModeCellMotion
    // Wrap view in [zone.Scan].
    view.SetContent(zone.Scan(r.someStyle.Render(generatedChildViews)))
	return view
}

In your children models View() method, use zone.Mark() to wrap the area you want to mark as a zone. Make sure you give the zone a unique ID (see also: tips: overlapping markers):

func (m model) View() string {
	// [...]
	buttons := lipgloss.JoinHorizontal(
		lipgloss.Top,
		zone.Mark("confirm", okButton),
		zone.Mark("cancel", cancelButton),
	)
	return m.someStyle.Render(buttons)
}

In your children models Update() method, use zone.Get(<id>).InBounds(mouseMsg) to check if the mouse event was in the bounds of the zone:

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	// [...]
	case tea.MouseReleaseMsg:
		if msg.Button != tea.MouseLeft {
			return m, nil
		}

		if zone.Get("confirm").InBounds(msg) {
			// Do something if it's in bounds, e.g. toggling a model flag to let
			// View() know to change its highlight colors.
			m.active = "confirm"
		} else if zone.Get("cancel").InBounds(msg) {
			m.active = "cancel"
		}

		// x, y := zone.Get("confirm").Pos() can be used to get the relative
		// coordinates within the zone. Useful if you need to move a cursor in a
		// input box as an example.

		return m, nil
	}
	return m, nil
}

... and that's it!


:clap: Examples

List example

  • All titles are marked as a unique zone, and upon left click, that item is focused.
  • Example source.

list-default example

Lipgloss full example

  • All items are marked as a unique zone (uses NewPrefix() as well).
  • Child models are used, and the resulting mouse events are passed down to each model.
  • Example source.

full-lipgloss example


:memo: Tips

Below are a couple of tips to ensure you have the best experience using BubbleZone.

Overlapping markers

To prevent overlapping marker ID's in child components, use NewPrefix() which will generate a guaranteed-unique prefix you can use in combination with your regular IDs.

Use lipgloss.Width

Use lipgloss.Width() for width measurements, rather than len() or similar. BubbleZone has been specifically designed so that markers will be ignored by lipgloss.Width() (in addition to this being the recommended width checking method even if you're not using BubbleZone, as len() breaks with fg/bg colors, and other control characters).

MaxHeight and MaxWidth

MaxHeight() and MaxWidth() do

View on GitHub
GitHub Stars847
CategoryDevelopment
Updated6h ago
Forks27

Languages

Go

Security Score

100/100

Audited on Mar 31, 2026

No findings