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