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 cmds = append(cmds, b.getRepoCmd(b.repos[msg.Index].Name))
87 }
88 if b.state == loadedState {
89 ab, cmd := b.boxes[b.activeBox].Update(msg)
90 b.boxes[b.activeBox] = ab
91 if cmd != nil {
92 cmds = append(cmds, cmd)
93 }
94 }
95 return b, tea.Batch(cmds...)
96}
97
98func (b *Bubble) viewForBox(i int, width int) string {
99 var ls lipgloss.Style
100 if i == b.activeBox {
101 ls = activeBoxStyle.Width(width)
102 } else {
103 ls = inactiveBoxStyle.Width(width)
104 }
105 return ls.Render(b.boxes[i].View())
106}
107
108func (b *Bubble) View() string {
109 h := headerStyle.Width(b.width - horizontalPadding).Render("Charm Beta")
110 f := footerStyle.Render("")
111 s := ""
112 content := ""
113 switch b.state {
114 case loadedState:
115 lb := b.viewForBox(0, boxLeftWidth)
116 rb := b.viewForBox(1, boxRightWidth)
117 s += lipgloss.JoinHorizontal(lipgloss.Top, lb, rb)
118 case errorState:
119 s += errorStyle.Render(fmt.Sprintf("Bummer: %s", b.error))
120 default:
121 s = normalStyle.Render(fmt.Sprintf("Doing something weird %d", b.state))
122 }
123 content = h + "\n\n" + s + "\n" + f
124 return appBoxStyle.Render(content)
125}
126
127func glamourReadme(md string) string {
128 tr, err := glamour.NewTermRenderer(
129 glamour.WithAutoStyle(),
130 glamour.WithWordWrap(boxRightWidth-2),
131 )
132 if err != nil {
133 log.Fatal(err)
134 }
135 mdt, err := tr.Render(md)
136 if err != nil {
137 return md
138 }
139 return mdt
140}
141
142func SessionHandler(reposPath string) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
143 rs := git.NewRepoSource(reposPath, time.Second*10, glamourReadme)
144 return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
145 if len(s.Command()) == 0 {
146 pty, changes, active := s.Pty()
147 if !active {
148 return nil, nil
149 }
150 return NewBubble(
151 pty.Window.Width,
152 pty.Window.Height,
153 changes,
154 rs),
155 []tea.ProgramOption{tea.WithAltScreen()}
156 }
157 return nil, nil
158 }
159}