repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

pico / tui
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}