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	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}
 41
 42type SessionConfig struct {
 43	Width         int
 44	Height        int
 45	WindowChanges <-chan ssh.Window
 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	repoMenu      []MenuEntry
 57	repos         []*git.Repo
 58	boxes         []tea.Model
 59	activeBox     int
 60	repoSelect    *selection.Bubble
 61	commitsLog    *commits.Bubble
 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	}
 73	b.state = startState
 74	return b
 75}
 76
 77func (b *Bubble) Init() tea.Cmd {
 78	return tea.Batch(b.windowChangesCmd, b.setupCmd)
 79}
 80
 81func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 82	cmds := make([]tea.Cmd, 0)
 83	// Always allow state, error, info, window resize and quit messages
 84	switch msg := msg.(type) {
 85	case tea.KeyMsg:
 86		switch msg.String() {
 87		case "q", "ctrl+c":
 88			return b, tea.Quit
 89		case "tab":
 90			b.activeBox = (b.activeBox + 1) % 2
 91		}
 92	case errMsg:
 93		b.error = msg.Error()
 94		b.state = errorState
 95		return b, nil
 96	case windowMsg:
 97		cmds = append(cmds, b.windowChangesCmd)
 98	case tea.WindowSizeMsg:
 99		b.width = msg.Width
100		b.height = msg.Height
101	case selection.SelectedMsg:
102		b.activeBox = 1
103		cmds = append(cmds, b.getRepoCmd(b.repoMenu[msg.Index].Repo))
104	case selection.ActiveMsg:
105		cmds = append(cmds, b.getRepoCmd(b.repoMenu[msg.Index].Repo))
106	}
107	if b.state == loadedState {
108		ab, cmd := b.boxes[b.activeBox].Update(msg)
109		b.boxes[b.activeBox] = ab
110		if cmd != nil {
111			cmds = append(cmds, cmd)
112		}
113	}
114	return b, tea.Batch(cmds...)
115}
116
117func (b *Bubble) viewForBox(i int, width int) string {
118	var ls lipgloss.Style
119	if i == b.activeBox {
120		ls = activeBoxStyle.Width(width)
121	} else {
122		ls = inactiveBoxStyle.Width(width)
123	}
124	return ls.Render(b.boxes[i].View())
125}
126
127func (b *Bubble) View() string {
128	h := headerStyle.Width(b.width - horizontalPadding).Render(b.config.Name)
129	f := footerStyle.Render("")
130	s := ""
131	content := ""
132	switch b.state {
133	case loadedState:
134		lb := b.viewForBox(0, boxLeftWidth)
135		rb := b.viewForBox(1, boxRightWidth)
136		s += lipgloss.JoinHorizontal(lipgloss.Top, lb, rb)
137	case errorState:
138		s += errorStyle.Render(fmt.Sprintf("Bummer: %s", b.error))
139	default:
140		s = normalStyle.Render(fmt.Sprintf("Doing something weird %d", b.state))
141	}
142	content = h + "\n\n" + s + "\n" + f
143	return appBoxStyle.Render(content)
144}
145
146func loadConfig(rs *git.RepoSource) (*Config, error) {
147	cfg := &Config{}
148	cfg.RepoSource = rs
149	cr, err := rs.GetRepo("config")
150	if err != nil {
151		return nil, fmt.Errorf("cannot load config repo: %s", err)
152	}
153	cs, err := cr.LatestFile("config.json")
154	if err != nil {
155		return nil, fmt.Errorf("cannot load config.json: %s", err)
156	}
157	err = json.Unmarshal([]byte(cs), cfg)
158	if err != nil {
159		return nil, fmt.Errorf("bad json in config.json: %s", err)
160	}
161	return cfg, nil
162}
163
164func SessionHandler(reposPath string, repoPoll time.Duration) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
165	rs := git.NewRepoSource(reposPath)
166	err := createDefaultConfigRepo(rs)
167	if err != nil {
168		if err != nil {
169			log.Fatalf("cannot create config repo: %s", err)
170		}
171	}
172	appCfg, err := loadConfig(rs)
173	if err != nil {
174		if err != nil {
175			log.Printf("cannot load config: %s", err)
176		}
177	}
178	go func() {
179		for {
180			time.Sleep(repoPoll)
181			err := rs.LoadRepos()
182			if err != nil {
183				log.Printf("cannot load repos: %s", err)
184				continue
185			}
186			cfg, err := loadConfig(rs)
187			if err != nil {
188				if err != nil {
189					log.Printf("cannot load config: %s", err)
190					continue
191				}
192			}
193			appCfg = cfg
194		}
195	}()
196
197	return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
198		if len(s.Command()) == 0 {
199			pty, changes, active := s.Pty()
200			if !active {
201				return nil, nil
202			}
203			cfg := &SessionConfig{
204				Width:         pty.Window.Width,
205				Height:        pty.Window.Height,
206				WindowChanges: changes,
207			}
208			return NewBubble(appCfg, cfg), []tea.ProgramOption{tea.WithAltScreen()}
209		}
210		return nil, nil
211	}
212}