Detailed changes
@@ -22,10 +22,10 @@ type IdentifiableItem interface {
}
// SelectMsg is a message that is sent when an item is selected.
-type SelectMsg string
+type SelectMsg struct{ IdentifiableItem }
// ActiveMsg is a message that is sent when an item is active but not selected.
-type ActiveMsg string
+type ActiveMsg struct{ IdentifiableItem }
// New creates a new selector.
func New(common common.Common, items []IdentifiableItem, delegate list.ItemDelegate) *Selector {
@@ -166,22 +166,27 @@ func (s *Selector) View() string {
return s.Model.View()
}
+// SelectItem is a command that selects the currently active item.
+func (s *Selector) SelectItem() tea.Msg {
+ return s.selectCmd()
+}
+
func (s *Selector) selectCmd() tea.Msg {
item := s.Model.SelectedItem()
i, ok := item.(IdentifiableItem)
if !ok {
- return SelectMsg("")
+ return SelectMsg{}
}
- return SelectMsg(i.ID())
+ return SelectMsg{i}
}
func (s *Selector) activeCmd() tea.Msg {
item := s.Model.SelectedItem()
i, ok := item.(IdentifiableItem)
if !ok {
- return ActiveMsg("")
+ return ActiveMsg{}
}
- return ActiveMsg(i.ID())
+ return ActiveMsg{i}
}
func (s *Selector) activeFilterCmd() tea.Msg {
@@ -197,5 +202,5 @@ func (s *Selector) activeFilterCmd() tea.Msg {
if !ok {
return nil
}
- return ActiveMsg(i.ID())
+ return ActiveMsg{i}
}
@@ -41,6 +41,11 @@ func (v *Viewport) View() string {
return v.Viewport.View()
}
+// SetContent sets the viewport's content.
+func (v *Viewport) SetContent(content string) {
+ v.Viewport.SetContent(content)
+}
+
// GotoTop moves the viewport to the top of the log.
func (v *Viewport) GotoTop() {
v.Viewport.GotoTop()
@@ -2,14 +2,22 @@ package repo
import (
"fmt"
+ "strings"
+ "time"
+ "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/glamour"
+ gansi "github.com/charmbracelet/glamour/ansi"
+ "github.com/charmbracelet/lipgloss"
ggit "github.com/charmbracelet/soft-serve/git"
"github.com/charmbracelet/soft-serve/ui/common"
"github.com/charmbracelet/soft-serve/ui/components/selector"
"github.com/charmbracelet/soft-serve/ui/components/viewport"
"github.com/charmbracelet/soft-serve/ui/git"
+ "github.com/muesli/reflow/wrap"
+ "github.com/muesli/termenv"
)
type view int
@@ -23,15 +31,21 @@ type LogCountMsg int64
type LogItemsMsg []list.Item
+type LogCommitMsg *ggit.Commit
+
+type LogDiffMsg *ggit.Diff
+
type Log struct {
- common common.Common
- selector *selector.Selector
- vp *viewport.Viewport
- activeView view
- repo git.GitRepo
- ref *ggit.Reference
- count int64
- nextPage int
+ common common.Common
+ selector *selector.Selector
+ vp *viewport.Viewport
+ activeView view
+ repo git.GitRepo
+ ref *ggit.Reference
+ count int64
+ nextPage int
+ selectedCommit *ggit.Commit
+ currentDiff *ggit.Diff
}
func NewLog(common common.Common) *Log {
@@ -60,6 +74,40 @@ func (l *Log) SetSize(width, height int) {
l.vp.SetSize(width, height)
}
+func (l *Log) ShortHelp() []key.Binding {
+ switch l.activeView {
+ case logView:
+ return []key.Binding{
+ key.NewBinding(
+ key.WithKeys(
+ "l",
+ "right",
+ ),
+ key.WithHelp(
+ "→",
+ "select",
+ ),
+ ),
+ }
+ case commitView:
+ return []key.Binding{
+ l.common.KeyMap.UpDown,
+ key.NewBinding(
+ key.WithKeys(
+ "h",
+ "left",
+ ),
+ key.WithHelp(
+ "←",
+ "back",
+ ),
+ ),
+ }
+ default:
+ return []key.Binding{}
+ }
+}
+
func (l *Log) Init() tea.Cmd {
cmds := make([]tea.Cmd, 0)
cmds = append(cmds, l.updateCommitsCmd)
@@ -73,6 +121,7 @@ func (l *Log) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
l.count = 0
l.selector.Select(0)
l.nextPage = 0
+ l.activeView = 0
l.repo = git.GitRepo(msg)
case RefMsg:
l.ref = msg
@@ -85,8 +134,16 @@ func (l *Log) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
l.selector.SetPage(l.nextPage)
l.SetSize(l.common.Width, l.common.Height)
case tea.KeyMsg, tea.MouseMsg:
- // This is a hack for loading commits on demand based on list.Pagination.
- if l.activeView == logView {
+ switch l.activeView {
+ case logView:
+ switch key := msg.(type) {
+ case tea.KeyMsg:
+ switch key.String() {
+ case "l", "right":
+ cmds = append(cmds, l.selector.SelectItem)
+ }
+ }
+ // This is a hack for loading commits on demand based on list.Pagination.
curPage := l.selector.Page()
s, cmd := l.selector.Update(msg)
m := s.(*selector.Selector)
@@ -97,6 +154,47 @@ func (l *Log) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds = append(cmds, l.updateCommitsCmd)
}
cmds = append(cmds, cmd)
+ case commitView:
+ switch key := msg.(type) {
+ case tea.KeyMsg:
+ switch key.String() {
+ case "h", "left":
+ l.activeView = logView
+ }
+ }
+ }
+ case selector.SelectMsg:
+ switch sel := msg.IdentifiableItem.(type) {
+ case LogItem:
+ cmds = append(cmds, l.selectCommitCmd(sel.Commit))
+ }
+ case LogCommitMsg:
+ l.selectedCommit = msg
+ cmds = append(cmds, l.loadDiffCmd)
+ case LogDiffMsg:
+ l.currentDiff = msg
+ l.vp.SetContent(
+ lipgloss.JoinVertical(lipgloss.Top,
+ l.renderCommit(l.selectedCommit),
+ l.renderSummary(msg),
+ l.renderDiff(msg),
+ ),
+ )
+ l.vp.GotoTop()
+ l.activeView = commitView
+ cmds = append(cmds, updateStatusBarCmd)
+ case tea.WindowSizeMsg:
+ if l.selectedCommit != nil && l.currentDiff != nil {
+ l.vp.SetContent(
+ lipgloss.JoinVertical(lipgloss.Top,
+ l.renderCommit(l.selectedCommit),
+ l.renderSummary(l.currentDiff),
+ l.renderDiff(l.currentDiff),
+ ),
+ )
+ }
+ if l.repo != nil {
+ cmds = append(cmds, l.updateCommitsCmd)
}
}
switch l.activeView {
@@ -127,6 +225,8 @@ func (l *Log) StatusBarInfo() string {
// We're using l.nextPage instead of l.selector.Paginator.Page because
// of the paginator hack above.
return fmt.Sprintf("%d/%d", l.nextPage+1, l.selector.TotalPages())
+ case commitView:
+ return fmt.Sprintf("%.f%%", l.vp.ScrollPercent()*100)
default:
return ""
}
@@ -168,3 +268,77 @@ func (l *Log) updateCommitsCmd() tea.Msg {
}
return LogItemsMsg(items)
}
+
+func (l *Log) selectCommitCmd(commit *ggit.Commit) tea.Cmd {
+ return func() tea.Msg {
+ return LogCommitMsg(commit)
+ }
+}
+
+func (l *Log) loadDiffCmd() tea.Msg {
+ diff, err := l.repo.Diff(l.selectedCommit)
+ if err != nil {
+ return common.ErrorMsg(err)
+ }
+ return LogDiffMsg(diff)
+}
+
+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 (l *Log) renderCommit(c *ggit.Commit) string {
+ s := strings.Builder{}
+ // FIXME: lipgloss prints empty lines when CRLF is used
+ // sanitize commit message from CRLF
+ msg := strings.ReplaceAll(c.Message, "\r\n", "\n")
+ s.WriteString(fmt.Sprintf("%s\n%s\n%s\n%s\n",
+ l.common.Styles.LogCommitHash.Render("commit "+c.ID.String()),
+ l.common.Styles.LogCommitAuthor.Render(fmt.Sprintf("Author: %s <%s>", c.Author.Name, c.Author.Email)),
+ l.common.Styles.LogCommitDate.Render("Date: "+c.Committer.When.Format(time.UnixDate)),
+ l.common.Styles.LogCommitBody.Render(msg),
+ ))
+ return wrap.String(s.String(), l.common.Width-2)
+}
+
+func (l *Log) renderSummary(diff *ggit.Diff) string {
+ stats := strings.Split(diff.Stats().String(), "\n")
+ for i, line := range stats {
+ ch := strings.Split(line, "|")
+ if len(ch) > 1 {
+ adddel := ch[len(ch)-1]
+ adddel = strings.ReplaceAll(adddel, "+", l.common.Styles.LogCommitStatsAdd.Render("+"))
+ adddel = strings.ReplaceAll(adddel, "-", l.common.Styles.LogCommitStatsDel.Render("-"))
+ stats[i] = strings.Join(ch[:len(ch)-1], "|") + "|" + adddel
+ }
+ }
+ return wrap.String(strings.Join(stats, "\n"), l.common.Width-2)
+}
+
+func (l *Log) renderDiff(diff *ggit.Diff) string {
+ var s strings.Builder
+ var pr strings.Builder
+ diffChroma := &gansi.CodeBlockElement{
+ Code: diff.Patch(),
+ Language: "diff",
+ }
+ err := diffChroma.Render(&pr, renderCtx())
+ if err != nil {
+ s.WriteString(fmt.Sprintf("\n%s", err.Error()))
+ } else {
+ s.WriteString(fmt.Sprintf("\n%s", pr.String()))
+ }
+ return wrap.String(s.String(), l.common.Width-2)
+}
@@ -9,9 +9,11 @@ import (
ggit "github.com/charmbracelet/soft-serve/git"
"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/statusbar"
"github.com/charmbracelet/soft-serve/ui/components/tabs"
"github.com/charmbracelet/soft-serve/ui/git"
+ "github.com/charmbracelet/soft-serve/ui/pages/selection"
)
type tab int
@@ -24,6 +26,8 @@ const (
tagsTab
)
+type UpdateStatusBarMsg struct{}
+
// RepoMsg is a message that contains a git.Repository.
type RepoMsg git.GitRepo
@@ -35,6 +39,7 @@ type Repo struct {
common common.Common
rs git.GitRepoSource
selectedRepo git.GitRepo
+ selectedItem selection.Item
activeTab tab
tabs *tabs.Tabs
statusbar *statusbar.StatusBar
@@ -68,7 +73,8 @@ func (r *Repo) SetSize(width, height int) {
r.common.Styles.RepoHeader.GetHeight() +
r.common.Styles.RepoHeader.GetVerticalFrameSize() +
r.common.Styles.StatusBar.GetHeight() +
- r.common.Styles.Tabs.GetHeight()
+ r.common.Styles.Tabs.GetHeight() +
+ r.common.Styles.Tabs.GetVerticalFrameSize()
r.tabs.SetSize(width, height-hm)
r.statusbar.SetSize(width, height-hm)
r.readme.SetSize(width, height-hm)
@@ -80,8 +86,16 @@ func (r *Repo) ShortHelp() []key.Binding {
b := make([]key.Binding, 0)
tab := r.common.KeyMap.Section
tab.SetHelp("tab", "switch tab")
- b = append(b, r.common.KeyMap.Back)
+ back := r.common.KeyMap.Back
+ back.SetHelp("esc", "repos")
+ b = append(b, back)
b = append(b, tab)
+ switch r.activeTab {
+ case readmeTab:
+ b = append(b, r.common.KeyMap.UpDown)
+ case commitsTab:
+ b = append(b, r.log.ShortHelp()...)
+ }
return b
}
@@ -100,6 +114,11 @@ func (r *Repo) Init() tea.Cmd {
func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds := make([]tea.Cmd, 0)
switch msg := msg.(type) {
+ case selector.SelectMsg:
+ switch msg.IdentifiableItem.(type) {
+ case selection.Item:
+ r.selectedItem = msg.IdentifiableItem.(selection.Item)
+ }
case RepoMsg:
r.activeTab = 0
r.selectedRepo = git.GitRepo(msg)
@@ -142,6 +161,19 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if cmd != nil {
cmds = append(cmds, cmd)
}
+ case UpdateStatusBarMsg:
+ cmds = append(cmds, r.updateStatusBarCmd)
+ case tea.WindowSizeMsg:
+ b, cmd := r.readme.Update(msg)
+ r.readme = b.(*code.Code)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ l, cmd := r.log.Update(msg)
+ r.log = l.(*Log)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
}
t, cmd := r.tabs.Update(msg)
r.tabs = t.(*tabs.Tabs)
@@ -183,7 +215,8 @@ func (r *Repo) View() string {
r.common.Styles.RepoHeader.GetHeight() +
r.common.Styles.RepoHeader.GetVerticalFrameSize() +
r.common.Styles.StatusBar.GetHeight() +
- r.common.Styles.Tabs.GetHeight()
+ r.common.Styles.Tabs.GetHeight() +
+ r.common.Styles.Tabs.GetVerticalFrameSize()
mainStyle := repoBodyStyle.
Height(r.common.Height - hm)
main := mainStyle.Render("")
@@ -196,6 +229,7 @@ func (r *Repo) View() string {
}
view := lipgloss.JoinVertical(lipgloss.Top,
r.headerView(),
+ r.tabs.View(),
main,
r.statusbar.View(),
)
@@ -206,12 +240,18 @@ func (r *Repo) headerView() string {
if r.selectedRepo == nil {
return ""
}
- name := r.common.Styles.RepoHeaderName.Render(r.selectedRepo.Name())
+ name := r.common.Styles.RepoHeaderName.Render(r.selectedItem.Title())
+ // TODO move this into a style.
+ url := lipgloss.NewStyle().MarginLeft(2).Render(r.selectedItem.URL())
+ desc := r.common.Styles.RepoHeaderDesc.Render(r.selectedItem.Description())
style := r.common.Styles.RepoHeader.Copy().Width(r.common.Width)
return style.Render(
lipgloss.JoinVertical(lipgloss.Top,
- name,
- r.tabs.View(),
+ lipgloss.JoinHorizontal(lipgloss.Left,
+ name,
+ url,
+ ),
+ desc,
),
)
}
@@ -258,3 +298,7 @@ func (r *Repo) updateRefCmd() tea.Msg {
}
return RefMsg(head)
}
+
+func updateStatusBarCmd() tea.Msg {
+ return UpdateStatusBarMsg{}
+}
@@ -37,6 +37,10 @@ func (i Item) Description() string { return i.desc }
// FilterValue implements list.Item.
func (i Item) FilterValue() string { return i.name }
+func (i Item) URL() string {
+ return i.url.View()
+}
+
// ItemDelegate is the delegate for the item.
type ItemDelegate struct {
styles *styles.Styles
@@ -132,7 +132,7 @@ func (s *Selection) Init() tea.Cmd {
}
items := make([]list.Item, 0)
cfg := s.s.Config()
- // TODO clean up this
+ // TODO clean up this and move style to its own var.
yank := func(text string) *yankable.Yankable {
return yankable.New(
session,
@@ -205,7 +205,6 @@ func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds = append(cmds, s.changeActive(msg))
// reset readme position when active item change
s.readme.GotoTop()
- case selector.SelectMsg:
case tea.KeyMsg:
switch {
case key.Matches(msg, s.common.KeyMap.Section):
@@ -251,7 +250,7 @@ func (s *Selection) View() string {
func (s *Selection) changeActive(msg selector.ActiveMsg) tea.Cmd {
cfg := s.s.Config()
- r, err := cfg.Source.GetRepo(string(msg))
+ r, err := cfg.Source.GetRepo(msg.ID())
if err != nil {
return common.ErrorCmd(err)
}
@@ -36,6 +36,7 @@ type Styles struct {
RepoBody lipgloss.Style
RepoHeader lipgloss.Style
RepoHeaderName lipgloss.Style
+ RepoHeaderDesc lipgloss.Style
Footer lipgloss.Style
Branch lipgloss.Style
@@ -196,6 +197,7 @@ func DefaultStyles() *Styles {
Margin(1, 0)
s.RepoHeader = lipgloss.NewStyle().
+ Height(2).
Border(lipgloss.NormalBorder(), false, false, true, false).
BorderForeground(lipgloss.Color("241"))
@@ -203,6 +205,9 @@ func DefaultStyles() *Styles {
Foreground(lipgloss.Color("15")).
Bold(true)
+ s.RepoHeaderDesc = lipgloss.NewStyle().
+ Foreground(lipgloss.Color("15"))
+
s.Footer = lipgloss.NewStyle().
Height(1)
@@ -348,8 +353,7 @@ func DefaultStyles() *Styles {
Background(lipgloss.Color("#6E6ED8")).
Foreground(lipgloss.Color("#F1F1F1"))
- s.Tabs = lipgloss.NewStyle().
- Height(1)
+ s.Tabs = lipgloss.NewStyle()
s.TabInactive = lipgloss.NewStyle().
Foreground(lipgloss.Color("15"))
@@ -106,7 +106,7 @@ func (ui *UI) Init() tea.Cmd {
// Update implements tea.Model.
// TODO show full help
func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- log.Printf("%T", msg)
+ log.Printf("msg: %T", msg)
cmds := make([]tea.Cmd, 0)
switch msg := msg.(type) {
case tea.WindowSizeMsg:
@@ -143,9 +143,12 @@ func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
ui.state = errorState
return ui, nil
case selector.SelectMsg:
- if ui.activePage == 0 {
- ui.activePage = (ui.activePage + 1) % 2
- cmds = append(cmds, ui.setRepoCmd(string(msg)))
+ switch msg.IdentifiableItem.(type) {
+ case selection.Item:
+ if ui.activePage == 0 {
+ ui.activePage = 1
+ cmds = append(cmds, ui.setRepoCmd(msg.ID()))
+ }
}
}
m, cmd := ui.pages[ui.activePage].Update(msg)