@@ -161,6 +161,10 @@ func (r *Code) ScrollPercent() float64 {
func (r *Code) glamourize(w int, md string) (string, error) {
r.renderMutex.Lock()
defer r.renderMutex.Unlock()
+ // This fixes a bug with markdown text wrapping being off by one.
+ if w > 0 {
+ w--
+ }
tr, err := glamour.NewTermRenderer(
glamour.WithStyles(r.styleConfig),
glamour.WithWordWrap(w),
@@ -202,7 +206,7 @@ func (r *Code) renderFile(path, content string, width int) (string, error) {
rc := r.renderContext
if r.showLineNumber {
st := common.StyleConfig()
- var m uint = 0
+ m := uint(0)
st.CodeBlock.Margin = &m
rc = gansi.NewRenderContext(gansi.Options{
ColorProfile: termenv.TrueColor,
@@ -4,6 +4,7 @@ import (
"strings"
tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/soft-serve/ui/common"
)
@@ -15,17 +16,23 @@ type ActiveTabMsg int
// Tabs is bubbletea component that displays a list of tabs.
type Tabs struct {
- common common.Common
- tabs []string
- activeTab int
+ common common.Common
+ tabs []string
+ activeTab int
+ TabSeparator lipgloss.Style
+ TabInactive lipgloss.Style
+ TabActive lipgloss.Style
}
// New creates a new Tabs component.
func New(c common.Common, tabs []string) *Tabs {
r := &Tabs{
- common: c,
- tabs: tabs,
- activeTab: 0,
+ common: c,
+ tabs: tabs,
+ activeTab: 0,
+ TabSeparator: c.Styles.TabSeparator,
+ TabInactive: c.Styles.TabInactive,
+ TabActive: c.Styles.TabActive,
}
return r
}
@@ -66,11 +73,11 @@ func (t *Tabs) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// View implements tea.Model.
func (t *Tabs) View() string {
s := strings.Builder{}
- sep := t.common.Styles.TabSeparator
+ sep := t.TabSeparator
for i, tab := range t.tabs {
- style := t.common.Styles.TabInactive.Copy()
+ style := t.TabInactive.Copy()
if i == t.activeTab {
- style = t.common.Styles.TabActive.Copy()
+ style = t.TabActive.Copy()
}
s.WriteString(style.Render(tab))
if i != len(t.tabs)-1 {
@@ -0,0 +1,25 @@
+package ui
+
+import (
+ "github.com/charmbracelet/soft-serve/config"
+ "github.com/charmbracelet/soft-serve/ui/git"
+)
+
+// source is a wrapper around config.RepoSource that implements git.GitRepoSource.
+type source struct {
+ *config.RepoSource
+}
+
+// GetRepo implements git.GitRepoSource.
+func (s *source) GetRepo(name string) (git.GitRepo, error) {
+ return s.RepoSource.GetRepo(name)
+}
+
+// AllRepos implements git.GitRepoSource.
+func (s *source) AllRepos() []git.GitRepo {
+ rs := make([]git.GitRepo, 0)
+ for _, r := range s.RepoSource.AllRepos() {
+ rs = append(rs, r)
+ }
+ return rs
+}
@@ -1,6 +1,7 @@
package selection
import (
+ "fmt"
"strings"
"github.com/charmbracelet/bubbles/key"
@@ -10,6 +11,7 @@ import (
"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/tabs"
"github.com/charmbracelet/soft-serve/ui/git"
wgit "github.com/charmbracelet/wish/git"
"github.com/gliderlabs/ssh"
@@ -31,23 +33,34 @@ type Selection struct {
readmeHeight int
selector *selector.Selector
activeBox box
+ tabs *tabs.Tabs
}
// New creates a new selection model.
func New(cfg *config.Config, pk ssh.PublicKey, common common.Common) *Selection {
+ t := tabs.New(common, []string{"About", "Repositories"})
+ t.TabSeparator = lipgloss.NewStyle()
+ t.TabInactive = lipgloss.NewStyle().
+ Bold(true).
+ UnsetBackground().
+ Foreground(common.Styles.InactiveBorderColor).
+ Padding(0, 1)
+ t.TabActive = t.TabInactive.Copy().
+ Background(lipgloss.Color("62")).
+ Foreground(lipgloss.Color("230"))
sel := &Selection{
cfg: cfg,
pk: pk,
common: common,
- activeBox: selectorBox, // start with the selector focused
+ activeBox: readmeBox, // start with the selector focused
+ tabs: t,
}
readme := code.New(common, "", "")
readme.NoContentStyle = readme.NoContentStyle.SetString("No readme found.")
selector := selector.New(common,
[]selector.IdentifiableItem{},
ItemDelegate{&common, &sel.activeBox})
- selector.SetShowTitle(true)
- selector.Title = "Repositories"
+ selector.SetShowTitle(false)
selector.SetShowHelp(false)
selector.SetShowStatusBar(false)
selector.DisableQuitKeybindings()
@@ -56,21 +69,19 @@ func New(cfg *config.Config, pk ssh.PublicKey, common common.Common) *Selection
return sel
}
-func (s *Selection) getReadmeHeight() int {
- rh := s.readmeHeight
- if rh > s.common.Height/3 {
- rh = s.common.Height / 3
- }
- return rh
-}
-
func (s *Selection) getMargins() (wm, hm int) {
wm = 0
- hm = s.common.Styles.SelectorBox.GetVerticalFrameSize() +
- s.common.Styles.SelectorBox.GetHeight()
- if rh := s.getReadmeHeight(); rh > 0 {
+ hm = s.common.Styles.Tabs.GetVerticalFrameSize() +
+ s.common.Styles.Tabs.GetHeight() +
+ 2 // tabs margin see View()
+ switch s.activeBox {
+ case selectorBox:
+ hm += s.common.Styles.SelectorBox.GetVerticalFrameSize() +
+ s.common.Styles.SelectorBox.GetHeight()
+ case readmeBox:
hm += s.common.Styles.ReadmeBox.GetVerticalFrameSize() +
- rh
+ s.common.Styles.ReadmeBox.GetHeight() +
+ 1 // readme statusbar
}
return
}
@@ -79,8 +90,9 @@ func (s *Selection) getMargins() (wm, hm int) {
func (s *Selection) SetSize(width, height int) {
s.common.SetSize(width, height)
wm, hm := s.getMargins()
- s.readme.SetSize(width-wm, s.getReadmeHeight())
+ s.tabs.SetSize(width, height-hm)
s.selector.SetSize(width-wm, height-hm)
+ s.readme.SetSize(width-wm, height-hm)
}
// ShortHelp implements help.KeyMap.
@@ -232,13 +244,21 @@ func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if cmd != nil {
cmds = append(cmds, cmd)
}
- case tea.KeyMsg:
- switch {
- case key.Matches(msg, s.common.KeyMap.Section):
- s.activeBox = (s.activeBox + 1) % 2
- case key.Matches(msg, s.common.KeyMap.Back):
- cmds = append(cmds, s.selector.Init())
+ case tea.KeyMsg, tea.MouseMsg:
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, s.common.KeyMap.Back):
+ cmds = append(cmds, s.selector.Init())
+ }
}
+ t, cmd := s.tabs.Update(msg)
+ s.tabs = t.(*tabs.Tabs)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ case tabs.ActiveTabMsg:
+ s.activeBox = box(msg)
}
switch s.activeBox {
case readmeBox:
@@ -259,20 +279,32 @@ func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// View implements tea.Model.
func (s *Selection) View() string {
- rh := s.getReadmeHeight()
- rs := s.common.Styles.ReadmeBox.Copy().
- Width(s.common.Width).
- Height(rh)
- if s.activeBox == readmeBox {
- rs.BorderForeground(s.common.Styles.ActiveBorderColor)
- }
- view := s.selector.View()
- if rh > 0 {
- readme := rs.Render(s.readme.View())
- view = lipgloss.JoinVertical(lipgloss.Top,
- readme,
- view,
- )
+ var view string
+ wm, hm := s.getMargins()
+ hm++ // tabs margin
+ switch s.activeBox {
+ case selectorBox:
+ ss := s.common.Styles.SelectorBox.Copy().
+ Height(s.common.Height - hm)
+ view = ss.Render(s.selector.View())
+ case readmeBox:
+ rs := s.common.Styles.ReadmeBox.Copy().
+ Height(s.common.Height - hm)
+ status := fmt.Sprintf("☰ %.f%%", s.readme.ScrollPercent()*100)
+ readmeStatus := lipgloss.NewStyle().
+ Align(lipgloss.Right).
+ Width(s.common.Width - wm).
+ Foreground(s.common.Styles.InactiveBorderColor).
+ Render(status)
+ view = rs.Render(lipgloss.JoinVertical(lipgloss.Top,
+ s.readme.View(),
+ readmeStatus,
+ ))
}
- return view
+ ts := s.common.Styles.Tabs.Copy().
+ MarginBottom(1)
+ return lipgloss.JoinVertical(lipgloss.Top,
+ ts.Render(s.tabs.View()),
+ view,
+ )
}
@@ -136,11 +136,9 @@ func DefaultStyles() *Styles {
Foreground(lipgloss.Color("241")).
Align(lipgloss.Right)
- s.SelectorBox = lipgloss.NewStyle().
- Width(64)
+ s.SelectorBox = lipgloss.NewStyle()
- s.ReadmeBox = lipgloss.NewStyle().
- Margin(1, 1, 1, 0)
+ s.ReadmeBox = lipgloss.NewStyle()
s.RepoTitleBorder = lipgloss.Border{
Top: "─",