bubble.go

  1package tui
  2
  3import (
  4	"fmt"
  5	"log"
  6	"smoothie/git"
  7	"smoothie/tui/bubbles/commits"
  8	"smoothie/tui/bubbles/selection"
  9
 10	"github.com/charmbracelet/bubbles/viewport"
 11	tea "github.com/charmbracelet/bubbletea"
 12	"github.com/charmbracelet/glamour"
 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 Bubble struct {
 28	state          sessionState
 29	error          string
 30	width          int
 31	height         int
 32	session        ssh.Session
 33	windowChanges  <-chan ssh.Window
 34	repoSource     *git.RepoSource
 35	repos          []*git.Repo
 36	boxes          []tea.Model
 37	activeBox      int
 38	repoSelect     *selection.Bubble
 39	commitsLog     *commits.Bubble
 40	readmeViewport *ViewportBubble
 41}
 42
 43type Config struct {
 44	Width         int
 45	Height        int
 46	Session       ssh.Session
 47	WindowChanges <-chan ssh.Window
 48	RepoSource    *git.RepoSource
 49}
 50
 51func NewBubble(cfg Config) *Bubble {
 52	b := &Bubble{
 53		width:         cfg.Width,
 54		height:        cfg.Height,
 55		windowChanges: cfg.WindowChanges,
 56		repoSource:    cfg.RepoSource,
 57		boxes:         make([]tea.Model, 2),
 58		readmeViewport: &ViewportBubble{
 59			Viewport: &viewport.Model{
 60				Width:  boxRightWidth - horizontalPadding - 2,
 61				Height: cfg.Height - verticalPadding - viewportHeightConstant,
 62			},
 63		},
 64	}
 65	b.state = startState
 66	return b
 67}
 68
 69func (b *Bubble) Init() tea.Cmd {
 70	return tea.Batch(b.windowChangesCmd, b.loadGitCmd)
 71}
 72
 73func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 74	cmds := make([]tea.Cmd, 0)
 75	// Always allow state, error, info, window resize and quit messages
 76	switch msg := msg.(type) {
 77	case tea.KeyMsg:
 78		switch msg.String() {
 79		case "q", "ctrl+c":
 80			return b, tea.Quit
 81		case "tab":
 82			b.activeBox = (b.activeBox + 1) % 2
 83		}
 84	case errMsg:
 85		b.error = msg.Error()
 86		b.state = errorState
 87		return b, nil
 88	case windowMsg:
 89		cmds = append(cmds, b.windowChangesCmd)
 90	case tea.WindowSizeMsg:
 91		b.width = msg.Width
 92		b.height = msg.Height
 93	case selection.SelectedMsg:
 94		cmds = append(cmds, b.getRepoCmd(b.repos[msg.Index].Name))
 95	}
 96	if b.state == loadedState {
 97		ab, cmd := b.boxes[b.activeBox].Update(msg)
 98		b.boxes[b.activeBox] = ab
 99		if cmd != nil {
100			cmds = append(cmds, cmd)
101		}
102	}
103	return b, tea.Batch(cmds...)
104}
105
106func (b *Bubble) viewForBox(i int, width int) string {
107	var ls lipgloss.Style
108	if i == b.activeBox {
109		ls = activeBoxStyle.Width(width)
110	} else {
111		ls = inactiveBoxStyle.Width(width)
112	}
113	return ls.Render(b.boxes[i].View())
114}
115
116func (b *Bubble) View() string {
117	h := headerStyle.Width(b.width - horizontalPadding).Render("Charm Beta")
118	f := footerStyle.Render("")
119	s := ""
120	content := ""
121	switch b.state {
122	case loadedState:
123		lb := b.viewForBox(0, boxLeftWidth)
124		rb := b.viewForBox(1, boxRightWidth)
125		s += lipgloss.JoinHorizontal(lipgloss.Top, lb, rb)
126	case errorState:
127		s += errorStyle.Render(fmt.Sprintf("Bummer: %s", b.error))
128	default:
129		s = normalStyle.Render(fmt.Sprintf("Doing something weird %d", b.state))
130	}
131	content = h + "\n\n" + s + "\n" + f
132	return appBoxStyle.Render(content)
133}
134
135func glamourReadme(md string) string {
136	tr, err := glamour.NewTermRenderer(
137		glamour.WithAutoStyle(),
138		glamour.WithWordWrap(boxRightWidth-2),
139	)
140	if err != nil {
141		log.Fatal(err)
142	}
143	mdt, err := tr.Render(md)
144	if err != nil {
145		return md
146	}
147	return mdt
148}
149
150func SessionHandler(reposPath string) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
151	rs := git.NewRepoSource(reposPath, glamourReadme)
152	return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
153		if len(s.Command()) == 0 {
154			pty, changes, active := s.Pty()
155			if !active {
156				return nil, nil
157			}
158			cfg := Config{
159				Width:         pty.Window.Width,
160				Height:        pty.Window.Height,
161				WindowChanges: changes,
162				RepoSource:    rs,
163				Session:       s,
164			}
165			return NewBubble(cfg), []tea.ProgramOption{tea.WithAltScreen()}
166		}
167		return nil, nil
168	}
169}