From bb28e77b7745e0593146fe669d4a64938b89952a Mon Sep 17 00:00:00 2001 From: Oussama Makhlouk Date: Fri, 15 May 2026 11:39:32 +0100 Subject: [PATCH] issue #33: Add flag for human readable size for each file/folder --- cmd/flag.go | 1 + cmd/root_test.go | 20 ++++++++++++++++++++ module/types.go | 7 ++++--- service/service.go | 31 ++++++++++++++++++------------- service/service_test.go | 39 ++++++++++++++++++++++++++++++++++----- service/util.go | 23 +++++++++++++++++++++++ 6 files changed, 100 insertions(+), 21 deletions(-) diff --git a/cmd/flag.go b/cmd/flag.go index 9d8b893..13c7dd2 100644 --- a/cmd/flag.go +++ b/cmd/flag.go @@ -12,4 +12,5 @@ func FlagDefinition(cmd *cobra.Command, flags *module.Flags) { cmd.PersistentFlags().BoolVarP(&flags.ShowHidden, "all", "a", false, "List all files and directories") cmd.PersistentFlags().BoolVarP(&flags.ShowTreeView, "tree", "t", false, "Tree view of the directory") cmd.PersistentFlags().BoolVarP(&flags.HideIcon, "icon", "i", false, "Disable icons (Enabled by default)") + cmd.PersistentFlags().BoolVarP(&flags.ShowReadableSize, "readable", "r", false, "Show human-readable size for each file and directory") } diff --git a/cmd/root_test.go b/cmd/root_test.go index 3fd9185..6b9798b 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -52,6 +52,26 @@ func TestRootCmd_ValidPathWithFlags(t *testing.T) { } } +func TestRootCmd_ReadableFlag(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main"), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + output := captureStdout(t, func() { + cmd := newRootCmd() + cmd.SetArgs([]string{"-r", "--icon", dir}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v, want nil", err) + } + }) + + if !strings.Contains(output, "main.go (12 B)") { + t.Fatalf("Execute() output = %q, want readable file size", output) + } +} + func captureStdout(t *testing.T, run func()) string { t.Helper() diff --git a/module/types.go b/module/types.go index 401852c..41dacea 100644 --- a/module/types.go +++ b/module/types.go @@ -3,9 +3,10 @@ package module // Flags struct - represents flags for the application. type Flags struct { - ShowHidden bool - ShowTreeView bool - HideIcon bool + ShowHidden bool + ShowTreeView bool + HideIcon bool + ShowReadableSize bool } // Options struct - Contains configuration options for directory listing. diff --git a/service/service.go b/service/service.go index ca7723c..9dec019 100644 --- a/service/service.go +++ b/service/service.go @@ -82,12 +82,7 @@ func listDirectory(options module.Options, output io.Writer) (fileCount, dirCoun fileCount++ } - var formatted string - if !options.Flags.HideIcon { - formatted = printWithIconAndPrefix("", file) - } else { - formatted = printFilesAndFolderWithoutIcons("", file) - } + formatted := formatFileWithOptions("", file, options.Flags) entries = append(entries, formatted) @@ -152,7 +147,7 @@ func printFilesAndDirectoriesTreeFormat(files []os.FileInfo, options module.Opti isLast := i == lastVisibleIndex prefix, childIndent := calculateIndent(indent, isLast) - if err := printFileWithPrefix(output, prefix, file, options.Flags.HideIcon); err != nil { + if err := printFileWithPrefix(output, prefix, file, options.Flags); err != nil { return 0, 0, err } @@ -185,14 +180,24 @@ func calculateIndent(indent string, isLast bool) (prefix, childIndent string) { } // printFileWithPrefix prints the file with the given prefix and icon settings -func printFileWithPrefix(output io.Writer, prefix string, file os.FileInfo, hideIcon bool) error { - if hideIcon { - _, err := fmt.Fprintln(output, printFilesAndFolderWithoutIcons(prefix, file)) - return err +func printFileWithPrefix(output io.Writer, prefix string, file os.FileInfo, flags module.Flags) error { + _, err := fmt.Fprintln(output, formatFileWithOptions(prefix, file, flags)) + return err +} + +func formatFileWithOptions(prefix string, file os.FileInfo, flags module.Flags) string { + var formatted string + if flags.HideIcon { + formatted = printFilesAndFolderWithoutIcons(prefix, file) + } else { + formatted = printWithIconAndPrefix(prefix, file) } - _, err := fmt.Fprintln(output, printWithIconAndPrefix(prefix, file)) - return err + if flags.ShowReadableSize { + formatted = appendReadableSize(formatted, file.Size()) + } + + return formatted } // processDirectory recursively processes a subdirectory diff --git a/service/service_test.go b/service/service_test.go index 8534594..fbcda11 100644 --- a/service/service_test.go +++ b/service/service_test.go @@ -45,6 +45,12 @@ func TestDispatcher_ListDirectory(t *testing.T) { flags: module.Flags{HideIcon: true, ShowHidden: true}, wantContains: []string{".", ".hidden", "alpha.go", "subpkg", "1 directories, 2 files"}, }, + { + name: "shows readable file and directory sizes", + flags: module.Flags{HideIcon: true, ShowReadableSize: true}, + wantContains: []string{"alpha.go (12 B)", "subpkg (", "1 directories, 1 files"}, + wantMissing: []string{".hidden"}, + }, } for _, tt := range tests { @@ -55,7 +61,7 @@ func TestDispatcher_ListDirectory(t *testing.T) { t.Fatalf("dispatcher() error = %v, want nil", err) } - got := output.String() + got := stripANSI(output.String()) for _, want := range tt.wantContains { if !strings.Contains(got, want) { t.Fatalf("dispatcher() output = %q, want to contain %q", got, want) @@ -80,22 +86,45 @@ func TestDispatcher_TreeDirectory(t *testing.T) { err := dispatcher(module.Options{ Directory: dir, Flags: module.Flags{ - HideIcon: true, - ShowTreeView: true, + HideIcon: true, + ShowTreeView: true, + ShowReadableSize: true, }, }, &output) if err != nil { t.Fatalf("dispatcher() error = %v, want nil", err) } - got := output.String() - for _, want := range []string{"└── ", "subpkg", "nested.go", "1 directories, 2 files"} { + got := stripANSI(output.String()) + for _, want := range []string{"└── ", "subpkg (", "nested.go (14 B)", "1 directories, 2 files"} { if !strings.Contains(got, want) { t.Fatalf("dispatcher() output = %q, want to contain %q", got, want) } } } +func TestHumanReadableSize(t *testing.T) { + tests := []struct { + name string + size int64 + want string + }{ + {name: "zero bytes", size: 0, want: "0 B"}, + {name: "bytes", size: 42, want: "42 B"}, + {name: "kilobytes", size: 2048, want: "2.0 KB"}, + {name: "megabytes", size: 1536 * 1024, want: "1.5 MB"}, + {name: "negative size", size: -1, want: "0 B"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := humanReadableSize(tt.size); got != tt.want { + t.Fatalf("humanReadableSize(%d) = %q, want %q", tt.size, got, tt.want) + } + }) + } +} + func TestReadDirectory_FilePath(t *testing.T) { path := filepath.Join(t.TempDir(), "regular.txt") mustWriteFile(t, path, "content") diff --git a/service/util.go b/service/util.go index 7a4528d..3da5d3d 100644 --- a/service/util.go +++ b/service/util.go @@ -441,6 +441,29 @@ func printFilesAndFolderWithoutIcons(prefix string, file os.FileInfo) string { return format } +func appendReadableSize(formatted string, size int64) string { + return fmt.Sprintf("%s (%s)", formatted, humanReadableSize(size)) +} + +func humanReadableSize(size int64) string { + if size < 0 { + size = 0 + } + if size < 1024 { + return fmt.Sprintf("%d B", size) + } + + units := []string{"KB", "MB", "GB", "TB", "PB"} + value := float64(size) + unitIndex := -1 + for value >= 1024 && unitIndex < len(units)-1 { + value /= 1024 + unitIndex++ + } + + return fmt.Sprintf("%.1f %s", value, units[unitIndex]) +} + // sortSlice func - sorts a slice of os.FileInfo objects alphabetically by file name. // It modifies the original slice in place. func sortSlice(files []os.FileInfo) {