1package repo
2
3import (
4 "fmt"
5 "io"
6 "strings"
7 "time"
8
9 "github.com/charmbracelet/bubbles/v2/key"
10 "github.com/charmbracelet/bubbles/v2/list"
11 tea "github.com/charmbracelet/bubbletea/v2"
12 "github.com/charmbracelet/lipgloss/v2"
13 "github.com/charmbracelet/soft-serve/git"
14 "github.com/charmbracelet/soft-serve/pkg/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}
22
23// ID implements selector.IdentifiableItem.
24func (i LogItem) ID() string {
25 return i.Hash()
26}
27
28// Hash returns the commit hash.
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 item, ok := m.SelectedItem().(LogItem)
61 if !ok {
62 return nil
63 }
64 switch msg := msg.(type) {
65 case tea.KeyPressMsg:
66 switch {
67 case key.Matches(msg, d.common.KeyMap.Copy):
68 return copyCmd(item.Hash(), "Commit hash copied to clipboard")
69 }
70 }
71 return nil
72}
73
74// Render renders the item. Implements list.ItemDelegate.
75func (d LogItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
76 i, ok := listItem.(LogItem)
77 if !ok {
78 return
79 }
80 if i.Commit == nil {
81 return
82 }
83
84 styles := d.common.Styles.LogItem.Normal
85 if index == m.Index() {
86 styles = d.common.Styles.LogItem.Active
87 }
88
89 horizontalFrameSize := styles.Base.GetHorizontalFrameSize()
90
91 hash := i.Commit.ID.String()[:7]
92 title := styles.Title.Render(
93 common.TruncateString(i.Title(),
94 m.Width()-
95 horizontalFrameSize-
96 // 9 is the length of the hash (7) + the left padding (1) + the
97 // title truncation symbol (1)
98 9),
99 )
100 hashStyle := styles.Hash.
101 Align(lipgloss.Right).
102 PaddingLeft(1).
103 Width(m.Width() -
104 horizontalFrameSize -
105 lipgloss.Width(title) - 1) // 1 is for the left padding
106 if index == m.Index() {
107 hashStyle = hashStyle.Bold(true)
108 }
109 hash = hashStyle.Render(hash)
110 if m.Width()-horizontalFrameSize-hashStyle.GetHorizontalFrameSize()-hashStyle.GetWidth() <= 0 {
111 hash = ""
112 title = styles.Title.Render(
113 common.TruncateString(i.Title(),
114 m.Width()-horizontalFrameSize),
115 )
116 }
117 author := i.Author.Name
118 committer := i.Committer.Name
119 who := ""
120 if author != "" && committer != "" {
121 who = styles.Keyword.Render(committer) + styles.Desc.Render(" committed")
122 if author != committer {
123 who = styles.Keyword.Render(author) + styles.Desc.Render(" authored and ") + who
124 }
125 who += " "
126 }
127 date := i.Committer.When.Format("Jan 02")
128 if i.Committer.When.Year() != time.Now().Year() {
129 date += fmt.Sprintf(" %d", i.Committer.When.Year())
130 }
131 who += styles.Desc.Render("on ") + styles.Keyword.Render(date)
132 who = common.TruncateString(who, m.Width()-horizontalFrameSize)
133 fmt.Fprint(w,
134 d.common.Zone.Mark(
135 i.ID(),
136 styles.Base.Render(
137 lipgloss.JoinVertical(lipgloss.Left,
138 truncate.String(fmt.Sprintf("%s%s",
139 title,
140 hash,
141 ), uint(m.Width()-horizontalFrameSize)), //nolint:gosec
142 who,
143 ),
144 ),
145 ),
146 )
147}