Eric Bower
·
17 Dec 24
ui.go
1package tui
2
3import (
4 "fmt"
5
6 tea "github.com/charmbracelet/bubbletea"
7 "github.com/charmbracelet/lipgloss"
8 "github.com/charmbracelet/wish"
9 "github.com/muesli/reflow/wordwrap"
10 "github.com/muesli/reflow/wrap"
11 "github.com/picosh/pico/tui/analytics"
12 "github.com/picosh/pico/tui/chat"
13 "github.com/picosh/pico/tui/common"
14 "github.com/picosh/pico/tui/createaccount"
15 "github.com/picosh/pico/tui/createkey"
16 "github.com/picosh/pico/tui/createtoken"
17 "github.com/picosh/pico/tui/logs"
18 "github.com/picosh/pico/tui/menu"
19 "github.com/picosh/pico/tui/notifications"
20 "github.com/picosh/pico/tui/pages"
21 "github.com/picosh/pico/tui/plus"
22 "github.com/picosh/pico/tui/pubkeys"
23 "github.com/picosh/pico/tui/settings"
24 "github.com/picosh/pico/tui/tokens"
25)
26
27type state int
28
29const (
30 initState state = iota
31 readyState
32)
33
34// Just a generic tea.Model to demo terminal information of ssh.
35type UI struct {
36 shared *common.SharedModel
37
38 state state
39 activePage pages.Page
40 pages []tea.Model
41}
42
43func NewUI(shared *common.SharedModel) *UI {
44 m := &UI{
45 shared: shared,
46 state: initState,
47 pages: make([]tea.Model, 12),
48 }
49 return m
50}
51
52func (m *UI) updateActivePage(msg tea.Msg) tea.Cmd {
53 nm, cmd := m.pages[m.activePage].Update(msg)
54 m.pages[m.activePage] = nm
55 return cmd
56}
57
58func (m *UI) setupUser() error {
59 user, err := findUser(m.shared)
60 if err != nil {
61 m.shared.Logger.Error("cannot find user", "err", err)
62 wish.Errorf(m.shared.Session, "\nERROR: %s\n\n", err)
63 return err
64 }
65
66 m.shared.User = user
67 ff, _ := findPlusFeatureFlag(m.shared)
68 m.shared.PlusFeatureFlag = ff
69
70 return nil
71}
72
73func (m *UI) Init() tea.Cmd {
74 // header height is required to calculate viewport for
75 // some pages
76 m.shared.HeaderHeight = lipgloss.Height(m.header()) + 1
77
78 m.pages[pages.MenuPage] = menu.NewModel(m.shared)
79 m.pages[pages.CreateAccountPage] = createaccount.NewModel(m.shared)
80 m.pages[pages.CreatePubkeyPage] = createkey.NewModel(m.shared)
81 m.pages[pages.CreateTokenPage] = createtoken.NewModel(m.shared)
82 m.pages[pages.CreateAccountPage] = createaccount.NewModel(m.shared)
83 m.pages[pages.PubkeysPage] = pubkeys.NewModel(m.shared)
84 m.pages[pages.TokensPage] = tokens.NewModel(m.shared)
85 m.pages[pages.NotificationsPage] = notifications.NewModel(m.shared)
86 m.pages[pages.PlusPage] = plus.NewModel(m.shared)
87 m.pages[pages.SettingsPage] = settings.NewModel(m.shared)
88 m.pages[pages.LogsPage] = logs.NewModel(m.shared)
89 m.pages[pages.AnalyticsPage] = analytics.NewModel(m.shared)
90 m.pages[pages.ChatPage] = chat.NewModel(m.shared)
91 if m.shared.User == nil {
92 m.activePage = pages.CreateAccountPage
93 } else {
94 m.activePage = pages.MenuPage
95 }
96 m.state = readyState
97 return nil
98}
99
100func (m *UI) updateModels(msg tea.Msg) tea.Cmd {
101 cmds := []tea.Cmd{}
102 for i, page := range m.pages {
103 if page == nil {
104 continue
105 }
106 nm, cmd := page.Update(msg)
107 m.pages[i] = nm
108 cmds = append(cmds, cmd)
109 }
110 return tea.Batch(cmds...)
111}
112
113func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
114 var cmds []tea.Cmd
115
116 switch msg := msg.(type) {
117 case tea.WindowSizeMsg:
118 m.shared.Width = msg.Width
119 m.shared.Height = msg.Height
120 return m, m.updateModels(msg)
121
122 case tea.KeyMsg:
123 switch msg.Type {
124 case tea.KeyCtrlC:
125 m.shared.Dbpool.Close()
126 return m, tea.Quit
127 }
128
129 case pages.NavigateMsg:
130 // send message to the active page so it can teardown
131 // and reset itself
132 cmds = append(cmds, m.updateActivePage(msg))
133 m.activePage = msg.Page
134
135 // user created account
136 case createaccount.CreateAccountMsg:
137 _ = m.setupUser()
138 // reset model and pages
139 return m, m.Init()
140
141 case menu.MenuChoiceMsg:
142 switch msg.MenuChoice {
143 case menu.KeysChoice:
144 m.activePage = pages.PubkeysPage
145 case menu.TokensChoice:
146 m.activePage = pages.TokensPage
147 case menu.NotificationsChoice:
148 m.activePage = pages.NotificationsPage
149 case menu.PlusChoice:
150 m.activePage = pages.PlusPage
151 case menu.SettingsChoice:
152 m.activePage = pages.SettingsPage
153 case menu.LogsChoice:
154 m.activePage = pages.LogsPage
155 case menu.AnalyticsChoice:
156 m.activePage = pages.AnalyticsPage
157 case menu.ChatChoice:
158 m.activePage = pages.ChatPage
159 case menu.ExitChoice:
160 m.shared.Dbpool.Close()
161 return m, tea.Quit
162 }
163
164 cmds = append(cmds, m.pages[m.activePage].Init())
165 }
166
167 cmd := m.updateActivePage(msg)
168 cmds = append(cmds, cmd)
169
170 return m, tea.Batch(cmds...)
171}
172
173func (m *UI) header() string {
174 logoTxt := "pico.sh"
175 ff := m.shared.PlusFeatureFlag
176 if ff != nil && ff.IsValid() {
177 logoTxt = "pico+"
178 }
179
180 logo := m.shared.
181 Styles.
182 Logo.
183 SetString(logoTxt)
184 title := m.shared.
185 Styles.
186 Note.
187 SetString(pages.ToTitle(m.activePage))
188 div := m.shared.
189 Styles.
190 HelpDivider.
191 Foreground(common.Green)
192 s := fmt.Sprintf("%s%s%s\n\n", logo, div, title)
193 return s
194}
195
196func (m *UI) View() string {
197 s := m.header()
198
199 if m.pages[m.activePage] != nil {
200 s += m.pages[m.activePage].View()
201 }
202
203 width := m.shared.Width - m.shared.Styles.App.GetHorizontalFrameSize()
204 maxWidth := width
205 str := wrap.String(
206 wordwrap.String(s, maxWidth),
207 maxWidth,
208 )
209 return m.shared.Styles.App.Render(str)
210}