From c0654298a1010d04182f0dc3dc14b52594504992 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 24 May 2022 12:25:56 -0400 Subject: [PATCH] feat: selection tabs redesign --- ui/components/code/code.go | 6 +- ui/components/tabs/tabs.go | 25 +++++--- ui/git.go | 25 ++++++++ ui/pages/selection/selection.go | 106 +++++++++++++++++++++----------- ui/styles/styles.go | 6 +- 5 files changed, 117 insertions(+), 51 deletions(-) create mode 100644 ui/git.go diff --git a/ui/components/code/code.go b/ui/components/code/code.go index 18e96655abab0953c81b907a46ed6422349a7c7f..c475e9636c0485255859935b5fdbd1043f5d77c8 100644 --- a/ui/components/code/code.go +++ b/ui/components/code/code.go @@ -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, diff --git a/ui/components/tabs/tabs.go b/ui/components/tabs/tabs.go index 59b8deed043767bad4067c2c5977c3f68e11de11..6bc757fde177d0fe5e16a7ad4378df89380d5c28 100644 --- a/ui/components/tabs/tabs.go +++ b/ui/components/tabs/tabs.go @@ -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 { diff --git a/ui/git.go b/ui/git.go new file mode 100644 index 0000000000000000000000000000000000000000..e4dcf80a2453d0f946ae09ad5dbd7ba71eb53571 --- /dev/null +++ b/ui/git.go @@ -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 +} diff --git a/ui/pages/selection/selection.go b/ui/pages/selection/selection.go index a3d3345a0f4e80f68653089ca5aea953993b2ecd..4eb83369ce5084dd2f59da0b559089c247d85d41 100644 --- a/ui/pages/selection/selection.go +++ b/ui/pages/selection/selection.go @@ -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, + ) } diff --git a/ui/styles/styles.go b/ui/styles/styles.go index b71cfbce9bfb87c0e65d16869624883449a86373..58470cdef135e3ca6a1702c70de7278426e026ec 100644 --- a/ui/styles/styles.go +++ b/ui/styles/styles.go @@ -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: "─",