1package tui
2
3import (
4 "fmt"
5 "log"
6 "smoothie/git"
7 "smoothie/tui/bubbles/commits"
8 "smoothie/tui/bubbles/selection"
9 "time"
10
11 "github.com/charmbracelet/bubbles/viewport"
12 tea "github.com/charmbracelet/bubbletea"
13 "github.com/charmbracelet/glamour"
14 "github.com/charmbracelet/lipgloss"
15 "github.com/gliderlabs/ssh"
16)
17
18type sessionState int
19
20const (
21 startState sessionState = iota
22 errorState
23 loadedState
24 quittingState
25 quitState
26)
27
28type Bubble struct {
29 state sessionState
30 error string
31 width int
32 height int
33 windowChanges <-chan ssh.Window
34 repoSource *git.RepoSource
35 repos []*git.Repo
36 boxes []tea.Model
37 activeBox int
38 repoSelect *selection.Bubble
39 commitsLog *commits.Bubble
40 readmeViewport *ViewportBubble
41}
42
43func NewBubble(width int, height int, windowChanges <-chan ssh.Window, repoSource *git.RepoSource) *Bubble {
44 b := &Bubble{
45 width: width,
46 height: height,
47 windowChanges: windowChanges,
48 repoSource: repoSource,
49 boxes: make([]tea.Model, 2),
50 readmeViewport: &ViewportBubble{
51 Viewport: &viewport.Model{
52 Width: boxRightWidth - horizontalPadding - 2,
53 Height: height - verticalPadding - viewportHeightConstant,
54 },
55 },
56 }
57 b.state = startState
58 return b
59}
60
61func (b *Bubble) Init() tea.Cmd {
62 return tea.Batch(b.windowChangesCmd, b.loadGitCmd)
63}
64
65func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
66 cmds := make([]tea.Cmd, 0)
67 // Always allow state, error, info, window resize and quit messages
68 switch msg := msg.(type) {
69 case tea.KeyMsg:
70 switch msg.String() {
71 case "q", "ctrl+c":
72 return b, tea.Quit
73 case "tab":
74 b.activeBox = (b.activeBox + 1) % 2
75 }
76 case errMsg:
77 b.error = msg.Error()
78 b.state = errorState
79 return b, nil
80 case windowMsg:
81 cmds = append(cmds, b.windowChangesCmd)
82 case tea.WindowSizeMsg:
83 b.width = msg.Width
84 b.height = msg.Height
85 case selection.SelectedMsg:
86 rmd := b.repos[msg.Index].Readme
87 b.readmeViewport.Viewport.GotoTop()
88 b.readmeViewport.Viewport.Height = b.height - verticalPadding - viewportHeightConstant
89 b.readmeViewport.Viewport.Width = boxLeftWidth - 2
90 b.readmeViewport.Viewport.SetContent(rmd)
91 b.boxes[1] = b.readmeViewport
92 }
93 if b.state == loadedState {
94 ab, cmd := b.boxes[b.activeBox].Update(msg)
95 b.boxes[b.activeBox] = ab
96 if cmd != nil {
97 cmds = append(cmds, cmd)
98 }
99 }
100 return b, tea.Batch(cmds...)
101}
102
103func (b *Bubble) viewForBox(i int, width int) string {
104 var ls lipgloss.Style
105 if i == b.activeBox {
106 ls = activeBoxStyle.Width(width)
107 } else {
108 ls = inactiveBoxStyle.Width(width)
109 }
110 return ls.Render(b.boxes[i].View())
111}
112
113func (b *Bubble) View() string {
114 h := headerStyle.Width(b.width - horizontalPadding).Render("Charm Beta")
115 f := footerStyle.Render("")
116 s := ""
117 content := ""
118 switch b.state {
119 case loadedState:
120 lb := b.viewForBox(0, boxLeftWidth)
121 rb := b.viewForBox(1, boxRightWidth)
122 s += lipgloss.JoinHorizontal(lipgloss.Top, lb, rb)
123 case errorState:
124 s += errorStyle.Render(fmt.Sprintf("Bummer: %s", b.error))
125 default:
126 s = normalStyle.Render(fmt.Sprintf("Doing something weird %d", b.state))
127 }
128 content = h + "\n\n" + s + "\n" + f
129 return appBoxStyle.Render(content)
130}
131
132func glamourReadme(md string) string {
133 tr, err := glamour.NewTermRenderer(
134 glamour.WithAutoStyle(),
135 glamour.WithWordWrap(boxRightWidth-2),
136 )
137 if err != nil {
138 log.Fatal(err)
139 }
140 mdt, err := tr.Render(md)
141 if err != nil {
142 return md
143 }
144 return mdt
145}
146
147func SessionHandler(reposPath string) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
148 rs := git.NewRepoSource(reposPath, time.Second*10, glamourReadme)
149 return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
150 if len(s.Command()) == 0 {
151 pty, changes, active := s.Pty()
152 if !active {
153 return nil, nil
154 }
155 return NewBubble(
156 pty.Window.Width,
157 pty.Window.Height,
158 changes,
159 rs),
160 []tea.ProgramOption{tea.WithAltScreen()}
161 }
162 return nil, nil
163 }
164}