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