bubble.go

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