Detailed changes
@@ -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=
@@ -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)
+ }
+}
@@ -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
+}
@@ -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.
@@ -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)
+}
@@ -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()
+}
@@ -19,8 +19,8 @@ func DefaultKeyMap() *KeyMap {
km.Quit = key.NewBinding(
key.WithKeys(
- "ctrl-c",
"q",
+ "ctrl+c",
),
key.WithHelp(
"q",
@@ -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 {
@@ -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
}
@@ -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())
}