diff --git a/analyze/sort.go b/analyze/sort.go new file mode 100644 index 000000000..8db3dff4b --- /dev/null +++ b/analyze/sort.go @@ -0,0 +1,19 @@ +package analyze + +func (f Files) Len() int { return len(f) } +func (f Files) Swap(i, j int) { f[i], f[j] = f[j], f[i] } +func (f Files) Less(i, j int) bool { return f[i].Size > f[j].Size } + +// ByItemCount sorts files by item count +type ByItemCount Files + +func (f ByItemCount) Len() int { return len(f) } +func (f ByItemCount) Swap(i, j int) { f[i], f[j] = f[j], f[i] } +func (f ByItemCount) Less(i, j int) bool { return f[i].ItemCount > f[j].ItemCount } + +// ByName sorts files by name +type ByName Files + +func (f ByName) Len() int { return len(f) } +func (f ByName) Swap(i, j int) { f[i], f[j] = f[j], f[i] } +func (f ByName) Less(i, j int) bool { return f[i].Name > f[j].Name } diff --git a/analyze/sort_test.go b/analyze/sort_test.go new file mode 100644 index 000000000..269169bbc --- /dev/null +++ b/analyze/sort_test.go @@ -0,0 +1,88 @@ +package analyze + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSortBySize(t *testing.T) { + files := Files{ + &File{ + Size: 1, + }, + &File{ + Size: 2, + }, + &File{ + Size: 3, + }, + } + + sort.Sort(files) + + assert.Equal(t, int64(3), files[0].Size) + assert.Equal(t, int64(2), files[1].Size) + assert.Equal(t, int64(1), files[2].Size) +} + +func TestSortBySizeAsc(t *testing.T) { + files := Files{ + &File{ + Size: 1, + }, + &File{ + Size: 2, + }, + &File{ + Size: 3, + }, + } + + sort.Sort(sort.Reverse(files)) + + assert.Equal(t, int64(1), files[0].Size) + assert.Equal(t, int64(2), files[1].Size) + assert.Equal(t, int64(3), files[2].Size) +} + +func TestSortByItemCount(t *testing.T) { + files := Files{ + &File{ + ItemCount: 1, + }, + &File{ + ItemCount: 2, + }, + &File{ + ItemCount: 3, + }, + } + + sort.Sort(ByItemCount(files)) + + assert.Equal(t, 3, files[0].ItemCount) + assert.Equal(t, 2, files[1].ItemCount) + assert.Equal(t, 1, files[2].ItemCount) +} + +func TestSortByName(t *testing.T) { + files := Files{ + &File{ + Name: "aa", + }, + &File{ + Name: "bb", + }, + &File{ + Name: "cc", + }, + } + + sort.Sort(ByName(files)) + + assert.Equal(t, "cc", files[0].Name) + assert.Equal(t, "bb", files[1].Name) + assert.Equal(t, "aa", files[2].Name) +} diff --git a/cli/cli.go b/cli/cli.go index 7c75e13dc..e41a2d759 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -19,6 +19,9 @@ const helpText = ` [red]enter, right, l [white]Select directory/device [red]left, h [white]Go to parent directory [red]d [white]Delete selected file or directory + [red]n [white]Sort by name (asc/desc) + [red]s [white]Sort by size (asc/desc) + [red]c [white]Sort by items (asc/desc) ` // UI struct @@ -36,12 +39,16 @@ type UI struct { currentDirPath string askBeforeDelete bool ignorePaths []string + sortBy string + sortOrder string } // CreateUI creates the whole UI app func CreateUI(screen tcell.Screen) *UI { ui := &UI{ askBeforeDelete: true, + sortBy: "size", + sortOrder: "desc", } ui.app = tview.NewApplication() @@ -174,9 +181,7 @@ func (ui *UI) showDir() { rowIndex++ } - sort.Slice(ui.currentDir.Files, func(i, j int) bool { - return ui.currentDir.Files[i].Size > ui.currentDir.Files[j].Size - }) + ui.sortItems() for i, item := range ui.currentDir.Files { cell := tview.NewTableCell(formatFileRow(item)) @@ -185,12 +190,40 @@ func (ui *UI) showDir() { rowIndex++ } - ui.footer.SetText("Apparent size: " + formatSize(ui.currentDir.Size) + " Items: " + fmt.Sprint(ui.currentDir.ItemCount)) + ui.footer.SetText( + "Apparent size: " + + formatSize(ui.currentDir.Size) + + " Items: " + fmt.Sprint(ui.currentDir.ItemCount) + + " Sorting by: " + ui.sortBy + " " + ui.sortOrder) ui.table.Select(0, 0) ui.table.ScrollToBeginning() ui.app.SetFocus(ui.table) } +func (ui *UI) sortItems() { + if ui.sortBy == "size" { + if ui.sortOrder == "desc" { + sort.Sort(ui.currentDir.Files) + } else { + sort.Sort(sort.Reverse(ui.currentDir.Files)) + } + } + if ui.sortBy == "itemCount" { + if ui.sortOrder == "desc" { + sort.Sort(analyze.ByItemCount(ui.currentDir.Files)) + } else { + sort.Sort(sort.Reverse(analyze.ByItemCount(ui.currentDir.Files))) + } + } + if ui.sortBy == "name" { + if ui.sortOrder == "desc" { + sort.Sort(analyze.ByName(ui.currentDir.Files)) + } else { + sort.Sort(sort.Reverse(analyze.ByName(ui.currentDir.Files))) + } + } +} + func (ui *UI) fileItemSelected(row, column int) { selectedDir := ui.table.GetCell(row, column).GetReference().(*analyze.File) if !selectedDir.IsDir { @@ -279,10 +312,33 @@ func (ui *UI) keyPressed(key *tcell.EventKey) *tcell.EventKey { ui.deleteSelected() } break + case 's': + ui.setSorting("size") + break + case 'c': + ui.setSorting("itemCount") + break + case 'n': + ui.setSorting("name") + break } return key } +func (ui *UI) setSorting(newOrder string) { + if newOrder == ui.sortBy { + if ui.sortOrder == "asc" { + ui.sortOrder = "desc" + } else { + ui.sortOrder = "asc" + } + } else { + ui.sortBy = newOrder + ui.sortOrder = "asc" + } + ui.showDir() +} + func (ui *UI) updateProgress(progress *analyze.CurrentProgress) { for { progress.Mutex.Lock() diff --git a/cli/sort_test.go b/cli/sort_test.go new file mode 100644 index 000000000..68f5b375a --- /dev/null +++ b/cli/sort_test.go @@ -0,0 +1,170 @@ +package cli + +import ( + "testing" + "time" + + "github.com/dundee/gdu/analyze" + "github.com/gdamore/tcell/v2" + "github.com/stretchr/testify/assert" +) + +func TestSortBySizeAsc(t *testing.T) { + fin := analyze.CreateTestDir() + defer fin() + + simScreen := tcell.NewSimulationScreen("UTF-8") + simScreen.Init() + simScreen.SetSize(50, 50) + + ui := CreateUI(simScreen) + + ui.AnalyzePath("test_dir") + + go func() { + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'j', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'l', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 's', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(100 * time.Millisecond) + }() + + ui.StartUILoop() + + dir := ui.currentDir + + assert.Equal(t, "file2", dir.Files[0].Name) +} + +func TestSortByName(t *testing.T) { + fin := analyze.CreateTestDir() + defer fin() + + simScreen := tcell.NewSimulationScreen("UTF-8") + simScreen.Init() + simScreen.SetSize(50, 50) + + ui := CreateUI(simScreen) + + ui.AnalyzePath("test_dir") + + go func() { + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'j', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'l', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'n', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(100 * time.Millisecond) + }() + + ui.StartUILoop() + + dir := ui.currentDir + + assert.Equal(t, "file2", dir.Files[0].Name) +} + +func TestSortByNameDesc(t *testing.T) { + fin := analyze.CreateTestDir() + defer fin() + + simScreen := tcell.NewSimulationScreen("UTF-8") + simScreen.Init() + simScreen.SetSize(50, 50) + + ui := CreateUI(simScreen) + + ui.AnalyzePath("test_dir") + + go func() { + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'j', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'l', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'n', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'n', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(100 * time.Millisecond) + }() + + ui.StartUILoop() + + dir := ui.currentDir + + assert.Equal(t, "subnested", dir.Files[0].Name) +} + +func TestSortByItemCount(t *testing.T) { + fin := analyze.CreateTestDir() + defer fin() + + simScreen := tcell.NewSimulationScreen("UTF-8") + simScreen.Init() + simScreen.SetSize(50, 50) + + ui := CreateUI(simScreen) + + ui.AnalyzePath("test_dir") + + go func() { + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'j', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'l', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'c', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(100 * time.Millisecond) + }() + + ui.StartUILoop() + + dir := ui.currentDir + + assert.Equal(t, "file2", dir.Files[0].Name) +} + +func TestSortByItemCountDesc(t *testing.T) { + // fin := analyze.CreateTestDir() + // defer fin() + analyze.CreateTestDir() + + simScreen := tcell.NewSimulationScreen("UTF-8") + simScreen.Init() + simScreen.SetSize(50, 50) + + ui := CreateUI(simScreen) + + ui.AnalyzePath("test_dir") + + go func() { + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'j', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'l', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'c', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'c', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(100 * time.Millisecond) + }() + + ui.StartUILoop() + + dir := ui.currentDir + + assert.Equal(t, "subnested", dir.Files[0].Name) +}