kota's memex

https://github.com/charmbracelet/bubbletea

Bubble Tea programs are comprised of a model that describes the application state and three simple methods on that model:

model

A models stores are application's state. Usually with a struct:

type model struct {
  choices  []string           // items on the to-do list
  cursor   int                // which to-do list item our cursor is pointing at
  selected map[int]struct{}   // which to-do items are selected
}

init

We then create a variable with our model's initial state:

var initialModel = model{
  // Our to-do list is just a grocery list
  choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},

  // A map which indicates which choices are selected. We're using
  // the  map like a mathematical set. The keys refer to the indexes
  // of the `choices` slice, above.
  selected: make(map[int]struct{}),
}

func (m model) Init() tea.Cmd {
  // Just return `nil`, which means "no I/O right now, please."
  return nil
}

Our Init method just returns an empty tea.Cmd, but more complicated programs could use this to perform initial I/O.

update

Update is called when "things happen" and will update the model accordingly. Optionally it can return a tea.Cmd trigger more events, such as to quit the program.

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg: // is it a keypress?
		switch msg.String() { // what key was pressed?
		case "ctrl+c", "q":
			return m, tea.Quit
		case "up", "k":
			if m.cursor > 0 {
				m.cursor--
			}
		case "down", "j":
			if m.cursor < len(m.choices)-1 {
				m.cursor++
			}
		case "enter", " ":
			_, ok := m.selected[m.cursor]
			if ok {
				delete(m.selected, m.cursor)
			} else {
				m.selected[m.cursor] = struct{}{}
			}
		}
	}
	return m, nil
}

view

This is where we can render out UI. We look at the model in it's current state and use it to return a string. That's it.

func (m model) View() string {
	// the header
	s := "What should we buy at the market?\n\n"
	for i, choice := range m.choices {
		cursor := " " // no cursor
		if m.cursor == i {
			cursor = ">" // cursor!
		}
		checked := " " // no selection
		if _, ok := m.selected[i]; ok {
			checked = "x" // selected!
		}
		// render the row
		s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
	}
	// the footer
	s += "\nPress q to quit.\n"
	// Send the UI for rendering
	return s
}

extras

https://github.com/muesli/reflow

ANSI aware text reflow operations.

https://github.com/muesli/termenv

ANSI styling and detection.

https://github.com/charmbracelet/lipgloss

A higher level combination of the above two for making Bubble tea views.

https://github.com/charmbracelet/bubbles

UI components for Bubble Tea.

https://github.com/erikgeiser/promptkit

Prompt components for Bubble Tea

https://github.com/mattn/go-runewidth

Calculate correct length of strings, len() doesn't account for double width characters.

https://github.com/charmbracelet/glamour

Stylesheet-based markdown rendering for your CLI apps.