bubble.go

  1package tui
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"log"
  7	"smoothie/git"
  8	"smoothie/tui/bubbles/commits"
  9	"smoothie/tui/bubbles/selection"
 10	"time"
 11
 12	"github.com/charmbracelet/bubbles/viewport"
 13	tea "github.com/charmbracelet/bubbletea"
 14	"github.com/charmbracelet/glamour"
 15	"github.com/charmbracelet/lipgloss"
 16	"github.com/gliderlabs/ssh"
 17)
 18
 19type sessionState int
 20
 21const (
 22	startState sessionState = iota
 23	errorState
 24	loadedState
 25	quittingState
 26	quitState
 27)
 28
 29type MenuEntry struct {
 30	Name string `json:"name"`
 31	Repo string `json:"repo"`
 32}
 33
 34type Config struct {
 35	Name         string      `json:"name"`
 36	ShowAllRepos bool        `json:"show_all_repos"`
 37	Menu         []MenuEntry `json:"menu"`
 38	RepoSource   *git.RepoSource
 39}
 40
 41type SessionConfig struct {
 42	Width         int
 43	Height        int
 44	WindowChanges <-chan ssh.Window
 45}
 46
 47type Bubble struct {
 48	config         *Config
 49	state          sessionState
 50	error          string
 51	width          int
 52	height         int
 53	windowChanges  <-chan ssh.Window
 54	repoSource     *git.RepoSource
 55	repoMenu       []MenuEntry
 56	repos          []*git.Repo
 57	boxes          []tea.Model
 58	activeBox      int
 59	repoSelect     *selection.Bubble
 60	commitsLog     *commits.Bubble
 61	readmeViewport *ViewportBubble
 62}
 63
 64func NewBubble(cfg *Config, sCfg *SessionConfig) *Bubble {
 65	b := &Bubble{
 66		config:        cfg,
 67		width:         sCfg.Width,
 68		height:        sCfg.Height,
 69		windowChanges: sCfg.WindowChanges,
 70		repoSource:    cfg.RepoSource,
 71		boxes:         make([]tea.Model, 2),
 72		readmeViewport: &ViewportBubble{
 73			Viewport: &viewport.Model{
 74				Width:  boxRightWidth - horizontalPadding - 2,
 75				Height: sCfg.Height - verticalPadding - viewportHeightConstant,
 76			},
 77		},
 78	}
 79	b.state = startState
 80	return b
 81}
 82
 83func (b *Bubble) Init() tea.Cmd {
 84	return tea.Batch(b.windowChangesCmd, b.loadGitCmd)
 85}
 86
 87func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 88	cmds := make([]tea.Cmd, 0)
 89	// Always allow state, error, info, window resize and quit messages
 90	switch msg := msg.(type) {
 91	case tea.KeyMsg:
 92		switch msg.String() {
 93		case "q", "ctrl+c":
 94			return b, tea.Quit
 95		case "tab":
 96			b.activeBox = (b.activeBox + 1) % 2
 97		}
 98	case errMsg:
 99		b.error = msg.Error()
100		b.state = errorState
101		return b, nil
102	case windowMsg:
103		cmds = append(cmds, b.windowChangesCmd)
104	case tea.WindowSizeMsg:
105		b.width = msg.Width
106		b.height = msg.Height
107	case selection.SelectedMsg:
108		cmds = append(cmds, b.getRepoCmd(b.repoMenu[msg.Index].Repo))
109	}
110	if b.state == loadedState {
111		ab, cmd := b.boxes[b.activeBox].Update(msg)
112		b.boxes[b.activeBox] = ab
113		if cmd != nil {
114			cmds = append(cmds, cmd)
115		}
116	}
117	return b, tea.Batch(cmds...)
118}
119
120func (b *Bubble) viewForBox(i int, width int) string {
121	var ls lipgloss.Style
122	if i == b.activeBox {
123		ls = activeBoxStyle.Width(width)
124	} else {
125		ls = inactiveBoxStyle.Width(width)
126	}
127	return ls.Render(b.boxes[i].View())
128}
129
130func (b *Bubble) View() string {
131	h := headerStyle.Width(b.width - horizontalPadding).Render(b.config.Name)
132	f := footerStyle.Render("")
133	s := ""
134	content := ""
135	switch b.state {
136	case loadedState:
137		lb := b.viewForBox(0, boxLeftWidth)
138		rb := b.viewForBox(1, boxRightWidth)
139		s += lipgloss.JoinHorizontal(lipgloss.Top, lb, rb)
140	case errorState:
141		s += errorStyle.Render(fmt.Sprintf("Bummer: %s", b.error))
142	default:
143		s = normalStyle.Render(fmt.Sprintf("Doing something weird %d", b.state))
144	}
145	content = h + "\n\n" + s + "\n" + f
146	return appBoxStyle.Render(content)
147}
148
149func glamourReadme(md string) string {
150	tr, err := glamour.NewTermRenderer(
151		glamour.WithAutoStyle(),
152		glamour.WithWordWrap(boxRightWidth-2),
153	)
154	if err != nil {
155		log.Fatal(err)
156	}
157	mdt, err := tr.Render(md)
158	if err != nil {
159		return md
160	}
161	return mdt
162}
163
164func SessionHandler(reposPath string, repoPoll time.Duration) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
165	appCfg := &Config{}
166	rs := git.NewRepoSource(reposPath, glamourReadme)
167	appCfg.RepoSource = rs
168	go func() {
169		for {
170			_ = rs.LoadRepos()
171			cr, err := rs.GetRepo("config")
172			if err != nil {
173				log.Fatalf("cannot load config repo: %s", err)
174			}
175			cs, err := cr.LatestFile("config.json")
176			err = json.Unmarshal([]byte(cs), appCfg)
177			time.Sleep(repoPoll)
178		}
179	}()
180	err := createDefaultConfigRepo(rs)
181	if err != nil {
182		if err != nil {
183			log.Fatalf("cannot create config repo: %s", err)
184		}
185	}
186
187	return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
188		if len(s.Command()) == 0 {
189			pty, changes, active := s.Pty()
190			if !active {
191				return nil, nil
192			}
193			cfg := &SessionConfig{
194				Width:         pty.Window.Width,
195				Height:        pty.Window.Height,
196				WindowChanges: changes,
197			}
198			return NewBubble(appCfg, cfg), []tea.ProgramOption{tea.WithAltScreen()}
199		}
200		return nil, nil
201	}
202}