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.loadGitCmd)
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}