1package ui
2
3import (
4 "log"
5 "strings"
6
7 "github.com/charmbracelet/bubbles/key"
8 tea "github.com/charmbracelet/bubbletea"
9 "github.com/charmbracelet/lipgloss"
10 "github.com/charmbracelet/soft-serve/ui/common"
11 "github.com/charmbracelet/soft-serve/ui/components/footer"
12 "github.com/charmbracelet/soft-serve/ui/components/header"
13 "github.com/charmbracelet/soft-serve/ui/components/selector"
14 "github.com/charmbracelet/soft-serve/ui/pages/repo"
15 "github.com/charmbracelet/soft-serve/ui/pages/selection"
16 "github.com/charmbracelet/soft-serve/ui/session"
17)
18
19type sessionState int
20
21const (
22 startState sessionState = iota
23 errorState
24 loadedState
25)
26
27// UI is the main UI model.
28type UI struct {
29 s session.Session
30 common common.Common
31 pages []common.Page
32 activePage int
33 state sessionState
34 header *header.Header
35 footer *footer.Footer
36 error error
37}
38
39// New returns a new UI model.
40func New(s session.Session, c common.Common, initialRepo string) *UI {
41 h := header.New(c, s.Config().Name)
42 ui := &UI{
43 s: s,
44 common: c,
45 pages: make([]common.Page, 2), // selection & repo
46 activePage: 0,
47 state: startState,
48 header: h,
49 }
50 ui.footer = footer.New(c, ui)
51 ui.SetSize(c.Width, c.Height)
52 return ui
53}
54
55func (ui *UI) getMargins() (wm, hm int) {
56 wm = ui.common.Styles.App.GetHorizontalFrameSize()
57 hm = ui.common.Styles.App.GetVerticalFrameSize() +
58 ui.common.Styles.Header.GetHeight() +
59 ui.common.Styles.Footer.GetHeight()
60 return
61}
62
63// ShortHelp implements help.KeyMap.
64func (ui *UI) ShortHelp() []key.Binding {
65 b := make([]key.Binding, 0)
66 b = append(b, ui.pages[ui.activePage].ShortHelp()...)
67 b = append(b, ui.common.Keymap.Quit)
68 return b
69}
70
71// FullHelp implements help.KeyMap.
72func (ui *UI) FullHelp() [][]key.Binding {
73 b := make([][]key.Binding, 0)
74 b = append(b, ui.pages[ui.activePage].FullHelp()...)
75 b = append(b, []key.Binding{ui.common.Keymap.Quit})
76 return b
77}
78
79// SetSize implements common.Component.
80func (ui *UI) SetSize(width, height int) {
81 ui.common.SetSize(width, height)
82 wm, hm := ui.getMargins()
83 ui.header.SetSize(width-wm, height-hm)
84 ui.footer.SetSize(width-wm, height-hm)
85 for _, p := range ui.pages {
86 if p != nil {
87 p.SetSize(width-wm, height-hm)
88 }
89 }
90}
91
92// Init implements tea.Model.
93func (ui *UI) Init() tea.Cmd {
94 cfg := ui.s.Config()
95 ui.pages[0] = selection.New(ui.s, ui.common)
96 ui.pages[1] = repo.New(ui.common, &source{cfg.Source})
97 ui.SetSize(ui.common.Width, ui.common.Height)
98 ui.state = loadedState
99 return tea.Batch(
100 ui.pages[0].Init(),
101 ui.pages[1].Init(),
102 )
103}
104
105// Update implements tea.Model.
106// TODO show full help
107func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
108 log.Printf("%T", msg)
109 cmds := make([]tea.Cmd, 0)
110 switch msg := msg.(type) {
111 case tea.WindowSizeMsg:
112 h, cmd := ui.header.Update(msg)
113 ui.header = h.(*header.Header)
114 if cmd != nil {
115 cmds = append(cmds, cmd)
116 }
117 f, cmd := ui.footer.Update(msg)
118 ui.footer = f.(*footer.Footer)
119 if cmd != nil {
120 cmds = append(cmds, cmd)
121 }
122 for i, p := range ui.pages {
123 m, cmd := p.Update(msg)
124 ui.pages[i] = m.(common.Page)
125 if cmd != nil {
126 cmds = append(cmds, cmd)
127 }
128 }
129 ui.SetSize(msg.Width, msg.Height)
130 case tea.KeyMsg:
131 switch {
132 case key.Matches(msg, ui.common.Keymap.Quit):
133 return ui, tea.Quit
134 case ui.activePage == 1 && key.Matches(msg, ui.common.Keymap.Back):
135 ui.activePage = 0
136 }
137 case common.ErrorMsg:
138 ui.error = msg
139 ui.state = errorState
140 return ui, nil
141 case selector.SelectMsg:
142 ui.activePage = (ui.activePage + 1) % 2
143 }
144 m, cmd := ui.pages[ui.activePage].Update(msg)
145 ui.pages[ui.activePage] = m.(common.Page)
146 if cmd != nil {
147 cmds = append(cmds, cmd)
148 }
149 return ui, tea.Batch(cmds...)
150}
151
152// View implements tea.Model.
153func (ui *UI) View() string {
154 s := strings.Builder{}
155 switch ui.state {
156 case startState:
157 s.WriteString("Loading...")
158 case errorState:
159 err := ui.common.Styles.ErrorTitle.Render("Bummer")
160 err += ui.common.Styles.ErrorBody.Render(ui.error.Error())
161 s.WriteString(err)
162 case loadedState:
163 s.WriteString(lipgloss.JoinVertical(
164 lipgloss.Bottom,
165 ui.header.View(),
166 ui.pages[ui.activePage].View(),
167 ui.footer.View(),
168 ))
169 default:
170 s.WriteString("Unknown state :/ this is a bug!")
171 }
172 return ui.common.Styles.App.Render(s.String())
173}