1package tui
2
3import (
4 "fmt"
5 "smoothie/git"
6 "smoothie/tui/bubbles/commits"
7 "smoothie/tui/bubbles/repo"
8 "smoothie/tui/bubbles/selection"
9
10 tea "github.com/charmbracelet/bubbletea"
11 "github.com/charmbracelet/lipgloss"
12 "github.com/gliderlabs/ssh"
13)
14
15type sessionState int
16
17const (
18 startState sessionState = iota
19 errorState
20 loadedState
21 quittingState
22 quitState
23)
24
25type Config struct {
26 Name string `json:"name"`
27 Host string `json:"host"`
28 Port int64 `json:"port"`
29 ShowAllRepos bool `json:"show_all_repos"`
30 Menu []MenuEntry `json:"menu"`
31 RepoSource *git.RepoSource
32}
33
34type MenuEntry struct {
35 Name string `json:"name"`
36 Note string `json:"note"`
37 Repo string `json:"repo"`
38 bubble *repo.Bubble
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}
62
63func NewBubble(cfg *Config, sCfg *SessionConfig) *Bubble {
64 b := &Bubble{
65 config: cfg,
66 width: sCfg.Width,
67 height: sCfg.Height,
68 windowChanges: sCfg.WindowChanges,
69 repoSource: cfg.RepoSource,
70 repoMenu: make([]MenuEntry, 0),
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.setupCmd)
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 rb := b.repoMenu[msg.Index].bubble
104 rb.GotoTop()
105 b.boxes[1] = rb
106 case selection.ActiveMsg:
107 rb := b.repoMenu[msg.Index].bubble
108 rb.GotoTop()
109 b.boxes[1] = b.repoMenu[msg.Index].bubble
110 }
111 if b.state == loadedState {
112 ab, cmd := b.boxes[b.activeBox].Update(msg)
113 b.boxes[b.activeBox] = ab
114 if cmd != nil {
115 cmds = append(cmds, cmd)
116 }
117 }
118 return b, tea.Batch(cmds...)
119}
120
121func (b *Bubble) viewForBox(i int, width int) string {
122 var ls lipgloss.Style
123 if i == b.activeBox {
124 ls = activeBoxStyle.Width(width)
125 } else {
126 ls = inactiveBoxStyle.Width(width)
127 }
128 return ls.Render(b.boxes[i].View())
129}
130
131func (b *Bubble) View() string {
132 h := headerStyle.Width(b.width - horizontalPadding).Render(b.config.Name)
133 f := footerStyle.Render("")
134 s := ""
135 content := ""
136 switch b.state {
137 case loadedState:
138 lb := b.viewForBox(0, boxLeftWidth)
139 rb := b.viewForBox(1, boxRightWidth)
140 s += lipgloss.JoinHorizontal(lipgloss.Top, lb, rb)
141 case errorState:
142 s += errorStyle.Render(fmt.Sprintf("Bummer: %s", b.error))
143 }
144 content = h + "\n\n" + s + "\n" + f
145 return appBoxStyle.Render(content)
146}