Skip to content

alexanderbh/bubbleapp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

BubbleApp

Warning

This is work in progress. Help is welcome.

BubbleApp is an opinionated app framework for BubbleTea. Using composable functional components and hooks it becomes easy to make large BubbleTea apps without too much code. See the examples for how it works.

Features

  • Functional components
    • Create large apps in a style familiar to a certain web framework. UseState hook for state and UseEffect hook for... well side-effects.
  • Layout Engine
    • A multi-pass layout algorithm makes it possible to have growing components that take up available space. Enables resposive and flexible layouts.
  • Mouse support - using BubbleZone
    • Automatic mouse handling and propagation for all components.
  • Focus Management
    • Tab through your entire UI tree without any extra code. Tab order is the order in the UI tree.
  • Theming
    • Use the default provided theme or provide your own. A style.Theme uses named colors in a style.Color which are in turn defined by a provided style.Palette.

Components

  • Router
    • Easy navigation with nested routes and outlets
  • Context Provider
    • Share state and behavior with Contexts that can be consumed from any component below the Provider. This is how the Router works for example.
  • Layout Components
    • Stack and Box makes it easy to create flexible layouts. (Responsive Grid Layout Component planned)
  • Widget Components
  • Custom Components
    • Make your own components. All the provided components are built with the same hooks you have access to

How it works

BubbleApp works as an adaptor on top of BubbleTea. As a user of BubbleApp you will not have to interact with BubbleTea at all. That is abtracted away.

You provide a single function app.FC that takes in a Context app.Ctx and returns a "rendered" Component app.C. That is your App!

A rendered component is in the end just a string but in the form of a Component (app.C) that is registered with BubbleApp.

BubbleApp provides the means to Render a string with the render method:

func (c *Ctx) Render(fn func(c *Ctx, props Props) string, props Props) C

It might look complicated but it is esentially just wrapping a function call. It takes a function (fn) and some props. It then generates a unique ID for the component and prepares state for Hooks, Layout calculations and more.

In the end it then calls fn with the provided props which returns a string. This string is what is returned to BubbleTea and rendered on the screen.

Minimal example

This is the smallest example of a BubbleApp program.

package main

import (
  "os"
  "github.com/alexanderbh/bubbleapp/app"
  "github.com/alexanderbh/bubbleapp/component/text"
  tea "github.com/charmbracelet/bubbletea/v2"
)

// This is your root function that returns an *app.C (which is a rendered string)
func NewRoot(c *app.Ctx) *app.C {
  return text.New(c, "Hello World!")
}

func main() {
  c := app.NewCtx()

  bubbleApp := app.New(c, NewRoot) // Create a BubbleApp
  p := tea.NewProgram(bubbleApp, tea.WithAltScreen(), tea.WithMouseAllMotion())
  bubbleApp.SetTeaProgram(p) // Required: give BubbleApp a reference to your BubbleTea program
  if _, err := p.Run(); err != nil {
    os.Exit(1)
  }
}

Examples

An example of multiple views with some buttons. The login model is forgotten when navigating away from that view. It is easier to maintain large apps this way instead of a single root model.

Multiple Views


List all running processes in a table. This shows how to utilize the Global Data. Here a goroutine is maintaining the process list separately. Note: The API around updating components will change to something nice at some point.

There is not a lot of code here for the UI. Take a look.

Process list


Set up a router:

func MainRouter(c *app.Ctx) *app.C {
	return router.NewRouter(c, router.RouterProps{
		Routes: []router.Route{
			{Path: "/", Component: dashboard},
			{Path: "/shop", Component: shop},

			{Path: "/account", Component: account, Children: []router.Route{
				{Path: "/overview", Component: accountOverview},
				{Path: "/settings", Component: accountSettings},
				{Path: "/orders", Component: accountOrders},
			}},
		},
	})
}

How to navigate to a different route:

button.New(c, "My Account", func() {
  router.Push("/account/overview")
})

Nested routes are rendered in an Outlet:

router.NewOutlet(c)

Router


Widget Components

var tabsData = []tabs.Tab{
  {Title: "Overview", Content: overview},
  {Title: "Loaders", Content: loaders},
  {Title: "Boxes 🟨", Content: boxes},
}

func NewRoot(c *app.Ctx) *app.C {
	return tabs.New(c, tabsData)
}

Tabs


loader.New(c, loader.Dots, "Loading..."),

Loaders


Each table automatically handles mouse hovering rows. They send out messages on state change and focus and keys are handled automatically.

func NewRoot(c *app.Ctx) *app.C {
	return stack.New(c, func(c *app.Ctx) []*app.C {
		return []*app.C{
			table.New(c, table.WithDataFunc(func(c *app.Ctx) ([]table.Column, []table.Row) {
				return clms, rows
			})),
			table.New(c, table.WithDataFunc(func(c *app.Ctx) ([]table.Column, []table.Row) {
				return clms, rows
			})),
		}
	}, stack.WithDirection(app.Horizontal))
}

Table


Using huh for form rendering. Perhaps a native BubbleApp form will be created in the future for easier composability.

var loginForm = huh.NewForm(
  huh.NewGroup(
    huh.NewInput().Key("email").Title("Email"),
    huh.NewInput().Key("password").Title("Password").EchoMode(huh.EchoModePassword),
    huh.NewSelect[string]().Key("rememberme").Title("Remember me").Description("Log in automatically when using this SSH key").Options(huh.NewOptions("Yes", "No")...),
  ),
)
func NewRoot(c *app.Ctx) *app.C {
	formSubmit, setFormSubmit := app.UseState[*FormData](c, nil)

	return stack.New(c, func(c *app.Ctx) []*app.C {
		cs := []*app.C{}
		cs = append(cs, c.Render(loginLogo, nil))

		if formSubmit == nil {
			cs = append(cs, form.New(c, loginForm, func() {
				setFormSubmit(&FormData{
					email:    loginForm.GetString("email"),
					password: loginForm.GetString("password"),
					remember: loginForm.GetString("rememberme"),
				})
			}))
		}

		if formSubmit != nil {
			cs = append(cs,
				text.New(c, "Email: "+formSubmit.email, nil),
				text.New(c, "Password πŸ™ˆ: "+formSubmit.password, nil),
				text.New(c, "Remember me: "+formSubmit.remember, nil),
			)
		}

		return append(cs,
			box.NewEmpty(c),
			divider.New(c),
			button.New(c, "Quit", c.Quit, button.WithVariant(button.Danger)),
		)
	})
}

form

Using Glamour for markdown rendering.

func NewRoot(c *app.Ctx) *app.C {
	return stack.New(c, func(c *app.Ctx) []*app.C {
		return []*app.C{
			text.New(c, "Markdown example!"),
			divider.New(c),

			box.New(c, func(c *app.Ctx) *app.C {
				return markdown.New(c, mdContent)
			}, box.WithDisableFollow(true)),

			divider.New(c),
			text.New(c, "Press [ctrl-c] to quit.", text.WithFg(c.Theme.Colors.Danger)),
		}
	})
}

Markdown


Layout Components

Stack layouts vertically or horizontally.

func NewRoot(c *app.Ctx) *app.C {
	return stack.New(c, func(c *app.Ctx) []*app.C {
		return []*app.C{
			box.NewEmpty(c, box.WithBg(c.Theme.Colors.Danger)),
			box.New(c, func(c *app.Ctx) *app.C {
				return stack.New(c, func(c *app.Ctx) []*app.C {
					return []*app.C{
						box.NewEmpty(c, box.WithBg(c.Theme.Colors.Primary)),
						box.NewEmpty(c, box.WithBg(c.Theme.Colors.Secondary)),
						box.NewEmpty(c, box.WithBg(c.Theme.Colors.Tertiary)),
					}
				}, stack.WithDirection(app.Horizontal))
			}),
			box.NewEmpty(c, box.WithBg(c.Theme.Colors.Warning)),
		}
	})
}

Stack


Features

Functional

Functional components and hooks as you might be familiar with

func NewRoot(c *app.Ctx) *app.C {
	clicks, setClicks := app.UseState(c, 0)
	greeting, setGreeting := app.UseState(c, "Knock knock!")

	app.UseEffect(c, func() {
		go func() {
			time.Sleep(2 * time.Second)
			setGreeting("Who's there?")
		}()
	}, []any{})

	return stack.New(c, func(c *app.Ctx) []*app.C {
		return []*app.C{
			button.New(c, "Count clicks here!", func() {
				setClicks(clicks + 1)
			}),

			text.New(c, "Clicks: "+strconv.Itoa(clicks), text.WithFg(c.Theme.Colors.Warning)),
			text.New(c, "Greeting: "+greeting, text.WithFg(c.Theme.Colors.Warning)),

			box.NewEmpty(c),

			button.New(c, "Quit", c.Quit, button.WithVariant(button.Danger)),
		}
	}, stack.WithGap(1), stack.WithGrow(true))
}

FC

Global tab management without any extra code. All focusable components are automatically in a tab order (their order in the UI tree).

func NewRoot(c *app.Ctx) *app.C {
	presses, setPresses := app.UseState(c, 0)
	log, setLog := app.UseState(c, []string{})

	return stack.New(c, func(c *app.Ctx) []*app.C {
		return []*app.C{
			text.New(c, "Tab through the buttons to see focus state!"),

			button.New(c, "Button 1", func() {
				currentLog := log
				currentPresses := presses
				newLog := append(currentLog, "["+strconv.Itoa(currentPresses)+"] "+"Button 1 pressed")
				setLog(newLog)
				setPresses(currentPresses + 1)
			}, button.WithVariant(button.Primary)),

			divider.New(c),

			box.New(c, func(c *app.Ctx) *app.C {
				return text.New(c, strings.Join(log, "\n"))
			}),

			divider.New(c),

			button.New(c, "Quit App", func() {
				c.Quit()
			}, button.WithVariant(button.Danger)),
		}

	}, stack.WithGrow(true))
}

Focus Tabbing


Development

Try out the examples to get a feel for how it works in the terminal.

git clone [email protected]:alexanderbh/bubbleapp.git
cd bubbleapp/examples/multiple-views
go run .

Planned Features

Here are some planned features in no particular order. Feel free to suggest something.

  • Component Structure/API - Finalize the API for rendering components
  • Router - Add a router component that can handle screens, navigation, back history, etc.
  • Theme - Support for theme/palette
  • Shortcut support - global and locally within components in focus perhaps
  • Mouse wheel propagation - propagate mouse wheel messages to hovered component tree
  • Button loading state - Add loading state to button
  • Scroll Box with mouse - Scroll overflow Box with mouse wheel
  • Scroll Box with keyboard - Support scrolling (mouse and keyboard) for Boxes with vertical overflowing content
  • Form and input fields - Move away from huh for forms and use BubbleApp components for it
  • Alignments - Add justify and align options on relevant components
  • Border and title on Box - Add borders and titles to Box component
  • Performance - Figure out where CPU is spent and optimize (perhaps prevent rerenders if no props or state changes)
  • Modal Component - Using canvas/layers approach
  • Confirm Component - Using modal but is an ok, cancel modal with text
  • Help Text Component
  • Context Menu Component
  • Table DataSource - attach a datasource to a table that can handle fetching, sorting, filtering, etc.
  • Animation Component - give it a list of frames and an FPS and it handles the rest

Shout outs

  • Thank you Charm for the amazing BubbleTea framework.
  • Thank you BubbleZone for making mouse support easy.

About

An opinionated App Framework for BubbleTea

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Languages