diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 781d2ff..b471c12 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,15 @@ # Contributing +## Table of contents + +- [Getting started](#getting-started) +- [Development workflow](#development-workflow) +- [Contribution guidelines](#contribution-guidelines) +- [Adding or changing CLI behavior](#adding-or-changing-cli-behavior) +- [Documentation style](#documentation-style) +- [Reporting bugs](#reporting-bugs) +- [Feature requests](#feature-requests) + Thanks for your interest in contributing to `treels`. ## Getting started diff --git a/README.md b/README.md index 7d66b2a..5f967ba 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,17 @@ Use it to inspect directories as a compact grid, expand them as a tree, hide pro treels preview

+## Table of contents + +- [Quick start](#quick-start) +- [Installation](#installation) +- [Features](#features) +- [Usage](#usage) +- [Preview](#preview) +- [Documentation](#documentation) +- [Development](#development) +- [License](#license) + ## Quick start ```bash @@ -20,6 +31,8 @@ treels --tree --depth 2 # tree view limited to two levels treels --tree --gitignore # exclude root .gitignore matches treels --tree --dirs-only # show directory structure only treels --long --readable # detailed listing with readable sizes +treels --sort size --reverse # sort entries by largest first +treels --dirs-first # group directories before files treels --json # machine-readable output treels --no-icons # fallback for terminals without Nerd Fonts ``` @@ -66,6 +79,7 @@ go build . | Depth limit | `--depth N` | | Detailed metadata | `-l`, `--long` | | Human-readable sizes | `-r`, `--readable` | +| Sorting | `--sort name|size|modified|type`, `--reverse`, `--dirs-first` | | JSON output | `--json` | | Hidden files | `-a`, `--all` | | Directory-only view | `--dirs-only` | diff --git a/cmd/flag.go b/cmd/flag.go index a8d4829..26cd337 100644 --- a/cmd/flag.go +++ b/cmd/flag.go @@ -16,6 +16,9 @@ func FlagDefinition(cmd *cobra.Command, flags *module.Flags) { cmd.PersistentFlags().BoolVar(&flags.HideIcon, "no-icons", false, "Disable icons") cmd.PersistentFlags().BoolVar(&flags.HideSummary, "no-summary", false, "Hide the final file and directory count") cmd.PersistentFlags().BoolVar(&flags.RespectGitIgnore, "gitignore", false, "Respect .gitignore rules") + cmd.PersistentFlags().StringVar(&flags.SortBy, "sort", "name", "Sort entries by name, size, modified, or type") + cmd.PersistentFlags().BoolVar(&flags.ReverseSort, "reverse", false, "Reverse sort order") + cmd.PersistentFlags().BoolVar(&flags.DirsFirst, "dirs-first", false, "Show directories before files") cmd.PersistentFlags().IntVar(&flags.TreeDepth, "depth", -1, "Limit tree view recursion depth") cmd.PersistentFlags().Lookup("depth").DefValue = "unlimited" cmd.PersistentFlags().BoolVarP(&flags.ShowReadableSize, "readable", "r", false, "Show human-readable size for each file and directory") diff --git a/cmd/root.go b/cmd/root.go index 14cedcf..dca413f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "strings" "github.com/oussamaM1/treels/module" "github.com/oussamaM1/treels/service" @@ -13,6 +14,13 @@ import ( const version = "v1.3.1" +var validSortFields = map[string]struct{}{ + "name": {}, + "size": {}, + "modified": {}, + "type": {}, +} + // Execute func - runs the root command. func Execute() { execute(newRootCmd(), os.Stderr, os.Exit) @@ -40,6 +48,10 @@ func newRootCmd() *cobra.Command { if cmd.Flags().Changed("depth") && flag.TreeDepth < 0 { return fmt.Errorf("--depth must be greater than or equal to 0") } + flag.SortBy = strings.ToLower(flag.SortBy) + if _, ok := validSortFields[flag.SortBy]; !ok { + return fmt.Errorf("--sort must be one of: name, size, modified, type") + } flag.LimitTreeDepth = cmd.Flags().Changed("depth") options := module.Options{Flags: flag} diff --git a/cmd/root_test.go b/cmd/root_test.go index 8cbeffb..22adf77 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -199,6 +199,62 @@ func TestRootCmd_LongFlag(t *testing.T) { } } +func TestRootCmd_SortFlags(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "small.txt"), []byte("x"), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + if err := os.WriteFile(filepath.Join(dir, "large.txt"), []byte("1234567890"), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + if err := os.Mkdir(filepath.Join(dir, "folder"), 0o755); err != nil { + t.Fatalf("Mkdir() error = %v", err) + } + + output := captureStdout(t, func() { + cmd := newRootCmd() + cmd.SetArgs([]string{"--long", "--no-icons", "--sort", "size", "--reverse", "--dirs-first", dir}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v, want nil", err) + } + }) + + assertOutputOrder(t, output, []string{"folder", "large.txt", "small.txt"}) +} + +func TestRootCmd_SortFlagRejectsInvalidValue(t *testing.T) { + cmd := newRootCmd() + cmd.SetArgs([]string{"--sort", "unknown"}) + + err := cmd.Execute() + if err == nil { + t.Fatal("Execute() error = nil, want invalid sort error") + } + if !strings.Contains(err.Error(), "--sort must be one of: name, size, modified, type") { + t.Fatalf("Execute() error = %q, want invalid sort validation error", err) + } +} + +func TestRootCmd_SortFlagIsCaseInsensitive(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("x"), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + output := captureStdout(t, func() { + cmd := newRootCmd() + cmd.SetArgs([]string{"--sort", "SIZE", "--no-icons", dir}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v, want nil", err) + } + }) + if !strings.Contains(output, "a.txt") { + t.Fatalf("Execute() output = %q, want sorted output", output) + } +} + func TestRootCmd_DepthFlag(t *testing.T) { dir := t.TempDir() if err := os.Mkdir(filepath.Join(dir, "subpkg"), 0o755); err != nil { @@ -394,3 +450,18 @@ func captureStdout(t *testing.T, run func()) string { return string(output) } + +func assertOutputOrder(t *testing.T, output string, orderedNames []string) { + t.Helper() + previousIndex := -1 + for _, name := range orderedNames { + index := strings.Index(output, name) + if index == -1 { + t.Fatalf("output = %q, want to contain %q", output, name) + } + if index <= previousIndex { + t.Fatalf("output = %q, want %q after previous entries %v", output, name, orderedNames) + } + previousIndex = index + } +} diff --git a/docs/development.md b/docs/development.md index 044ddab..65560fc 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,5 +1,18 @@ # Development guide +## Table of contents + +- [Requirements](#requirements) +- [Setup](#setup) +- [Run locally](#run-locally) +- [Build](#build) +- [Test](#test) +- [Vet and lint](#vet-and-lint) +- [CI](#ci) +- [Project structure](#project-structure) +- [Adding a new flag](#adding-a-new-flag) +- [Testing documentation examples](#testing-documentation-examples) + This guide covers local development commands for `treels`. ## Requirements diff --git a/docs/gitignore.md b/docs/gitignore.md index c0491a0..e7ae578 100644 --- a/docs/gitignore.md +++ b/docs/gitignore.md @@ -1,5 +1,13 @@ # Gitignore support +## Table of contents + +- [Current behavior](#current-behavior) +- [Supported rule features](#supported-rule-features) +- [Examples](#examples) +- [Interaction with other flags](#interaction-with-other-flags) +- [Limitations](#limitations) + Use `--gitignore` to hide entries matched by `.gitignore` rules from the target directory. ```bash diff --git a/docs/icons.md b/docs/icons.md index 3e87339..32e71a8 100644 --- a/docs/icons.md +++ b/docs/icons.md @@ -1,5 +1,12 @@ # Icons and fonts +## Table of contents + +- [Requirements](#requirements) +- [Disable icons](#disable-icons) +- [Colors](#colors) +- [File type support](#file-type-support) + `treels` uses Nerd Font icons by default to make file types easier to scan. ## Requirements diff --git a/docs/json-output.md b/docs/json-output.md index 41b319f..f3f3b6f 100644 --- a/docs/json-output.md +++ b/docs/json-output.md @@ -1,5 +1,13 @@ # JSON output +## Table of contents + +- [Flat JSON example](#flat-json-example) +- [Tree JSON example](#tree-json-example) +- [Schema](#schema) +- [Flag behavior](#flag-behavior) +- [Stability notes](#stability-notes) + Use `--json` when `treels` output needs to be consumed by scripts or other tools. ```bash @@ -116,6 +124,9 @@ Most filtering flags affect JSON output: | `--all` | Includes hidden entries. | | `--dirs-only` | Omits file entries. | | `--gitignore` | Omits entries matched by the target directory's `.gitignore`. | +| `--sort name|size|modified|type` | Sorts JSON entries using the selected field. | +| `--reverse` | Reverses JSON entry order for the selected sort. | +| `--dirs-first` | Groups directory entries before file entries. | Text formatting flags do not affect JSON output: diff --git a/docs/usage.md b/docs/usage.md index 6d1cc6a..f9664dd 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,5 +1,13 @@ # Usage guide +## Table of contents + +- [Output modes](#output-modes) +- [Common workflows](#common-workflows) +- [Flags](#flags) +- [Flag interactions](#flag-interactions) +- [Exit codes](#exit-codes) + `treels` lists one directory at a time. If no path is provided, it lists the current working directory. ```bash @@ -121,6 +129,18 @@ treels --tree --dirs-only treels --long --readable ``` +### Sort by largest files first + +```bash +treels --sort size --reverse --long --readable +``` + +### Show directories before files + +```bash +treels --dirs-first +``` + ### Disable icons for plain terminals or logs ```bash @@ -144,6 +164,9 @@ treels --json | `--gitignore` | Respect `.gitignore` rules from the target directory. | | `--json` | Output machine-readable JSON. | | `-l`, `--long` | Show detailed file metadata. | +| `--sort name|size|modified|type` | Sort entries by name, size, modification time, or file type. Defaults to `name`. | +| `--reverse` | Reverse the selected sort order. | +| `--dirs-first` | Group directories before files. | | `--no-icons` | Disable file and folder icons. | | `--no-summary` | Hide the final text summary. | | `-r`, `--readable` | Show human-readable file and directory sizes. | @@ -158,6 +181,9 @@ treels --json | `--tree --dirs-only` | Recursively shows directories while omitting files. | | `--long --readable` | Shows human-readable sizes in the long metadata column. | | `--tree --long` | Shows tree branches plus metadata for each entry. | +| `--sort size --reverse` | Shows largest entries first. | +| `--sort modified --reverse` | Shows newest entries first. | +| `--dirs-first --reverse` | Keeps directories grouped first, then reverses the selected sort within each group. | | `--json --tree` | Emits recursive JSON with `children` arrays for directories. | | `--json --long` | JSON output is unchanged; `--long` only affects text output. | | `--gitignore --all` | Hidden files are included only if they are not ignored by `.gitignore`. | diff --git a/module/types.go b/module/types.go index 87c3562..669e96c 100644 --- a/module/types.go +++ b/module/types.go @@ -13,6 +13,9 @@ type Flags struct { ShowLongFormat bool HideSummary bool RespectGitIgnore bool + SortBy string + ReverseSort bool + DirsFirst bool TreeDepth int LimitTreeDepth bool } diff --git a/service/json.go b/service/json.go index 0c136aa..55b9a94 100644 --- a/service/json.go +++ b/service/json.go @@ -73,7 +73,7 @@ func collectJSONFlatEntries(options directoryOptions) (entries []jsonEntry, summ } }() - sortSlice(files) + sortSlice(files, options.Flags) for _, file := range files { if !shouldShowFile(file, options) { continue @@ -103,7 +103,7 @@ func collectJSONTreeEntries(options directoryOptions, depth int) (entries []json } }() - sortSlice(files) + sortSlice(files, options.Flags) for _, file := range files { if !shouldShowFile(file, options) { continue diff --git a/service/service.go b/service/service.go index 0a27306..825713b 100644 --- a/service/service.go +++ b/service/service.go @@ -89,8 +89,8 @@ func listDirectory(options directoryOptions, output io.Writer) (fileCount, dirCo } }() - // sort files by name - sortSlice(files) + // sort files by requested order + sortSlice(files, options.Flags) // Collect formatted file entries var entries []string @@ -151,8 +151,8 @@ func treeDirectory(options directoryOptions, output io.Writer, indent string, is } }() - // Sort files by name - sortSlice(files) + // Sort files by requested order + sortSlice(files, options.Flags) // Print files and directories fc, dc, err := printFilesAndDirectoriesTreeFormat(files, options, output, indent, isLastFolder, depth) diff --git a/service/service_test.go b/service/service_test.go index c7cb3a5..246c026 100644 --- a/service/service_test.go +++ b/service/service_test.go @@ -124,6 +124,61 @@ func TestDispatcher_ListDirectory(t *testing.T) { } } +func TestDispatcher_ListDirectorySorting(t *testing.T) { + dir := t.TempDir() + oldTime := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + midTime := time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC) + newTime := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC) + + mustWriteFile(t, filepath.Join(dir, "small.txt"), "x") + mustWriteFile(t, filepath.Join(dir, "medium.md"), "12345") + mustWriteFile(t, filepath.Join(dir, "large.go"), "1234567890") + mustMkdir(t, filepath.Join(dir, "folder")) + mustChtimes(t, filepath.Join(dir, "small.txt"), oldTime) + mustChtimes(t, filepath.Join(dir, "medium.md"), midTime) + mustChtimes(t, filepath.Join(dir, "large.go"), newTime) + mustChtimes(t, filepath.Join(dir, "folder"), midTime) + + tests := []struct { + name string + flags module.Flags + order []string + }{ + { + name: "sort by size", + flags: module.Flags{HideIcon: true, ShowLongFormat: true, SortBy: "size"}, + order: []string{"small.txt", "medium.md", "large.go", "folder"}, + }, + { + name: "sort by modified reverse", + flags: module.Flags{HideIcon: true, ShowLongFormat: true, SortBy: "modified", ReverseSort: true}, + order: []string{"large.go", "medium.md", "folder", "small.txt"}, + }, + { + name: "sort by type", + flags: module.Flags{HideIcon: true, ShowLongFormat: true, SortBy: "type"}, + order: []string{"folder", "large.go", "medium.md", "small.txt"}, + }, + { + name: "dirs first with reverse name", + flags: module.Flags{HideIcon: true, ShowLongFormat: true, SortBy: "name", ReverseSort: true, DirsFirst: true}, + order: []string{"folder", "small.txt", "medium.md", "large.go"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var output bytes.Buffer + err := dispatcher(module.Options{Directory: dir, Flags: tt.flags}, &output) + if err != nil { + t.Fatalf("dispatcher() error = %v, want nil", err) + } + + assertOutputOrder(t, stripANSI(output.String()), tt.order) + }) + } +} + func TestDispatcher_LongListDirectory(t *testing.T) { dir := t.TempDir() mustWriteFile(t, filepath.Join(dir, "alpha.go"), "package main") @@ -1351,6 +1406,40 @@ func TestDispatcher_JSONFlatDirectory(t *testing.T) { } } +func TestDispatcher_JSONFlatDirectorySorting(t *testing.T) { + dir := t.TempDir() + mustWriteFile(t, filepath.Join(dir, "small.txt"), "x") + mustWriteFile(t, filepath.Join(dir, "large.go"), "1234567890") + mustWriteFile(t, filepath.Join(dir, "medium.md"), "12345") + + var output bytes.Buffer + err := dispatcher(module.Options{ + Directory: dir, + Flags: module.Flags{ + ShowJSON: true, + SortBy: "size", + ReverseSort: true, + }, + }, &output) + if err != nil { + t.Fatalf("dispatcher() error = %v, want nil", err) + } + + var got jsonOutput + if err := json.Unmarshal(output.Bytes(), &got); err != nil { + t.Fatalf("json.Unmarshal() error = %v, output = %q", err, output.String()) + } + if len(got.Entries) != 3 { + t.Fatalf("json entries length = %d, want 3", len(got.Entries)) + } + wantOrder := []string{"large.go", "medium.md", "small.txt"} + for i, want := range wantOrder { + if got.Entries[i].Name != want { + t.Fatalf("json entry %d = %q, want %q; entries = %+v", i, got.Entries[i].Name, want, got.Entries) + } + } +} + func TestDispatcher_JSONIgnoresLongFormat(t *testing.T) { dir := t.TempDir() mustWriteFile(t, filepath.Join(dir, "main.go"), "package main") @@ -1417,9 +1506,9 @@ func TestDispatcher_JSONTreeDirectoryWithDepth(t *testing.T) { if len(got.Entries) != 2 { t.Fatalf("json entries length = %d, want 2", len(got.Entries)) } - cmdEntry := got.Entries[1] - if cmdEntry.Name != "cmd" { - t.Fatalf("second json entry = %+v, want cmd directory", cmdEntry) + cmdEntry, ok := findJSONEntry(got.Entries, "cmd") + if !ok { + t.Fatalf("json entries = %+v, want cmd directory", got.Entries) } if len(cmdEntry.Children) != 0 { t.Fatalf("cmd children = %+v, want none at depth 1", cmdEntry.Children) @@ -1525,6 +1614,99 @@ func TestJSONEntryAndSummaryHelpers(t *testing.T) { } } +func TestSortSlice(t *testing.T) { + oldTime := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + newTime := time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC) + + tests := []struct { + name string + files []os.FileInfo + flags module.Flags + want []string + }{ + { + name: "defaults to case-insensitive name sort", + files: []os.FileInfo{ + fakeFileInfo{name: "beta.go"}, + fakeFileInfo{name: "Alpha.go"}, + }, + want: []string{"Alpha.go", "beta.go"}, + }, + { + name: "sorts by size with name tie-breaker", + files: []os.FileInfo{ + fakeFileInfo{name: "z.txt", size: 10}, + fakeFileInfo{name: "a.txt", size: 10}, + fakeFileInfo{name: "m.txt", size: 1}, + }, + flags: module.Flags{SortBy: "size"}, + want: []string{"m.txt", "a.txt", "z.txt"}, + }, + { + name: "sorts by modified", + files: []os.FileInfo{ + fakeFileInfo{name: "new.go", modTime: newTime}, + fakeFileInfo{name: "old.go", modTime: oldTime}, + }, + flags: module.Flags{SortBy: "modified"}, + want: []string{"old.go", "new.go"}, + }, + { + name: "sorts by type and reverse", + files: []os.FileInfo{ + fakeFileInfo{name: "a.go"}, + fakeFileInfo{name: "b.txt"}, + fakeFileInfo{name: "c.md"}, + }, + flags: module.Flags{SortBy: "type", ReverseSort: true}, + want: []string{"b.txt", "c.md", "a.go"}, + }, + { + name: "dirs first is not reversed", + files: []os.FileInfo{ + fakeFileInfo{name: "a.go"}, + fakeFileInfo{name: "dir", isDir: true}, + fakeFileInfo{name: "z.go"}, + }, + flags: module.Flags{ReverseSort: true, DirsFirst: true}, + want: []string{"dir", "z.go", "a.go"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sortSlice(tt.files, tt.flags) + if got := fileInfoNames(tt.files); strings.Join(got, ",") != strings.Join(tt.want, ",") { + t.Fatalf("sortSlice() order = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCompareHelpers(t *testing.T) { + if got := sortField(""); got != "name" { + t.Fatalf("sortField(empty) = %q, want name", got) + } + if got := compareString("a", "b"); got != -1 { + t.Fatalf("compareString(a, b) = %d, want -1", got) + } + if got := compareString("b", "a"); got != 1 { + t.Fatalf("compareString(b, a) = %d, want 1", got) + } + if got := compareString("a", "a"); got != 0 { + t.Fatalf("compareString(a, a) = %d, want 0", got) + } + if got := compareInt64(1, 2); got != -1 { + t.Fatalf("compareInt64(1, 2) = %d, want -1", got) + } + if got := compareInt64(2, 1); got != 1 { + t.Fatalf("compareInt64(2, 1) = %d, want 1", got) + } + if got := compareInt64(1, 1); got != 0 { + t.Fatalf("compareInt64(1, 1) = %d, want 0", got) + } +} + func TestIsHidden(t *testing.T) { tests := []struct { name string @@ -1559,6 +1741,45 @@ func mustMkdir(t *testing.T, path string) { } } +func mustChtimes(t *testing.T, path string, timestamp time.Time) { + t.Helper() + if err := os.Chtimes(path, timestamp, timestamp); err != nil { + t.Fatalf("Chtimes(%q) error = %v", path, err) + } +} + +func assertOutputOrder(t *testing.T, output string, orderedNames []string) { + t.Helper() + previousIndex := -1 + for _, name := range orderedNames { + index := strings.Index(output, name) + if index == -1 { + t.Fatalf("output = %q, want to contain %q", output, name) + } + if index <= previousIndex { + t.Fatalf("output = %q, want %q after previous entries %v", output, name, orderedNames) + } + previousIndex = index + } +} + +func fileInfoNames(files []os.FileInfo) []string { + names := make([]string, 0, len(files)) + for _, file := range files { + names = append(names, file.Name()) + } + return names +} + +func findJSONEntry(entries []jsonEntry, name string) (jsonEntry, bool) { + for _, entry := range entries { + if entry.Name == name { + return entry, true + } + } + return jsonEntry{}, false +} + type fakeFileInfo struct { name string size int64 diff --git a/service/util.go b/service/util.go index 38649e0..0628bfb 100644 --- a/service/util.go +++ b/service/util.go @@ -771,15 +771,81 @@ func humanReadableSize(size int64) string { return fmt.Sprintf("%.1f %s", value, units[unitIndex]) } -// sortSlice func - sorts a slice of os.FileInfo objects alphabetically by file name. +// sortSlice func - sorts a slice of os.FileInfo objects based on CLI flags. // It modifies the original slice in place. -func sortSlice(files []os.FileInfo) { - // Sort files by name +func sortSlice(files []os.FileInfo, flags module.Flags) { sort.Slice(files, func(i, j int) bool { - return files[i].Name() < files[j].Name() + return lessFileInfo(files[i], files[j], flags) }) } +func lessFileInfo(left, right os.FileInfo, flags module.Flags) bool { + if flags.DirsFirst && left.IsDir() != right.IsDir() { + return left.IsDir() + } + + result := compareFileInfo(left, right, sortField(flags.SortBy)) + if result == 0 { + result = compareName(left, right) + } + if flags.ReverseSort { + return result > 0 + } + + return result < 0 +} + +func sortField(value string) string { + if value == "" { + return "name" + } + return value +} + +func compareFileInfo(left, right os.FileInfo, sortBy string) int { + switch sortBy { + case "size": + return compareInt64(left.Size(), right.Size()) + case "modified": + return compareInt64(left.ModTime().UnixNano(), right.ModTime().UnixNano()) + case "type": + return compareString(fileType(left), fileType(right)) + default: + return compareName(left, right) + } +} + +func compareName(left, right os.FileInfo) int { + return compareString(strings.ToLower(left.Name()), strings.ToLower(right.Name())) +} + +func fileType(file os.FileInfo) string { + if file.IsDir() { + return "" + } + return strings.ToLower(filepath.Ext(file.Name())) +} + +func compareString(left, right string) int { + if left < right { + return -1 + } + if left > right { + return 1 + } + return 0 +} + +func compareInt64(left, right int64) int { + if left < right { + return -1 + } + if left > right { + return 1 + } + return 0 +} + // getTerminalWidth returns the width of the terminal func getTerminalWidth() int { width, _, err := terminalSize(int(os.Stdout.Fd()))