1package tui
2
3import (
4 "fmt"
5 "smoothie/git"
6 "time"
7
8 tea "github.com/charmbracelet/bubbletea"
9 "github.com/charmbracelet/lipgloss"
10 "github.com/gliderlabs/ssh"
11)
12
13type sessionState int
14
15const (
16 startState sessionState = iota
17 errorState
18 loadedState
19 quittingState
20 quitState
21)
22
23type Model struct {
24 state sessionState
25 error string
26 info string
27 width int
28 height int
29 windowChanges <-chan ssh.Window
30 repoSource *git.RepoSource
31 repos []*git.Repo
32 activeBubble int
33 bubbles []tea.Model
34}
35
36func NewModel(width int, height int, windowChanges <-chan ssh.Window, repoSource *git.RepoSource) *Model {
37 m := &Model{
38 width: width,
39 height: height,
40 windowChanges: windowChanges,
41 repoSource: repoSource,
42 bubbles: make([]tea.Model, 2),
43 }
44 m.state = startState
45 return m
46}
47
48func (m *Model) Init() tea.Cmd {
49 return tea.Batch(m.windowChangesCmd, m.loadGitCmd)
50}
51
52func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
53 cmds := make([]tea.Cmd, 0)
54 // Always allow state, error, info, window resize and quit messages
55 switch msg := msg.(type) {
56 case stateMsg:
57 m.state = msg.state
58 case tea.KeyMsg:
59 switch msg.String() {
60 case "q", "ctrl+c":
61 return m, tea.Quit
62 case "tab":
63 m.activeBubble = (m.activeBubble + 1) % 2
64 }
65 case errMsg:
66 m.error = msg.Error()
67 m.state = errorState
68 return m, nil
69 case infoMsg:
70 m.info = msg.text
71 case windowMsg:
72 cmds = append(cmds, m.windowChangesCmd)
73 case tea.WindowSizeMsg:
74 m.width = msg.Width
75 m.height = msg.Height
76 }
77 if m.state == loadedState {
78 b, cmd := m.bubbles[m.activeBubble].Update(msg)
79 m.bubbles[m.activeBubble] = b
80 if cmd != nil {
81 cmds = append(cmds, cmd)
82 }
83 }
84 return m, tea.Batch(cmds...)
85}
86
87func (m *Model) viewForBubble(i int, width int) string {
88 var ls lipgloss.Style
89 if i == m.activeBubble {
90 ls = activeBoxStyle.Width(width)
91 } else {
92 ls = inactiveBoxStyle.Width(width)
93 }
94 return ls.Render(m.bubbles[i].View())
95}
96
97func (m *Model) View() string {
98 pad := 6
99 h := headerStyle.Width(m.width - pad).Render("Charm Beta")
100 f := footerStyle.Render(m.info)
101 s := ""
102 content := ""
103 switch m.state {
104 case loadedState:
105 lb := m.viewForBubble(0, 25)
106 rb := m.viewForBubble(1, 84)
107 s += lipgloss.JoinHorizontal(lipgloss.Top, lb, rb)
108 case errorState:
109 s += errorStyle.Render(fmt.Sprintf("Bummer: %s", m.error))
110 default:
111 s = normalStyle.Render(fmt.Sprintf("Doing something weird %d", m.state))
112 }
113 content = h + "\n" + s + "\n" + f
114 return appBoxStyle.Render(content)
115}
116
117func SessionHandler(reposPath string) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
118 rs := git.NewRepoSource(reposPath, time.Second*10)
119 return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
120 if len(s.Command()) == 0 {
121 pty, changes, active := s.Pty()
122 if !active {
123 return nil, nil
124 }
125 return NewModel(pty.Window.Width, pty.Window.Height, changes, rs), []tea.ProgramOption{tea.WithAltScreen()}
126 }
127 return nil, nil
128 }
129}