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		return fileSlice[i].History.LatestVersion.CreatedAt > fileSlice[j].History.LatestVersion.CreatedAt
 63	})
 64
 65	// Determine how many items to show
 66	maxItems := len(fileSlice)
 67	if opts.MaxItems > 0 {
 68		maxItems = min(opts.MaxItems, len(fileSlice))
 69	}
 70
 71	filesShown := 0
 72	for _, file := range fileSlice {
 73		if file.Additions == 0 && file.Deletions == 0 {
 74			continue // skip files with no changes
 75		}
 76		if filesShown >= maxItems {
 77			break
 78		}
 79
 80		var statusParts []string
 81		if file.Additions > 0 {
 82			statusParts = append(statusParts, t.S().Base.Foreground(t.Success).Render(fmt.Sprintf("+%d", file.Additions)))
 83		}
 84		if file.Deletions > 0 {
 85			statusParts = append(statusParts, t.S().Base.Foreground(t.Error).Render(fmt.Sprintf("-%d", file.Deletions)))
 86		}
 87
 88		extraContent := strings.Join(statusParts, " ")
 89		cwd := config.Get().WorkingDir() + string(os.PathSeparator)
 90		filePath := file.FilePath
 91		filePath = strings.TrimPrefix(filePath, cwd)
 92		filePath = fsext.DirTrim(fsext.PrettyPath(filePath), 2)
 93		filePath = ansi.Truncate(filePath, opts.MaxWidth-lipgloss.Width(extraContent)-2, "…")
 94
 95		fileList = append(fileList,
 96			core.Status(
 97				core.StatusOpts{
 98					IconColor:    t.FgMuted,
 99					NoIcon:       true,
100					Title:        filePath,
101					ExtraContent: extraContent,
102				},
103				opts.MaxWidth,
104			),
105		)
106		filesShown++
107	}
108
109	return fileList
110}
111
112// RenderFileBlock renders a complete file block with optional truncation indicator.
113func RenderFileBlock(fileSlice []SessionFile, opts RenderOptions, showTruncationIndicator bool) string {
114	t := styles.CurrentTheme()
115	fileList := RenderFileList(fileSlice, opts)
116
117	// Add truncation indicator if needed
118	if showTruncationIndicator && opts.MaxItems > 0 {
119		totalFilesWithChanges := 0
120		for _, file := range fileSlice {
121			if file.Additions > 0 || file.Deletions > 0 {
122				totalFilesWithChanges++
123			}
124		}
125		if totalFilesWithChanges > opts.MaxItems {
126			remaining := totalFilesWithChanges - opts.MaxItems
127			if remaining == 1 {
128				fileList = append(fileList, t.S().Base.Foreground(t.FgMuted).Render("…"))
129			} else {
130				fileList = append(fileList,
131					t.S().Base.Foreground(t.FgSubtle).Render(fmt.Sprintf("…and %d more", remaining)),
132				)
133			}
134		}
135	}
136
137	content := lipgloss.JoinVertical(lipgloss.Left, fileList...)
138	if opts.MaxWidth > 0 {
139		return lipgloss.NewStyle().Width(opts.MaxWidth).Render(content)
140	}
141	return content
142}