bubble.go

  1package tui
  2
  3import (
  4	"fmt"
  5	"smoothie/git"
  6	"time"
  7
  8	tea "github.com/charmbracelet/bubbletea"
  9	"github.com/charmbracelet/lipgloss"
 10	"github.com/gliderlabs/ssh"
 11)
 12
 13type sessionState int
 14
 15const (
 16	startState sessionState = iota
 17	errorState
 18	loadedState
 19	quittingState
 20	quitState
 21)
 22
 23type Model struct {
 24	state         sessionState
 25	error         string
 26	info          string
 27	width         int
 28	height        int
 29	windowChanges <-chan ssh.Window
 30	repoSource    *git.RepoSource
 31	repos         []*git.Repo
 32	activeBubble  int
 33	bubbles       []tea.Model
 34}
 35
 36func NewModel(width int, height int, windowChanges <-chan ssh.Window, repoSource *git.RepoSource) *Model {
 37	m := &Model{
 38		width:         width,
 39		height:        height,
 40		windowChanges: windowChanges,
 41		repoSource:    repoSource,
 42		bubbles:       make([]tea.Model, 2),
 43	}
 44	m.state = startState
 45	return m
 46}
 47
 48func (m *Model) Init() tea.Cmd {
 49	return tea.Batch(m.windowChangesCmd, m.loadGitCmd)
 50}
 51
 52func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 53	cmds := make([]tea.Cmd, 0)
 54	// Always allow state, error, info, window resize and quit messages
 55	switch msg := msg.(type) {
 56	case stateMsg:
 57		m.state = msg.state
 58	case tea.KeyMsg:
 59		switch msg.String() {
 60		case "q", "ctrl+c":
 61			return m, tea.Quit
 62		case "tab":
 63			m.activeBubble = (m.activeBubble + 1) % 2
 64		}
 65	case errMsg:
 66		m.error = msg.Error()
 67		m.state = errorState
 68		return m, nil
 69	case infoMsg:
 70		m.info = msg.text
 71	case windowMsg:
 72		cmds = append(cmds, m.windowChangesCmd)
 73	case tea.WindowSizeMsg:
 74		m.width = msg.Width
 75		m.height = msg.Height
 76	}
 77	if m.state == loadedState {
 78		b, cmd := m.bubbles[m.activeBubble].Update(msg)
 79		m.bubbles[m.activeBubble] = b
 80		if cmd != nil {
 81			cmds = append(cmds, cmd)
 82		}
 83	}
 84	return m, tea.Batch(cmds...)
 85}
 86
 87func (m *Model) viewForBubble(i int, width int) string {
 88	var ls lipgloss.Style
 89	if i == m.activeBubble {
 90		ls = activeBoxStyle.Width(width)
 91	} else {
 92		ls = inactiveBoxStyle.Width(width)
 93	}
 94	return ls.Render(m.bubbles[i].View())
 95}
 96
 97func (m *Model) View() string {
 98	pad := 6
 99	h := headerStyle.Width(m.width - pad).Render("Charm Beta")
100	f := footerStyle.Render(m.info)
101	s := ""
102	content := ""
103	switch m.state {
104	case loadedState:
105		lb := m.viewForBubble(0, 25)
106		rb := m.viewForBubble(1, 84)
107		s += lipgloss.JoinHorizontal(lipgloss.Top, lb, rb)
108	case errorState:
109		s += errorStyle.Render(fmt.Sprintf("Bummer: %s", m.error))
110	default:
111		s = normalStyle.Render(fmt.Sprintf("Doing something weird %d", m.state))
112	}
113	content = h + "\n" + s + "\n" + f
114	return appBoxStyle.Render(content)
115}
116
117func SessionHandler(reposPath string) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
118	rs := git.NewRepoSource(reposPath, time.Second*10)
119	return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
120		if len(s.Command()) == 0 {
121			pty, changes, active := s.Pty()
122			if !active {
123				return nil, nil
124			}
125			return NewModel(pty.Window.Width, pty.Window.Height, changes, rs), []tea.ProgramOption{tea.WithAltScreen()}
126		}
127		return nil, nil
128	}
129}