1package files
2
3import (
4 "fmt"
5 "os"
6 "sort"
7 "strings"
8
9 "github.com/charmbracelet/lipgloss/v2"
10 "github.com/charmbracelet/x/ansi"
11
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/fsext"
14 "github.com/charmbracelet/crush/internal/history"
15 "github.com/charmbracelet/crush/internal/tui/components/core"
16 "github.com/charmbracelet/crush/internal/tui/styles"
17)
18
19// FileHistory represents a file history with initial and latest versions.
20type FileHistory struct {
21 InitialVersion history.File
22 LatestVersion history.File
23}
24
25// SessionFile represents a file with its history information.
26type SessionFile struct {
27 History FileHistory
28 FilePath string
29 Additions int
30 Deletions int
31}
32
33// RenderOptions contains options for rendering file lists.
34type RenderOptions struct {
35 MaxWidth int
36 MaxItems int
37 ShowSection bool
38 SectionName string
39}
40
41// RenderFileList renders a list of file status items with the given options.
42func RenderFileList(fileSlice []SessionFile, opts RenderOptions) []string {
43 t := styles.CurrentTheme()
44 fileList := []string{}
45
46 if opts.ShowSection {
47 sectionName := opts.SectionName
48 if sectionName == "" {
49 sectionName = "Modified Files"
50 }
51 section := t.S().Subtle.Render(sectionName)
52 fileList = append(fileList, section, "")
53 }
54
55 if len(fileSlice) == 0 {
56 fileList = append(fileList, t.S().Base.Foreground(t.Border).Render("None"))
57 return fileList
58 }
59
60 // Sort files by the latest version's created time
61 sort.Slice(fileSlice, func(i, j int) bool {
62 if fileSlice[i].History.LatestVersion.CreatedAt == fileSlice[j].History.LatestVersion.CreatedAt {
63 return strings.Compare(fileSlice[i].FilePath, fileSlice[j].FilePath) < 0
64 }
65 return fileSlice[i].History.LatestVersion.CreatedAt > fileSlice[j].History.LatestVersion.CreatedAt
66 })
67
68 // Determine how many items to show
69 maxItems := len(fileSlice)
70 if opts.MaxItems > 0 {
71 maxItems = min(opts.MaxItems, len(fileSlice))
72 }
73
74 filesShown := 0
75 for _, file := range fileSlice {
76 if file.Additions == 0 && file.Deletions == 0 {
77 continue // skip files with no changes
78 }
79 if filesShown >= maxItems {
80 break
81 }
82
83 var statusParts []string
84 if file.Additions > 0 {
85 statusParts = append(statusParts, t.S().Base.Foreground(t.Success).Render(fmt.Sprintf("+%d", file.Additions)))
86 }
87 if file.Deletions > 0 {
88 statusParts = append(statusParts, t.S().Base.Foreground(t.Error).Render(fmt.Sprintf("-%d", file.Deletions)))
89 }
90
91 extraContent := strings.Join(statusParts, " ")
92 cwd := config.Get().WorkingDir() + string(os.PathSeparator)
93 filePath := file.FilePath
94 filePath = strings.TrimPrefix(filePath, cwd)
95 filePath = fsext.DirTrim(fsext.PrettyPath(filePath), 2)
96 filePath = ansi.Truncate(filePath, opts.MaxWidth-lipgloss.Width(extraContent)-2, "β¦")
97
98 fileList = append(fileList,
99 core.Status(
100 core.StatusOpts{
101 Title: filePath,
102 ExtraContent: extraContent,
103 },
104 opts.MaxWidth,
105 ),
106 )
107 filesShown++
108 }
109
110 return fileList
111}
112
113// RenderFileBlock renders a complete file block with optional truncation indicator.
114func RenderFileBlock(fileSlice []SessionFile, opts RenderOptions, showTruncationIndicator bool) string {
115 t := styles.CurrentTheme()
116 fileList := RenderFileList(fileSlice, opts)
117
118 // Add truncation indicator if needed
119 if showTruncationIndicator && opts.MaxItems > 0 {
120 totalFilesWithChanges := 0
121 for _, file := range fileSlice {
122 if file.Additions > 0 || file.Deletions > 0 {
123 totalFilesWithChanges++
124 }
125 }
126 if totalFilesWithChanges > opts.MaxItems {
127 remaining := totalFilesWithChanges - opts.MaxItems
128 if remaining == 1 {
129 fileList = append(fileList, t.S().Base.Foreground(t.FgMuted).Render("β¦"))
130 } else {
131 fileList = append(fileList,
132 t.S().Base.Foreground(t.FgSubtle).Render(fmt.Sprintf("β¦and %d more", remaining)),
133 )
134 }
135 }
136 }
137
138 content := lipgloss.JoinVertical(lipgloss.Left, fileList...)
139 if opts.MaxWidth > 0 {
140 return lipgloss.NewStyle().Width(opts.MaxWidth).Render(content)
141 }
142 return content
143}