bubble.go

  1package tui
  2
  3import (
  4	"fmt"
  5	"io"
  6	"smoothie/git"
  7	"smoothie/tui/bubbles/commits"
  8	"smoothie/tui/bubbles/repo"
  9	"smoothie/tui/bubbles/selection"
 10	"strings"
 11
 12	tea "github.com/charmbracelet/bubbletea"
 13	"github.com/charmbracelet/lipgloss"
 14	"github.com/gliderlabs/ssh"
 15)
 16
 17type sessionState int
 18
 19const (
 20	startState sessionState = iota
 21	errorState
 22	loadedState
 23	quittingState
 24	quitState
 25)
 26
 27type Config struct {
 28	Name         string      `json:"name"`
 29	Host         string      `json:"host"`
 30	Port         int64       `json:"port"`
 31	ShowAllRepos bool        `json:"show_all_repos"`
 32	Menu         []MenuEntry `json:"menu"`
 33	RepoSource   *git.RepoSource
 34}
 35
 36type MenuEntry struct {
 37	Name   string `json:"name"`
 38	Note   string `json:"note"`
 39	Repo   string `json:"repo"`
 40	bubble *repo.Bubble
 41}
 42
 43type SessionConfig struct {
 44	Width         int
 45	Height        int
 46	WindowChanges <-chan ssh.Window
 47	InitialRepo   string
 48}
 49
 50type Bubble struct {
 51	config        *Config
 52	state         sessionState
 53	error         string
 54	width         int
 55	height        int
 56	windowChanges <-chan ssh.Window
 57	repoSource    *git.RepoSource
 58	initialRepo   string
 59	repoMenu      []MenuEntry
 60	repos         []*git.Repo
 61	boxes         []tea.Model
 62	activeBox     int
 63	repoSelect    *selection.Bubble
 64	commitsLog    *commits.Bubble
 65}
 66
 67func NewBubble(cfg *Config, sCfg *SessionConfig) *Bubble {
 68	b := &Bubble{
 69		config:        cfg,
 70		width:         sCfg.Width,
 71		height:        sCfg.Height,
 72		windowChanges: sCfg.WindowChanges,
 73		repoSource:    cfg.RepoSource,
 74		repoMenu:      make([]MenuEntry, 0),
 75		boxes:         make([]tea.Model, 2),
 76		initialRepo:   sCfg.InitialRepo,
 77	}
 78	b.state = startState
 79	return b
 80}
 81
 82func (b *Bubble) Init() tea.Cmd {
 83	return tea.Batch(b.windowChangesCmd, b.setupCmd)
 84}
 85
 86func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 87	cmds := make([]tea.Cmd, 0)
 88	// Always allow state, error, info, window resize and quit messages
 89	switch msg := msg.(type) {
 90	case tea.KeyMsg:
 91		switch msg.String() {
 92		case "q", "ctrl+c":
 93			return b, tea.Quit
 94		case "tab":
 95			b.activeBox = (b.activeBox + 1) % 2
 96		}
 97	case errMsg:
 98		b.error = msg.Error()
 99		b.state = errorState
100		return b, nil
101	case windowMsg:
102		cmds = append(cmds, b.windowChangesCmd)
103	case tea.WindowSizeMsg:
104		b.width = msg.Width
105		b.height = msg.Height
106		if b.state == loadedState {
107			ab, cmd := b.boxes[b.activeBox].Update(msg)
108			b.boxes[b.activeBox] = ab
109			if cmd != nil {
110				cmds = append(cmds, cmd)
111			}
112		}
113	case selection.SelectedMsg:
114		b.activeBox = 1
115		rb := b.repoMenu[msg.Index].bubble
116		rb.GotoTop()
117		b.boxes[1] = rb
118	case selection.ActiveMsg:
119		rb := b.repoMenu[msg.Index].bubble
120		rb.GotoTop()
121		b.boxes[1] = b.repoMenu[msg.Index].bubble
122	}
123	if b.state == loadedState {
124		ab, cmd := b.boxes[b.activeBox].Update(msg)
125		b.boxes[b.activeBox] = ab
126		if cmd != nil {
127			cmds = append(cmds, cmd)
128		}
129	}
130	return b, tea.Batch(cmds...)
131}
132
133func (b *Bubble) viewForBox(i int, width int, height int) string {
134	var ls lipgloss.Style
135	if i == b.activeBox {
136		ls = activeBoxStyle.Copy()
137	} else {
138		ls = inactiveBoxStyle.Copy()
139	}
140	ls.Width(width)
141	if height > 0 {
142		ls.Height(height).MarginBottom(3)
143	}
144	return ls.Render(b.boxes[i].View())
145}
146
147func (b Bubble) footerView(w io.Writer) {
148	h := []helpEntry{
149		{"tab", "section"},
150		{"↑/↓", "navigate"},
151		{"q", "quit"},
152	}
153	for i, v := range h {
154		fmt.Fprint(w, v)
155		if i != len(h)-1 {
156			fmt.Fprint(w, helpDivider)
157		}
158	}
159}
160
161func (b *Bubble) View() string {
162	s := strings.Builder{}
163	w := b.width - 3
164	s.WriteString(headerStyle.Width(w - 2).Render(b.config.Name))
165	s.WriteRune('\n')
166	switch b.state {
167	case loadedState:
168		lb := b.viewForBox(0, boxLeftWidth, 0)
169		rb := b.viewForBox(1, b.width-boxLeftWidth-10, b.height-8)
170		s.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, lb, rb))
171	case errorState:
172		s.WriteString(errorStyle.Render(fmt.Sprintf("Bummer: %s", b.error)))
173	}
174	s.WriteRune('\n')
175	b.footerView(&s)
176	return appBoxStyle.Width(w).Height(b.height).Render(s.String())
177}