ui/common/common.go 🔗
@@ -12,7 +12,7 @@ type Common struct {
Height int
}
-func (c Common) SetSize(width, height int) {
+func (c *Common) SetSize(width, height int) {
c.Width = width
c.Height = height
}
Ayman Bagabas created
ui/common/common.go | 2
ui/common/component.go | 2
ui/common/error.go | 2
ui/components/code/code.go | 30 +++++-
ui/components/footer/footer.go | 6 +
ui/components/header/header.go | 6 +
ui/components/selector/selector.go | 29 +++++-
ui/components/viewport/viewport_patch.go | 5 +
ui/keymap/keymap.go | 12 ++
ui/pages/selection/item.go | 39 ++++++---
ui/pages/selection/selection.go | 105 +++++++++++++++++++++----
ui/styles/styles.go | 32 +++++-
ui/ui.go | 8 +
13 files changed, 224 insertions(+), 54 deletions(-)
@@ -12,7 +12,7 @@ type Common struct {
Height int
}
-func (c Common) SetSize(width, height int) {
+func (c *Common) SetSize(width, height int) {
c.Width = width
c.Height = height
}
@@ -5,11 +5,13 @@ import (
tea "github.com/charmbracelet/bubbletea"
)
+// Component represents a Bubble Tea model that implements a SetSize function.
type Component interface {
tea.Model
SetSize(width, height int)
}
+// Page represents a component that implements help.KeyMap.
type Page interface {
Component
help.KeyMap
@@ -2,8 +2,10 @@ package common
import tea "github.com/charmbracelet/bubbletea"
+// ErrorMsg is a Bubble Tea message that represents an error.
type ErrorMsg error
+// ErrorCmd returns an ErrorMsg from error.
func ErrorCmd(err error) tea.Cmd {
return func() tea.Msg {
return ErrorMsg(err)
@@ -8,19 +8,23 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/glamour"
gansi "github.com/charmbracelet/glamour/ansi"
+ "github.com/charmbracelet/lipgloss"
"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"
)
+// Code is a code snippet.
type Code struct {
- common common.Common
- content string
- extension string
- viewport *vp.ViewportBubble
+ common common.Common
+ content string
+ extension string
+ viewport *vp.ViewportBubble
+ NoContentStyle lipgloss.Style
}
+// New returns a new Code.
func New(c common.Common, content, extension string) *Code {
r := &Code{
common: c,
@@ -31,28 +35,36 @@ func New(c common.Common, content, extension string) *Code {
MouseWheelEnabled: true,
},
},
+ NoContentStyle: c.Styles.CodeNoContent.Copy(),
}
+ r.SetSize(c.Width, c.Height)
return r
}
+// SetSize implements common.Component.
func (r *Code) SetSize(width, height int) {
- r.common.Width = width
- r.common.Height = height
+ r.common.SetSize(width, height)
r.viewport.SetSize(width, height)
}
+// SetContent sets the content of the Code.
func (r *Code) SetContent(c, ext string) tea.Cmd {
r.content = c
r.extension = ext
return r.Init()
}
+// GotoTop reset the viewport to the top.
+func (r *Code) GotoTop() {
+ r.viewport.Viewport.GotoTop()
+}
+
+// Init implements tea.Model.
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.")
+ c = r.NoContentStyle.String()
}
f, err := renderFile(r.extension, c, w)
if err != nil {
@@ -63,12 +75,14 @@ func (r *Code) Init() tea.Cmd {
return nil
}
+// Update implements tea.Model.
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
}
+// View implements tea.View.
func (r *Code) View() string {
return r.viewport.View()
}
@@ -6,12 +6,14 @@ import (
"github.com/charmbracelet/soft-serve/ui/common"
)
+// Footer is a Bubble Tea model that displays help and other info.
type Footer struct {
common common.Common
help help.Model
keymap help.KeyMap
}
+// New creates a new Footer.
func New(c common.Common, keymap help.KeyMap) *Footer {
h := help.New()
h.Styles.ShortKey = c.Styles.HelpKey
@@ -24,19 +26,23 @@ func New(c common.Common, keymap help.KeyMap) *Footer {
return f
}
+// SetSize implements common.Component.
func (f *Footer) SetSize(width, height int) {
f.common.Width = width
f.common.Height = height
}
+// Init implements tea.Model.
func (f *Footer) Init() tea.Cmd {
return nil
}
+// Update implements tea.Model.
func (f *Footer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return f, nil
}
+// View implements tea.Model.
func (f *Footer) View() string {
if f.keymap == nil {
return ""
@@ -7,11 +7,13 @@ import (
"github.com/charmbracelet/soft-serve/ui/common"
)
+// Header represents a header component.
type Header struct {
common common.Common
text string
}
+// New creates a new header component.
func New(c common.Common, text string) *Header {
h := &Header{
common: c,
@@ -20,19 +22,23 @@ func New(c common.Common, text string) *Header {
return h
}
+// SetSize implements common.Component.
func (h *Header) SetSize(width, height int) {
h.common.Width = width
h.common.Height = height
}
+// Init implements tea.Model.
func (h *Header) Init() tea.Cmd {
return nil
}
+// Update implements tea.Model.
func (h *Header) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return h, nil
}
+// View implements tea.Model.
func (h *Header) View() string {
s := h.common.Styles.Header.Copy().Width(h.common.Width)
return s.Render(strings.TrimSpace(h.text))
@@ -7,18 +7,28 @@ import (
"github.com/charmbracelet/soft-serve/ui/common"
)
+// Selector is a list of items that can be selected.
type Selector struct {
list list.Model
common common.Common
active int
}
+// IdentifiableItem is an item that can be identified by a string and extends list.Item.
+type IdentifiableItem interface {
+ list.Item
+ ID() string
+}
+
+// SelectMsg is a message that is sent when an item is selected.
type SelectMsg string
+// ActiveMsg is a message that is sent when an item is active but not selected.
type ActiveMsg string
-func New(common common.Common, items []list.Item) *Selector {
- l := list.New(items, ItemDelegate{common.Styles}, common.Width, common.Height)
+// New creates a new selector.
+func New(common common.Common, items []list.Item, delegate list.ItemDelegate) *Selector {
+ l := list.New(items, delegate, common.Width, common.Height)
l.SetShowTitle(false)
l.SetShowHelp(false)
l.SetShowStatusBar(false)
@@ -31,27 +41,33 @@ func New(common common.Common, items []list.Item) *Selector {
return s
}
+// KeyMap returns the underlying list's keymap.
func (s *Selector) KeyMap() list.KeyMap {
return s.list.KeyMap
}
+// SetSize implements common.Component.
func (s *Selector) SetSize(width, height int) {
s.common.SetSize(width, height)
s.list.SetSize(width, height)
}
+// SetItems sets the items in the selector.
func (s *Selector) SetItems(items []list.Item) tea.Cmd {
return s.list.SetItems(items)
}
+// Index returns the index of the selected item.
func (s *Selector) Index() int {
return s.list.Index()
}
+// Init implements tea.Model.
func (s *Selector) Init() tea.Cmd {
return s.activeCmd
}
+// Update implements tea.Model.
func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds := make([]tea.Cmd, 0)
switch msg := msg.(type) {
@@ -74,18 +90,19 @@ func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return s, tea.Batch(cmds...)
}
+// View implements tea.Model.
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)
+ i := item.(IdentifiableItem)
+ return SelectMsg(i.ID())
}
func (s *Selector) activeCmd() tea.Msg {
item := s.list.SelectedItem()
- i := item.(Item)
- return ActiveMsg(i.Name)
+ i := item.(IdentifiableItem)
+ return ActiveMsg(i.ID())
}
@@ -5,25 +5,30 @@ import (
tea "github.com/charmbracelet/bubbletea"
)
+// ViewportBubble represents a viewport component.
type ViewportBubble struct {
Viewport *viewport.Model
}
+// SetSize implements common.Component.
func (v *ViewportBubble) SetSize(width, height int) {
v.Viewport.Width = width
v.Viewport.Height = height
}
+// Init implements tea.Model.
func (v *ViewportBubble) Init() tea.Cmd {
return nil
}
+// Update implements tea.Model.
func (v *ViewportBubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
vp, cmd := v.Viewport.Update(msg)
v.Viewport = &vp
return v, cmd
}
+// View implements tea.Model.
func (v *ViewportBubble) View() string {
return v.Viewport.View()
}
@@ -11,6 +11,7 @@ type KeyMap struct {
LeftRight key.Binding
Arrows key.Binding
Select key.Binding
+ Section key.Binding
}
// DefaultKeyMap returns the default key map.
@@ -103,5 +104,16 @@ func DefaultKeyMap() *KeyMap {
),
)
+ km.Section = key.NewBinding(
+ key.WithKeys(
+ "tab",
+ "shift+tab",
+ ),
+ key.WithHelp(
+ "tab",
+ "section",
+ ),
+ )
+
return km
}
@@ -1,4 +1,4 @@
-package selector
+package selection
import (
"fmt"
@@ -14,6 +14,7 @@ import (
"github.com/dustin/go-humanize"
)
+// Item represents a single item in the selector.
type Item struct {
Title string
Name string
@@ -22,35 +23,41 @@ type Item struct {
URL *yankable.Yankable
}
-func (i *Item) Init() tea.Cmd {
- return nil
-}
-
-func (i *Item) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- return i, nil
-}
-
-func (i *Item) View() string {
- return ""
+// ID implements selector.IdentifiableItem.
+func (i Item) ID() string {
+ return i.Name
}
+// FilterValue implements list.Item.
func (i Item) FilterValue() string { return i.Title }
+// ItemDelegate is the delegate for the item.
type ItemDelegate struct {
- styles *styles.Styles
+ styles *styles.Styles
+ activeBox *box
}
+// Width returns the item width.
func (d ItemDelegate) Width() int {
width := d.styles.MenuItem.GetHorizontalFrameSize() + d.styles.MenuItem.GetWidth()
return width
}
+
+// Height returns the item height. Implements list.ItemDelegate.
func (d ItemDelegate) Height() int {
height := d.styles.MenuItem.GetVerticalFrameSize() + d.styles.MenuItem.GetHeight()
return height
}
+
+// Spacing returns the spacing between items. Implements list.ItemDelegate.
func (d ItemDelegate) Spacing() int { return 1 }
+
+// Update implements list.ItemDelegate.
func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
cmds := make([]tea.Cmd, 0)
+ if d.activeBox == nil || *d.activeBox != selectorBox {
+ return nil
+ }
for i, item := range m.VisibleItems() {
itm, ok := item.(Item)
if !ok {
@@ -78,12 +85,18 @@ func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
}
return tea.Batch(cmds...)
}
+
+// Render implements list.ItemDelegate.
func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
i := listItem.(Item)
s := strings.Builder{}
style := d.styles.MenuItem.Copy()
if index == m.Index() {
- style = d.styles.SelectedMenuItem.Copy()
+ style = style.BorderForeground(d.styles.ActiveBorderColor)
+ if d.activeBox != nil && *d.activeBox == readmeBox {
+ // TODO make this into its own color
+ style = style.BorderForeground(lipgloss.Color("15"))
+ }
}
titleStr := i.Title
updatedStr := fmt.Sprintf(" Updated %s", humanize.Time(i.LastUpdate))
@@ -16,39 +16,67 @@ import (
"github.com/charmbracelet/soft-serve/ui/session"
)
+type box int
+
+const (
+ readmeBox box = iota
+ selectorBox
+)
+
+// Selection is the model for the selection screen/page.
type Selection struct {
- s session.Session
- common common.Common
- readme *code.Code
- selector *selector.Selector
+ s session.Session
+ common common.Common
+ readme *code.Code
+ selector *selector.Selector
+ activeBox box
}
+// New creates a new selection model.
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{}),
+ s: s,
+ common: common,
+ activeBox: 1,
}
+ readme := code.New(common, "", "")
+ readme.NoContentStyle = readme.NoContentStyle.SetString("No readme found.")
+ sel.readme = readme
+ sel.selector = selector.New(common, []list.Item{}, ItemDelegate{common.Styles, &sel.activeBox})
return sel
}
+// SetSize implements common.Component.
func (s *Selection) SetSize(width, height int) {
s.common.SetSize(width, height)
- s.readme.SetSize(width, height)
- s.selector.SetSize(width, height)
+ sw := s.common.Styles.SelectorBox.GetWidth()
+ wm := sw +
+ s.common.Styles.SelectorBox.GetHorizontalFrameSize() +
+ s.common.Styles.ReadmeBox.GetHorizontalFrameSize()
+ hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize()
+ s.readme.SetSize(width-wm, height-hm)
+ s.selector.SetSize(sw, height)
}
+// ShortHelp implements help.KeyMap.
func (s *Selection) ShortHelp() []key.Binding {
k := s.selector.KeyMap()
- return []key.Binding{
+ kb := make([]key.Binding, 0)
+ kb = append(kb,
s.common.Keymap.UpDown,
s.common.Keymap.Select,
- k.Filter,
- k.ClearFilter,
+ )
+ if s.activeBox == selectorBox {
+ kb = append(kb,
+ k.Filter,
+ k.ClearFilter,
+ )
}
+ return kb
}
+// FullHelp implements help.KeyMap.
+// TODO implement full help on ?
func (s *Selection) FullHelp() [][]key.Binding {
k := s.selector.KeyMap()
return [][]key.Binding{
@@ -74,9 +102,11 @@ func (s *Selection) FullHelp() [][]key.Binding {
}
}
+// Init implements tea.Model.
func (s *Selection) Init() tea.Cmd {
items := make([]list.Item, 0)
cfg := s.s.Config()
+ // TODO fix yankable component
yank := func(text string) *yankable.Yankable {
return yankable.New(
lipgloss.NewStyle().Foreground(lipgloss.Color("168")),
@@ -86,7 +116,7 @@ func (s *Selection) Init() tea.Cmd {
}
// Put configured repos first
for _, r := range cfg.Repos {
- items = append(items, selector.Item{
+ items = append(items, Item{
Title: r.Name,
Name: r.Repo,
Description: r.Note,
@@ -97,14 +127,14 @@ func (s *Selection) Init() tea.Cmd {
for _, r := range cfg.Source.AllRepos() {
exists := false
for _, item := range items {
- item := item.(selector.Item)
+ item := item.(Item)
if item.Name == r.Name() {
exists = true
break
}
}
if !exists {
- items = append(items, selector.Item{
+ items = append(items, Item{
Title: r.Name(),
Name: r.Name(),
Description: "",
@@ -119,12 +149,39 @@ func (s *Selection) Init() tea.Cmd {
)
}
+// Update implements tea.Model.
func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds := make([]tea.Cmd, 0)
switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ r, cmd := s.readme.Update(msg)
+ s.readme = r.(*code.Code)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ m, cmd := s.selector.Update(msg)
+ s.selector = m.(*selector.Selector)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
case selector.ActiveMsg:
cmds = append(cmds, s.changeActive(msg))
- default:
+ // reset readme position
+ s.readme.GotoTop()
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, s.common.Keymap.Section):
+ s.activeBox = (s.activeBox + 1) % 2
+ }
+ }
+ switch s.activeBox {
+ case readmeBox:
+ r, cmd := s.readme.Update(msg)
+ s.readme = r.(*code.Code)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ case selectorBox:
m, cmd := s.selector.Update(msg)
s.selector = m.(*selector.Selector)
if cmd != nil {
@@ -134,10 +191,22 @@ func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return s, tea.Batch(cmds...)
}
+// View implements tea.Model.
func (s *Selection) View() string {
+ wm := s.common.Styles.SelectorBox.GetWidth() +
+ s.common.Styles.SelectorBox.GetHorizontalFrameSize() +
+ s.common.Styles.ReadmeBox.GetHorizontalFrameSize()
+ hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize()
+ rs := s.common.Styles.ReadmeBox.Copy().
+ Width(s.common.Width - wm).
+ Height(s.common.Height - hm)
+ if s.activeBox == readmeBox {
+ rs.BorderForeground(s.common.Styles.ActiveBorderColor)
+ }
+ readme := rs.Render(s.readme.View())
return lipgloss.JoinHorizontal(
lipgloss.Top,
- s.readme.View(),
+ readme,
s.selector.View(),
)
}
@@ -15,11 +15,14 @@ type Styles struct {
App lipgloss.Style
Header lipgloss.Style
- Menu lipgloss.Style
- MenuCursor lipgloss.Style
- MenuItem lipgloss.Style
- MenuLastUpdate lipgloss.Style
- SelectedMenuItem lipgloss.Style
+ Menu lipgloss.Style
+ MenuCursor lipgloss.Style
+ MenuItem lipgloss.Style
+ MenuLastUpdate lipgloss.Style
+
+ // Selection page styles
+ SelectorBox lipgloss.Style
+ ReadmeBox lipgloss.Style
RepoTitleBorder lipgloss.Border
RepoNoteBorder lipgloss.Border
@@ -74,6 +77,8 @@ type Styles struct {
TreeNoItems lipgloss.Style
Spinner lipgloss.Style
+
+ CodeNoContent lipgloss.Style
}
// DefaultStyles returns default styles for the UI.
@@ -81,7 +86,7 @@ func DefaultStyles() *Styles {
s := new(Styles)
s.ActiveBorderColor = lipgloss.Color("62")
- s.InactiveBorderColor = lipgloss.Color("236")
+ s.InactiveBorderColor = lipgloss.Color("241")
s.App = lipgloss.NewStyle().
Margin(1, 2)
@@ -113,8 +118,13 @@ func DefaultStyles() *Styles {
Foreground(lipgloss.Color("241")).
Align(lipgloss.Right)
- s.SelectedMenuItem = s.MenuItem.Copy().
- BorderForeground(s.ActiveBorderColor)
+ s.SelectorBox = lipgloss.NewStyle().
+ Width(64)
+
+ s.ReadmeBox = lipgloss.NewStyle().
+ BorderForeground(s.InactiveBorderColor).
+ Padding(1).
+ MarginRight(1)
s.RepoTitleBorder = lipgloss.Border{
Top: "─",
@@ -288,5 +298,11 @@ func DefaultStyles() *Styles {
MarginLeft(2).
Foreground(lipgloss.Color("205"))
+ s.CodeNoContent = lipgloss.NewStyle().
+ SetString("No Content.").
+ MarginTop(1).
+ MarginLeft(2).
+ Foreground(lipgloss.Color("#626262"))
+
return s
}
@@ -21,6 +21,7 @@ const (
loadedState
)
+// UI is the main UI model.
type UI struct {
s session.Session
common common.Common
@@ -32,6 +33,7 @@ type UI struct {
error error
}
+// New returns a new UI model.
func New(s session.Session, c common.Common, initialRepo string) *UI {
h := header.New(c, s.Config().Name)
ui := &UI{
@@ -55,6 +57,7 @@ func (ui *UI) getMargins() (wm, hm int) {
return
}
+// ShortHelp implements help.KeyMap.
func (ui *UI) ShortHelp() []key.Binding {
b := make([]key.Binding, 0)
b = append(b, ui.pages[ui.activePage].ShortHelp()...)
@@ -62,6 +65,7 @@ func (ui *UI) ShortHelp() []key.Binding {
return b
}
+// FullHelp implements help.KeyMap.
func (ui *UI) FullHelp() [][]key.Binding {
b := make([][]key.Binding, 0)
b = append(b, ui.pages[ui.activePage].FullHelp()...)
@@ -69,6 +73,7 @@ func (ui *UI) FullHelp() [][]key.Binding {
return b
}
+// SetSize implements common.Component.
func (ui *UI) SetSize(width, height int) {
ui.common.SetSize(width, height)
wm, hm := ui.getMargins()
@@ -81,6 +86,7 @@ func (ui *UI) SetSize(width, height int) {
}
}
+// Init implements tea.Model.
func (ui *UI) Init() tea.Cmd {
ui.pages[0] = selection.New(ui.s, ui.common)
ui.pages[1] = selection.New(ui.s, ui.common)
@@ -89,6 +95,7 @@ func (ui *UI) Init() tea.Cmd {
return ui.pages[ui.activePage].Init()
}
+// Update implements tea.Model.
// TODO update help when page change.
func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds := make([]tea.Cmd, 0)
@@ -130,6 +137,7 @@ func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return ui, tea.Batch(cmds...)
}
+// View implements tea.Model.
func (ui *UI) View() string {
s := strings.Builder{}
switch ui.state {