From 24a234508c0f70a29355a8653ad87661dd0224f3 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Wed, 13 Apr 2022 18:52:44 -0400 Subject: [PATCH] wip: selection readme --- go.sum | 4 - ui/common/error.go | 11 ++ ui/components/code/code.go | 139 +++++++++++++++++++++++ ui/components/selector/item.go | 1 - ui/components/selector/selector.go | 43 ++++++- ui/components/viewport/viewport_patch.go | 29 +++++ ui/keymap/keymap.go | 2 +- ui/pages/selection/selection.go | 27 ++++- ui/session/session.go | 3 + ui/ui.go | 30 ++--- 10 files changed, 260 insertions(+), 29 deletions(-) create mode 100644 ui/common/error.go create mode 100644 ui/components/code/code.go create mode 100644 ui/components/viewport/viewport_patch.go diff --git a/go.sum b/go.sum index c1d1aed15ec05f1721c66dacd23c223ac40317a0..698185837e619e01b8771b068a06d1c7514bef08 100644 --- a/go.sum +++ b/go.sum @@ -23,10 +23,6 @@ github.com/caarlos0/env/v6 v6.9.1 h1:zOkkjM0F6ltnQ5eBX6IPI41UP/KDGEK7rRPwGCNos8k github.com/caarlos0/env/v6 v6.9.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3 h1:w2ANoiT4ubmh4Nssa3/QW1M7lj3FZkma8f8V5aBDxXM= github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA= -github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho= -github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA= -github.com/charmbracelet/bubbles v0.10.4-0.20220302223835-88562515cf7b h1:o+LFpRn1fXtu1hDJLtBFjp7tMZ8AqwSpl84w1TnUj0Y= -github.com/charmbracelet/bubbles v0.10.4-0.20220302223835-88562515cf7b/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA= github.com/charmbracelet/bubbles v0.10.4-0.20220412141214-292a1dd7ba97 h1:NJqAUfS+JNHqodsbhLR0zD3sDkXI7skwjAwd77HXe/Q= github.com/charmbracelet/bubbles v0.10.4-0.20220412141214-292a1dd7ba97/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA= github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA= diff --git a/ui/common/error.go b/ui/common/error.go new file mode 100644 index 0000000000000000000000000000000000000000..bbf5bbe5f47e808acceb22c8a340bb2a67a8756e --- /dev/null +++ b/ui/common/error.go @@ -0,0 +1,11 @@ +package common + +import tea "github.com/charmbracelet/bubbletea" + +type ErrorMsg error + +func ErrorCmd(err error) tea.Cmd { + return func() tea.Msg { + return ErrorMsg(err) + } +} diff --git a/ui/components/code/code.go b/ui/components/code/code.go new file mode 100644 index 0000000000000000000000000000000000000000..2ee4aec1b0f902ccf50bd91095f22ada64f03e3c --- /dev/null +++ b/ui/components/code/code.go @@ -0,0 +1,139 @@ +package code + +import ( + "strings" + + "github.com/alecthomas/chroma/lexers" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/glamour" + gansi "github.com/charmbracelet/glamour/ansi" + "github.com/charmbracelet/soft-serve/ui/common" + vp "github.com/charmbracelet/soft-serve/ui/components/viewport" + "github.com/muesli/reflow/wrap" + "github.com/muesli/termenv" +) + +type Code struct { + common common.Common + content string + extension string + viewport *vp.ViewportBubble +} + +func New(c common.Common, content, extension string) *Code { + r := &Code{ + common: c, + content: content, + extension: extension, + viewport: &vp.ViewportBubble{ + Viewport: &viewport.Model{ + MouseWheelEnabled: true, + }, + }, + } + return r +} + +func (r *Code) SetSize(width, height int) { + r.common.Width = width + r.common.Height = height + r.viewport.SetSize(width, height) +} + +func (r *Code) SetContent(c, ext string) tea.Cmd { + r.content = c + r.extension = ext + return r.Init() +} + +func (r *Code) Init() tea.Cmd { + w := r.common.Width + s := r.common.Styles + c := r.content + if c == "" { + c = s.AboutNoReadme.Render("File is empty.") + } + f, err := renderFile(r.extension, c, w) + if err != nil { + return common.ErrorCmd(err) + } + c = wrap.String(f, w) + r.viewport.Viewport.SetContent(c) + return nil +} + +func (r *Code) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + v, cmd := r.viewport.Update(msg) + r.viewport = v.(*vp.ViewportBubble) + return r, cmd +} + +func (r *Code) View() string { + return r.viewport.View() +} + +func styleConfig() gansi.StyleConfig { + noColor := "" + s := glamour.DarkStyleConfig + s.Document.StylePrimitive.Color = &noColor + s.CodeBlock.Chroma.Text.Color = &noColor + s.CodeBlock.Chroma.Name.Color = &noColor + return s +} + +func renderCtx() gansi.RenderContext { + return gansi.NewRenderContext(gansi.Options{ + ColorProfile: termenv.TrueColor, + Styles: styleConfig(), + }) +} + +func glamourize(w int, md string) (string, error) { + if w > 120 { + w = 120 + } + tr, err := glamour.NewTermRenderer( + glamour.WithStyles(styleConfig()), + glamour.WithWordWrap(w), + ) + + if err != nil { + return "", err + } + mdt, err := tr.Render(md) + if err != nil { + return "", err + } + return mdt, nil +} + +func renderFile(path, content string, width int) (string, error) { + lexer := lexers.Fallback + if path == "" { + lexer = lexers.Analyse(content) + } else { + lexer = lexers.Match(path) + } + lang := "" + if lexer != nil && lexer.Config() != nil { + lang = lexer.Config().Name + } + if lang == "markdown" { + md, err := glamourize(width, content) + if err != nil { + return "", err + } + return md, nil + } + formatter := &gansi.CodeBlockElement{ + Code: content, + Language: lang, + } + r := strings.Builder{} + err := formatter.Render(&r, renderCtx()) + if err != nil { + return "", err + } + return r.String(), nil +} diff --git a/ui/components/selector/item.go b/ui/components/selector/item.go index 67a23e66c48796df6d0639eac74ec718eb30b912..16a66458414ce9cd60386691e9e3f726fedc4b7a 100644 --- a/ui/components/selector/item.go +++ b/ui/components/selector/item.go @@ -85,7 +85,6 @@ func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list if index == m.Index() { style = d.styles.SelectedMenuItem.Copy() } - style.Width(m.Width() - 2) // FIXME figure out where this "2" comes from titleStr := i.Title updatedStr := fmt.Sprintf(" Updated %s", humanize.Time(i.LastUpdate)) updated := d.styles.MenuLastUpdate. diff --git a/ui/components/selector/selector.go b/ui/components/selector/selector.go index ee9ac8ba04b06bad70f8acdd870021fb50230275..d4fc300aead274491a7397a75d658223f92cc3da 100644 --- a/ui/components/selector/selector.go +++ b/ui/components/selector/selector.go @@ -1,6 +1,7 @@ package selector import ( + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/soft-serve/ui/common" @@ -9,8 +10,13 @@ import ( type Selector struct { list list.Model common common.Common + active int } +type SelectMsg string + +type ActiveMsg string + func New(common common.Common, items []list.Item) *Selector { l := list.New(items, ItemDelegate{common.Styles}, common.Width, common.Height) l.SetShowTitle(false) @@ -38,23 +44,48 @@ func (s *Selector) SetItems(items []list.Item) tea.Cmd { return s.list.SetItems(items) } +func (s *Selector) Index() int { + return s.list.Index() +} + func (s *Selector) Init() tea.Cmd { - return nil + return s.activeCmd } func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds := make([]tea.Cmd, 0) switch msg := msg.(type) { - default: - m, cmd := s.list.Update(msg) - s.list = m - if cmd != nil { - cmds = append(cmds, cmd) + case tea.KeyMsg: + switch { + case key.Matches(msg, s.common.Keymap.Select): + cmds = append(cmds, s.selectCmd) } } + m, cmd := s.list.Update(msg) + s.list = m + if cmd != nil { + cmds = append(cmds, cmd) + } + // Send ActiveMsg when index change. + if s.active != s.list.Index() { + cmds = append(cmds, s.activeCmd) + } + s.active = s.list.Index() return s, tea.Batch(cmds...) } func (s *Selector) View() string { return s.list.View() } + +func (s *Selector) selectCmd() tea.Msg { + item := s.list.SelectedItem() + i := item.(Item) + return SelectMsg(i.Name) +} + +func (s *Selector) activeCmd() tea.Msg { + item := s.list.SelectedItem() + i := item.(Item) + return ActiveMsg(i.Name) +} diff --git a/ui/components/viewport/viewport_patch.go b/ui/components/viewport/viewport_patch.go new file mode 100644 index 0000000000000000000000000000000000000000..16427a7d2fe47df09b05a15688b031f3c042e9f1 --- /dev/null +++ b/ui/components/viewport/viewport_patch.go @@ -0,0 +1,29 @@ +package viewport + +import ( + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" +) + +type ViewportBubble struct { + Viewport *viewport.Model +} + +func (v *ViewportBubble) SetSize(width, height int) { + v.Viewport.Width = width + v.Viewport.Height = height +} + +func (v *ViewportBubble) Init() tea.Cmd { + return nil +} + +func (v *ViewportBubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + vp, cmd := v.Viewport.Update(msg) + v.Viewport = &vp + return v, cmd +} + +func (v *ViewportBubble) View() string { + return v.Viewport.View() +} diff --git a/ui/keymap/keymap.go b/ui/keymap/keymap.go index 684080e393926cb7176cb9cb014e6613f3667bf7..8b879a82c8f0ef2daa3a353083c0b1419c1fed85 100644 --- a/ui/keymap/keymap.go +++ b/ui/keymap/keymap.go @@ -19,8 +19,8 @@ func DefaultKeyMap() *KeyMap { km.Quit = key.NewBinding( key.WithKeys( - "ctrl-c", "q", + "ctrl+c", ), key.WithHelp( "q", diff --git a/ui/pages/selection/selection.go b/ui/pages/selection/selection.go index fe42467372100438a3ab69db1661647f3098875d..349e46960f0caf2604fe5ab8f602047d017791d4 100644 --- a/ui/pages/selection/selection.go +++ b/ui/pages/selection/selection.go @@ -10,6 +10,7 @@ import ( "github.com/charmbracelet/lipgloss" appCfg "github.com/charmbracelet/soft-serve/config" "github.com/charmbracelet/soft-serve/ui/common" + "github.com/charmbracelet/soft-serve/ui/components/code" "github.com/charmbracelet/soft-serve/ui/components/selector" "github.com/charmbracelet/soft-serve/ui/components/yankable" "github.com/charmbracelet/soft-serve/ui/session" @@ -18,6 +19,7 @@ import ( type Selection struct { s session.Session common common.Common + readme *code.Code selector *selector.Selector } @@ -25,6 +27,7 @@ func New(s session.Session, common common.Common) *Selection { sel := &Selection{ s: s, common: common, + readme: code.New(common, "", ""), selector: selector.New(common, []list.Item{}), } return sel @@ -32,6 +35,7 @@ func New(s session.Session, common common.Common) *Selection { func (s *Selection) SetSize(width, height int) { s.common.SetSize(width, height) + s.readme.SetSize(width, height) s.selector.SetSize(width, height) } @@ -109,12 +113,17 @@ func (s *Selection) Init() tea.Cmd { }) } } - return s.selector.SetItems(items) + return tea.Batch( + s.selector.Init(), + s.selector.SetItems(items), + ) } func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds := make([]tea.Cmd, 0) switch msg := msg.(type) { + case selector.ActiveMsg: + cmds = append(cmds, s.changeActive(msg)) default: m, cmd := s.selector.Update(msg) s.selector = m.(*selector.Selector) @@ -126,7 +135,21 @@ func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (s *Selection) View() string { - return s.selector.View() + return lipgloss.JoinHorizontal( + lipgloss.Top, + s.readme.View(), + s.selector.View(), + ) +} + +func (s *Selection) changeActive(msg selector.ActiveMsg) tea.Cmd { + cfg := s.s.Config() + r, err := cfg.Source.GetRepo(string(msg)) + if err != nil { + return common.ErrorCmd(err) + } + rm, rp := r.Readme() + return s.readme.SetContent(rm, rp) } func repoUrl(cfg *appCfg.Config, name string) string { diff --git a/ui/session/session.go b/ui/session/session.go index 9342cb93befe027cad71dbecdaaad26cd72c64b7..43ef7ec86698ac47857d83f99c61b2dee1da3af6 100644 --- a/ui/session/session.go +++ b/ui/session/session.go @@ -8,7 +8,10 @@ import ( // Session is a interface representing a UI session. type Session interface { + // Send sends a message to the parent Bubble Tea program. Send(tea.Msg) + // Config returns the app configuration. Config() *appCfg.Config + // PublicKey returns the public key of the user. PublicKey() ssh.PublicKey } diff --git a/ui/ui.go b/ui/ui.go index 57ae6aff6adc542254355b131b46ec7875bec5d6..4a3cf1f09edec657b94f539b764eb92ca24408a9 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -29,6 +29,7 @@ type UI struct { state sessionState header *header.Header footer *footer.Footer + error error } func New(s session.Session, c common.Common, initialRepo string) *UI { @@ -115,19 +116,16 @@ func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch { case key.Matches(msg, ui.common.Keymap.Quit): return ui, tea.Quit - default: - m, cmd := ui.pages[ui.activePage].Update(msg) - ui.pages[ui.activePage] = m.(common.Page) - if cmd != nil { - cmds = append(cmds, cmd) - } - } - default: - m, cmd := ui.pages[ui.activePage].Update(msg) - ui.pages[ui.activePage] = m.(common.Page) - if cmd != nil { - cmds = append(cmds, cmd) } + case common.ErrorMsg: + ui.error = msg + ui.state = errorState + return ui, nil + } + m, cmd := ui.pages[ui.activePage].Update(msg) + ui.pages[ui.activePage] = m.(common.Page) + if cmd != nil { + cmds = append(cmds, cmd) } return ui, tea.Batch(cmds...) } @@ -136,9 +134,11 @@ func (ui *UI) View() string { s := strings.Builder{} switch ui.state { case startState: - return "Loading..." + return "\n Loading..." case errorState: - return "Error" + err := ui.common.Styles.ErrorTitle.Render("Bummer") + err += ui.common.Styles.ErrorBody.Render(ui.error.Error()) + return err case loadedState: s.WriteString(lipgloss.JoinVertical( lipgloss.Bottom, @@ -147,7 +147,7 @@ func (ui *UI) View() string { ui.footer.View(), )) default: - return "Unknown state :/ this is a bug!" + return "\n Unknown state :/ this is a bug!" } return ui.common.Styles.App.Render(s.String()) }