From ab9e9a43e1a00e83ea7eced09745ff7626105363 Mon Sep 17 00:00:00 2001 From: Daniel Milde Date: Thu, 18 Feb 2021 10:28:56 +0000 Subject: [PATCH 1/1] Import gdu_4.6.3.orig.tar.gz [dgit import orig gdu_4.6.3.orig.tar.gz] --- .gitignore | 4 + .travis.yml | 12 + LICENSE.md | 8 + Makefile | 80 ++++ README.md | 151 ++++++++ analyze/dir.go | 131 +++++++ analyze/dir_other.go | 9 + analyze/dir_test.go | 115 ++++++ analyze/dir_unix.go | 22 ++ analyze/file.go | 158 ++++++++ analyze/file_test.go | 266 +++++++++++++ analyze/sort_test.go | 128 ++++++ build/build.go | 10 + cmd/run.go | 92 +++++ cmd/run_test.go | 164 ++++++++ codecov.yml | 10 + common/app.go | 17 + common/ui.go | 14 + device/dev.go | 33 ++ device/dev_linux.go | 85 ++++ device/dev_other.go | 21 + device/dev_test.go | 64 +++ gdu.1 | 61 +++ gdu.1.md | 70 ++++ go.mod | 14 + go.sum | 321 +++++++++++++++ internal/testanalyze/analyze.go | 39 ++ internal/testapp/app.go | 71 ++++ internal/testdev/dev.go | 18 + internal/testdir/test_dir.go | 19 + main.go | 70 ++++ snapcraft.yaml | 27 ++ stdout/stdout.go | 253 ++++++++++++ stdout/stdout_test.go | 118 ++++++ tui/sort_test.go | 161 ++++++++ tui/tui.go | 667 ++++++++++++++++++++++++++++++++ tui/tui_test.go | 577 +++++++++++++++++++++++++++ 37 files changed, 4080 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 analyze/dir.go create mode 100644 analyze/dir_other.go create mode 100644 analyze/dir_test.go create mode 100644 analyze/dir_unix.go create mode 100644 analyze/file.go create mode 100644 analyze/file_test.go create mode 100644 analyze/sort_test.go create mode 100644 build/build.go create mode 100644 cmd/run.go create mode 100644 cmd/run_test.go create mode 100644 codecov.yml create mode 100644 common/app.go create mode 100644 common/ui.go create mode 100644 device/dev.go create mode 100644 device/dev_linux.go create mode 100644 device/dev_other.go create mode 100644 device/dev_test.go create mode 100644 gdu.1 create mode 100644 gdu.1.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/testanalyze/analyze.go create mode 100644 internal/testapp/app.go create mode 100644 internal/testdev/dev.go create mode 100644 internal/testdir/test_dir.go create mode 100644 main.go create mode 100644 snapcraft.yaml create mode 100644 stdout/stdout.go create mode 100644 stdout/stdout_test.go create mode 100644 tui/sort_test.go create mode 100644 tui/tui.go create mode 100644 tui/tui_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a99efb --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.vscode +/coverage.txt +/dist +/test_dir \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..12e9487 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: go + +go: + - tip + +env: + - GO111MODULE=on +script: + - make coverage + +after_success: + - bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..3d3b99f --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,8 @@ +Copyright 2020-2021 Daniel Milde + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a9ac2e8 --- /dev/null +++ b/Makefile @@ -0,0 +1,80 @@ +NAME := gdu +PACKAGE := github.com/dundee/$(NAME) +VERSION := $(shell git describe --tags) +GOFLAGS ?= -buildmode=pie -trimpath -mod=readonly -modcacherw +LDFLAGS := -s -w -extldflags '-static' \ + -X '$(PACKAGE)/build.Version=$(VERSION)' \ + -X '$(PACKAGE)/build.User=$(shell id -u -n)' \ + -X '$(PACKAGE)/build.Time=$(shell LC_ALL=en_US.UTF-8 date)' + +all: build-all gdu.1 + +run: + go run . + +build: + @echo "Version: " $(VERSION) + mkdir -p dist + GOFLAGS="$(GOFLAGS)" go build -a -ldflags="$(LDFLAGS)" -o dist/$(NAME) . + +build-all: + @echo "Version: " $(VERSION) + -mkdir dist + -CGO_ENABLED=0 gox \ + -os="darwin windows" \ + -arch="amd64" \ + -output="dist/{{.Dir}}_{{.OS}}_{{.Arch}}" \ + -ldflags="$(LDFLAGS)" + + -CGO_ENABLED=0 gox \ + -os="linux freebsd netbsd openbsd" \ + -output="dist/{{.Dir}}_{{.OS}}_{{.Arch}}" \ + -ldflags="$(LDFLAGS)" + + cd dist; GOFLAGS="$(GOFLAGS)" CGO_ENABLED=0 go build -a -ldflags="$(LDFLAGS)" -o gdu_linux_amd64 .. + + cd dist; CGO_ENABLED=0 GOOS=linux GOARM=5 GOARCH=arm go build -ldflags="$(LDFLAGS)" -o gdu_linux_armv5l .. + cd dist; CGO_ENABLED=0 GOOS=linux GOARM=6 GOARCH=arm go build -ldflags="$(LDFLAGS)" -o gdu_linux_armv6l .. + cd dist; CGO_ENABLED=0 GOOS=linux GOARM=7 GOARCH=arm go build -ldflags="$(LDFLAGS)" -o gdu_linux_armv7l .. + cd dist; CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o gdu_linux_arm64 .. + + cd dist; for file in gdu_linux_* gdu_darwin_* gdu_netbsd_* gdu_openbsd_* gdu_freebsd_*; do tar czf $$file.tgz $$file; done + cd dist; for file in gdu_windows_*; do zip $$file.zip $$file; done + +gdu.1: gdu.1.md + pandoc gdu.1.md -s -t man > gdu.1 + +show-man: + pandoc gdu.1.md -s -t man | man -l - + +test: + go test -v ./... + +coverage: + go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... + +coverage-html: coverage + go tool cover -html=coverage.txt + +gobench: + go test -bench=. ./... + +benchmark: + hyperfine --export-markdown=bench-cold.md \ + --prepare 'sync; echo 3 | sudo tee /proc/sys/vm/drop_caches' \ + 'gdu -npc ~' 'dua ~' 'duc index ~' 'ncdu -0 -o /dev/null ~' \ + 'diskus -b ~' 'du -hs ~' 'dust -d0 ~' + hyperfine --export-markdown=bench-warm.md \ + --warmup 5 \ + 'gdu -npc ~' 'dua ~' 'duc index ~' 'ncdu -0 -o /dev/null ~' \ + 'diskus -b ~' 'du -hs ~' 'dust -d0 ~' + +clean: + -rm coverage.txt + -rm -r test_dir + -rm -r dist + +clean-uncompressed-dist: + find dist -type f -not -name '*.tgz' -not -name '*.zip' -delete + +.PHONY: run build test coverage coverage-html clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..d4621cd --- /dev/null +++ b/README.md @@ -0,0 +1,151 @@ +# go DiskUsage() + +[![Build Status](https://travis-ci.com/dundee/gdu.svg?branch=master)](https://travis-ci.com/dundee/gdu) +[![codecov](https://codecov.io/gh/dundee/gdu/branch/master/graph/badge.svg)](https://codecov.io/gh/dundee/gdu) +[![Go Report Card](https://goreportcard.com/badge/github.com/dundee/gdu)](https://goreportcard.com/report/github.com/dundee/gdu) + +Pretty fast disk usage analyzer written in Go. + +Gdu is intended primarily for SSD disks where it can fully utilize parallel processing. +However HDDs work as well, but the performance gain is not so huge. + +[![asciicast](https://asciinema.org/a/382738.svg)](https://asciinema.org/a/382738) + + + Packaging status + + +## Installation + +Head for the [releases](https://github.com/dundee/gdu/releases) and download binary for your system. + +Using curl: + + curl -L https://github.com/dundee/gdu/releases/latest/download/gdu_linux_amd64.tgz | tar xz + chmod +x gdu_linux_amd64 + mv gdu_linux_amd64 /usr/bin/gdu + +[Arch Linux](https://aur.archlinux.org/packages/gdu/): + + yay -S gdu + +Debian: + + dpkg -i gdu_*_amd64.deb + +[NixOS](https://search.nixos.org/packages?channel=unstable&show=gdu&query=gdu): + + nix-env -iA nixos.gdu + +[Homebrew](https://formulae.brew.sh/formula/gdu): + + brew install gdu + +[Snap](https://snapcraft.io/gdu-disk-usage-analyzer): + + snap install gdu-disk-usage-analyzer + snap connect gdu-disk-usage-analyzer:mount-observe :mount-observe + snap connect gdu-disk-usage-analyzer:system-backup :system-backup + snap alias gdu-disk-usage-analyzer.gdu gdu + +[Binenv](https://github.com/devops-works/binenv) + + binenv install gdu + +[Go](https://pkg.go.dev/github.com/dundee/gdu): + + go get -u github.com/dundee/gdu + + +## Usage + +``` + gdu [flags] [directory_to_scan] + +Flags: + -h, --help help for gdu + -i, --ignore-dirs strings Absolute paths to ignore (separated by comma) (default [/proc,/dev,/sys,/run]) + -l, --log-file string Path to a logfile (default "/dev/null") + -c, --no-color Do not use colorized output + -x, --no-cross Do not cross filesystem boundaries + -p, --no-progress Do not show progress in non-interactive mode + -n, --non-interactive Do not run in interactive mode + -a, --show-apparent-size Show apparent size + -d, --show-disks Show all mounted disks + -v, --version Print version +``` + +## Examples + + gdu # analyze current dir + gdu -a # show apparent size instead of disk usage + gdu # analyze given dir + gdu -d # show all mounted disks + gdu -l ./gdu.log # write errors to log file + gdu -i /sys,/proc / # ignore some paths + gdu -c / # use only white/gray/black colors + + gdu -n / # only print stats, do not start interactive mode + gdu -np / # do not show progress, useful when using its output in a script + gdu / > file # write stats to file, do not start interactive mode + +Gdu has two modes: interactive (default) and non-interactive. + +Non-interactive mode is started automtically when TTY is not detected (using [go-isatty](https://github.com/mattn/go-isatty)), for example if the output is being piped to a file, or it can be started explicitly by using a flag. + +Hard links are counted only once. + +## File flags + +Files and directories may be prefixed by a one-character +flag with following meaning: + +* `!` An error occurred while reading this directory. + +* `.` An error occurred while reading a subdirectory, size may be not correct. + +* `@` File is symlink or socket. + +* `H` Same file was already counted (hard link). + +* `e` Directory is empty. + +## Running tests + + make test + + +## Benchmarks + +Benchmarks performed on 50G directory (100k directories, 400k files) on 500 GB SSD using [hyperfine](https://github.com/sharkdp/hyperfine). +See `benchmark` target in [Makefile](Makefile) for more info. + +### Cold cache + +Filesystem cache was cleared using `sync; echo 3 | sudo tee /proc/sys/vm/drop_caches`. + +| Command | Mean [s] | Min [s] | Max [s] | Relative | +|:---|---:|---:|---:|---:| +| `gdu -npc ~` | 3.634 ± 0.016 | 3.613 | 3.657 | 1.13 ± 0.02 | +| `dua ~` | 4.029 ± 0.051 | 3.993 | 4.160 | 1.25 ± 0.03 | +| `duc index ~` | 27.731 ± 0.436 | 27.128 | 28.283 | 8.61 ± 0.22 | +| `ncdu -0 -o /dev/null ~` | 27.238 ± 0.198 | 26.908 | 27.604 | 8.45 ± 0.18 | +| `diskus -b ~` | 3.222 ± 0.063 | 3.149 | 3.351 | 1.00 | +| `du -hs ~` | 25.966 ± 0.910 | 24.056 | 26.997 | 8.06 ± 0.32 | +| `dust -d0 ~` | 18.661 ± 1.629 | 14.461 | 19.672 | 5.79 ± 0.52 | + +### Warm cache + +| Command | Mean [ms] | Min [ms] | Max [ms] | Relative | +|:---|---:|---:|---:|---:| +| `gdu -npc ~` | 619.6 ± 28.0 | 574.0 | 657.7 | 2.57 ± 0.25 | +| `dua ~` | 344.0 ± 10.8 | 327.3 | 358.8 | 1.43 ± 0.13 | +| `duc index ~` | 1092.4 ± 11.3 | 1073.9 | 1103.6 | 4.54 ± 0.39 | +| `ncdu -0 -o /dev/null ~` | 1512.5 ± 18.4 | 1488.4 | 1546.5 | 6.28 ± 0.54 | +| `diskus -b ~` | 240.8 ± 20.6 | 207.8 | 289.1 | 1.00 | +| `du -hs ~` | 876.9 ± 19.5 | 843.9 | 910.3 | 3.64 ± 0.32 | +| `dust -d0 ~` | 7614.2 ± 45.6 | 7557.0 | 7687.5 | 31.61 ± 2.72 | + + + +Gdu is inspired by [ncdu](https://dev.yorhel.nl/ncdu), [godu](https://github.com/viktomas/godu), [dua](https://github.com/Byron/dua-cli) and [df](https://www.gnu.org/software/coreutils/manual/html_node/df-invocation.html). diff --git a/analyze/dir.go b/analyze/dir.go new file mode 100644 index 0000000..865bb0c --- /dev/null +++ b/analyze/dir.go @@ -0,0 +1,131 @@ +package analyze + +import ( + "io/ioutil" + "log" + "os" + "path/filepath" + "runtime" + "sync" +) + +// CurrentProgress struct +type CurrentProgress struct { + Mutex *sync.Mutex + CurrentItemName string + ItemCount int + TotalSize int64 + Done bool +} + +// ShouldDirBeIgnored whether path should be ignored +type ShouldDirBeIgnored func(path string) bool + +// Analyzer is type for dir analyzing function +type Analyzer func(path string, progress *CurrentProgress, ignore ShouldDirBeIgnored) *File + +// ProcessDir analyzes given path +func ProcessDir(path string, progress *CurrentProgress, ignore ShouldDirBeIgnored) *File { + concurrencyLimitChannel := make(chan bool, 2*runtime.NumCPU()) + var wait sync.WaitGroup + dir := processDir(path, progress, concurrencyLimitChannel, &wait, ignore) + dir.BasePath = filepath.Dir(path) + wait.Wait() + + links := make(AlreadyCountedHardlinks, 10) + dir.UpdateStats(links) + + progress.Mutex.Lock() + progress.Done = true + progress.Mutex.Unlock() + + return dir +} + +func processDir(path string, progress *CurrentProgress, concurrencyLimitChannel chan bool, wait *sync.WaitGroup, ignoreDir ShouldDirBeIgnored) *File { + var file *File + var err error + + files, err := ioutil.ReadDir(path) + if err != nil { + log.Print(err.Error()) + } + + var flag rune + switch { + case err != nil: + flag = '!' + case len(files) == 0: + flag = 'e' + default: + flag = ' ' + } + + dir := File{ + Name: filepath.Base(path), + Flag: flag, + IsDir: true, + ItemCount: 1, + Files: make([]*File, 0, len(files)), + } + + var mutex sync.Mutex + var totalSize int64 + + for _, f := range files { + entryPath := filepath.Join(path, f.Name()) + + if f.IsDir() { + if ignoreDir(entryPath) { + continue + } + + wait.Add(1) + go func() { + concurrencyLimitChannel <- true + subdir := processDir(entryPath, progress, concurrencyLimitChannel, wait, ignoreDir) + subdir.Parent = &dir + + mutex.Lock() + dir.Files = append(dir.Files, subdir) + mutex.Unlock() + + <-concurrencyLimitChannel + wait.Done() + }() + } else { + switch { + case f.Mode()&os.ModeSymlink != 0: + fallthrough + case f.Mode()&os.ModeSocket != 0: + flag = '@' + default: + flag = ' ' + } + + file = &File{ + Name: f.Name(), + Flag: flag, + Size: f.Size(), + ItemCount: 1, + Parent: &dir, + } + + setPlatformSpecificAttrs(file, f) + + totalSize += f.Size() + + mutex.Lock() + dir.Files = append(dir.Files, file) + mutex.Unlock() + } + } + + progress.Mutex.Lock() + progress.CurrentItemName = path + progress.ItemCount += len(files) + progress.TotalSize += totalSize + progress.Mutex.Unlock() + + return &dir +} diff --git a/analyze/dir_other.go b/analyze/dir_other.go new file mode 100644 index 0000000..b8e2c9b --- /dev/null +++ b/analyze/dir_other.go @@ -0,0 +1,9 @@ +// +build windows plan9 + +package analyze + +import ( + "os" +) + +func setPlatformSpecificAttrs(file *File, f os.FileInfo) {} diff --git a/analyze/dir_test.go b/analyze/dir_test.go new file mode 100644 index 0000000..83d0b80 --- /dev/null +++ b/analyze/dir_test.go @@ -0,0 +1,115 @@ +package analyze + +import ( + "os" + "sort" + "sync" + "testing" + + "github.com/dundee/gdu/internal/testdir" + "github.com/stretchr/testify/assert" +) + +func TestProcessDir(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + dir := ProcessDir("test_dir", &CurrentProgress{Mutex: &sync.Mutex{}}, func(_ string) bool { return false }) + + // test dir info + assert.Equal(t, "test_dir", dir.Name) + assert.Equal(t, int64(7+4096*3), dir.Size) + assert.Equal(t, 5, dir.ItemCount) + assert.True(t, dir.IsDir) + + // test dir tree + assert.Equal(t, "nested", dir.Files[0].Name) + assert.Equal(t, "subnested", dir.Files[0].Files[1].Name) + + // test file + assert.Equal(t, "file2", dir.Files[0].Files[0].Name) + assert.Equal(t, int64(2), dir.Files[0].Files[0].Size) + + assert.Equal(t, "file", dir.Files[0].Files[1].Files[0].Name) + assert.Equal(t, int64(5), dir.Files[0].Files[1].Files[0].Size) + + // test parent link + assert.Equal(t, "test_dir", dir.Files[0].Files[1].Files[0].Parent.Parent.Parent.Name) +} + +func TestIgnoreDir(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + dir := ProcessDir("test_dir", &CurrentProgress{Mutex: &sync.Mutex{}}, func(_ string) bool { return true }) + + assert.Equal(t, "test_dir", dir.Name) + assert.Equal(t, 1, dir.ItemCount) +} + +func TestFlags(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + os.Mkdir("test_dir/empty", 0644) + + os.Symlink("test_dir/nested/file2", "test_dir/nested/file3") + + dir := ProcessDir("test_dir", &CurrentProgress{Mutex: &sync.Mutex{}}, func(_ string) bool { return false }) + sort.Sort(dir.Files) + + assert.Equal(t, int64(28+4096*4), dir.Size) + assert.Equal(t, 7, dir.ItemCount) + + // test file3 + assert.Equal(t, "nested", dir.Files[0].Name) + assert.Equal(t, "file3", dir.Files[0].Files[1].Name) + assert.Equal(t, int64(21), dir.Files[0].Files[1].Size) + assert.Equal(t, int64(0), dir.Files[0].Files[1].Usage) + assert.Equal(t, '@', dir.Files[0].Files[1].Flag) + + assert.Equal(t, 'e', dir.Files[1].Flag) +} + +func TestHardlink(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + os.Link("test_dir/nested/file2", "test_dir/nested/file3") + + dir := ProcessDir("test_dir", &CurrentProgress{Mutex: &sync.Mutex{}}, func(_ string) bool { return false }) + + assert.Equal(t, int64(7+4096*3), dir.Size) // file2 and file3 are counted just once for size + assert.Equal(t, 6, dir.ItemCount) // but twice for item count + + // test file3 + assert.Equal(t, "file3", dir.Files[0].Files[1].Name) + assert.Equal(t, int64(2), dir.Files[0].Files[1].Size) + assert.Equal(t, 'H', dir.Files[0].Files[1].Flag) +} + +func TestErr(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + os.Chmod("test_dir/nested", 0) + defer os.Chmod("test_dir/nested", 0755) + + dir := ProcessDir("test_dir", &CurrentProgress{Mutex: &sync.Mutex{}}, func(_ string) bool { return false }) + + assert.Equal(t, "test_dir", dir.Name) + assert.Equal(t, 2, dir.ItemCount) + assert.Equal(t, '.', dir.Flag) + + assert.Equal(t, "nested", dir.Files[0].Name) + assert.Equal(t, '!', dir.Files[0].Flag) +} + +func BenchmarkProcessDir(b *testing.B) { + fin := testdir.CreateTestDir() + defer fin() + + b.ResetTimer() + + ProcessDir("test_dir", &CurrentProgress{Mutex: &sync.Mutex{}}, func(_ string) bool { return false }) +} diff --git a/analyze/dir_unix.go b/analyze/dir_unix.go new file mode 100644 index 0000000..5779687 --- /dev/null +++ b/analyze/dir_unix.go @@ -0,0 +1,22 @@ +// +build !windows +// +build !plan9 + +package analyze + +import ( + "os" + "syscall" +) + +const devBSize = 512 + +func setPlatformSpecificAttrs(file *File, f os.FileInfo) { + switch stat := f.Sys().(type) { + case *syscall.Stat_t: + file.Usage = stat.Blocks * devBSize + + if stat.Nlink > 1 { + file.MutliLinkInode = stat.Ino + } + } +} diff --git a/analyze/file.go b/analyze/file.go new file mode 100644 index 0000000..3c9b80b --- /dev/null +++ b/analyze/file.go @@ -0,0 +1,158 @@ +package analyze + +import ( + "os" + "path/filepath" +) + +// File struct +type File struct { + Name string + BasePath string + Flag rune + Size int64 + Usage int64 + ItemCount int + IsDir bool + Files Files + Parent *File + MutliLinkInode uint64 // Inode number of file with multiple links (hard link) +} + +// AlreadyCountedHardlinks holds all files with hardlinks that have already been counted +type AlreadyCountedHardlinks map[uint64]bool + +// Path retruns absolute path of the file +func (f *File) Path() string { + if f.BasePath != "" { + return filepath.Join(f.BasePath, f.Name) + } + return filepath.Join(f.Parent.Path(), f.Name) +} + +// RemoveFile removes file from dir +func (f *File) RemoveFile(file *File) error { + error := os.RemoveAll(file.Path()) + if error != nil { + return error + } + + f.Files = f.Files.Remove(file) + + cur := f + for { + cur.ItemCount -= file.ItemCount + cur.Size -= file.Size + cur.Usage -= file.Usage + + if cur.Parent == nil { + break + } + cur = cur.Parent + } + return nil +} + +// UpdateStats recursively updates size and item count +func (f *File) UpdateStats(links AlreadyCountedHardlinks) { + if !f.IsDir { + return + } + totalSize := int64(4096) + totalUsage := int64(4096) + var itemCount int + for _, entry := range f.Files { + if entry.IsDir { + entry.UpdateStats(links) + } + + switch entry.Flag { + case '!', '.': + if f.Flag != '!' { + f.Flag = '.' + } + } + + itemCount += entry.ItemCount + + if entry.MutliLinkInode > 0 { + if !links[entry.MutliLinkInode] { + links[entry.MutliLinkInode] = true + } else { + entry.Flag = 'H' + continue + } + } + totalSize += entry.Size + totalUsage += entry.Usage + } + f.ItemCount = itemCount + 1 + f.Size = totalSize + f.Usage = totalUsage +} + +// Files - slice of pointers to File +type Files []*File + +// IndexOf searches File in Files and returns its index +func (f Files) IndexOf(file *File) (int, bool) { + for i, item := range f { + if item == file { + return i, true + } + } + return 0, false +} + +// FindByName searches name in Files and returns its index +func (f Files) FindByName(name string) (int, bool) { + for i, item := range f { + if item.Name == name { + return i, true + } + } + return 0, false +} + +// Remove removes File from Files +func (f Files) Remove(file *File) Files { + index, ok := f.IndexOf(file) + if !ok { + return f + } + return append(f[:index], f[index+1:]...) +} + +// RemoveByName removes File from Files +func (f Files) RemoveByName(name string) Files { + index, ok := f.FindByName(name) + if !ok { + return f + } + return append(f[:index], f[index+1:]...) +} + +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].Usage > f[j].Usage } + +// ByApparentSize sorts files by apparent size +type ByApparentSize Files + +func (f ByApparentSize) Len() int { return len(f) } +func (f ByApparentSize) Swap(i, j int) { f[i], f[j] = f[j], f[i] } +func (f ByApparentSize) 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/file_test.go b/analyze/file_test.go new file mode 100644 index 0000000..c7ee131 --- /dev/null +++ b/analyze/file_test.go @@ -0,0 +1,266 @@ +package analyze + +import ( + "os" + "testing" + + "github.com/dundee/gdu/internal/testdir" + "github.com/stretchr/testify/assert" +) + +func TestFind(t *testing.T) { + dir := File{ + Name: "xxx", + Size: 5, + ItemCount: 2, + } + + file := &File{ + Name: "yyy", + Size: 2, + ItemCount: 1, + Parent: &dir, + } + file2 := &File{ + Name: "zzz", + Size: 3, + ItemCount: 1, + Parent: &dir, + } + dir.Files = []*File{file, file2} + + i, _ := dir.Files.IndexOf(file) + assert.Equal(t, 0, i) + i, _ = dir.Files.IndexOf(file2) + assert.Equal(t, 1, i) +} + +func TestRemove(t *testing.T) { + dir := File{ + Name: "xxx", + Size: 5, + ItemCount: 2, + } + + file := &File{ + Name: "yyy", + Size: 2, + ItemCount: 1, + Parent: &dir, + } + file2 := &File{ + Name: "zzz", + Size: 3, + ItemCount: 1, + Parent: &dir, + } + dir.Files = []*File{file, file2} + + dir.Files = dir.Files.Remove(file) + + assert.Equal(t, 1, len(dir.Files)) + assert.Equal(t, file2, dir.Files[0]) +} + +func TestRemoveByName(t *testing.T) { + dir := File{ + Name: "xxx", + Size: 5, + Usage: 8, + ItemCount: 2, + } + + file := &File{ + Name: "yyy", + Size: 2, + Usage: 4, + ItemCount: 1, + Parent: &dir, + } + file2 := &File{ + Name: "zzz", + Size: 3, + Usage: 4, + ItemCount: 1, + Parent: &dir, + } + dir.Files = []*File{file, file2} + + dir.Files = dir.Files.RemoveByName("yyy") + + assert.Equal(t, 1, len(dir.Files)) + assert.Equal(t, file2, dir.Files[0]) +} + +func TestRemoveNotInDir(t *testing.T) { + dir := File{ + Name: "xxx", + Size: 5, + Usage: 8, + ItemCount: 2, + } + + file := &File{ + Name: "yyy", + Size: 2, + Usage: 4, + ItemCount: 1, + Parent: &dir, + } + file2 := &File{ + Name: "zzz", + Size: 3, + Usage: 4, + ItemCount: 1, + } + dir.Files = []*File{file} + + _, ok := dir.Files.IndexOf(file2) + assert.Equal(t, false, ok) + + dir.Files = dir.Files.Remove(file2) + + assert.Equal(t, 1, len(dir.Files)) +} + +func TestRemoveByNameNotInDir(t *testing.T) { + dir := File{ + Name: "xxx", + Size: 5, + Usage: 8, + ItemCount: 2, + } + + file := &File{ + Name: "yyy", + Size: 2, + Usage: 4, + ItemCount: 1, + Parent: &dir, + } + file2 := &File{ + Name: "zzz", + Size: 3, + Usage: 4, + ItemCount: 1, + } + dir.Files = []*File{file} + + _, ok := dir.Files.IndexOf(file2) + assert.Equal(t, false, ok) + + dir.Files = dir.Files.RemoveByName("zzz") + + assert.Equal(t, 1, len(dir.Files)) +} + +func TestRemoveFile(t *testing.T) { + dir := &File{ + Name: "xxx", + BasePath: ".", + Size: 5, + Usage: 12, + ItemCount: 3, + } + + subdir := &File{ + Name: "yyy", + Size: 4, + Usage: 8, + ItemCount: 2, + Parent: dir, + } + file := &File{ + Name: "zzz", + Size: 3, + Usage: 4, + ItemCount: 1, + Parent: subdir, + } + dir.Files = []*File{subdir} + subdir.Files = []*File{file} + + subdir.RemoveFile(file) + + assert.Equal(t, 0, len(subdir.Files)) + assert.Equal(t, 1, subdir.ItemCount) + assert.Equal(t, int64(1), subdir.Size) + assert.Equal(t, int64(4), subdir.Usage) + assert.Equal(t, 1, len(dir.Files)) + assert.Equal(t, 2, dir.ItemCount) + assert.Equal(t, int64(2), dir.Size) +} + +func TestRemoveFileWithErr(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + os.Chmod("test_dir/nested", 0) + defer os.Chmod("test_dir/nested", 0755) + + dir := &File{ + Name: "test_dir", + BasePath: ".", + } + + subdir := &File{ + Name: "nested", + Parent: dir, + } + + err := dir.RemoveFile(subdir) + assert.Contains(t, err.Error(), "permission denied") +} + +func TestUpdateStats(t *testing.T) { + dir := File{ + Name: "xxx", + Size: 1, + ItemCount: 1, + IsDir: true, + } + + file := &File{ + Name: "yyy", + Size: 2, + ItemCount: 1, + Parent: &dir, + } + file2 := &File{ + Name: "zzz", + Size: 3, + ItemCount: 1, + Parent: &dir, + } + dir.Files = []*File{file, file2} + + dir.UpdateStats(nil) + + assert.Equal(t, int64(4096+5), dir.Size) +} + +func TestUpdateStatsFile(t *testing.T) { + notDir := File{ + Name: "xxx", + Size: 1, + ItemCount: 1, + } + + file := &File{ + Name: "yyy", + Size: 2, + ItemCount: 1, + Parent: ¬Dir, + } + file2 := &File{ + Name: "zzz", + Size: 3, + ItemCount: 1, + Parent: ¬Dir, + } + notDir.Files = []*File{file, file2} + + notDir.UpdateStats(nil) + + assert.Equal(t, int64(1), notDir.Size) +} diff --git a/analyze/sort_test.go b/analyze/sort_test.go new file mode 100644 index 0000000..a009a16 --- /dev/null +++ b/analyze/sort_test.go @@ -0,0 +1,128 @@ +package analyze + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSortByUsage(t *testing.T) { + files := Files{ + &File{ + Usage: 1, + }, + &File{ + Usage: 2, + }, + &File{ + Usage: 3, + }, + } + + sort.Sort(files) + + assert.Equal(t, int64(3), files[0].Usage) + assert.Equal(t, int64(2), files[1].Usage) + assert.Equal(t, int64(1), files[2].Usage) +} + +func TestSortByUsageAsc(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 TestSortBySize(t *testing.T) { + files := Files{ + &File{ + Size: 1, + }, + &File{ + Size: 2, + }, + &File{ + Size: 3, + }, + } + + sort.Sort(ByApparentSize(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(ByApparentSize(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/build/build.go b/build/build.go new file mode 100644 index 0000000..34fb28f --- /dev/null +++ b/build/build.go @@ -0,0 +1,10 @@ +package build + +// Version stores the current version of the app +var Version = "development" + +// Time of the build +var Time string + +// User who built it +var User string diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..938fa75 --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,92 @@ +package cmd + +import ( + "fmt" + "io" + "log" + "os" + + "github.com/dundee/gdu/analyze" + "github.com/dundee/gdu/build" + "github.com/dundee/gdu/common" + "github.com/dundee/gdu/device" + "github.com/dundee/gdu/stdout" + "github.com/dundee/gdu/tui" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +// RunFlags define flags accepted by Run +type RunFlags struct { + LogFile string + IgnoreDirs []string + ShowDisks bool + ShowApparentSize bool + ShowVersion bool + NoColor bool + NonInteractive bool + NoProgress bool + NoCross bool +} + +// Run starts gdu main logic +func Run(flags *RunFlags, args []string, istty bool, writer io.Writer, app common.Application, getter device.DevicesInfoGetter) error { + if flags.ShowVersion { + fmt.Fprintln(writer, "Version:\t", build.Version) + fmt.Fprintln(writer, "Built time:\t", build.Time) + fmt.Fprintln(writer, "Built user:\t", build.User) + return nil + } + + var path string + var ui common.UI + + f, err := os.OpenFile(flags.LogFile, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return fmt.Errorf("Error opening log file: %w", err) + } + defer f.Close() + log.SetOutput(f) + + if len(args) == 1 { + path = args[0] + } else { + path = "." + } + + if flags.NonInteractive || !istty { + ui = stdout.CreateStdoutUI( + writer, + !flags.NoColor && istty, + !flags.NoProgress && istty, + flags.ShowApparentSize, + ) + } else { + ui = tui.CreateUI(app, !flags.NoColor, flags.ShowApparentSize) + + if !flags.NoColor { + tview.Styles.TitleColor = tcell.NewRGBColor(27, 161, 227) + } + } + + if flags.NoCross { + mounts, err := getter.GetMounts() + if err != nil { + return fmt.Errorf("Error loading mount points: %w", err) + } + paths := device.GetNestedMountpointsPaths(path, mounts) + flags.IgnoreDirs = append(flags.IgnoreDirs, paths...) + } + + ui.SetIgnoreDirPaths(flags.IgnoreDirs) + + if flags.ShowDisks { + if err := ui.ListDevices(getter); err != nil { + return fmt.Errorf("Error loading mount points: %w", err) + } + } else { + ui.AnalyzePath(path, analyze.ProcessDir, nil) + } + + return ui.StartUILoop() +} diff --git a/cmd/run_test.go b/cmd/run_test.go new file mode 100644 index 0000000..11d29f3 --- /dev/null +++ b/cmd/run_test.go @@ -0,0 +1,164 @@ +package cmd + +import ( + "bytes" + "testing" + + "github.com/dundee/gdu/device" + "github.com/dundee/gdu/internal/testapp" + "github.com/dundee/gdu/internal/testdev" + "github.com/dundee/gdu/internal/testdir" + "github.com/stretchr/testify/assert" +) + +func TestVersion(t *testing.T) { + + buff := bytes.NewBuffer(make([]byte, 10)) + + Run( + &RunFlags{ShowVersion: true}, + []string{}, + false, + buff, + testapp.CreateMockedApp(false), + testdev.DevicesInfoGetterMock{}, + ) + + assert.Contains(t, buff.String(), "Version:\t development") +} + +func TestLogError(t *testing.T) { + buff := bytes.NewBuffer(make([]byte, 10)) + err := Run( + &RunFlags{LogFile: "/xyzxyz"}, + []string{}, + false, + buff, + testapp.CreateMockedApp(false), + testdev.DevicesInfoGetterMock{}, + ) + + assert.Contains(t, err.Error(), "permission denied") +} + +func TestAnalyzePath(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + buff := bytes.NewBuffer(make([]byte, 10)) + + Run( + &RunFlags{LogFile: "/dev/null"}, + []string{"test_dir"}, + false, + buff, + testapp.CreateMockedApp(false), + testdev.DevicesInfoGetterMock{}, + ) + + assert.Contains(t, buff.String(), "nested") +} + +func TestAnalyzePathWithGui(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + buff := bytes.NewBuffer(make([]byte, 10)) + + Run( + &RunFlags{LogFile: "/dev/null"}, + []string{"test_dir"}, + true, + buff, + testapp.CreateMockedApp(false), testdev.DevicesInfoGetterMock{}, + ) +} + +func TestNoCross(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + buff := bytes.NewBuffer(make([]byte, 10)) + + Run( + &RunFlags{LogFile: "/dev/null", NoCross: true}, + []string{"test_dir"}, + false, + buff, + testapp.CreateMockedApp(false), + testdev.DevicesInfoGetterMock{}, + ) + + assert.Contains(t, buff.String(), "nested") +} + +func TestNoCrossWithErr(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + buff := bytes.NewBuffer(make([]byte, 10)) + + getter := device.LinuxDevicesInfoGetter{MountsPath: "/xxxyyy"} + err := Run( + &RunFlags{LogFile: "/dev/null", NoCross: true}, + []string{"test_dir"}, + false, + buff, + testapp.CreateMockedApp(false), + getter, + ) + + assert.Equal(t, "Error loading mount points: open /xxxyyy: no such file or directory", err.Error()) +} + +func TestListDevices(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + buff := bytes.NewBuffer(make([]byte, 10)) + + Run( + &RunFlags{LogFile: "/dev/null", ShowDisks: true}, + nil, + false, + buff, + testapp.CreateMockedApp(false), testdev.DevicesInfoGetterMock{}, + ) + + assert.Contains(t, buff.String(), "Device") +} + +func TestListDevicesWithErr(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + buff := bytes.NewBuffer(make([]byte, 10)) + getter := device.LinuxDevicesInfoGetter{MountsPath: "/xxxyyy"} + + err := Run( + &RunFlags{LogFile: "/dev/null", ShowDisks: true}, + nil, + false, + buff, + testapp.CreateMockedApp(false), + getter, + ) + + assert.Equal(t, "Error loading mount points: open /xxxyyy: no such file or directory", err.Error()) +} + +func TestListDevicesWithGui(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + buff := bytes.NewBuffer(make([]byte, 10)) + + Run( + &RunFlags{LogFile: "/dev/null", ShowDisks: true}, + nil, + true, + buff, + testapp.CreateMockedApp(false), + testdev.DevicesInfoGetterMock{}, + ) +} diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..c9ef2c2 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,10 @@ +coverage: + status: + project: + default: + target: auto + threshold: 2% + informational: true + patch: + default: + informational: true \ No newline at end of file diff --git a/common/app.go b/common/app.go new file mode 100644 index 0000000..1f95a7f --- /dev/null +++ b/common/app.go @@ -0,0 +1,17 @@ +package common + +import ( + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +// Application is interface for the UI app +type Application interface { + Run() error + Stop() + SetRoot(root tview.Primitive, fullscreen bool) *tview.Application + SetFocus(p tview.Primitive) *tview.Application + SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) *tview.Application + QueueUpdateDraw(f func()) *tview.Application + SetBeforeDrawFunc(func(screen tcell.Screen) bool) *tview.Application +} diff --git a/common/ui.go b/common/ui.go new file mode 100644 index 0000000..b49bc2f --- /dev/null +++ b/common/ui.go @@ -0,0 +1,14 @@ +package common + +import ( + "github.com/dundee/gdu/analyze" + "github.com/dundee/gdu/device" +) + +// UI is common interface for both terminal UI and text output +type UI interface { + ListDevices(getter device.DevicesInfoGetter) error + AnalyzePath(path string, analyzer analyze.Analyzer, parentDir *analyze.File) + SetIgnoreDirPaths(paths []string) + StartUILoop() error +} diff --git a/device/dev.go b/device/dev.go new file mode 100644 index 0000000..0e315cb --- /dev/null +++ b/device/dev.go @@ -0,0 +1,33 @@ +package device + +import "strings" + +// Device struct +type Device struct { + Name string + MountPoint string + Fstype string + Size int64 + Free int64 +} + +// DevicesInfoGetter is type for GetDevicesInfo function +type DevicesInfoGetter interface { + GetMounts() (Devices, error) + GetDevicesInfo() (Devices, error) +} + +// Devices if slice of Device items +type Devices []*Device + +// GetNestedMountpointsPaths returns paths of nested mount points +func GetNestedMountpointsPaths(path string, mounts Devices) []string { + paths := make([]string, 0, len(mounts)) + + for _, mount := range mounts { + if strings.HasPrefix(mount.MountPoint, path) && mount.MountPoint != path { + paths = append(paths, mount.MountPoint) + } + } + return paths +} diff --git a/device/dev_linux.go b/device/dev_linux.go new file mode 100644 index 0000000..19e81cd --- /dev/null +++ b/device/dev_linux.go @@ -0,0 +1,85 @@ +// +build linux + +package device + +import ( + "bufio" + "io" + "os" + "strings" + "syscall" +) + +// LinuxDevicesInfoGetter retruns info for Linux devices +type LinuxDevicesInfoGetter struct { + MountsPath string +} + +// Getter is current instance of DevicesInfoGetter +var Getter DevicesInfoGetter = LinuxDevicesInfoGetter{MountsPath: "/proc/mounts"} + +// GetMounts returns all mounted filesystems from /proc/mounts +func (t LinuxDevicesInfoGetter) GetMounts() (Devices, error) { + file, err := os.Open(t.MountsPath) + if err != nil { + return nil, err + } + defer file.Close() + + return readMountsFile(file) +} + +// GetDevicesInfo returns result of GetMounts with usage info about mounted devices (by calling Statfs syscall) +func (t LinuxDevicesInfoGetter) GetDevicesInfo() (Devices, error) { + mounts, err := t.GetMounts() + if err != nil { + return nil, err + } + + return processMounts(mounts) +} + +func readMountsFile(file io.Reader) (Devices, error) { + mounts := Devices{} + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Fields(line) + + device := &Device{ + Name: parts[0], + MountPoint: parts[1], + Fstype: parts[2], + } + mounts = append(mounts, device) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return mounts, nil +} + +func processMounts(mounts Devices) (Devices, error) { + devices := Devices{} + + for _, mount := range mounts { + if strings.Contains(mount.MountPoint, "/snap/") { + continue + } + + if strings.HasPrefix(mount.Name, "/dev") || mount.Fstype == "zfs" { + info := &syscall.Statfs_t{} + syscall.Statfs(mount.MountPoint, info) + + mount.Size = int64(info.Bsize) * int64(info.Blocks) + mount.Free = int64(info.Bsize) * int64(info.Bavail) + + devices = append(devices, mount) + } + } + + return devices, nil +} diff --git a/device/dev_other.go b/device/dev_other.go new file mode 100644 index 0000000..ad0d1f8 --- /dev/null +++ b/device/dev_other.go @@ -0,0 +1,21 @@ +// +build windows darwin openbsd freebsd netbsd plan9 + +package device + +import "errors" + +// OtherDevicesInfoGetter retruns info for other devices +type OtherDevicesInfoGetter struct{} + +// Getter is current instance of DevicesInfoGetter +var Getter DevicesInfoGetter = OtherDevicesInfoGetter{} + +// GetDevicesInfo returns result of GetMounts with usage info about mounted devices +func (t OtherDevicesInfoGetter) GetDevicesInfo() (Devices, error) { + return nil, errors.New("Only Linux platform is supported for listing devices") +} + +// GetMounts returns all mounted filesystems +func (t OtherDevicesInfoGetter) GetMounts() (Devices, error) { + return nil, errors.New("Only Linux platform is supported for listing mount points") +} diff --git a/device/dev_test.go b/device/dev_test.go new file mode 100644 index 0000000..0a35fbb --- /dev/null +++ b/device/dev_test.go @@ -0,0 +1,64 @@ +package device + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetDevicesInfo(t *testing.T) { + getter := LinuxDevicesInfoGetter{MountsPath: "/proc/mounts"} + devices, _ := getter.GetDevicesInfo() + assert.IsType(t, Devices{}, devices) +} + +func TestGetDevicesInfoFail(t *testing.T) { + getter := LinuxDevicesInfoGetter{MountsPath: "/xxxyyy"} + _, err := getter.GetDevicesInfo() + assert.Equal(t, "open /xxxyyy: no such file or directory", err.Error()) +} + +func TestSnapMountsNotShown(t *testing.T) { + mounts, _ := readMountsFile(strings.NewReader(`/dev/loop4 /var/lib/snapd/snap/core18/1944 squashfs ro,nodev,relatime 0 0 +/dev/loop3 /var/lib/snapd/snap/core20/904 squashfs ro,nodev,relatime 0 0 +/dev/nvme0n1p1 /boot vfat rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,utf8,errors=remount-ro 0 0`)) + + devices, err := processMounts(mounts) + assert.Len(t, devices, 1) + assert.Nil(t, err) +} + +func TestZfsMountsShown(t *testing.T) { + mounts, _ := readMountsFile(strings.NewReader(`rootpool/opt /opt zfs rw,nodev,relatime,xattr,posixacl 0 0 +rootpool/usr/local /usr/local zfs rw,nodev,relatime,xattr,posixacl 0 0 +rootpool/home/root /root zfs rw,nodev,relatime,xattr,posixacl 0 0 +rootpool/usr/games /usr/games zfs rw,nodev,relatime,xattr,posixacl 0 0 +rootpool/home /home zfs rw,nodev,relatime,xattr,posixacl 0 0 +/dev/loop4 /var/lib/snapd/snap/core18/1944 squashfs ro,nodev,relatime 0 0 +/dev/loop3 /var/lib/snapd/snap/core20/904 squashfs ro,nodev,relatime 0 0 +/dev/nvme0n1p1 /boot vfat rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,utf8,errors=remount-ro 0 0`)) + + devices, err := processMounts(mounts) + assert.Len(t, devices, 6) + assert.Nil(t, err) +} + +func TestNested(t *testing.T) { + item := &Device{ + MountPoint: "/xxx", + } + nested := &Device{ + MountPoint: "/xxx/yyy", + } + notNested := &Device{ + MountPoint: "/zzz/yyy", + } + + mounts := Devices{item, nested, notNested} + + mountsNested := GetNestedMountpointsPaths("/xxx", mounts) + + assert.Len(t, mountsNested, 1) + assert.Equal(t, "/xxx/yyy", mountsNested[0]) +} diff --git a/gdu.1 b/gdu.1 new file mode 100644 index 0000000..fcc8155 --- /dev/null +++ b/gdu.1 @@ -0,0 +1,61 @@ +.\" Automatically generated by Pandoc 2.11.3 +.\" +.TH "gdu" "1" "Jan 2021" "" "" +.hy +.SH NAME +.PP +gdu - Pretty fast disk usage analyzer written in Go +.SH SYNOPSIS +.PP +\f[B]gdu [flags] [directory_to_scan]\f[R] +.SH DESCRIPTION +.PP +Pretty fast disk usage analyzer written in Go. +.PP +Gdu is intended primarily for SSD disks where it can fully utilize +parallel processing. +However HDDs work as well, but the performance gain is not so huge. +.SH OPTIONS +.PP +\f[B]-h\f[R], \f[B]--help\f[R][=false] help for gdu +.PP +\f[B]-i\f[R], \f[B]--ignore-dirs\f[R]=[/proc,/dev,/sys,/run] Absolute +paths to ignore (separated by comma) +.PP +\f[B]-l\f[R], \f[B]--log-file\f[R]=\[dq]/dev/null\[dq] Path to a logfile +.PP +\f[B]-c\f[R], \f[B]--no-color\f[R][=false] Do not use colorized output +.PP +\f[B]-x\f[R], \f[B]--no-cross\f[R][=false] Do not cross filesystem +boundaries +.PP +\f[B]-p\f[R], \f[B]--no-progress\f[R][=false] Do not show progress in +non-interactive mode +.PP +\f[B]-n\f[R], \f[B]--non-interactive\f[R][=false] Do not run in +interactive mode +.PP +\f[B]-d\f[R], \f[B]--show-disks\f[R][=false] Show all mounted disks +.PP +\f[B]-a\f[R], \f[B]--show-apparent-size\f[R][=false] Show apparent size +.PP +\f[B]-v\f[R], \f[B]--version\f[R][=false] Print version +.SH FILE FLAGS +.PP +Files and directories may be prefixed by a one-character flag with +following meaning: +.TP +\f[B]!\f[R] +An error occurred while reading this directory. +.TP +\f[B].\f[R] +An error occurred while reading a subdirectory, size may be not correct. +.TP +\f[B]\[at]\f[R] +File is symlink or socket. +.TP +\f[B]H\f[R] +Same file was already counted (hard link). +.TP +\f[B]e\f[R] +Directory is empty. diff --git a/gdu.1.md b/gdu.1.md new file mode 100644 index 0000000..d69d0cc --- /dev/null +++ b/gdu.1.md @@ -0,0 +1,70 @@ +--- +date: Jan 2021 +section: 1 +title: gdu +--- + +# NAME + +gdu - Pretty fast disk usage analyzer written in Go + +# SYNOPSIS + +**gdu \[flags\] \[directory_to_scan\]** + +# DESCRIPTION + +Pretty fast disk usage analyzer written in Go. + +Gdu is intended primarily for SSD disks where it can fully utilize +parallel processing. However HDDs work as well, but the performance gain +is not so huge. + +# OPTIONS + +**-h**, **\--help**\[=false\] help for gdu + +**-i**, **\--ignore-dirs**=\[/proc,/dev,/sys,/run\] Absolute paths to +ignore (separated by comma) + +**-l**, **\--log-file**=\"/dev/null\" Path to a logfile + +**-c**, **\--no-color**\[=false\] Do not use colorized output + +**-x**, **\--no-cross**\[=false\] Do not cross filesystem boundaries + +**-p**, **\--no-progress**\[=false\] Do not show progress in +non-interactive mode + +**-n**, **\--non-interactive**\[=false\] Do not run in interactive mode + +**-d**, **\--show-disks**\[=false\] Show all mounted disks + +**-a**, **\--show-apparent-size**\[=false\] Show apparent size + +**-v**, **\--version**\[=false\] Print version + +# FILE FLAGS + +Files and directories may be prefixed by a one-character +flag with following meaning: + +**!** + +: An error occurred while reading this directory. + +**.** + +: An error occurred while reading a subdirectory, size may be not correct. + +**\@** + +: File is symlink or socket. + +**H** + +: Same file was already counted (hard link). + +**e** + +: Directory is empty. \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f0f4bdb --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/dundee/gdu + +go 1.15 + +require ( + github.com/fatih/color v1.7.0 + github.com/gdamore/tcell/v2 v2.1.0 + github.com/mattn/go-isatty v0.0.12 + github.com/rivo/tview v0.0.0-20201204190810-5406288b8e4e + github.com/spf13/cobra v1.1.1 + github.com/stretchr/testify v1.6.1 + golang.org/x/sys v0.0.0-20201223074533-0d417f636930 // indirect + golang.org/x/text v0.3.4 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6d22ef0 --- /dev/null +++ b/go.sum @@ -0,0 +1,321 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA= +github.com/gdamore/tcell/v2 v2.1.0 h1:UnSmozHgBkQi2PGsFr+rpdXuAPRRucMegpQp3Z3kDro= +github.com/gdamore/tcell/v2 v2.1.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rivo/tview v0.0.0-20201204190810-5406288b8e4e h1:eP1XZiExUPO/FjS2q/PBo3CYbEtVvoMi8b7IpCBDWSo= +github.com/rivo/tview v0.0.0-20201204190810-5406288b8e4e/go.mod h1:0ha5CGekam8ZV1kxkBxSlh7gfQ7YolUj2P/VruwH0QY= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201223074533-0d417f636930 h1:vRgIt+nup/B/BwIS0g2oC0haq0iqbV3ZA+u6+0TlNCo= +golang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/internal/testanalyze/analyze.go b/internal/testanalyze/analyze.go new file mode 100644 index 0000000..1c9ac6b --- /dev/null +++ b/internal/testanalyze/analyze.go @@ -0,0 +1,39 @@ +package testanalyze + +import "github.com/dundee/gdu/analyze" + +// MockedProcessDir returns dir with files with diferent size exponents +func MockedProcessDir(path string, progress *analyze.CurrentProgress, ignore analyze.ShouldDirBeIgnored) *analyze.File { + dir := &analyze.File{ + Name: "test_dir", + BasePath: ".", + Usage: 1e12 + 1, + } + file := &analyze.File{ + Name: "a", + Usage: 1e12 + 1, + Parent: dir, + } + file2 := &analyze.File{ + Name: "b", + Usage: 1e9 + 1, + Parent: dir, + } + file3 := &analyze.File{ + Name: "c", + Usage: 1e6 + 1, + Parent: dir, + } + file4 := &analyze.File{ + Name: "d", + Usage: 1e3 + 1, + Parent: dir, + } + dir.Files = analyze.Files{file, file2, file3, file4} + + progress.Mutex.Lock() + progress.Done = true + progress.Mutex.Unlock() + + return dir +} diff --git a/internal/testapp/app.go b/internal/testapp/app.go new file mode 100644 index 0000000..eebc0a9 --- /dev/null +++ b/internal/testapp/app.go @@ -0,0 +1,71 @@ +package testapp + +import ( + "errors" + + "github.com/dundee/gdu/common" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +// CreateTestAppWithSimScreen returns app with simulation screen for tests +func CreateTestAppWithSimScreen(width, height int) (*tview.Application, tcell.SimulationScreen) { + screen := tcell.NewSimulationScreen("UTF-8") + screen.Init() + screen.SetSize(width, height) + + app := tview.NewApplication() + app.SetScreen(screen) + + return app, screen +} + +// CreateMockedApp returns app with simulation screen for tests +func CreateMockedApp(failRun bool) common.Application { + app := &MockedApp{ + FailRun: failRun, + } + return app +} + +// MockedApp is tview.Application with mocked methods +type MockedApp struct { + FailRun bool +} + +// Run does nothing +func (app *MockedApp) Run() error { + if app.FailRun { + return errors.New("Fail") + } + + return nil +} + +// Stop does nothing +func (app *MockedApp) Stop() {} + +// SetRoot does nothing +func (app *MockedApp) SetRoot(root tview.Primitive, fullscreen bool) *tview.Application { + return nil +} + +// SetFocus does nothing +func (app *MockedApp) SetFocus(p tview.Primitive) *tview.Application { + return nil +} + +// SetInputCapture does nothing +func (app *MockedApp) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) *tview.Application { + return nil +} + +// QueueUpdateDraw does nothing +func (app *MockedApp) QueueUpdateDraw(f func()) *tview.Application { + return nil +} + +// SetBeforeDrawFunc does nothing +func (app *MockedApp) SetBeforeDrawFunc(func(screen tcell.Screen) bool) *tview.Application { + return nil +} diff --git a/internal/testdev/dev.go b/internal/testdev/dev.go new file mode 100644 index 0000000..93e7547 --- /dev/null +++ b/internal/testdev/dev.go @@ -0,0 +1,18 @@ +package testdev + +import "github.com/dundee/gdu/device" + +// DevicesInfoGetterMock is mock of DevicesInfoGetter +type DevicesInfoGetterMock struct { + Devices device.Devices +} + +// GetDevicesInfo returns mocked devices +func (t DevicesInfoGetterMock) GetDevicesInfo() (device.Devices, error) { + return t.Devices, nil +} + +// GetMounts returns all mounted filesystems from /proc/mounts +func (t DevicesInfoGetterMock) GetMounts() (device.Devices, error) { + return t.Devices, nil +} diff --git a/internal/testdir/test_dir.go b/internal/testdir/test_dir.go new file mode 100644 index 0000000..2809a97 --- /dev/null +++ b/internal/testdir/test_dir.go @@ -0,0 +1,19 @@ +package testdir + +import ( + "io/ioutil" + "os" +) + +// CreateTestDir creates test dir structure +func CreateTestDir() func() { + os.MkdirAll("test_dir/nested/subnested", os.ModePerm) + ioutil.WriteFile("test_dir/nested/subnested/file", []byte("hello"), 0644) + ioutil.WriteFile("test_dir/nested/file2", []byte("go"), 0644) + return func() { + err := os.RemoveAll("test_dir") + if err != nil { + panic(err) + } + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..00e53e3 --- /dev/null +++ b/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + "os" + "runtime" + + "github.com/dundee/gdu/cmd" + "github.com/dundee/gdu/device" + "github.com/gdamore/tcell/v2" + "github.com/mattn/go-isatty" + "github.com/rivo/tview" + "github.com/spf13/cobra" +) + +func main() { + rf := &cmd.RunFlags{} + + var rootCmd = &cobra.Command{ + Use: "gdu [directory_to_scan]", + Short: "Pretty fast disk usage analyzer written in Go", + Long: `Pretty fast disk usage analyzer written in Go. + +Gdu is intended primarily for SSD disks where it can fully utilize parallel processing. +However HDDs work as well, but the performance gain is not so huge. +`, + Args: cobra.MaximumNArgs(1), + SilenceUsage: true, + RunE: func(command *cobra.Command, args []string) error { + istty := isatty.IsTerminal(os.Stdout.Fd()) + + // we are not able to analyze disk usage on Windows and Plan9 + if runtime.GOOS == "windows" || runtime.GOOS == "plan9" { + rf.ShowApparentSize = true + } + + var app *tview.Application = nil + + if !rf.ShowVersion && !rf.NonInteractive && istty { + screen, err := tcell.NewScreen() + if err != nil { + return fmt.Errorf("Error creating screen: %w", err) + } + screen.Init() + defer screen.Clear() + defer screen.Fini() + + app = tview.NewApplication() + app.SetScreen(screen) + } + + return cmd.Run(rf, args, istty, os.Stdout, app, device.Getter) + }, + } + + flags := rootCmd.Flags() + flags.StringVarP(&rf.LogFile, "log-file", "l", "/dev/null", "Path to a logfile") + flags.StringSliceVarP(&rf.IgnoreDirs, "ignore-dirs", "i", []string{"/proc", "/dev", "/sys", "/run"}, "Absolute paths to ignore (separated by comma)") + flags.BoolVarP(&rf.ShowDisks, "show-disks", "d", false, "Show all mounted disks") + flags.BoolVarP(&rf.ShowApparentSize, "show-apparent-size", "a", false, "Show apparent size") + flags.BoolVarP(&rf.ShowVersion, "version", "v", false, "Print version") + flags.BoolVarP(&rf.NoColor, "no-color", "c", false, "Do not use colorized output") + flags.BoolVarP(&rf.NonInteractive, "non-interactive", "n", false, "Do not run in interactive mode") + flags.BoolVarP(&rf.NoProgress, "no-progress", "p", false, "Do not show progress in non-interactive mode") + flags.BoolVarP(&rf.NoCross, "no-cross", "x", false, "Do not cross filesystem boundaries") + + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/snapcraft.yaml b/snapcraft.yaml new file mode 100644 index 0000000..53dec70 --- /dev/null +++ b/snapcraft.yaml @@ -0,0 +1,27 @@ +name: gdu-disk-usage-analyzer +version: git +summary: Pretty fast disk usage analyzer written in Go. +description: | + Gdu is intended primarily for SSD disks where it can fully utilize parallel processing. + However HDDs work as well, but the performance gain is not so huge. +confinement: strict +base: core20 +parts: + gdu: + plugin: go + source: . + override-build: | + GO111MODULE=on CGO_ENABLED=0 go build \ + -ldflags \ + "-s -w \ + -X 'github.com/dundee/gdu/build.Version=$(git describe)' \ + -X 'github.com/dundee/gdu/build.User=$(id -u -n)' \ + -X 'github.com/dundee/gdu/build.Time=$(LC_ALL=en_US.UTF-8 date)'" \ + -o $SNAPCRAFT_PART_INSTALL/gdu + $SNAPCRAFT_PART_INSTALL/gdu -v +apps: + gdu: + command: gdu + plugs: + - mount-observe + - system-backup diff --git a/stdout/stdout.go b/stdout/stdout.go new file mode 100644 index 0000000..04543df --- /dev/null +++ b/stdout/stdout.go @@ -0,0 +1,253 @@ +package stdout + +import ( + "fmt" + "io" + "math" + "path/filepath" + "sort" + "sync" + "time" + + "github.com/dundee/gdu/analyze" + "github.com/dundee/gdu/device" + "github.com/fatih/color" +) + +// UI struct +type UI struct { + output io.Writer + ignoreDirPaths map[string]bool + useColors bool + showProgress bool + showApparentSize bool + red *color.Color + orange *color.Color + blue *color.Color +} + +// CreateStdoutUI creates UI for stdout +func CreateStdoutUI(output io.Writer, useColors bool, showProgress bool, showApparentSize bool) *UI { + ui := &UI{ + output: output, + useColors: useColors, + showProgress: showProgress, + showApparentSize: showApparentSize, + } + + ui.red = color.New(color.FgRed).Add(color.Bold) + ui.orange = color.New(color.FgYellow).Add(color.Bold) + ui.blue = color.New(color.FgBlue).Add(color.Bold) + + if !useColors { + color.NoColor = true + } + + return ui +} + +// StartUILoop stub +func (ui *UI) StartUILoop() error { + return nil +} + +// ListDevices lists mounted devices and shows their disk usage +func (ui *UI) ListDevices(getter device.DevicesInfoGetter) error { + devices, err := getter.GetDevicesInfo() + if err != nil { + return err + } + + maxDeviceNameLenght := maxInt(maxLength( + devices, + func(device *device.Device) string { return device.Name }, + ), len("Devices")) + + var sizeLength, percentLength int + if ui.useColors { + sizeLength = 20 + percentLength = 16 + } else { + sizeLength = 9 + percentLength = 5 + } + + lineFormat := fmt.Sprintf( + "%%%ds %%%ds %%%ds %%%ds %%%ds %%s\n", + maxDeviceNameLenght, + sizeLength, + sizeLength, + sizeLength, + percentLength, + ) + + fmt.Fprintf( + ui.output, + fmt.Sprintf("%%%ds %%9s %%9s %%9s %%5s %%s\n", maxDeviceNameLenght), + "Device", + "Size", + "Used", + "Free", + "Used%", + "Mount point", + ) + + for _, device := range devices { + usedPercent := math.Round(float64(device.Size-device.Free) / float64(device.Size) * 100) + + fmt.Fprintf( + ui.output, + lineFormat, + device.Name, + ui.formatSize(device.Size), + ui.formatSize(device.Size-device.Free), + ui.formatSize(device.Free), + ui.red.Sprintf("%.f%%", usedPercent), + device.MountPoint) + } + + return nil +} + +// AnalyzePath analyzes recursively disk usage in given path +func (ui *UI) AnalyzePath(path string, analyzer analyze.Analyzer, _ *analyze.File) { + abspath, _ := filepath.Abs(path) + var dir *analyze.File + + progress := &analyze.CurrentProgress{ + Mutex: &sync.Mutex{}, + Done: false, + ItemCount: 0, + TotalSize: int64(0), + } + var wait sync.WaitGroup + + if ui.showProgress { + wait.Add(1) + go func() { + defer wait.Done() + ui.updateProgress(progress) + }() + } + + wait.Add(1) + go func() { + defer wait.Done() + dir = analyzer(abspath, progress, ui.ShouldDirBeIgnored) + }() + + wait.Wait() + + sort.Sort(dir.Files) + + var lineFormat string + if ui.useColors { + lineFormat = "%s %20s %s\n" + } else { + lineFormat = "%s %9s %s\n" + } + + var size int64 + + for _, file := range dir.Files { + if ui.showApparentSize { + size = file.Size + } else { + size = file.Usage + } + + if file.IsDir { + fmt.Fprintf(ui.output, + lineFormat, + string(file.Flag), + ui.formatSize(size), + ui.blue.Sprintf("/"+file.Name)) + } else { + fmt.Fprintf(ui.output, + lineFormat, + string(file.Flag), + ui.formatSize(size), + file.Name) + } + } +} + +// SetIgnoreDirPaths sets paths to ignore +func (ui *UI) SetIgnoreDirPaths(paths []string) { + ui.ignoreDirPaths = make(map[string]bool, len(paths)) + for _, path := range paths { + ui.ignoreDirPaths[path] = true + } +} + +// ShouldDirBeIgnored returns true if given path should be ignored +func (ui *UI) ShouldDirBeIgnored(path string) bool { + return ui.ignoreDirPaths[path] +} + +func (ui *UI) updateProgress(progress *analyze.CurrentProgress) { + emptyRow := "\r" + for j := 0; j < 100; j++ { + emptyRow += " " + } + + progressRunes := []rune(`⠇⠏⠋⠙⠹⠸⠼⠴⠦⠧`) + + i := 0 + for { + progress.Mutex.Lock() + + fmt.Fprint(ui.output, emptyRow) + + if progress.Done { + fmt.Fprint(ui.output, "\r") + return + } + + fmt.Fprintf(ui.output, "\r %s ", string(progressRunes[i])) + + fmt.Fprint(ui.output, "Scanning... Total items: "+ + ui.red.Sprint(progress.ItemCount)+ + " size: "+ + ui.formatSize(progress.TotalSize)) + progress.Mutex.Unlock() + + time.Sleep(100 * time.Millisecond) + i++ + i %= 10 + } +} + +func (ui *UI) formatSize(size int64) string { + switch { + case size > 1e12: + return ui.orange.Sprintf("%.1f", float64(size)/math.Pow(2, 40)) + " TiB" + case size > 1e9: + return ui.orange.Sprintf("%.1f", float64(size)/math.Pow(2, 30)) + " GiB" + case size > 1e6: + return ui.orange.Sprintf("%.1f", float64(size)/math.Pow(2, 20)) + " MiB" + case size > 1e3: + return ui.orange.Sprintf("%.1f", float64(size)/math.Pow(2, 10)) + " KiB" + default: + return ui.orange.Sprintf("%d", size) + " B" + } +} + +func maxLength(list []*device.Device, keyGetter func(*device.Device) string) int { + maxLen := 0 + var s string + for _, item := range list { + s = keyGetter(item) + if len(s) > maxLen { + maxLen = len(s) + } + } + return maxLen +} + +func maxInt(x int, y int) int { + if x > y { + return x + } + return y +} diff --git a/stdout/stdout_test.go b/stdout/stdout_test.go new file mode 100644 index 0000000..9887af4 --- /dev/null +++ b/stdout/stdout_test.go @@ -0,0 +1,118 @@ +package stdout + +import ( + "bytes" + "testing" + + "github.com/dundee/gdu/analyze" + "github.com/dundee/gdu/device" + "github.com/dundee/gdu/internal/testanalyze" + "github.com/dundee/gdu/internal/testdev" + "github.com/dundee/gdu/internal/testdir" + "github.com/stretchr/testify/assert" +) + +func TestAnalyzePath(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + buff := make([]byte, 10) + output := bytes.NewBuffer(buff) + + ui := CreateStdoutUI(output, false, false, false) + ui.SetIgnoreDirPaths([]string{"/xxx"}) + ui.AnalyzePath("test_dir", analyze.ProcessDir, nil) + ui.StartUILoop() + + assert.Contains(t, output.String(), "nested") +} + +func TestAnalyzePathWithColors(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + buff := make([]byte, 10) + output := bytes.NewBuffer(buff) + + ui := CreateStdoutUI(output, true, false, true) + ui.SetIgnoreDirPaths([]string{"/xxx"}) + ui.AnalyzePath("test_dir/nested", analyze.ProcessDir, nil) + + assert.Contains(t, output.String(), "subnested") +} + +func TestItemRows(t *testing.T) { + output := bytes.NewBuffer(make([]byte, 10)) + + ui := CreateStdoutUI(output, false, true, false) + ui.AnalyzePath("test_dir", testanalyze.MockedProcessDir, nil) + + assert.Contains(t, output.String(), "TiB") + assert.Contains(t, output.String(), "GiB") + assert.Contains(t, output.String(), "MiB") + assert.Contains(t, output.String(), "KiB") +} + +func TestAnalyzePathWithProgress(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + output := bytes.NewBuffer(make([]byte, 10)) + + ui := CreateStdoutUI(output, false, true, true) + ui.SetIgnoreDirPaths([]string{"/xxx"}) + ui.AnalyzePath("test_dir", analyze.ProcessDir, nil) + + assert.Contains(t, output.String(), "nested") +} + +func TestShowDevices(t *testing.T) { + output := bytes.NewBuffer(make([]byte, 10)) + + ui := CreateStdoutUI(output, false, true, false) + ui.ListDevices(getDevicesInfoMock()) + + assert.Contains(t, output.String(), "Device") + assert.Contains(t, output.String(), "xxx") +} + +func TestShowDevicesWithColor(t *testing.T) { + output := bytes.NewBuffer(make([]byte, 10)) + + ui := CreateStdoutUI(output, true, true, true) + ui.ListDevices(getDevicesInfoMock()) + + assert.Contains(t, output.String(), "Device") + assert.Contains(t, output.String(), "xxx") +} + +func TestShowDevicesWithErr(t *testing.T) { + output := bytes.NewBuffer(make([]byte, 10)) + + getter := device.LinuxDevicesInfoGetter{MountsPath: "/xyzxyz"} + ui := CreateStdoutUI(output, false, true, false) + err := ui.ListDevices(getter) + + assert.Contains(t, err.Error(), "no such file") +} + +func TestMaxInt(t *testing.T) { + assert.Equal(t, 5, maxInt(2, 5)) + assert.Equal(t, 4, maxInt(4, 2)) +} + +func printBuffer(buff *bytes.Buffer) { + for i, x := range buff.String() { + println(i, string(x)) + } +} + +func getDevicesInfoMock() device.DevicesInfoGetter { + item := &device.Device{ + Name: "xxx", + } + + mock := testdev.DevicesInfoGetterMock{} + mock.Devices = []*device.Device{item} + return mock +} diff --git a/tui/sort_test.go b/tui/sort_test.go new file mode 100644 index 0000000..f71fb5b --- /dev/null +++ b/tui/sort_test.go @@ -0,0 +1,161 @@ +package tui + +import ( + "testing" + "time" + + "github.com/dundee/gdu/analyze" + "github.com/dundee/gdu/internal/testapp" + "github.com/dundee/gdu/internal/testdir" + "github.com/gdamore/tcell/v2" + "github.com/stretchr/testify/assert" +) + +func TestSortBySizeAsc(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + + ui := CreateUI(app, true, true) + + ui.AnalyzePath("test_dir", analyze.ProcessDir, nil) + + 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 := testdir.CreateTestDir() + defer fin() + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + + ui := CreateUI(app, false, false) + + ui.AnalyzePath("test_dir", analyze.ProcessDir, nil) + + 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 := testdir.CreateTestDir() + defer fin() + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + + ui := CreateUI(app, false, true) + + ui.AnalyzePath("test_dir", analyze.ProcessDir, nil) + + 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 := testdir.CreateTestDir() + defer fin() + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + + ui := CreateUI(app, true, false) + + ui.AnalyzePath("test_dir", analyze.ProcessDir, nil) + + 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 := testdir.CreateTestDir() + defer fin() + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + + ui := CreateUI(app, false, true) + + ui.AnalyzePath("test_dir", analyze.ProcessDir, nil) + + 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) +} diff --git a/tui/tui.go b/tui/tui.go new file mode 100644 index 0000000..504bd68 --- /dev/null +++ b/tui/tui.go @@ -0,0 +1,667 @@ +package tui + +import ( + "fmt" + "math" + "path/filepath" + "sort" + "sync" + "time" + + "github.com/dundee/gdu/analyze" + "github.com/dundee/gdu/common" + "github.com/dundee/gdu/device" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +const helpTextColorized = ` + [red]up, down, k, j [white]Move cursor up/down +[red]pgup, pgdn, g, G [white]Move cursor top/bottom + [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]r [white]Rescan current directory + [red]a [white]Toggle between showing disk usage and apparent size + [red]n [white]Sort by name (asc/desc) + [red]s [white]Sort by size (asc/desc) + [red]c [white]Sort by items (asc/desc) +` +const helpText = ` + [::b]up, down, k, j [white:black:-]Move cursor up/down +[::b]pgup, pgdn, g, G [white:black:-]Move cursor top/bottom + [::b]enter, right, l [white:black:-]Select directory/device + [::b]left, h [white:black:-]Go to parent directory + [::b]d [white:black:-]Delete selected file or directory + [::b]r [white:black:-]Rescan current directory + [::b]a [white:black:-]Toggle between showing disk usage and apparent size + [::b]n [white:black:-]Sort by name (asc/desc) + [::b]s [white:black:-]Sort by size (asc/desc) + [::b]c [white:black:-]Sort by items (asc/desc) +` + +// UI struct +type UI struct { + app common.Application + header *tview.TextView + footer *tview.TextView + currentDirLabel *tview.TextView + pages *tview.Pages + progress *tview.TextView + help *tview.Flex + table *tview.Table + currentDir *analyze.File + devices []*device.Device + analyzer analyze.Analyzer + topDir *analyze.File + topDirPath string + currentDirPath string + askBeforeDelete bool + ignoreDirPaths map[string]bool + sortBy string + sortOrder string + useColors bool + showApparentSize bool +} + +// CreateUI creates the whole UI app +func CreateUI(app common.Application, useColors bool, showApparentSize bool) *UI { + ui := &UI{ + askBeforeDelete: true, + sortBy: "size", + sortOrder: "desc", + useColors: useColors, + showApparentSize: showApparentSize, + analyzer: analyze.ProcessDir, + } + + app.SetBeforeDrawFunc(func(screen tcell.Screen) bool { + screen.Clear() + return false + }) + + ui.app = app + ui.app.SetInputCapture(ui.keyPressed) + + ui.header = tview.NewTextView() + ui.header.SetText(" gdu ~ Use arrow keys to navigate, press ? for help ") + if ui.useColors { + ui.header.SetTextColor(tcell.NewRGBColor(0, 0, 0)) + ui.header.SetBackgroundColor(tcell.NewRGBColor(36, 121, 208)) + } else { + ui.header.SetTextColor(tcell.NewRGBColor(0, 0, 0)) + ui.header.SetBackgroundColor(tcell.NewRGBColor(255, 255, 255)) + } + + ui.currentDirLabel = tview.NewTextView() + ui.currentDirLabel.SetBackgroundColor(tcell.ColorDefault) + + ui.table = tview.NewTable().SetSelectable(true, false) + ui.table.SetBackgroundColor(tcell.ColorDefault) + ui.table.SetSelectedStyle(tcell.Style{}. + Foreground(tcell.ColorBlack). + Background(tcell.ColorWhite).Bold(true)) + + ui.footer = tview.NewTextView().SetDynamicColors(true) + if ui.useColors { + ui.footer.SetTextColor(tcell.NewRGBColor(0, 0, 0)) + ui.footer.SetBackgroundColor(tcell.NewRGBColor(36, 121, 208)) + } else { + ui.footer.SetTextColor(tcell.NewRGBColor(0, 0, 0)) + ui.footer.SetBackgroundColor(tcell.NewRGBColor(255, 255, 255)) + } + ui.footer.SetText(" No items to display. ") + + grid := tview.NewGrid().SetRows(1, 1, 0, 1).SetColumns(0) + grid.AddItem(ui.header, 0, 0, 1, 1, 0, 0, false). + AddItem(ui.currentDirLabel, 1, 0, 1, 1, 0, 0, false). + AddItem(ui.table, 2, 0, 1, 1, 0, 0, true). + AddItem(ui.footer, 3, 0, 1, 1, 0, 0, false) + + ui.pages = tview.NewPages(). + AddPage("background", grid, true, true) + ui.pages.SetBackgroundColor(tcell.ColorDefault) + + ui.app.SetRoot(ui.pages, true) + + return ui +} + +// ListDevices lists mounted devices and shows their disk usage +func (ui *UI) ListDevices(getter device.DevicesInfoGetter) error { + var err error + ui.devices, err = getter.GetDevicesInfo() + if err != nil { + return err + } + + ui.table.SetCell(0, 0, tview.NewTableCell("Device name").SetSelectable(false)) + ui.table.SetCell(0, 1, tview.NewTableCell("Size").SetSelectable(false)) + ui.table.SetCell(0, 2, tview.NewTableCell("Used").SetSelectable(false)) + ui.table.SetCell(0, 3, tview.NewTableCell("Used part").SetSelectable(false)) + ui.table.SetCell(0, 4, tview.NewTableCell("Free").SetSelectable(false)) + ui.table.SetCell(0, 5, tview.NewTableCell("Mount point").SetSelectable(false)) + + var textColor, sizeColor string + if ui.useColors { + textColor = "[#3498db:-:b]" + sizeColor = "[#edb20a:-:b]" + } else { + textColor = "[white:-:b]" + sizeColor = "[white:-:b]" + } + + for i, device := range ui.devices { + ui.table.SetCell(i+1, 0, tview.NewTableCell(textColor+device.Name).SetReference(ui.devices[i])) + ui.table.SetCell(i+1, 1, tview.NewTableCell(ui.formatSize(device.Size, false))) + ui.table.SetCell(i+1, 2, tview.NewTableCell(sizeColor+ui.formatSize(device.Size-device.Free, false))) + ui.table.SetCell(i+1, 3, tview.NewTableCell(getDeviceUsagePart(device))) + ui.table.SetCell(i+1, 4, tview.NewTableCell(ui.formatSize(device.Free, false))) + ui.table.SetCell(i+1, 5, tview.NewTableCell(textColor+device.MountPoint)) + } + + ui.table.Select(1, 0) + ui.footer.SetText("") + ui.table.SetSelectedFunc(ui.deviceItemSelected) + + return nil +} + +// AnalyzePath analyzes recursively disk usage in given path +func (ui *UI) AnalyzePath(path string, analyzer analyze.Analyzer, parentDir *analyze.File) { + abspath, _ := filepath.Abs(path) + + ui.progress = tview.NewTextView().SetText("Scanning...") + ui.progress.SetBorder(true).SetBorderPadding(2, 2, 2, 2).SetBackgroundColor(tcell.ColorDefault) + ui.progress.SetTitle(" Scanning... ") + ui.progress.SetDynamicColors(true) + + flex := tview.NewFlex(). + AddItem(nil, 0, 1, false). + AddItem(tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(nil, 10, 1, false). + AddItem(ui.progress, 8, 1, false). + AddItem(nil, 10, 1, false), 0, 50, false). + AddItem(nil, 0, 1, false) + + ui.pages.AddPage("progress", flex, true, true) + ui.table.SetSelectedFunc(ui.fileItemSelected) + + progress := &analyze.CurrentProgress{ + Mutex: &sync.Mutex{}, + Done: false, + ItemCount: 0, + TotalSize: int64(0), + } + go ui.updateProgress(progress) + + go func() { + ui.currentDir = analyzer(abspath, progress, ui.ShouldDirBeIgnored) + + if parentDir != nil { + ui.currentDir.Parent = parentDir + parentDir.Files = parentDir.Files.RemoveByName(ui.currentDir.Name) + parentDir.Files = append(parentDir.Files, ui.currentDir) + + links := make(analyze.AlreadyCountedHardlinks, 10) + ui.topDir.UpdateStats(links) + } else { + ui.topDirPath = abspath + ui.topDir = ui.currentDir + } + + ui.app.QueueUpdateDraw(func() { + ui.showDir() + ui.pages.RemovePage("progress") + }) + }() +} + +// StartUILoop starts tview application +func (ui *UI) StartUILoop() error { + if err := ui.app.Run(); err != nil { + return err + } + return nil +} + +// SetIgnoreDirPaths sets paths to ignore +func (ui *UI) SetIgnoreDirPaths(paths []string) { + ui.ignoreDirPaths = make(map[string]bool, len(paths)) + for _, path := range paths { + ui.ignoreDirPaths[path] = true + } +} + +func (ui *UI) rescanDir() { + ui.AnalyzePath(ui.currentDirPath, ui.analyzer, ui.currentDir.Parent) +} + +// ShouldDirBeIgnored returns true if given path should be ignored +func (ui *UI) ShouldDirBeIgnored(path string) bool { + return ui.ignoreDirPaths[path] +} + +func (ui *UI) showDir() { + ui.currentDirPath = ui.currentDir.Path() + ui.currentDirLabel.SetText("[::b] --- " + ui.currentDirPath + " ---").SetDynamicColors(true) + + ui.table.Clear() + + rowIndex := 0 + if ui.currentDirPath != ui.topDirPath { + cell := tview.NewTableCell(" [::b]/..") + cell.SetReference(ui.currentDir.Parent) + ui.table.SetCell(0, 0, cell) + rowIndex++ + } + + ui.sortItems() + + for i, item := range ui.currentDir.Files { + cell := tview.NewTableCell(ui.formatFileRow(item)) + cell.SetReference(ui.currentDir.Files[i]) + + ui.table.SetCell(rowIndex, 0, cell) + rowIndex++ + } + + var footerNumberColor, footerTextColor string + if ui.useColors { + footerNumberColor = "[#e67100:#2479d0:b]" + footerTextColor = "[black:#2479d0:-]" + } else { + footerNumberColor = "[black:white:b]" + footerTextColor = "[black:white:-]" + } + + ui.footer.SetText( + " Total disk usage: " + + footerNumberColor + + ui.formatSize(ui.currentDir.Usage, true) + + " Apparent size: " + + footerNumberColor + + ui.formatSize(ui.currentDir.Size, true) + + " Items: " + footerNumberColor + fmt.Sprint(ui.currentDir.ItemCount) + + footerTextColor + + " 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.showApparentSize { + if ui.sortOrder == "desc" { + sort.Sort(analyze.ByApparentSize(ui.currentDir.Files)) + } else { + sort.Sort(sort.Reverse(analyze.ByApparentSize(ui.currentDir.Files))) + } + } else { + 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 { + return + } + + ui.currentDir = selectedDir + ui.showDir() +} + +func (ui *UI) deviceItemSelected(row, column int) { + selectedDevice := ui.table.GetCell(row, column).GetReference().(*device.Device) + + paths := device.GetNestedMountpointsPaths(selectedDevice.MountPoint, ui.devices) + for _, path := range paths { + ui.ignoreDirPaths[path] = true + } + + ui.AnalyzePath(selectedDevice.MountPoint, ui.analyzer, nil) +} + +func (ui *UI) confirmDeletion() { + row, column := ui.table.GetSelection() + selectedFile := ui.table.GetCell(row, column).GetReference().(*analyze.File) + modal := tview.NewModal(). + SetText("Are you sure you want to delete \"" + selectedFile.Name + "\""). + AddButtons([]string{"yes", "no", "don't ask me again"}). + SetDoneFunc(func(buttonIndex int, buttonLabel string) { + switch buttonIndex { + case 2: + ui.askBeforeDelete = false + fallthrough + case 0: + ui.deleteSelected() + } + ui.pages.RemovePage("confirm") + }) + + if !ui.useColors { + modal.SetBackgroundColor(tcell.ColorGray) + } + + ui.pages.AddPage("confirm", modal, true, true) +} + +func (ui *UI) showErr(msg string, err error) { + modal := tview.NewModal(). + SetText(msg + ": " + err.Error()). + AddButtons([]string{"ok"}). + SetDoneFunc(func(buttonIndex int, buttonLabel string) { + ui.pages.RemovePage("error") + }) + + if !ui.useColors { + modal.SetBackgroundColor(tcell.ColorGray) + } + + ui.pages.AddPage("error", modal, true, true) +} + +func (ui *UI) deleteSelected() { + row, column := ui.table.GetSelection() + selectedFile := ui.table.GetCell(row, column).GetReference().(*analyze.File) + + modal := tview.NewModal().SetText("Deleting " + selectedFile.Name + "...") + ui.pages.AddPage("deleting", modal, true, true) + + currentDir := ui.currentDir + + go func() { + if err := currentDir.RemoveFile(selectedFile); err != nil { + msg := "Can't delete " + selectedFile.Name + ui.app.QueueUpdateDraw(func() { + ui.pages.RemovePage("deleting") + ui.showErr(msg, err) + }) + return + } + + ui.app.QueueUpdateDraw(func() { + ui.pages.RemovePage("deleting") + ui.showDir() + ui.table.Select(min(row, ui.table.GetRowCount()-1), 0) + }) + }() +} + +func (ui *UI) keyPressed(key *tcell.EventKey) *tcell.EventKey { + if (key.Key() == tcell.KeyEsc || key.Rune() == 'q') && ui.pages.HasPage("help") { + ui.pages.RemovePage("help") + ui.app.SetFocus(ui.table) + return key + } + + if ui.pages.HasPage("deleting") && key.Rune() != 'q' { + return key + } + + if key.Rune() == 'h' || key.Key() == tcell.KeyLeft { + if ui.pages.HasPage("confirm") { + return key + } + + if ui.currentDirPath == ui.topDirPath { + return key + } + if ui.currentDir != nil { + subDir := ui.currentDir + ui.fileItemSelected(0, 0) + index, _ := ui.currentDir.Files.IndexOf(subDir) + if ui.currentDir != ui.topDir { + index++ + } + ui.table.Select(index, 0) + } + return key + } + + if key.Rune() == 'l' || key.Key() == tcell.KeyRight { + if ui.pages.HasPage("confirm") { + return key + } + + row, column := ui.table.GetSelection() + if ui.currentDirPath != ui.topDirPath && row == 0 { // do not select /.. + return key + } + + if ui.currentDir != nil { + ui.fileItemSelected(row, column) + } else { + ui.deviceItemSelected(row, column) + } + return key + } + + switch key.Rune() { + case 'q': + ui.app.Stop() + return nil + case '?': + ui.showHelp() + break + case 'd': + if ui.currentDir == nil { + break + } + + // do not allow deleting parent dir + row, column := ui.table.GetSelection() + selectedFile := ui.table.GetCell(row, column).GetReference().(*analyze.File) + if selectedFile == ui.currentDir.Parent { + break + } + + if ui.askBeforeDelete { + ui.confirmDeletion() + } else { + ui.deleteSelected() + } + break + case 'a': + ui.showApparentSize = !ui.showApparentSize + if ui.currentDir != nil { + ui.showDir() + } + break + case 'r': + if ui.currentDir == nil { + break + } + ui.rescanDir() + 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) { + color := "[white:-:b]" + if ui.useColors { + color = "[red:-:b]" + } + + for { + progress.Mutex.Lock() + + if progress.Done { + return + } + + ui.app.QueueUpdateDraw(func() { + ui.progress.SetText("Total items: " + + color + + fmt.Sprint(progress.ItemCount) + + "[white:-:-] size: " + + color + + ui.formatSize(progress.TotalSize, false) + + "[white:-:-]\nCurrent item: [white:-:b]" + + progress.CurrentItemName) + }) + progress.Mutex.Unlock() + + time.Sleep(100 * time.Millisecond) + } +} + +func (ui *UI) showHelp() { + text := tview.NewTextView().SetDynamicColors(true) + text.SetBorder(true).SetBorderPadding(2, 2, 2, 2) + text.SetTitle(" gdu help ") + + if ui.useColors { + text.SetText(helpTextColorized) + } else { + text.SetText(helpText) + } + + flex := tview.NewFlex(). + AddItem(nil, 0, 1, false). + AddItem(tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(nil, 0, 1, false). + AddItem(text, 15, 1, false). + AddItem(nil, 0, 1, false), 80, 1, false). + AddItem(nil, 0, 1, false) + + ui.help = flex + ui.pages.AddPage("help", flex, true, true) +} + +func (ui *UI) formatFileRow(item *analyze.File) string { + var part int + + if ui.showApparentSize { + part = int(float64(item.Size) / float64(item.Parent.Size) * 10.0) + } else { + part = int(float64(item.Usage) / float64(item.Parent.Usage) * 10.0) + } + + row := string(item.Flag) + + if ui.useColors { + row += "[#e67100:-:b]" + } else { + row += "[white:-:b]" + } + + if ui.showApparentSize { + row += fmt.Sprintf("%21s", ui.formatSize(item.Size, false)) + } else { + row += fmt.Sprintf("%21s", ui.formatSize(item.Usage, false)) + } + + row += " [" + for i := 0; i < 10; i++ { + if part > i { + row += "#" + } else { + row += " " + } + } + row += "] " + + if item.IsDir { + if ui.useColors { + row += "[#3498db::b]/" + } else { + row += "[::b]/" + } + } + row += item.Name + return row +} + +func (ui *UI) formatSize(size int64, reverseColor bool) string { + var color string + if reverseColor { + if ui.useColors { + color = "[black:#2479d0:-]" + } else { + color = "[black:white:-]" + } + } else { + color = "[white:-:-]" + } + + switch { + case size > 1e12: + return fmt.Sprintf("%.1f%s TiB", float64(size)/math.Pow(2, 40), color) + case size > 1e9: + return fmt.Sprintf("%.1f%s GiB", float64(size)/math.Pow(2, 30), color) + case size > 1e6: + return fmt.Sprintf("%.1f%s MiB", float64(size)/math.Pow(2, 20), color) + case size > 1e3: + return fmt.Sprintf("%.1f%s KiB", float64(size)/math.Pow(2, 10), color) + default: + return fmt.Sprintf("%d%s B", size, color) + } +} + +func getDeviceUsagePart(item *device.Device) string { + part := int(float64(item.Size-item.Free) / float64(item.Size) * 10.0) + row := "[" + for i := 0; i < 10; i++ { + if part > i { + row += "#" + } else { + row += " " + } + } + row += "]" + return row +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/tui/tui_test.go b/tui/tui_test.go new file mode 100644 index 0000000..cf902b1 --- /dev/null +++ b/tui/tui_test.go @@ -0,0 +1,577 @@ +package tui + +import ( + "os" + "path/filepath" + "runtime" + "sync" + "testing" + "time" + + "github.com/dundee/gdu/analyze" + "github.com/dundee/gdu/device" + "github.com/dundee/gdu/internal/testanalyze" + "github.com/dundee/gdu/internal/testapp" + "github.com/dundee/gdu/internal/testdev" + "github.com/dundee/gdu/internal/testdir" + "github.com/gdamore/tcell/v2" + "github.com/stretchr/testify/assert" +) + +func TestFooter(t *testing.T) { + app, simScreen := testapp.CreateTestAppWithSimScreen(15, 15) + defer simScreen.Fini() + + ui := CreateUI(app, false, true) + + dir := analyze.File{ + Name: "xxx", + BasePath: ".", + Size: 5, + Usage: 4096, + ItemCount: 2, + } + + file := analyze.File{ + Name: "yyy", + Size: 2, + Usage: 4096, + ItemCount: 1, + Parent: &dir, + } + dir.Files = []*analyze.File{&file} + + ui.currentDir = &dir + ui.showDir() + ui.pages.HidePage("progress") + + ui.footer.Draw(simScreen) + simScreen.Show() + + b, _, _ := simScreen.GetContents() + + text := []byte(" Total disk usage: 4.0 KiB Apparent size: 5 B Items: 2") + for i, r := range b { + if i >= len(text) { + break + } + assert.Equal(t, string(text[i]), string(r.Bytes[0])) + } +} + +func TestUpdateProgress(t *testing.T) { + app, simScreen := testapp.CreateTestAppWithSimScreen(15, 15) + defer simScreen.Fini() + + progress := &analyze.CurrentProgress{Mutex: &sync.Mutex{}, Done: true} + + ui := CreateUI(app, false, false) + progress.CurrentItemName = "xxx" + ui.updateProgress(progress) + assert.True(t, true) +} + +func TestHelp(t *testing.T) { + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + defer simScreen.Fini() + + ui := CreateUI(app, false, true) + ui.showHelp() + ui.help.Draw(simScreen) + simScreen.Show() + + b, _, _ := simScreen.GetContents() + + cells := b[356 : 356+9] + + text := []byte("directory") + for i, r := range cells { + assert.Equal(t, text[i], r.Bytes[0]) + } +} + +func TestDeleteDir(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + + ui := CreateUI(app, true, false) + ui.askBeforeDelete = false + + ui.AnalyzePath("test_dir", analyze.ProcessDir, nil) + + go func() { + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, '?', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyEnter, '1', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'j', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'j', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'l', 1) // test selecting file + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'd', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'a', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(10 * time.Millisecond) + }() + + ui.StartUILoop() + + assert.NoFileExists(t, "test_dir/nested/file2") +} + +func TestDoNotDeleteParentDir(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + + ui := CreateUI(app, true, true) + ui.askBeforeDelete = false + + ui.AnalyzePath("test_dir", analyze.ProcessDir, nil) + + go func() { + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'l', 1) + time.Sleep(10 * time.Millisecond) + // .. is selected now, cannot be deleted + simScreen.InjectKey(tcell.KeyRune, 'd', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(10 * time.Millisecond) + }() + + ui.StartUILoop() + + assert.FileExists(t, "test_dir/nested/file2") +} + +func TestDeleteDirWithConfirm(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + + ui := CreateUI(app, false, false) + + ui.AnalyzePath("test_dir", analyze.ProcessDir, nil) + + go func() { + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, '?', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyEnter, '1', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'j', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'j', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'd', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyEnter, ' ', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(10 * time.Millisecond) + }() + + ui.StartUILoop() + + assert.NoFileExists(t, "test_dir/nested/file2") +} + +func TestDeleteDirWithConfirmNoAskAgain(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + + ui := CreateUI(app, false, false) + + ui.AnalyzePath("test_dir", analyze.ProcessDir, nil) + + go func() { + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, '?', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyEnter, '1', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'j', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'j', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'd', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRight, ' ', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRight, ' ', 1) // select "do not ask again" + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyEnter, ' ', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(10 * time.Millisecond) + }() + + ui.StartUILoop() + + assert.NoFileExists(t, "test_dir/nested/file2") +} + +func TestShowConfirm(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + + ui := CreateUI(app, true, true) + + ui.AnalyzePath("test_dir", analyze.ProcessDir, nil) + + go func() { + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'h', 1) // cannot go up + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, '?', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRight, '1', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRight, '1', 1) // `..` cannot be selected by `l` or `right` + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'j', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'j', 1) // select file + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'd', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'h', 1) // cannot go up when confirm is shown + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'l', 1) // cannot go down when confirm is shown + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(10 * time.Millisecond) + }() + + ui.StartUILoop() + + assert.FileExists(t, "test_dir/nested/file2") +} + +func TestDeleteWithErr(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + os.Chmod("test_dir/nested", 0) + defer os.Chmod("test_dir/nested", 0755) + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + + ui := CreateUI(app, true, true) + ui.askBeforeDelete = false + + ui.AnalyzePath("test_dir", analyze.ProcessDir, nil) + + go func() { + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'd', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyEnter, ' ', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(10 * time.Millisecond) + }() + + ui.StartUILoop() + + assert.DirExists(t, "test_dir/nested") +} + +func TestDeleteWithErrBW(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + os.Chmod("test_dir/nested", 0) + defer os.Chmod("test_dir/nested", 0755) + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + + ui := CreateUI(app, false, false) + ui.askBeforeDelete = false + + ui.AnalyzePath("test_dir", analyze.ProcessDir, nil) + + go func() { + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'd', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyEnter, ' ', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(10 * time.Millisecond) + }() + + ui.StartUILoop() + + assert.DirExists(t, "test_dir/nested") +} + +func TestRescan(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + + ui := CreateUI(app, true, false) + + ui.AnalyzePath("test_dir", analyze.ProcessDir, nil) + + go func() { + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyEnter, '1', 1) + time.Sleep(10 * time.Millisecond) + + // rescan subdir + simScreen.InjectKey(tcell.KeyRune, 'r', 1) + time.Sleep(100 * time.Millisecond) + + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(10 * time.Millisecond) + }() + + ui.StartUILoop() +} + +// TestItemRows tests that item with different sizes are shown +func TestItemRows(t *testing.T) { + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + + ui := CreateUI(app, true, false) + + ui.AnalyzePath("test_dir", testanalyze.MockedProcessDir, nil) + + go func() { + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(10 * time.Millisecond) + }() + + ui.StartUILoop() +} + +func TestShowDevices(t *testing.T) { + if runtime.GOOS != "linux" { + return + } + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + defer simScreen.Fini() + + ui := CreateUI(app, true, true) + ui.ListDevices(getDevicesInfoMock()) + ui.table.Draw(simScreen) + simScreen.Show() + + b, _, _ := simScreen.GetContents() + + text := []byte("Device name") + for i, r := range b[0:11] { + assert.Equal(t, text[i], r.Bytes[0]) + } +} + +func TestShowDevicesBW(t *testing.T) { + if runtime.GOOS != "linux" { + return + } + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + defer simScreen.Fini() + + ui := CreateUI(app, false, false) + ui.ListDevices(getDevicesInfoMock()) + ui.table.Draw(simScreen) + simScreen.Show() + + b, _, _ := simScreen.GetContents() + + text := []byte("Device name") + for i, r := range b[0:11] { + assert.Equal(t, text[i], r.Bytes[0]) + } +} + +func TestShowDevicesWithError(t *testing.T) { + if runtime.GOOS != "linux" { + return + } + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + defer simScreen.Fini() + + getter := device.LinuxDevicesInfoGetter{MountsPath: "/xyzxyz"} + + ui := CreateUI(app, false, false) + err := ui.ListDevices(getter) + + assert.Contains(t, err.Error(), "no such file") +} + +func TestSelectDevice(t *testing.T) { + if runtime.GOOS != "linux" { + return + } + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + + ui := CreateUI(app, true, true) + ui.analyzer = analyzeMock + ui.SetIgnoreDirPaths([]string{"/proc"}) + ui.ListDevices(getDevicesInfoMock()) + + go func() { + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'd', 1) // device cannot be deleted + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'r', 1) // or refreshed + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'l', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + }() + + ui.StartUILoop() +} + +func TestKeys(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + + ui := CreateUI(app, false, false) + ui.askBeforeDelete = false + + ui.AnalyzePath("test_dir", analyze.ProcessDir, nil) + + go func() { + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 's', 1) // sort asc + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 's', 1) // sort desc + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'j', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'l', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'j', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'l', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'j', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'd', 1) + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'h', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'h', 1) + time.Sleep(10 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(10 * time.Millisecond) + }() + + ui.StartUILoop() + + assert.NoFileExists(t, "test_dir/nested/subnested/file") +} + +func TestSetIgnoreDirPaths(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) + + ui := CreateUI(app, false, true) + + path, _ := filepath.Abs("test_dir/nested/subnested") + ui.SetIgnoreDirPaths([]string{path}) + + ui.AnalyzePath("test_dir", analyze.ProcessDir, nil) + + go func() { + time.Sleep(100 * time.Millisecond) + simScreen.InjectKey(tcell.KeyRune, 'q', 1) + time.Sleep(10 * time.Millisecond) + }() + + ui.StartUILoop() + + dir := ui.currentDir + + assert.Equal(t, 3, dir.ItemCount) +} + +func TestAppRunWithErr(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + app := testapp.CreateMockedApp(true) + + ui := CreateUI(app, false, true) + + err := ui.StartUILoop() + + assert.Equal(t, "Fail", err.Error()) +} + +func TestMin(t *testing.T) { + assert.Equal(t, 2, min(2, 5)) + assert.Equal(t, 3, min(4, 3)) +} + +func printScreen(simScreen tcell.SimulationScreen) { + b, _, _ := simScreen.GetContents() + + for i, r := range b { + println(i, string(r.Bytes)) + } +} + +func analyzeMock(path string, progress *analyze.CurrentProgress, ignore analyze.ShouldDirBeIgnored) *analyze.File { + return &analyze.File{ + Name: "xxx", + BasePath: ".", + } +} + +func getDevicesInfoMock() device.DevicesInfoGetter { + item := &device.Device{ + Name: "/dev/root", + MountPoint: "/", + Size: 1e9, + Free: 1e3, + } + item2 := &device.Device{ + Name: "/dev/boot", + MountPoint: "/boot", + Size: 1e12, + Free: 1e6, + } + + mock := testdev.DevicesInfoGetterMock{} + mock.Devices = []*device.Device{item, item2} + return mock +} -- 2.30.2