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 InitialRepo string
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 initialRepo string
57 repoMenu []MenuEntry
58 repos []*git.Repo
59 boxes []tea.Model
60 activeBox int
61 repoSelect *selection.Bubble
62 commitsLog *commits.Bubble
63}
64
65func NewBubble(cfg *Config, sCfg *SessionConfig) *Bubble {
66 b := &Bubble{
67 config: cfg,
68 width: sCfg.Width,
69 height: sCfg.Height,
70 windowChanges: sCfg.WindowChanges,
71 repoSource: cfg.RepoSource,
72 repoMenu: make([]MenuEntry, 0),
73 boxes: make([]tea.Model, 2),
74 initialRepo: sCfg.InitialRepo,
75 }
76 b.state = startState
77 return b
78}
79
80func (b *Bubble) Init() tea.Cmd {
81 return tea.Batch(b.windowChangesCmd, b.setupCmd)
82}
83
84func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
85 cmds := make([]tea.Cmd, 0)
86 // Always allow state, error, info, window resize and quit messages
87 switch msg := msg.(type) {
88 case tea.KeyMsg:
89 switch msg.String() {
90 case "q", "ctrl+c":
91 return b, tea.Quit
92 case "tab":
93 b.activeBox = (b.activeBox + 1) % 2
94 }
95 case errMsg:
96 b.error = msg.Error()
97 b.state = errorState
98 return b, nil
99 case windowMsg:
100 cmds = append(cmds, b.windowChangesCmd)
101 case tea.WindowSizeMsg:
102 b.width = msg.Width
103 b.height = msg.Height
104 case selection.SelectedMsg:
105 b.activeBox = 1
106 rb := b.repoMenu[msg.Index].bubble
107 rb.GotoTop()
108 b.boxes[1] = rb
109 case selection.ActiveMsg:
110 rb := b.repoMenu[msg.Index].bubble
111 rb.GotoTop()
112 b.boxes[1] = b.repoMenu[msg.Index].bubble
113 }
114 if b.state == loadedState {
115 ab, cmd := b.boxes[b.activeBox].Update(msg)
116 b.boxes[b.activeBox] = ab
117 if cmd != nil {
118 cmds = append(cmds, cmd)
119 }
120 }
121 return b, tea.Batch(cmds...)
122}
123
124func (b *Bubble) viewForBox(i int, width int) string {
125 var ls lipgloss.Style
126 if i == b.activeBox {
127 ls = activeBoxStyle.Width(width)
128 } else {
129 ls = inactiveBoxStyle.Width(width)
130 }
131 return ls.Render(b.boxes[i].View())
132}
133
134func (b *Bubble) View() string {
135 h := headerStyle.Width(b.width - horizontalPadding).Render(b.config.Name)
136 f := footerStyle.Render("")
137 s := ""
138 content := ""
139 switch b.state {
140 case loadedState:
141 lb := b.viewForBox(0, boxLeftWidth)
142 rb := b.viewForBox(1, boxRightWidth)
143 s += lipgloss.JoinHorizontal(lipgloss.Top, lb, rb)
144 case errorState:
145 s += errorStyle.Render(fmt.Sprintf("Bummer: %s", b.error))
146 }
147 content = h + "\n\n" + s + "\n" + f
148 return appBoxStyle.Render(content)
149}