1package repo
2
3import (
4 "fmt"
5 "io"
6 "strings"
7 "time"
8
9 "github.com/charmbracelet/bubbles/key"
10 "github.com/charmbracelet/bubbles/list"
11 tea "github.com/charmbracelet/bubbletea"
12 "github.com/charmbracelet/lipgloss"
13 "github.com/charmbracelet/soft-serve/git"
14 "github.com/charmbracelet/soft-serve/ui/common"
15 "github.com/muesli/reflow/truncate"
16)
17
18// LogItem is a item in the log list that displays a git commit.
19type LogItem struct {
20 *git.Commit
21 copied time.Time
22}
23
24// ID implements selector.IdentifiableItem.
25func (i LogItem) ID() string {
26 return i.Hash()
27}
28
29func (i LogItem) Hash() string {
30 return i.Commit.ID.String()
31}
32
33// Title returns the item title. Implements list.DefaultItem.
34func (i LogItem) Title() string {
35 if i.Commit != nil {
36 return strings.Split(i.Commit.Message, "\n")[0]
37 }
38 return ""
39}
40
41// Description returns the item description. Implements list.DefaultItem.
42func (i LogItem) Description() string { return "" }
43
44// FilterValue implements list.Item.
45func (i LogItem) FilterValue() string { return i.Title() }
46
47// LogItemDelegate is the delegate for LogItem.
48type LogItemDelegate struct {
49 common *common.Common
50}
51
52// Height returns the item height. Implements list.ItemDelegate.
53func (d LogItemDelegate) Height() int { return 2 }
54
55// Spacing returns the item spacing. Implements list.ItemDelegate.
56func (d LogItemDelegate) Spacing() int { return 1 }
57
58// Update updates the item. Implements list.ItemDelegate.
59func (d LogItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
60 idx := m.Index()
61 item, ok := m.SelectedItem().(LogItem)
62 if !ok {
63 return nil
64 }
65 switch msg := msg.(type) {
66 case tea.KeyMsg:
67 switch {
68 case key.Matches(msg, d.common.KeyMap.Copy):
69 item.copied = time.Now()
70 d.common.Copy.Copy(item.Hash())
71 return m.SetItem(idx, item)
72 }
73 }
74 return nil
75}
76
77var (
78 highlight = lipgloss.NewStyle().Foreground(lipgloss.Color("#F1F1F1"))
79)
80
81// Render renders the item. Implements list.ItemDelegate.
82func (d LogItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
83 styles := d.common.Styles
84 i, ok := listItem.(LogItem)
85 if !ok {
86 return
87 }
88 if i.Commit == nil {
89 return
90 }
91
92 width := lipgloss.Width
93 titleStyle := styles.LogItemTitle.Copy()
94 style := styles.LogItemInactive
95 if index == m.Index() {
96 titleStyle.Bold(true)
97 style = styles.LogItemActive
98 }
99 hash := " " + i.Commit.ID.String()[:7]
100 if !i.copied.IsZero() && i.copied.Add(time.Second).After(time.Now()) {
101 hash = "copied"
102 }
103 title := titleStyle.Render(
104 truncateString(i.Title(), m.Width()-style.GetHorizontalFrameSize()-width(hash)-2, "…"),
105 )
106 hashStyle := styles.LogItemHash.Copy().
107 Align(lipgloss.Right).
108 Width(m.Width() -
109 style.GetHorizontalFrameSize() -
110 width(title) -
111 // FIXME where this "1" is coming from?
112 1)
113 if index == m.Index() {
114 hashStyle = hashStyle.Bold(true)
115 }
116 hash = hashStyle.Render(hash)
117 author := highlight.Render(i.Author.Name)
118 commiter := highlight.Render(i.Committer.Name)
119 who := ""
120 if author != "" && commiter != "" {
121 who = fmt.Sprintf("%s committed", commiter)
122 if author != commiter {
123 who = fmt.Sprintf("%s authored and %s", author, who)
124 }
125 who += " "
126 }
127 date := fmt.Sprintf("on %s", i.Committer.When.Format("Feb 02"))
128 if i.Committer.When.Year() != time.Now().Year() {
129 date += fmt.Sprintf(" %d", i.Committer.When.Year())
130 }
131 who += date
132 who = truncateString(who, m.Width()-style.GetHorizontalFrameSize(), "…")
133 fmt.Fprint(w,
134 style.Render(
135 lipgloss.JoinVertical(lipgloss.Top,
136 lipgloss.JoinHorizontal(lipgloss.Left,
137 title,
138 hash,
139 ),
140 who,
141 ),
142 ),
143 )
144}
145
146func truncateString(s string, max int, tail string) string {
147 if max < 0 {
148 max = 0
149 }
150 return truncate.StringWithTail(s, uint(max), tail)
151}