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}