{"id":13563130,"url":"https://github.com/lrstanley/bubblezone","last_synced_at":"2025-05-14T04:08:46.352Z","repository":{"id":44369850,"uuid":"512300427","full_name":"lrstanley/bubblezone","owner":"lrstanley","description":"helper utility for BubbleTea, allowing easy mouse event tracking","archived":false,"fork":false,"pushed_at":"2025-05-06T18:05:51.000Z","size":440,"stargazers_count":652,"open_issues_count":6,"forks_count":20,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-05-06T19:29:41.517Z","etag":null,"topics":["bubbletea","cli","go","golang","lipgloss","terminal","tui"],"latest_commit_sha":null,"homepage":"https://pkg.go.dev/github.com/lrstanley/bubblezone","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lrstanley.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":".github/SECURITY.md","support":".github/SUPPORT.md","governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":"lrstanley"}},"created_at":"2022-07-09T22:52:07.000Z","updated_at":"2025-05-06T17:29:18.000Z","dependencies_parsed_at":"2024-01-25T05:29:00.926Z","dependency_job_id":"a87fa910-8e61-455b-aec3-99810050c09b","html_url":"https://github.com/lrstanley/bubblezone","commit_stats":{"total_commits":119,"total_committers":6,"mean_commits":"19.833333333333332","dds":"0.32773109243697474","last_synced_commit":"0f12a2876fb2881c21551520d042b804bab69dc2"},"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lrstanley%2Fbubblezone","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lrstanley%2Fbubblezone/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lrstanley%2Fbubblezone/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lrstanley%2Fbubblezone/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lrstanley","download_url":"https://codeload.github.com/lrstanley/bubblezone/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254069907,"owners_count":22009558,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bubbletea","cli","go","golang","lipgloss","terminal","tui"],"created_at":"2024-08-01T13:01:15.458Z","updated_at":"2025-05-14T04:08:41.326Z","avatar_url":"https://github.com/lrstanley.png","language":"Go","readme":"\u003c!-- template:define:options\n{\n  \"nodescription\": true\n}\n--\u003e\n\u003cimg title=\"Logo\" src=\"./examples/_images/logo.png\" width=\"961\"\u003e\n\n\u003c!-- template:begin:header --\u003e\n\u003c!-- do not edit anything in this \"template\" block, its auto-generated --\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/lrstanley/bubblezone/tags\"\u003e\n    \u003cimg title=\"Latest Semver Tag\" src=\"https://img.shields.io/github/v/tag/lrstanley/bubblezone?style=flat-square\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/lrstanley/bubblezone/commits/master\"\u003e\n    \u003cimg title=\"Last commit\" src=\"https://img.shields.io/github/last-commit/lrstanley/bubblezone?style=flat-square\"\u003e\n  \u003c/a\u003e\n\n\n\n  \u003ca href=\"https://github.com/lrstanley/bubblezone/actions?query=workflow%3Atest+event%3Apush\"\u003e\n    \u003cimg title=\"GitHub Workflow Status (test @ master)\" src=\"https://img.shields.io/github/actions/workflow/status/lrstanley/bubblezone/test.yml?branch=master\u0026label=test\u0026style=flat-square\"\u003e\n  \u003c/a\u003e\n\n\n\n  \u003ca href=\"https://codecov.io/gh/lrstanley/bubblezone\"\u003e\n    \u003cimg title=\"Code Coverage\" src=\"https://img.shields.io/codecov/c/github/lrstanley/bubblezone/master?style=flat-square\"\u003e\n  \u003c/a\u003e\n\n  \u003ca href=\"https://pkg.go.dev/github.com/lrstanley/bubblezone\"\u003e\n    \u003cimg title=\"Go Documentation\" src=\"https://pkg.go.dev/badge/github.com/lrstanley/bubblezone?style=flat-square\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://goreportcard.com/report/github.com/lrstanley/bubblezone\"\u003e\n    \u003cimg title=\"Go Report Card\" src=\"https://goreportcard.com/badge/github.com/lrstanley/bubblezone?style=flat-square\"\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/lrstanley/bubblezone/issues?q=is:open+is:issue+label:bug\"\u003e\n    \u003cimg title=\"Bug reports\" src=\"https://img.shields.io/github/issues/lrstanley/bubblezone/bug?label=issues\u0026style=flat-square\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/lrstanley/bubblezone/issues?q=is:open+is:issue+label:enhancement\"\u003e\n    \u003cimg title=\"Feature requests\" src=\"https://img.shields.io/github/issues/lrstanley/bubblezone/enhancement?label=feature%20requests\u0026style=flat-square\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/lrstanley/bubblezone/pulls\"\u003e\n    \u003cimg title=\"Open Pull Requests\" src=\"https://img.shields.io/github/issues-pr/lrstanley/bubblezone?label=prs\u0026style=flat-square\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/lrstanley/bubblezone/discussions/new?category=q-a\"\u003e\n    \u003cimg title=\"Ask a Question\" src=\"https://img.shields.io/badge/support-ask_a_question!-blue?style=flat-square\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://liam.sh/chat\"\u003e\u003cimg src=\"https://img.shields.io/badge/discord-bytecord-blue.svg?style=flat-square\" title=\"Discord Chat\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\u003c!-- template:end:header --\u003e\n\n\u003c!-- template:begin:toc --\u003e\n\u003c!-- do not edit anything in this \"template\" block, its auto-generated --\u003e\n## :link: Table of Contents\n\n  - [Problem](#x-problem)\n  - [Solution](#heavy_check_mark-solution)\n  - [Features](#sparkles-features)\n  - [Usage](#gear-usage)\n  - [Examples](#clap-examples)\n    - [List example](#list-example)\n    - [Lipgloss full example](#lipgloss-full-example)\n  - [Tips](#memo-tips)\n    - [Overlapping markers](#overlapping-markers)\n    - [Use lipgloss.Width](#use-lipglosswidth)\n    - [MaxHeight and MaxWidth](#maxheight-and-maxwidth)\n    - [Only scan at the root model](#only-scan-at-the-root-model)\n    - [Organic shapes](#organic-shapes)\n  - [Support \u0026amp; Assistance](#raising_hand_man-support--assistance)\n  - [Contributing](#handshake-contributing)\n  - [License](#balance_scale-license)\n\u003c!-- template:end:toc --\u003e\n\n## :x: Problem\n\n[BubbleTea](https://github.com/charmbracelet/bubbletea) and [lipgloss](https://github.com/charmbracelet/lipgloss)\nallow you to build extremely fast terminal interfaces, in a semantic and scalable\nway. Through abstracting layout, colors, events, and more, it's very easy to build\na user-friendly application. BubbleTea also supports mouse events, either through\nthe \"basic\" mouse events, like `MouseButtonLeft`, `MouseButtonRight`, `MouseButtonWheelUp` and\n`MouseButtonWheelDown` ([and more](https://github.com/charmbracelet/bubbletea/blob/0a0182e55a30e85640a53b8e01dc9ef06824cce5/mouse.go#L38-L48)),\nor through full motion tracking, allowing hover and mouse movement tracking.\n\nThis works great for a single-component application, where the state is managed in one\nlocation. However, when you start expanding your application, where components have\nvarious children, and those children have children, calculating mouse events like\n`MouseButtonLeft` and `MouseButtonRight` and determining which component was clicked\nbecomes complicated, and rather tedious.\n\n## :heavy_check_mark: Solution\n\n**BubbleZone** is one solution to this problem. BubbleZone allows you to wrap your\ncomponents in **zero-printable-width** (to not impact `lipgloss.Width()` calculations)\nidentifiers. Additionally, there is a scan method that wraps the entire application,\nstores the offsets of those identifiers as `zones`, and then removes them from\nthe resulting output.\n\nAny time there is a mouse event, pass it down to all children, thus allowing you\nto easily check if the event is within the bounds of the components `zone`. This\nmakes it very simple to do things like focusing on various components, clicking\n\"buttons\", and more. Take a look at this example, where I didn't have to calculate\nwhere the mouse was being clicked, and which component was under the mouse:\n\n![bubblezone example](https://cdn.liam.sh/share/2022/07/WindowsTerminal_XxiuWQ2hVL.gif)\n\n## :sparkles: Features\n\n- :heavy_check_mark: It's **_fast_** -- given it has to process this information for every render, I\n  tried to focus on performance where possible. If you see where improvements can\n  be made, let me know!\n- :heavy_check_mark: It doesn't impact width calculations when using `lipgloss.Width()` (if you're\n  using `len()` it will).\n- :heavy_check_mark: It's simple -- easily determine offset or if an event was within the bounds of\n  a zone.\n- :heavy_check_mark: Want the mouse event position relative to the component? Easy!\n- :heavy_check_mark: Provides an _optional_ global manager, when you have full access to all components,\n  so you don't have to inject it as a dependency to all components.\n\n---\n\n## :gear: Usage\n\n\u003c!-- template:begin:goget --\u003e\n\u003c!-- do not edit anything in this \"template\" block, its auto-generated --\u003e\n```console\ngo get -u github.com/lrstanley/bubblezone@latest\n```\n\u003c!-- template:end:goget --\u003e\n\nBubbleZone supports either a global zone manager (initialized via `NewGlobal()`),\nor non-global (via `New()`). Using the global zone manager, simply use `zone.\u003cmethod\u003e`.\nThe below examples will use the global manager.\n\nInitialize the zone manager:\n\n```go\npackage main\n\nimport (\n\t// [...]\n\tzone \"github.com/lrstanley/bubblezone\"\n)\n\n\nfunc main() {\n\t// [...]\n\tzone.NewGlobal()\n\t// If the UI will be closed at some point and the application will still run,\n\t// use zone.Close() to stop all background workers:\n\t// defer zone.Close()\n\t//\n\t// [...]\n\t//\n\t// Initialize your application here.\n}\n```\n\nEnsure the mouse is enabled and the program is running in alt screen mode (i.e. full window mode).\n\n```go\nfunc main() {\n\t// [...]\n\tp := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())\n\t// [...]\n}\n```\n\nIn your root model, wrap your `View()` output in `zone.Scan()`, which will register\nand monitor all zones, including stripping the ANSI sequences injected by `zone.Mark()`.\n\n```go\nfunc (r app) View() string {\n\t// [...]\n\treturn zone.Scan(r.someStyle.Render(generatedChildViews))\n}\n```\n\nIn your children models `View()` method, use `zone.Mark()` to wrap the area you want\nto mark as a zone. Make sure you give the zone a unique ID (see also: [tips: overlapping markers](#overlapping-markers)):\n\n```go\nfunc (m model) View() string {\n\t// [...]\n\tbuttons := lipgloss.JoinHorizontal(\n\t\tlipgloss.Top,\n\t\tzone.Mark(\"confirm\", okButton),\n\t\tzone.Mark(\"cancel\", cancelButton),\n\t)\n\treturn m.someStyle.Render(buttons)\n}\n```\n\nIn your children models `Update()` method, use `zone.Get(\u003cid\u003e).InBounds(mouseMsg)` to\ncheck if the mouse event was in the bounds of the zone:\n\n```go\nfunc (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\t// [...]\n\tcase tea.MouseMsg:\n\t\tif msg.Action != tea.MouseActionRelease || msg.Button != tea.MouseButtonLeft {\n\t\t\treturn m, nil\n\t\t}\n\n\t\tif zone.Get(\"confirm\").InBounds(msg) {\n\t\t\t// Do something if it's in bounds, e.g. toggling a model flag to let\n\t\t\t// View() know to change its highlight colors.\n\t\t\tm.active = \"confirm\"\n\t\t} else if zone.Get(\"cancel\").InBounds(msg) {\n\t\t\tm.active = \"cancel\"\n\t\t}\n\n\t\t// x, y := zone.Get(\"confirm\").Pos() can be used to get the relative\n\t\t// coordinates within the zone. Useful if you need to move a cursor in a\n\t\t// input box as an example.\n\n\t\treturn m, nil\n\t}\n\treturn m, nil\n}\n```\n\n... and that's it!\n\n---\n\n## :clap: Examples\n\n### List example\n\n- All titles are marked as a unique zone, and upon left click, that item is focused.\n- [Example source](./examples/list-default/main.go).\n\n![list-default example](https://cdn.liam.sh/share/2022/07/WindowsTerminal_SelC1Vzdas.gif)\n\n### Lipgloss full example\n\n- All items are marked as a unique zone (uses `NewPrefix()` as well).\n- Child models are used, and the resulting mouse events are passed down to each\n  model.\n- [Example source](./examples/full-lipgloss).\n\n![full-lipgloss example](https://cdn.liam.sh/share/2022/07/WindowsTerminal_tirP0rGZ2z.gif)\n\n---\n\n## :memo: Tips\n\nBelow are a couple of tips to ensure you have the best experience using BubbleZone.\n\n### Overlapping markers\n\nTo prevent overlapping marker ID's in child components, use `NewPrefix()` which\nwill generate a guaranteed-unique prefix you can use in combination with your\nregular IDs.\n\n### Use lipgloss.Width\n\nUse `lipgloss.Width()` for width measurements, rather than `len()` or similar.\nBubbleZone has been specifically designed so that markers will be ignored by\n`lipgloss.Width()` (in addition to this being the recommended width checking\nmethod even if you're not using BubbleZone, as `len()` breaks with fg/bg colors,\nand other control characters).\n\n### MaxHeight and MaxWidth\n\n`MaxHeight()` and `MaxWidth()` do a hard-trim of characters to enforce a specific\nheight and width. As such, if a child component is wrapped in a zone, and overlaps\nthe maximum height/width, the zone will break, and standard bounds checks\n**will not work**. Due to this, it is recommended to ensure `MaxHeight` and\n`MaxWidth()` are only enforcing limits that should already be set by normal\nheight/width limits on your components (i.e. just don't exceed the max viewport\ndimensions 😅).\n\n### Only scan at the root model\n\nMake sure `zone.Scan()` is only used at the root level model, it will likely not\nwork as you intend it in any other situation.\n\n### Organic shapes\n\nBubbleZones `InBounds()` checks calculate bounds based on a box region. For\nexample, if you have a model that generates a large circle, make sure the zone\nis properly padded (e.g. `lipgloss.Place()` or similar), to capture the entire\ncircle. Though note that because it checks for the entire box, a mouse event\nwill still be considered in bounds if the outer corners outside of the circle\nare clicked.\n\nExample:\n\n![bounding box](https://cdn.liam.sh/share/2022/07/dxehJb52R5.png)\n\n---\n\n\u003c!-- template:begin:support --\u003e\n\u003c!-- do not edit anything in this \"template\" block, its auto-generated --\u003e\n## :raising_hand_man: Support \u0026 Assistance\n\n* :heart: Please review the [Code of Conduct](.github/CODE_OF_CONDUCT.md) for\n     guidelines on ensuring everyone has the best experience interacting with\n     the community.\n* :raising_hand_man: Take a look at the [support](.github/SUPPORT.md) document on\n     guidelines for tips on how to ask the right questions.\n* :lady_beetle: For all features/bugs/issues/questions/etc, [head over here](https://github.com/lrstanley/bubblezone/issues/new/choose).\n\u003c!-- template:end:support --\u003e\n\n\u003c!-- template:begin:contributing --\u003e\n\u003c!-- do not edit anything in this \"template\" block, its auto-generated --\u003e\n## :handshake: Contributing\n\n* :heart: Please review the [Code of Conduct](.github/CODE_OF_CONDUCT.md) for guidelines\n     on ensuring everyone has the best experience interacting with the\n    community.\n* :clipboard: Please review the [contributing](.github/CONTRIBUTING.md) doc for submitting\n     issues/a guide on submitting pull requests and helping out.\n* :old_key: For anything security related, please review this repositories [security policy](https://github.com/lrstanley/bubblezone/security/policy).\n\u003c!-- template:end:contributing --\u003e\n\n\u003c!-- template:begin:license --\u003e\n\u003c!-- do not edit anything in this \"template\" block, its auto-generated --\u003e\n## :balance_scale: License\n\n```\nMIT License\n\nCopyright (c) 2022 Liam Stanley \u003cliam@liam.sh\u003e\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n```\n\n_Also located [here](LICENSE)_\n\u003c!-- template:end:license --\u003e\n","funding_links":["https://github.com/sponsors/lrstanley"],"categories":["Go"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flrstanley%2Fbubblezone","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flrstanley%2Fbubblezone","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flrstanley%2Fbubblezone/lists"}