files.go

  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					IconColor:    t.FgMuted,
102					NoIcon:       true,
103					Title:        filePath,
104					ExtraContent: extraContent,
105				},
106				opts.MaxWidth,
107			),
108		)
109		filesShown++
110	}
111
112	return fileList
113}
114
115// RenderFileBlock renders a complete file block with optional truncation indicator.
116func RenderFileBlock(fileSlice []SessionFile, opts RenderOptions, showTruncationIndicator bool) string {
117	t := styles.CurrentTheme()
118	fileList := RenderFileList(fileSlice, opts)
119
120	// Add truncation indicator if needed
121	if showTruncationIndicator && opts.MaxItems > 0 {
122		totalFilesWithChanges := 0
123		for _, file := range fileSlice {
124			if file.Additions > 0 || file.Deletions > 0 {
125				totalFilesWithChanges++
126			}
127		}
128		if totalFilesWithChanges > opts.MaxItems {
129			remaining := totalFilesWithChanges - opts.MaxItems
130			if remaining == 1 {
131				fileList = append(fileList, t.S().Base.Foreground(t.FgMuted).Render("…"))
132			} else {
133				fileList = append(fileList,
134					t.S().Base.Foreground(t.FgSubtle).Render(fmt.Sprintf("…and %d more", remaining)),
135				)
136			}
137		}
138	}
139
140	content := lipgloss.JoinVertical(lipgloss.Left, fileList...)
141	if opts.MaxWidth > 0 {
142		return lipgloss.NewStyle().Width(opts.MaxWidth).Render(content)
143	}
144	return content
145}