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}