files.go 4.0 KB

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