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(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 := config.Get().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(fileSlice []SessionFile, opts RenderOptions, showTruncationIndicator bool) string {
118	t := styles.CurrentTheme()
119	fileList := RenderFileList(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}