diff --git a/cmd/sync.go b/cmd/sync.go index b4b72ff..89cf788 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -408,8 +408,11 @@ type groupedEvent struct { // groupFilesByTopLevel collapses file entries into top-level directories // and root files. "cmd/sync.go" + "cmd/init.go" become one entry "cmd/" with count=2. +// When a directory contains only one file, the full relative path is kept. func groupFilesByTopLevel(files []syncer.FileEntry) []groupedEvent { dirMap := make(map[string]*groupedEvent) + // Track the original filename for single-file groups. + dirFirstFile := make(map[string]string) var rootFiles []groupedEvent var dirOrder []string @@ -429,6 +432,7 @@ func groupFilesByTopLevel(files []syncer.FileEntry) []groupedEvent { g.bytes += f.Bytes } else { dirMap[dir] = &groupedEvent{name: dir, count: 1, bytes: f.Bytes} + dirFirstFile[dir] = f.Name dirOrder = append(dirOrder, dir) } } @@ -436,7 +440,11 @@ func groupFilesByTopLevel(files []syncer.FileEntry) []groupedEvent { var out []groupedEvent for _, dir := range dirOrder { - out = append(out, *dirMap[dir]) + g := *dirMap[dir] + if g.count == 1 { + g.name = dirFirstFile[dir] + } + out = append(out, g) } out = append(out, rootFiles...) return out diff --git a/internal/tui/dashboard.go b/internal/tui/dashboard.go index 33b2efa..7baabb8 100644 --- a/internal/tui/dashboard.go +++ b/internal/tui/dashboard.go @@ -256,14 +256,14 @@ func (m DashboardModel) renderEvent(evt SyncEvent) string { ts := dimStyle.Render(evt.Time.Format("15:04:05")) switch evt.Status { case "synced": - name := padRight(evt.File, 30) + name := padRight(abbreviatePath(evt.File, 30), 30) detail := "" if evt.Size != "" { detail = dimStyle.Render(fmt.Sprintf("%18s %s", evt.Size, evt.Duration.Truncate(100*time.Millisecond))) } return ts + " " + statusSynced.Render("✓") + " " + name + detail case "error": - name := padRight(evt.File, 30) + name := padRight(abbreviatePath(evt.File, 30), 30) return ts + " " + statusError.Render("✗") + " " + name + statusError.Render("error") default: return ts + " " + evt.File @@ -285,6 +285,29 @@ func (m DashboardModel) filteredEvents() []SyncEvent { return out } +// abbreviatePath shortens a file path to fit within maxLen by replacing +// leading directory segments with their first letter. +// "internal/syncer/syncer.go" → "i/s/syncer.go" +func abbreviatePath(p string, maxLen int) string { + if len(p) <= maxLen { + return p + } + parts := strings.Split(p, "/") + if len(parts) <= 1 { + return p + } + // Shorten directory segments from the left, keep the filename intact. + for i := 0; i < len(parts)-1; i++ { + if len(parts[i]) > 1 { + parts[i] = parts[i][:1] + } + if len(strings.Join(parts, "/")) <= maxLen { + break + } + } + return strings.Join(parts, "/") +} + // padRight pads s with spaces to width n, truncating if necessary. func padRight(s string, n int) string { if len(s) >= n {