--- /dev/null
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [ master ]
+ schedule:
+ - cron: '21 0 * * 3'
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ 'go' ]
+ # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
+ # Learn more:
+ # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v1
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+ # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v1
+
+ # âšī¸ Command-line programs to run using the OS shell.
+ # đ https://git.io/JvXDl
+
+ # âī¸ If the Autobuild fails above, remove it and uncomment the following three lines
+ # and modify them (or add more) to build your code if your project
+ # uses a compiled language
+
+ #- run: |
+ # make bootstrap
+ # make release
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v1
--- /dev/null
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - master
+
+name: run tests
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Install Go
+ uses: actions/setup-go@v2
+ with:
+ go-version: 1.16.x
+ - name: Checkout code
+ uses: actions/checkout@v2
+ - name: Run linters
+ uses: golangci/golangci-lint-action@v2
+ with:
+ version: v1.29
+
+ test:
+ strategy:
+ matrix:
+ go-version: [1.16.x]
+ platform: [ubuntu-latest, macos-latest]
+ runs-on: ${{ matrix.platform }}
+ steps:
+ - name: Install Go
+ if: success()
+ uses: actions/setup-go@v2
+ with:
+ go-version: ${{ matrix.go-version }}
+ - name: Checkout code
+ uses: actions/checkout@v2
+ - name: Run tests
+ run: go test -v -covermode=count ./...
+
+ coverage:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Install Go
+ if: success()
+ uses: actions/setup-go@v2
+ with:
+ go-version: 1.16.x
+ - name: Checkout code
+ uses: actions/checkout@v2
+ - name: Calc coverage
+ run: |
+ go test -v -race -covermode=atomic -coverprofile=coverage.out ./...
+ - name: Upload coverage report
+ uses: codecov/codecov-action@v2
+ with:
+ files: ./coverage.out
+ fail_ci_if_error: true
+ verbose: true
+ - name: Convert coverage.out to coverage.lcov
+ uses: jandelgado/gcov2lcov-action@v1.0.6
+ - name: Coveralls
+ uses: coverallsapp/github-action@v1.1.2
+ with:
+ github-token: ${{ secrets.github_token }}
+ path-to-lcov: coverage.lcov
--- /dev/null
+/.vscode
+/.idea
+/coverage.txt
+/dist
+/test_dir
+
--- /dev/null
+Copyright 2020-2021 Daniel Milde <daniel@milde.cz>
+
+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.
+
--- /dev/null
+NAME := gdu
+MAJOR_VER := v5
+PACKAGE := github.com/dundee/$(NAME)/$(MAJOR_VER)
+CMD_GDU := cmd/gdu
+VERSION := $(shell git describe --tags 2>/dev/null)
+DATE := $(shell date +'%Y-%m-%d')
+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: clean build-all man clean-uncompressed-dist shasums
+
+run:
+ go run $(PACKAGE)/$(CMD_GDU)
+
+build:
+ @echo "Version: " $(VERSION)
+ mkdir -p dist
+ GOFLAGS="$(GOFLAGS)" CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o dist/$(NAME) $(PACKAGE)/$(CMD_GDU)
+
+build-all:
+ @echo "Version: " $(VERSION)
+ -mkdir dist
+ -CGO_ENABLED=0 gox \
+ -os="darwin windows" \
+ -arch="amd64" \
+ -output="dist/gdu_{{.OS}}_{{.Arch}}" \
+ -ldflags="$(LDFLAGS)" \
+ $(PACKAGE)/$(CMD_GDU)
+
+ -CGO_ENABLED=0 gox \
+ -os="linux freebsd netbsd openbsd" \
+ -output="dist/gdu_{{.OS}}_{{.Arch}}" \
+ -ldflags="$(LDFLAGS)" \
+ $(PACKAGE)/$(CMD_GDU)
+
+ cd dist; GOFLAGS="$(GOFLAGS)" CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o gdu_linux_amd64 $(PACKAGE)/$(CMD_GDU)
+
+ cd dist; CGO_ENABLED=0 GOOS=linux GOARM=5 GOARCH=arm go build -ldflags="$(LDFLAGS)" -o gdu_linux_armv5l $(PACKAGE)/$(CMD_GDU)
+ cd dist; CGO_ENABLED=0 GOOS=linux GOARM=6 GOARCH=arm go build -ldflags="$(LDFLAGS)" -o gdu_linux_armv6l $(PACKAGE)/$(CMD_GDU)
+ cd dist; CGO_ENABLED=0 GOOS=linux GOARM=7 GOARCH=arm go build -ldflags="$(LDFLAGS)" -o gdu_linux_armv7l $(PACKAGE)/$(CMD_GDU)
+ cd dist; CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o gdu_linux_arm64 $(PACKAGE)/$(CMD_GDU)
+
+ 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
+ sed 's/{{date}}/$(DATE)/g' gdu.1.md > gdu.1.date.md
+ pandoc gdu.1.date.md -s -t man > gdu.1
+ rm -f gdu.1.date.md
+
+man: gdu.1
+ cp gdu.1 dist
+ cd dist; tar czf gdu.1.tgz gdu.1
+
+show-man:
+ sed 's/{{date}}/$(DATE)/g' gdu.1.md > gdu.1.date.md
+ pandoc gdu.1.date.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=. $(PACKAGE)/pkg/analyze
+
+benchmark:
+ hyperfine --export-markdown=bench-cold.md \
+ --prepare 'sync; echo 3 | sudo tee /proc/sys/vm/drop_caches' \
+ 'gdu -npc ~' 'gdu -gnpc ~' 'dua ~' 'duc index ~' 'ncdu -0 -o /dev/null ~' \
+ 'diskus ~' 'du -hs ~' 'dust -d0 ~'
+ hyperfine --export-markdown=bench-warm.md \
+ --warmup 5 \
+ 'gdu -npc ~' 'gdu -gnpc ~' 'dua ~' 'duc index ~' 'ncdu -0 -o /dev/null ~' \
+ 'diskus ~' 'du -hs ~' 'dust -d0 ~'
+
+clean:
+ go mod tidy
+ -rm coverage.txt
+ -rm -r test_dir
+ -rm -r dist
+
+clean-uncompressed-dist:
+ find dist -type f -not -name '*.tgz' -not -name '*.zip' -delete
+
+shasums:
+ cd dist; sha256sum * > sha256sums.txt
+ cd dist; gpg --sign --armor --detach-sign sha256sums.txt
+
+release:
+ gh release create -t "gdu $(VERSION)" $(VERSION) ./dist/*
+
+.PHONY: run build test coverage coverage-html clean man
--- /dev/null
+# go DiskUsage()
+
+[](https://codecov.io/gh/dundee/gdu)
+[](https://goreportcard.com/report/github.com/dundee/gdu)
+[](https://codeclimate.com/github/dundee/gdu/maintainability)
+[](https://codescene.io/projects/13129)
+
+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.
+
+[](https://asciinema.org/a/382738)
+
+<a href="https://repology.org/project/gdu/versions">
+ <img src="https://repology.org/badge/vertical-allrepos/gdu.svg" alt="Packaging status" align="right">
+</a>
+
+## Installation
+
+Head for the [releases page](https://github.com/dundee/gdu/releases) and download the 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](https://packages.debian.org/bullseye/gdu):
+
+ apt install gdu
+
+[Ubuntu](https://launchpad.net/~daniel-milde/+archive/ubuntu/gdu)
+
+ add-apt-repository ppa:daniel-milde/gdu
+ apt-get update
+ apt-get install gdu
+
+
+[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 -f gdu
+ brew link --overwrite gdu # if you have coreutils installed as well
+
+[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 install github.com/dundee/gdu/v5/cmd/gdu@latest
+
+## Usage
+
+```
+ gdu [flags] [directory_to_scan]
+
+Flags:
+ -g, --enable-gc Enable memory garbage collection during analysis
+ --enable-profiling Enable collection of profiling data and provide it on http://localhost:6060/debug/pprof/
+ -h, --help help for gdu
+ -i, --ignore-dirs strings Absolute paths to ignore (separated by comma) (default [/proc,/dev,/sys,/run])
+ -I, --ignore-dirs-pattern strings Absolute path patterns to ignore (separated by comma)
+ -X, --ignore-from string Read absolute path patterns to ignore from file
+ -f, --input-file string Import analysis from JSON file
+ -l, --log-file string Path to a logfile (default "/dev/null")
+ -m, --max-cores int Set max cores that GDU will use. 8 cores available (default 8)
+ -c, --no-color Do not use colorized output
+ -x, --no-cross Do not cross filesystem boundaries
+ -H, --no-hidden Ignore hidden directories (beginning with dot)
+ -p, --no-progress Do not show progress in non-interactive mode
+ -n, --non-interactive Do not run in interactive mode
+ -o, --output-file string Export all info into file as JSON
+ -a, --show-apparent-size Show apparent size
+ -d, --show-disks Show all mounted disks
+ -s, --summarize Show only a total in non-interactive mode
+ -v, --version Print version
+```
+
+## Examples
+
+ gdu # analyze current dir
+ gdu -a # show apparent size instead of disk usage
+ gdu <some_dir_to_analyze> # analyze given dir
+ gdu -d # show all mounted disks
+ gdu -l ./gdu.log <some_dir> # write errors to log file
+ gdu -i /sys,/proc / # ignore some paths
+ gdu -I '.*[abc]+' # ignore paths by regular pattern
+ gdu -X ignore_file / # ignore paths by regular patterns from file
+ 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 -nps /some/dir # show only total usage for given dir
+ gdu / > file # write stats to file, do not start interactive mode
+
+ gdu -o- / | gzip -c >report.json.gz # write all info to JSON file for later analysis
+ zcat report.json.gz | gdu -f- # read analysis from file
+
+## Modes
+
+Gdu has three modes: interactive (default), non-interactive and export.
+
+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.
+
+Export mode (flag `-o`) outputs all usage data as JSON, which can be later opened using the `-f` 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.
+
+## RAM usage
+
+Gdu can consume a lot of memory - around 500-1000 MB of RAM for 1 mil files. It's a trade off for being so fast.
+
+If you want lower memory usage and don't mind giving up some speed, you can use `--enable-gc` / `-g` flag.
+It will run garbage collection during the analysis phase.
+As a result, the analysis will be about 25% slower and will consume about 30% less memory.
+You should use this option if you are scanning more than circa 3 milion files.
+
+If you need some more fine tunning, you can use the `GOGC` environment variable to set how often the garbage collection will happen.
+Default value is `100`. Lower value means GC will run more often. Higher than 100 means less often. Negative number will stop GC.
+
+Example running gdu with GC enabled, but not so aggresive as default:
+
+```
+GOGC=200 gdu -g /
+```
+
+## Running tests
+
+ make test
+
+## Benchmarks
+
+Benchmarks were 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.
+
+## Profiling
+
+Gdu can collect profiling data when the `--enable-profiling` flag is set.
+The data are provided via embeded http server on URL `http://localhost:6060/debug/pprof/`.
+
+You can then use e.g. `go tool pprof -web http://localhost:6060/debug/pprof/heap`
+to open the heap profile as SVG image in your web browser.
+
+### 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 ~` | 5.390 Âą 0.094 | 5.303 | 5.644 | 1.00 Âą 0.02 |
+| `gdu -gnpc ~` | 6.275 Âą 2.406 | 5.379 | 13.097 | 1.17 Âą 0.45 |
+| `dua ~` | 6.727 Âą 0.019 | 6.689 | 6.748 | 1.25 Âą 0.01 |
+| `duc index ~` | 31.377 Âą 0.176 | 31.085 | 31.701 | 5.83 Âą 0.06 |
+| `ncdu -0 -o /dev/null ~` | 31.311 Âą 0.100 | 31.170 | 31.507 | 5.82 Âą 0.05 |
+| `diskus ~` | 5.383 Âą 0.044 | 5.287 | 5.440 | 1.00 |
+| `du -hs ~` | 30.333 Âą 0.408 | 29.865 | 31.086 | 5.63 Âą 0.09 |
+| `dust -d0 ~` | 6.889 Âą 0.354 | 6.738 | 7.889 | 1.28 Âą 0.07 |
+
+### Warm cache
+
+| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
+|:---|---:|---:|---:|---:|
+| `gdu -npc ~` | 840.3 Âą 13.4 | 817.7 | 867.8 | 1.74 Âą 0.06 |
+| `gdu -gnpc ~` | 1038.4 Âą 9.7 | 1021.3 | 1054.1 | 2.15 Âą 0.07 |
+| `dua ~` | 635.0 Âą 20.6 | 602.6 | 669.9 | 1.32 Âą 0.06 |
+| `duc index ~` | 1879.5 Âą 18.5 | 1853.5 | 1922.1 | 3.90 Âą 0.13 |
+| `ncdu -0 -o /dev/null ~` | 2618.5 Âą 10.0 | 2607.9 | 2634.8 | 5.43 Âą 0.18 |
+| `diskus ~` | 482.4 Âą 15.6 | 456.5 | 516.9 | 1.00 |
+| `du -hs ~` | 1508.7 Âą 8.2 | 1501.1 | 1524.3 | 3.13 Âą 0.10 |
+| `dust -d0 ~` | 832.5 Âą 27.0 | 797.3 | 895.5 | 1.73 Âą 0.08 |
+
+## Alternatives
+
+* [ncdu](https://dev.yorhel.nl/ncdu) - NCurses based tool written in pure C
+* [godu](https://github.com/viktomas/godu) - Analyzer with carousel like user interface
+* [dua](https://github.com/Byron/dua-cli) - Tool written in Rust with interface similar to gdu (and ncdu)
+* [diskus](https://github.com/sharkdp/diskus) - Very simple but very fast tool written in Rust
+* [duc](https://duc.zevv.nl/) - Collection of tools with many possibilities for inspecting and visualising disk usage
+* [dust](https://github.com/bootandy/dust) - Tool written in Rust showing tree like structures of disk usage
--- /dev/null
+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
+
+// RootPathPrefix stores path to be prepended to given absolute path
+// e.g. /var/lib/snapd/hostfs for snap
+var RootPathPrefix = ""
--- /dev/null
+package app
+
+import (
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+
+ "net/http"
+ _ "net/http/pprof" // load profiling http handler
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/dundee/gdu/v5/build"
+ "github.com/dundee/gdu/v5/internal/common"
+ "github.com/dundee/gdu/v5/pkg/analyze"
+ "github.com/dundee/gdu/v5/pkg/device"
+ "github.com/dundee/gdu/v5/report"
+ "github.com/dundee/gdu/v5/stdout"
+ "github.com/dundee/gdu/v5/tui"
+ "github.com/gdamore/tcell/v2"
+ "github.com/rivo/tview"
+)
+
+// UI is common interface for both terminal UI and text output
+type UI interface {
+ ListDevices(getter device.DevicesInfoGetter) error
+ AnalyzePath(path string, parentDir *analyze.Dir) error
+ ReadAnalysis(input io.Reader) error
+ SetIgnoreDirPaths(paths []string)
+ SetIgnoreDirPatterns(paths []string) error
+ SetIgnoreFromFile(ignoreFile string) error
+ SetIgnoreHidden(value bool)
+ StartUILoop() error
+}
+
+// Flags define flags accepted by Run
+type Flags struct {
+ LogFile string
+ InputFile string
+ OutputFile string
+ IgnoreDirs []string
+ IgnoreDirPatterns []string
+ IgnoreFromFile string
+ MaxCores int
+ ShowDisks bool
+ ShowApparentSize bool
+ ShowVersion bool
+ NoColor bool
+ NonInteractive bool
+ NoProgress bool
+ NoCross bool
+ NoHidden bool
+ Profiling bool
+ EnableGC bool
+ Summarize bool
+}
+
+// App defines the main application
+type App struct {
+ Args []string
+ Flags *Flags
+ Istty bool
+ Writer io.Writer
+ TermApp common.TermApplication
+ Screen tcell.Screen
+ Getter device.DevicesInfoGetter
+ PathChecker func(string) (fs.FileInfo, error)
+}
+
+// Run starts gdu main logic
+func (a *App) Run() (err error) {
+ var (
+ f *os.File
+ ui UI
+ )
+
+ if a.Flags.ShowVersion {
+ fmt.Fprintln(a.Writer, "Version:\t", build.Version)
+ fmt.Fprintln(a.Writer, "Built time:\t", build.Time)
+ fmt.Fprintln(a.Writer, "Built user:\t", build.User)
+ return
+ }
+
+ f, err = os.OpenFile(a.Flags.LogFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+ if err != nil {
+ err = fmt.Errorf("opening log file: %w", err)
+ return
+ }
+ defer func() {
+ cerr := f.Close()
+ if err == nil {
+ err = cerr
+ }
+ }()
+ log.SetOutput(f)
+
+ log.Printf("Runtime flags: %+v", *a.Flags)
+
+ path := a.getPath()
+ path, _ = filepath.Abs(path)
+
+ ui, err = a.createUI()
+ if err != nil {
+ return
+ }
+
+ if err = a.setNoCross(path); err != nil {
+ return
+ }
+
+ ui.SetIgnoreDirPaths(a.Flags.IgnoreDirs)
+
+ if len(a.Flags.IgnoreDirPatterns) > 0 {
+ if err = ui.SetIgnoreDirPatterns(a.Flags.IgnoreDirPatterns); err != nil {
+ return
+ }
+ }
+
+ if a.Flags.IgnoreFromFile != "" {
+ if err = ui.SetIgnoreFromFile(a.Flags.IgnoreFromFile); err != nil {
+ return
+ }
+ }
+
+ if a.Flags.NoHidden {
+ ui.SetIgnoreHidden(true)
+ }
+
+ a.setMaxProcs()
+
+ if err = a.runAction(ui, path); err != nil {
+ return
+ }
+
+ err = ui.StartUILoop()
+ return
+}
+
+func (a *App) getPath() string {
+ if len(a.Args) == 1 {
+ return a.Args[0]
+ }
+ return "."
+}
+
+func (a *App) setMaxProcs() {
+ if a.Flags.MaxCores < 1 || a.Flags.MaxCores > runtime.NumCPU() {
+ return
+ }
+
+ runtime.GOMAXPROCS(a.Flags.MaxCores)
+
+ // runtime.GOMAXPROCS(n) with n < 1 doesn't change current setting so we use it to check current value
+ log.Printf("Max cores set to %d", runtime.GOMAXPROCS(0))
+}
+
+func (a *App) createUI() (UI, error) {
+ var ui UI
+
+ if a.Flags.OutputFile != "" {
+ var output io.Writer
+ var err error
+ if a.Flags.OutputFile == "-" {
+ output = os.Stdout
+ } else {
+ output, err = os.OpenFile(a.Flags.OutputFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+ if err != nil {
+ return nil, fmt.Errorf("opening output file: %w", err)
+ }
+ }
+ ui = report.CreateExportUI(
+ a.Writer,
+ output,
+ !a.Flags.NoColor && a.Istty,
+ !a.Flags.NoProgress && a.Istty,
+ a.Flags.EnableGC,
+ )
+ return ui, nil
+ }
+
+ if a.Flags.NonInteractive || !a.Istty {
+ ui = stdout.CreateStdoutUI(
+ a.Writer,
+ !a.Flags.NoColor && a.Istty,
+ !a.Flags.NoProgress && a.Istty,
+ a.Flags.ShowApparentSize,
+ a.Flags.Summarize,
+ a.Flags.EnableGC,
+ )
+ } else {
+ ui = tui.CreateUI(
+ a.TermApp,
+ a.Screen,
+ os.Stdout,
+ !a.Flags.NoColor,
+ a.Flags.ShowApparentSize,
+ a.Flags.EnableGC,
+ )
+
+ if !a.Flags.NoColor {
+ tview.Styles.TitleColor = tcell.NewRGBColor(27, 161, 227)
+ }
+ tview.Styles.BorderColor = tcell.ColorDefault
+ }
+ return ui, nil
+}
+
+func (a *App) setNoCross(path string) error {
+ if a.Flags.NoCross {
+ mounts, err := a.Getter.GetMounts()
+ if err != nil {
+ return fmt.Errorf("loading mount points: %w", err)
+ }
+ paths := device.GetNestedMountpointsPaths(path, mounts)
+ log.Printf("Ignoring mount points: %s", strings.Join(paths, ", "))
+ a.Flags.IgnoreDirs = append(a.Flags.IgnoreDirs, paths...)
+ }
+ return nil
+}
+
+func (a *App) runAction(ui UI, path string) error {
+ if a.Flags.Profiling {
+ go func() {
+ log.Println(http.ListenAndServe("localhost:6060", nil))
+ }()
+ }
+
+ if a.Flags.ShowDisks {
+ if err := ui.ListDevices(a.Getter); err != nil {
+ return fmt.Errorf("loading mount points: %w", err)
+ }
+ } else if a.Flags.InputFile != "" {
+ var input io.Reader
+ var err error
+ if a.Flags.InputFile == "-" {
+ input = os.Stdin
+ } else {
+ input, err = os.OpenFile(a.Flags.InputFile, os.O_RDONLY, 0600)
+ if err != nil {
+ return fmt.Errorf("opening input file: %w", err)
+ }
+ }
+
+ if err := ui.ReadAnalysis(input); err != nil {
+ return fmt.Errorf("reading analysis: %w", err)
+ }
+ } else {
+ if build.RootPathPrefix != "" {
+ path = build.RootPathPrefix + path
+ }
+
+ _, err := a.PathChecker(path)
+ if err != nil {
+ return err
+ }
+
+ log.Printf("Analyzing path: %s", path)
+ if err := ui.AnalyzePath(path, nil); err != nil {
+ return fmt.Errorf("scanning dir: %w", err)
+ }
+ }
+ return nil
+}
--- /dev/null
+//go:build linux
+// +build linux
+
+package app
+
+import (
+ "testing"
+
+ "github.com/dundee/gdu/v5/internal/testdev"
+ "github.com/dundee/gdu/v5/internal/testdir"
+ "github.com/dundee/gdu/v5/pkg/device"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNoCrossWithErr(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ out, err := runApp(
+ &Flags{LogFile: "/dev/null", NoCross: true},
+ []string{"test_dir"},
+ false,
+ device.LinuxDevicesInfoGetter{MountsPath: "/xxxyyy"},
+ )
+
+ assert.Equal(t, "loading mount points: open /xxxyyy: no such file or directory", err.Error())
+ assert.Empty(t, out)
+}
+
+func TestListDevicesWithErr(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ _, err := runApp(
+ &Flags{LogFile: "/dev/null", ShowDisks: true},
+ []string{},
+ false,
+ device.LinuxDevicesInfoGetter{MountsPath: "/xxxyyy"},
+ )
+
+ assert.Equal(t, "loading mount points: open /xxxyyy: no such file or directory", err.Error())
+}
+
+func TestLogError(t *testing.T) {
+ out, err := runApp(
+ &Flags{LogFile: "/xyzxyz"},
+ []string{},
+ false,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.Empty(t, out)
+ assert.Contains(t, err.Error(), "permission denied")
+}
+
+func TestOutputFileError(t *testing.T) {
+ out, err := runApp(
+ &Flags{LogFile: "/dev/null", OutputFile: "/xyzxyz"},
+ []string{},
+ false,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.Empty(t, out)
+ assert.Contains(t, err.Error(), "permission denied")
+}
--- /dev/null
+package app
+
+import (
+ "bytes"
+ "os"
+ "runtime"
+ "strings"
+ "testing"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/dundee/gdu/v5/internal/testapp"
+ "github.com/dundee/gdu/v5/internal/testdev"
+ "github.com/dundee/gdu/v5/internal/testdir"
+ "github.com/dundee/gdu/v5/pkg/device"
+ "github.com/stretchr/testify/assert"
+)
+
+func init() {
+ log.SetLevel(log.WarnLevel)
+}
+
+func TestVersion(t *testing.T) {
+ out, err := runApp(
+ &Flags{ShowVersion: true},
+ []string{},
+ false,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.Contains(t, out, "Version:\t development")
+ assert.Nil(t, err)
+}
+
+func TestAnalyzePath(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ out, err := runApp(
+ &Flags{LogFile: "/dev/null"},
+ []string{"test_dir"},
+ false,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.Contains(t, out, "nested")
+ assert.Nil(t, err)
+}
+
+func TestAnalyzePathProfiling(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ out, err := runApp(
+ &Flags{LogFile: "/dev/null", Profiling: true},
+ []string{"test_dir"},
+ false,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.Contains(t, out, "nested")
+ assert.Nil(t, err)
+}
+
+func TestAnalyzePathWithIgnoring(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ out, err := runApp(
+ &Flags{
+ LogFile: "/dev/null",
+ IgnoreDirPatterns: []string{"/[abc]+"},
+ NoHidden: true,
+ },
+ []string{"test_dir"},
+ false,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.Contains(t, out, "nested")
+ assert.Nil(t, err)
+}
+
+func TestAnalyzePathWithIgnoringPatternError(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ out, err := runApp(
+ &Flags{
+ LogFile: "/dev/null",
+ IgnoreDirPatterns: []string{"[[["},
+ NoHidden: true,
+ },
+ []string{"test_dir"},
+ false,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.Equal(t, out, "")
+ assert.NotNil(t, err)
+}
+
+func TestAnalyzePathWithIgnoringFromNotExistingFile(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ out, err := runApp(
+ &Flags{
+ LogFile: "/dev/null",
+ IgnoreFromFile: "file",
+ NoHidden: true,
+ },
+ []string{"test_dir"},
+ false,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.Equal(t, out, "")
+ assert.NotNil(t, err)
+}
+
+func TestAnalyzePathWithGui(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ out, err := runApp(
+ &Flags{LogFile: "/dev/null"},
+ []string{"test_dir"},
+ true,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.Empty(t, out)
+ assert.Nil(t, err)
+}
+
+func TestAnalyzePathWithExport(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ defer func() {
+ os.Remove("output.json")
+ }()
+
+ out, err := runApp(
+ &Flags{LogFile: "/dev/null", OutputFile: "output.json"},
+ []string{"test_dir"},
+ true,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.NotEmpty(t, out)
+ assert.Nil(t, err)
+}
+
+func TestReadAnalysisFromFile(t *testing.T) {
+ out, err := runApp(
+ &Flags{LogFile: "/dev/null", InputFile: "../../../internal/testdata/test.json"},
+ []string{"test_dir"},
+ false,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.NotEmpty(t, out)
+ assert.Contains(t, out, "main.go")
+ assert.Nil(t, err)
+}
+
+func TestReadWrongAnalysisFromFile(t *testing.T) {
+ out, err := runApp(
+ &Flags{LogFile: "/dev/null", InputFile: "../../../internal/testdata/wrong.json"},
+ []string{"test_dir"},
+ false,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.Empty(t, out)
+ assert.Contains(t, err.Error(), "Array of maps not found")
+}
+
+func TestReadWrongAnalysisFromNotExistingFile(t *testing.T) {
+ out, err := runApp(
+ &Flags{LogFile: "/dev/null", InputFile: "xxx.json"},
+ []string{"test_dir"},
+ false,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.Empty(t, out)
+ assert.Contains(t, err.Error(), "no such file or directory")
+}
+
+func TestAnalyzePathWithErr(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ buff := bytes.NewBufferString("")
+
+ app := App{
+ Flags: &Flags{LogFile: "/dev/null"},
+ Args: []string{"xxx"},
+ Istty: false,
+ Writer: buff,
+ TermApp: testapp.CreateMockedApp(false),
+ Getter: testdev.DevicesInfoGetterMock{},
+ PathChecker: os.Stat,
+ }
+ err := app.Run()
+
+ assert.Equal(t, "", strings.TrimSpace(buff.String()))
+ assert.Contains(t, err.Error(), "no such file or directory")
+}
+
+func TestNoCross(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ out, err := runApp(
+ &Flags{LogFile: "/dev/null", NoCross: true},
+ []string{"test_dir"},
+ false,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.Contains(t, out, "nested")
+ assert.Nil(t, err)
+}
+
+func TestListDevices(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ out, err := runApp(
+ &Flags{LogFile: "/dev/null", ShowDisks: true},
+ []string{},
+ false,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.Contains(t, out, "Device")
+ assert.Nil(t, err)
+}
+
+func TestListDevicesToFile(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ defer func() {
+ os.Remove("output.json")
+ }()
+
+ out, err := runApp(
+ &Flags{LogFile: "/dev/null", ShowDisks: true, OutputFile: "output.json"},
+ []string{},
+ false,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.Equal(t, "", out)
+ assert.Contains(t, err.Error(), "not supported")
+}
+
+func TestListDevicesWithGui(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ out, err := runApp(
+ &Flags{LogFile: "/dev/null", ShowDisks: true},
+ []string{},
+ true,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.Nil(t, err)
+ assert.Empty(t, out)
+}
+
+func TestMaxCores(t *testing.T) {
+ _, err := runApp(
+ &Flags{LogFile: "/dev/null", MaxCores: 1},
+ []string{},
+ true,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.Equal(t, 1, runtime.GOMAXPROCS(0))
+ assert.Nil(t, err)
+}
+
+func TestMaxCoresHighEdge(t *testing.T) {
+ if runtime.NumCPU() < 2 {
+ t.Skip("Skipping on a single core CPU")
+ }
+ out, err := runApp(
+ &Flags{LogFile: "/dev/null", MaxCores: runtime.NumCPU() + 1},
+ []string{},
+ true,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.NotEqual(t, runtime.NumCPU(), runtime.GOMAXPROCS(0))
+ assert.Empty(t, out)
+ assert.Nil(t, err)
+}
+
+func TestMaxCoresLowEdge(t *testing.T) {
+ if runtime.NumCPU() < 2 {
+ t.Skip("Skipping on a single core CPU")
+ }
+ out, err := runApp(
+ &Flags{LogFile: "/dev/null", MaxCores: -100},
+ []string{},
+ true,
+ testdev.DevicesInfoGetterMock{},
+ )
+
+ assert.NotEqual(t, runtime.NumCPU(), runtime.GOMAXPROCS(0))
+ assert.Empty(t, out)
+ assert.Nil(t, err)
+}
+
+func runApp(flags *Flags, args []string, istty bool, getter device.DevicesInfoGetter) (string, error) {
+ buff := bytes.NewBufferString("")
+
+ app := App{
+ Flags: flags,
+ Args: args,
+ Istty: istty,
+ Writer: buff,
+ TermApp: testapp.CreateMockedApp(false),
+ Getter: getter,
+ PathChecker: testdir.MockedPathChecker,
+ }
+ err := app.Run()
+
+ return strings.TrimSpace(buff.String()), err
+}
--- /dev/null
+package main
+
+import (
+ "fmt"
+ "os"
+ "runtime"
+
+ "github.com/dundee/gdu/v5/cmd/gdu/app"
+ "github.com/dundee/gdu/v5/pkg/device"
+ "github.com/gdamore/tcell/v2"
+ "github.com/mattn/go-isatty"
+ "github.com/rivo/tview"
+ "github.com/spf13/cobra"
+)
+
+var af *app.Flags
+
+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: runE,
+}
+
+func init() {
+ af = &app.Flags{}
+ flags := rootCmd.Flags()
+ flags.StringVarP(&af.LogFile, "log-file", "l", "/dev/null", "Path to a logfile")
+ flags.StringVarP(&af.OutputFile, "output-file", "o", "", "Export all info into file as JSON")
+ flags.StringVarP(&af.InputFile, "input-file", "f", "", "Import analysis from JSON file")
+ flags.IntVarP(&af.MaxCores, "max-cores", "m", runtime.NumCPU(), fmt.Sprintf("Set max cores that GDU will use. %d cores available", runtime.NumCPU()))
+ flags.BoolVarP(&af.ShowVersion, "version", "v", false, "Print version")
+
+ flags.StringSliceVarP(&af.IgnoreDirs, "ignore-dirs", "i", []string{"/proc", "/dev", "/sys", "/run"}, "Absolute paths to ignore (separated by comma)")
+ flags.StringSliceVarP(&af.IgnoreDirPatterns, "ignore-dirs-pattern", "I", []string{}, "Absolute path patterns to ignore (separated by comma)")
+ flags.StringVarP(&af.IgnoreFromFile, "ignore-from", "X", "", "Read absolute path patterns to ignore from file")
+ flags.BoolVarP(&af.NoHidden, "no-hidden", "H", false, "Ignore hidden directories (beginning with dot)")
+ flags.BoolVarP(&af.NoCross, "no-cross", "x", false, "Do not cross filesystem boundaries")
+ flags.BoolVarP(&af.EnableGC, "enable-gc", "g", false, "Enable memory garbage collection during analysis")
+ flags.BoolVar(&af.Profiling, "enable-profiling", false, "Enable collection of profiling data and provide it on http://localhost:6060/debug/pprof/")
+
+ flags.BoolVarP(&af.ShowDisks, "show-disks", "d", false, "Show all mounted disks")
+ flags.BoolVarP(&af.ShowApparentSize, "show-apparent-size", "a", false, "Show apparent size")
+ flags.BoolVarP(&af.NoColor, "no-color", "c", false, "Do not use colorized output")
+ flags.BoolVarP(&af.NonInteractive, "non-interactive", "n", false, "Do not run in interactive mode")
+ flags.BoolVarP(&af.NoProgress, "no-progress", "p", false, "Do not show progress in non-interactive mode")
+ flags.BoolVarP(&af.Summarize, "summarize", "s", false, "Show only a total in non-interactive mode")
+}
+
+func runE(command *cobra.Command, args []string) error {
+ var (
+ termApp *tview.Application
+ screen tcell.Screen
+ err 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" {
+ af.ShowApparentSize = true
+ }
+ if runtime.GOOS == "windows" && af.LogFile == "/dev/null" {
+ af.LogFile = "nul"
+ }
+
+ if !af.ShowVersion && !af.NonInteractive && istty && af.OutputFile == "" {
+ screen, err = tcell.NewScreen()
+ if err != nil {
+ return fmt.Errorf("Error creating screen: %w", err)
+ }
+ err = screen.Init()
+ if err != nil {
+ return fmt.Errorf("Error initializing screen: %w", err)
+ }
+ defer screen.Clear()
+ defer screen.Fini()
+
+ termApp = tview.NewApplication()
+ termApp.SetScreen(screen)
+ }
+
+ a := app.App{
+ Flags: af,
+ Args: args,
+ Istty: istty,
+ Writer: os.Stdout,
+ TermApp: termApp,
+ Screen: screen,
+ Getter: device.Getter,
+ PathChecker: os.Stat,
+ }
+ return a.Run()
+}
+
+func main() {
+ if err := rootCmd.Execute(); err != nil {
+ os.Exit(1)
+ }
+}
--- /dev/null
+coverage:
+ status:
+ project:
+ default:
+ target: auto
+ threshold: 2%
+ informational: true
+ patch:
+ default:
+ informational: true
\ No newline at end of file
--- /dev/null
+.\" Automatically generated by Pandoc 2.14.1
+.\"
+.TH "gdu" "1" "2021-11-27" "" ""
+.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]-I\f[R], \f[B]--ignore-dirs-pattern\f[R] Absolute path patterns to
+ignore (separated by comma)
+.PP
+\f[B]-X\f[R], \f[B]--ignore-from\f[R] Read absolute path patterns to
+ignore from file
+.PP
+\f[B]-l\f[R], \f[B]--log-file\f[R]=\[dq]/dev/null\[dq] Path to a logfile
+.PP
+\f[B]-m\f[R], \f[B]--max-cores\f[R] Set max cores that GDU will use.
+.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]-H\f[R], \f[B]--no-hidden\f[R][=false] Ignore hidden directories
+(beginning with dot)
+.PP
+\f[B]-n\f[R], \f[B]--non-interactive\f[R][=false] Do not run in
+interactive mode
+.PP
+\f[B]-p\f[R], \f[B]--no-progress\f[R][=false] Do not show progress in
+non-interactive mode
+.PP
+\f[B]-s\f[R], \f[B]--summarize\f[R][=false] Show only a total in
+non-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]-f\f[R], \f[B]-\[em]input-file\f[R] Import analysis from JSON file.
+If the file is \[dq]-\[dq], read from standard input.
+.PP
+\f[B]-o\f[R], \f[B]-\[em]output-file\f[R] Export all info into file as
+JSON.
+If the file is \[dq]-\[dq], write to standard output.
+.PP
+\f[B]-g\f[R], \f[B]--enable-gc\f[R][=false] Enable garbage collection
+during analysis
+.PP
+\f[B]--enable-profiling\f[R][=false] Enable collection of profiling data
+and provide it on http://localhost:6060/debug/pprof/
+.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.
--- /dev/null
+---
+date: {{date}}
+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)
+
+**-I**, **\--ignore-dirs-pattern** Absolute path patterns to
+ignore (separated by comma)
+
+**-X**, **\--ignore-from** Read absolute path patterns to ignore from file
+
+**-l**, **\--log-file**=\"/dev/null\" Path to a logfile
+
+**-m**, **\--max-cores** Set max cores that GDU will use.
+
+**-c**, **\--no-color**\[=false\] Do not use colorized output
+
+**-x**, **\--no-cross**\[=false\] Do not cross filesystem boundaries
+
+**-H**, **\--no-hidden**\[=false\] Ignore hidden directories (beginning with dot)
+
+**-n**, **\--non-interactive**\[=false\] Do not run in interactive mode
+
+**-p**, **\--no-progress**\[=false\] Do not show progress in
+non-interactive mode
+
+**-s**, **\--summarize**\[=false\] Show only a total in non-interactive mode
+
+**-d**, **\--show-disks**\[=false\] Show all mounted disks
+
+**-a**, **\--show-apparent-size**\[=false\] Show apparent size
+
+**-f**, **\----input-file** Import analysis from JSON file. If the file is \"-\", read from standard input.
+
+**-o**, **\----output-file** Export all info into file as JSON. If the file is \"-\", write to standard output.
+
+**-g**, **\--enable-gc**\[=false\] Enable garbage collection during analysis
+
+**\--enable-profiling**\[=false\] Enable collection of profiling data and provide it on http://localhost:6060/debug/pprof/
+
+**-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.
--- /dev/null
+module github.com/dundee/gdu/v5
+
+go 1.16
+
+require (
+ github.com/fatih/color v1.12.0
+ github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1
+ github.com/mattn/go-isatty v0.0.13
+ github.com/rivo/tview v0.0.0-20210624165335-29d673af0ce2
+ github.com/sirupsen/logrus v1.8.1
+ github.com/spf13/cobra v1.2.1
+ github.com/stretchr/testify v1.7.0
+ golang.org/x/sys v0.0.0-20210903071746-97244b99971b
+ golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
+ golang.org/x/text v0.3.7 // indirect
+)
--- /dev/null
+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 v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
+cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
+cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
+cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
+cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+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/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+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/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
+github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+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.3.3/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
+github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 h1:QqwPZCwh/k1uYqq6uXSb9TRDhTkfQbO80v8zhnIe5zM=
+github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04=
+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-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/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/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
+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/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+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/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+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/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+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/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+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/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
+github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
+github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
+github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+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-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/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
+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_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rivo/tview v0.0.0-20210624165335-29d673af0ce2 h1:I5N0WNMgPSq5NKUFspB4jMJ6n2P0ipz5FlOlB4BXviQ=
+github.com/rivo/tview v0.0.0-20210624165335-29d673af0ce2/go.mod h1:IxQujbYMAh4trWr0Dwa8jfciForjVmxyHpskZX6aydQ=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+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 v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+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.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
+github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+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/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
+github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
+github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+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.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
+github.com/stretchr/objx v0.1.0/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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
+go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
+go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
+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/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+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/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+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/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+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/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+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-20181201002055-351d144fa1fc/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/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+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/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+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/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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-20181026203630-95b1ffbd15a5/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-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210903071746-97244b99971b h1:3Dq0eVHn0uaQJmPO+/aYPI/fRMqdrVDbu7MQcku54gg=
+golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
+golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+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/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+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/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+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-20190524140312-2c0ae7006135/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/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
+google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
+google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
+google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
+google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
+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/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+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/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
+google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+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=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+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.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
--- /dev/null
+package common
+
+import (
+ "github.com/gdamore/tcell/v2"
+ "github.com/rivo/tview"
+)
+
+// TermApplication is interface for the terminal UI app
+type TermApplication interface {
+ Run() error
+ Stop()
+ Suspend(f func()) bool
+ 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
+}
--- /dev/null
+package common
+
+import (
+ "bufio"
+ "os"
+ "regexp"
+ "strings"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/dundee/gdu/v5/pkg/analyze"
+)
+
+// CreateIgnorePattern creates one pattern from all path patterns
+func CreateIgnorePattern(paths []string) (*regexp.Regexp, error) {
+ var err error
+
+ for i, path := range paths {
+ if _, err = regexp.Compile(path); err != nil {
+ return nil, err
+ }
+ paths[i] = "(" + path + ")"
+ }
+
+ ignore := `^` + strings.Join(paths, "|") + `$`
+ return regexp.Compile(ignore)
+}
+
+// SetIgnoreDirPaths sets paths to ignore
+func (ui *UI) SetIgnoreDirPaths(paths []string) {
+ log.Printf("Ignoring dirs %s", strings.Join(paths, ", "))
+ ui.IgnoreDirPaths = make(map[string]struct{}, len(paths))
+ for _, path := range paths {
+ ui.IgnoreDirPaths[path] = struct{}{}
+ }
+}
+
+// SetIgnoreDirPatterns sets regular patters of dirs to ignore
+func (ui *UI) SetIgnoreDirPatterns(paths []string) error {
+ var err error
+ log.Printf("Ignoring dir patterns %s", strings.Join(paths, ", "))
+ ui.IgnoreDirPathPatterns, err = CreateIgnorePattern(paths)
+ return err
+}
+
+// SetIgnoreFromFile sets regular patters of dirs to ignore
+func (ui *UI) SetIgnoreFromFile(ignoreFile string) error {
+ var err error
+ var paths []string
+ log.Printf("Reading ignoring dir patterns from file '%s'", ignoreFile)
+
+ file, err := os.Open(ignoreFile)
+
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ scanner := bufio.NewScanner(file)
+ for scanner.Scan() {
+ paths = append(paths, scanner.Text())
+ }
+
+ if err := scanner.Err(); err != nil {
+ return err
+ }
+
+ ui.IgnoreDirPathPatterns, err = CreateIgnorePattern(paths)
+ return err
+}
+
+// SetIgnoreHidden sets flags if hidden dirs should be ignored
+func (ui *UI) SetIgnoreHidden(value bool) {
+ log.Printf("Ignoring hidden dirs")
+ ui.IgnoreHidden = value
+}
+
+// ShouldDirBeIgnored returns true if given path should be ignored
+func (ui *UI) ShouldDirBeIgnored(name, path string) bool {
+ _, shouldIgnore := ui.IgnoreDirPaths[path]
+ if shouldIgnore {
+ log.Printf("Directory %s ignored", path)
+ }
+ return shouldIgnore
+}
+
+// ShouldDirBeIgnoredUsingPattern returns true if given path should be ignored
+func (ui *UI) ShouldDirBeIgnoredUsingPattern(name, path string) bool {
+ shouldIgnore := ui.IgnoreDirPathPatterns.MatchString(path)
+ if shouldIgnore {
+ log.Printf("Directory %s ignored", path)
+ }
+ return shouldIgnore
+}
+
+// IsHiddenDir returns if the dir name begins with dot
+func (ui *UI) IsHiddenDir(name, path string) bool {
+ shouldIgnore := name[0] == '.'
+ if shouldIgnore {
+ log.Printf("Directory %s ignored", path)
+ }
+ return shouldIgnore
+}
+
+// CreateIgnoreFunc returns function for detecting if dir should be ignored
+func (ui *UI) CreateIgnoreFunc() analyze.ShouldDirBeIgnored {
+ switch {
+ case len(ui.IgnoreDirPaths) > 0 && ui.IgnoreDirPathPatterns == nil && !ui.IgnoreHidden:
+ return ui.ShouldDirBeIgnored
+ case len(ui.IgnoreDirPaths) > 0 && ui.IgnoreDirPathPatterns != nil && !ui.IgnoreHidden:
+ return func(name, path string) bool {
+ return ui.ShouldDirBeIgnored(name, path) || ui.ShouldDirBeIgnoredUsingPattern(name, path)
+ }
+ case len(ui.IgnoreDirPaths) > 0 && ui.IgnoreDirPathPatterns != nil && ui.IgnoreHidden:
+ return func(name, path string) bool {
+ return ui.ShouldDirBeIgnored(name, path) || ui.ShouldDirBeIgnoredUsingPattern(name, path) || ui.IsHiddenDir(name, path)
+ }
+ case len(ui.IgnoreDirPaths) == 0 && ui.IgnoreDirPathPatterns != nil && ui.IgnoreHidden:
+ return func(name, path string) bool {
+ return ui.ShouldDirBeIgnoredUsingPattern(name, path) || ui.IsHiddenDir(name, path)
+ }
+ case len(ui.IgnoreDirPaths) == 0 && ui.IgnoreDirPathPatterns != nil && !ui.IgnoreHidden:
+ return ui.ShouldDirBeIgnoredUsingPattern
+ case len(ui.IgnoreDirPaths) == 0 && ui.IgnoreDirPathPatterns == nil && ui.IgnoreHidden:
+ return ui.IsHiddenDir
+ case len(ui.IgnoreDirPaths) > 0 && ui.IgnoreDirPathPatterns == nil && ui.IgnoreHidden:
+ return func(name, path string) bool {
+ return ui.ShouldDirBeIgnored(name, path) || ui.IsHiddenDir(name, path)
+ }
+ default:
+ return func(name, path string) bool { return false }
+ }
+}
--- /dev/null
+package common_test
+
+import (
+ "os"
+ "testing"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/dundee/gdu/v5/internal/common"
+ "github.com/stretchr/testify/assert"
+)
+
+func init() {
+ log.SetLevel(log.WarnLevel)
+}
+
+func TestCreateIgnorePattern(t *testing.T) {
+ re, err := common.CreateIgnorePattern([]string{"[abc]+"})
+
+ assert.Nil(t, err)
+ assert.True(t, re.Match([]byte("aa")))
+}
+
+func TestCreateIgnorePatternWithErr(t *testing.T) {
+ re, err := common.CreateIgnorePattern([]string{"[[["})
+
+ assert.NotNil(t, err)
+ assert.Nil(t, re)
+}
+
+func TestEmptyIgnore(t *testing.T) {
+ ui := &common.UI{}
+ shouldBeIgnored := ui.CreateIgnoreFunc()
+
+ assert.False(t, shouldBeIgnored("abc", "/abc"))
+ assert.False(t, shouldBeIgnored("xxx", "/xxx"))
+}
+
+func TestIgnoreByAbsPath(t *testing.T) {
+ ui := &common.UI{}
+ ui.SetIgnoreDirPaths([]string{"/abc"})
+ shouldBeIgnored := ui.CreateIgnoreFunc()
+
+ assert.True(t, shouldBeIgnored("abc", "/abc"))
+ assert.False(t, shouldBeIgnored("xxx", "/xxx"))
+}
+
+func TestIgnoreByPattern(t *testing.T) {
+ ui := &common.UI{}
+ err := ui.SetIgnoreDirPatterns([]string{"/[abc]+"})
+ assert.Nil(t, err)
+ shouldBeIgnored := ui.CreateIgnoreFunc()
+
+ assert.True(t, shouldBeIgnored("aaa", "/aaa"))
+ assert.True(t, shouldBeIgnored("aaa", "/aaabc"))
+ assert.False(t, shouldBeIgnored("xxx", "/xxx"))
+}
+
+func TestIgnoreFromFile(t *testing.T) {
+ file, err := os.OpenFile("ignore", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+ if err != nil {
+ panic(err)
+ }
+ defer file.Close()
+
+ if _, err := file.Write([]byte("/aaa\n")); err != nil {
+ panic(err)
+ }
+ if _, err := file.Write([]byte("/aaabc\n")); err != nil {
+ panic(err)
+ }
+ if _, err := file.Write([]byte("/[abd]+\n")); err != nil {
+ panic(err)
+ }
+
+ ui := &common.UI{}
+ err = ui.SetIgnoreFromFile("ignore")
+ assert.Nil(t, err)
+ shouldBeIgnored := ui.CreateIgnoreFunc()
+
+ assert.True(t, shouldBeIgnored("aaa", "/aaa"))
+ assert.True(t, shouldBeIgnored("aaabc", "/aaabc"))
+ assert.True(t, shouldBeIgnored("aaabd", "/aaabd"))
+ assert.False(t, shouldBeIgnored("xxx", "/xxx"))
+}
+
+func TestIgnoreFromNotExistingFile(t *testing.T) {
+ ui := &common.UI{}
+ err := ui.SetIgnoreFromFile("xxx")
+ assert.NotNil(t, err)
+}
+
+func TestIgnoreHidden(t *testing.T) {
+ ui := &common.UI{}
+ ui.SetIgnoreHidden(true)
+ shouldBeIgnored := ui.CreateIgnoreFunc()
+
+ assert.True(t, shouldBeIgnored(".git", "/aaa/.git"))
+ assert.True(t, shouldBeIgnored(".bbb", "/aaa/.bbb"))
+ assert.False(t, shouldBeIgnored("xxx", "/xxx"))
+}
+
+func TestIgnoreByAbsPathAndHidden(t *testing.T) {
+ ui := &common.UI{}
+ ui.SetIgnoreDirPaths([]string{"/abc"})
+ ui.SetIgnoreHidden(true)
+ shouldBeIgnored := ui.CreateIgnoreFunc()
+
+ assert.True(t, shouldBeIgnored("abc", "/abc"))
+ assert.True(t, shouldBeIgnored(".git", "/aaa/.git"))
+ assert.True(t, shouldBeIgnored(".bbb", "/aaa/.bbb"))
+ assert.False(t, shouldBeIgnored("xxx", "/xxx"))
+}
+
+func TestIgnoreByAbsPathAndPattern(t *testing.T) {
+ ui := &common.UI{}
+ ui.SetIgnoreDirPaths([]string{"/abc"})
+ err := ui.SetIgnoreDirPatterns([]string{"/[abc]+"})
+ assert.Nil(t, err)
+ shouldBeIgnored := ui.CreateIgnoreFunc()
+
+ assert.True(t, shouldBeIgnored("abc", "/abc"))
+ assert.True(t, shouldBeIgnored("aabc", "/aabc"))
+ assert.True(t, shouldBeIgnored("ccc", "/ccc"))
+ assert.False(t, shouldBeIgnored("xxx", "/xxx"))
+}
+
+func TestIgnoreByPatternAndHidden(t *testing.T) {
+ ui := &common.UI{}
+ err := ui.SetIgnoreDirPatterns([]string{"/[abc]+"})
+ assert.Nil(t, err)
+ ui.SetIgnoreHidden(true)
+ shouldBeIgnored := ui.CreateIgnoreFunc()
+
+ assert.True(t, shouldBeIgnored("abbc", "/abbc"))
+ assert.True(t, shouldBeIgnored(".git", "/aaa/.git"))
+ assert.True(t, shouldBeIgnored(".bbb", "/aaa/.bbb"))
+ assert.False(t, shouldBeIgnored("xxx", "/xxx"))
+}
+
+func TestIgnoreByAll(t *testing.T) {
+ ui := &common.UI{}
+ ui.SetIgnoreDirPaths([]string{"/abc"})
+ err := ui.SetIgnoreDirPatterns([]string{"/[abc]+"})
+ assert.Nil(t, err)
+ ui.SetIgnoreHidden(true)
+ shouldBeIgnored := ui.CreateIgnoreFunc()
+
+ assert.True(t, shouldBeIgnored("abc", "/abc"))
+ assert.True(t, shouldBeIgnored("aabc", "/aabc"))
+ assert.True(t, shouldBeIgnored(".git", "/aaa/.git"))
+ assert.True(t, shouldBeIgnored(".bbb", "/aaa/.bbb"))
+ assert.False(t, shouldBeIgnored("xxx", "/xxx"))
+}
--- /dev/null
+package common
+
+import (
+ "regexp"
+ "strconv"
+
+ "github.com/dundee/gdu/v5/pkg/analyze"
+)
+
+// UI struct
+type UI struct {
+ Analyzer analyze.Analyzer
+ IgnoreDirPaths map[string]struct{}
+ IgnoreDirPathPatterns *regexp.Regexp
+ IgnoreHidden bool
+ UseColors bool
+ ShowProgress bool
+ ShowApparentSize bool
+ EnableGC bool
+}
+
+// file size constants
+const (
+ _ = iota
+ KB float64 = 1 << (10 * iota)
+ MB
+ GB
+ TB
+ PB
+ EB
+)
+
+// file count constants
+const (
+ K int = 1e3
+ M int = 1e6
+ G int = 1e9
+)
+
+// FormatNumber returns number as a string with thousands separator
+func FormatNumber(n int64) string {
+ in := []byte(strconv.FormatInt(n, 10))
+
+ var out []byte
+ if i := len(in) % 3; i != 0 {
+ if out, in = append(out, in[:i]...), in[i:]; len(in) > 0 {
+ out = append(out, ',')
+ }
+ }
+ for len(in) > 0 {
+ if out, in = append(out, in[:3]...), in[3:]; len(in) > 0 {
+ out = append(out, ',')
+ }
+ }
+ return string(out)
+}
--- /dev/null
+package common
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestFormatNumber(t *testing.T) {
+ res := FormatNumber(1234567890)
+ assert.Equal(t, "1,234,567,890", res)
+}
--- /dev/null
+package testanalyze
+
+import (
+ "errors"
+ "time"
+
+ "github.com/dundee/gdu/v5/pkg/analyze"
+)
+
+// MockedAnalyzer returns dir with files with different size exponents
+type MockedAnalyzer struct{}
+
+// AnalyzeDir returns dir with files with different size exponents
+func (a *MockedAnalyzer) AnalyzeDir(path string, ignore analyze.ShouldDirBeIgnored) *analyze.Dir {
+ dir := &analyze.Dir{
+ File: &analyze.File{
+ Name: "test_dir",
+ Usage: 1e12 + 1,
+ Size: 1e12 + 2,
+ Mtime: time.Date(2021, 8, 27, 22, 23, 24, 0, time.UTC),
+ },
+ BasePath: ".",
+ ItemCount: 12,
+ }
+ dir2 := &analyze.Dir{
+ File: &analyze.File{
+ Name: "aaa",
+ Usage: 1e12 + 1,
+ Size: 1e12 + 2,
+ Mtime: time.Date(2021, 8, 27, 22, 23, 27, 0, time.UTC),
+ Parent: dir,
+ },
+ }
+ dir3 := &analyze.Dir{
+ File: &analyze.File{
+ Name: "bbb",
+ Usage: 1e9 + 1,
+ Size: 1e9 + 2,
+ Mtime: time.Date(2021, 8, 27, 22, 23, 26, 0, time.UTC),
+ Parent: dir,
+ },
+ }
+ dir4 := &analyze.Dir{
+ File: &analyze.File{
+ Name: "ccc",
+ Usage: 1e6 + 1,
+ Size: 1e6 + 2,
+ Mtime: time.Date(2021, 8, 27, 22, 23, 25, 0, time.UTC),
+ Parent: dir,
+ },
+ }
+ file := &analyze.File{
+ Name: "ddd",
+ Usage: 1e3 + 1,
+ Size: 1e3 + 2,
+ Mtime: time.Date(2021, 8, 27, 22, 23, 24, 0, time.UTC),
+ Parent: dir,
+ }
+ dir.Files = analyze.Files{dir2, dir3, dir4, file}
+
+ return dir
+}
+
+// GetProgressChan returns always Done
+func (a *MockedAnalyzer) GetProgressChan() chan analyze.CurrentProgress {
+ return make(chan analyze.CurrentProgress)
+}
+
+// GetDoneChan returns always Done
+func (a *MockedAnalyzer) GetDoneChan() chan struct{} {
+ c := make(chan struct{}, 1)
+ defer func() { c <- struct{}{} }()
+ return c
+}
+
+// ResetProgress does nothing
+func (a *MockedAnalyzer) ResetProgress() {}
+
+// RemoveItemFromDirWithErr returns error
+func RemoveItemFromDirWithErr(dir *analyze.Dir, file analyze.Item) error {
+ return errors.New("Failed")
+}
--- /dev/null
+package testapp
+
+import (
+ "errors"
+ "sync"
+
+ "github.com/dundee/gdu/v5/internal/common"
+ "github.com/gdamore/tcell/v2"
+ "github.com/rivo/tview"
+)
+
+// CreateSimScreen returns tcell.SimulationScreen
+func CreateSimScreen(width, height int) tcell.SimulationScreen {
+ screen := tcell.NewSimulationScreen("UTF-8")
+ err := screen.Init()
+ if err != nil {
+ panic(err)
+ }
+ screen.SetSize(width, height)
+
+ return screen
+}
+
+// CreateTestAppWithSimScreen returns app with simulation screen for tests
+func CreateTestAppWithSimScreen(width, height int) (*tview.Application, tcell.SimulationScreen) {
+ app := tview.NewApplication()
+ screen := CreateSimScreen(width, height)
+ app.SetScreen(screen)
+ return app, screen
+}
+
+// MockedApp is tview.Application with mocked methods
+type MockedApp struct {
+ FailRun bool
+ UpdateDraws []func()
+ BeforeDraws []func(screen tcell.Screen) bool
+ mutex *sync.Mutex
+}
+
+// CreateMockedApp returns app with simulation screen for tests
+func CreateMockedApp(failRun bool) common.TermApplication {
+ app := &MockedApp{
+ FailRun: failRun,
+ UpdateDraws: make([]func(), 0, 1),
+ BeforeDraws: make([]func(screen tcell.Screen) bool, 0, 1),
+ mutex: &sync.Mutex{},
+ }
+ return app
+}
+
+// Run does nothing
+func (app *MockedApp) Run() error {
+ if app.FailRun {
+ return errors.New("Fail")
+ }
+
+ return nil
+}
+
+// Stop does nothing
+func (app *MockedApp) Stop() {}
+
+// Suspend runs given function
+func (app *MockedApp) Suspend(f func()) bool {
+ f()
+ return true
+}
+
+// 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 {
+ app.mutex.Lock()
+ app.UpdateDraws = append(app.UpdateDraws, f)
+ app.mutex.Unlock()
+ return nil
+}
+
+// SetBeforeDrawFunc does nothing
+func (app *MockedApp) SetBeforeDrawFunc(f func(screen tcell.Screen) bool) *tview.Application {
+ app.BeforeDraws = append(app.BeforeDraws, f)
+ return nil
+}
--- /dev/null
+[1,2,{"progname":"gdu","progver":"development","timestamp":1626807263},
+[{"name":"/home/gdu"},
+[{"name":"app"},
+{"name":"app.go","asize":4638,"dsize":8192},
+{"name":"app_linux_test.go","asize":1410,"dsize":4096},
+{"name":"app_test.go","asize":4974,"dsize":8192}],
+{"name":"main.go","asize":3205,"dsize":4096}]]
--- /dev/null
+package testdev
+
+import "github.com/dundee/gdu/v5/pkg/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
+}
--- /dev/null
+package testdir
+
+import (
+ "io/fs"
+ "os"
+)
+
+// CreateTestDir creates test dir structure
+func CreateTestDir() func() {
+ if err := os.MkdirAll("test_dir/nested/subnested", os.ModePerm); err != nil {
+ panic(err)
+ }
+ if err := os.WriteFile("test_dir/nested/subnested/file", []byte("hello"), 0600); err != nil {
+ panic(err)
+ }
+ if err := os.WriteFile("test_dir/nested/file2", []byte("go"), 0600); err != nil {
+ panic(err)
+ }
+ return func() {
+ err := os.RemoveAll("test_dir")
+ if err != nil {
+ panic(err)
+ }
+ }
+}
+
+// MockedPathChecker is mocked os.Stat, returns (nil, nil)
+func MockedPathChecker(path string) (fs.FileInfo, error) {
+ return nil, nil
+}
--- /dev/null
+package analyze
+
+import (
+ "os"
+ "path/filepath"
+ "runtime"
+
+ log "github.com/sirupsen/logrus"
+)
+
+// CurrentProgress struct
+type CurrentProgress struct {
+ CurrentItemName string
+ ItemCount int
+ TotalSize int64
+}
+
+var concurrencyLimit = make(chan struct{}, 3*runtime.GOMAXPROCS(0))
+
+// ShouldDirBeIgnored whether path should be ignored
+type ShouldDirBeIgnored func(name, path string) bool
+
+// Analyzer is type for dir analyzing function
+type Analyzer interface {
+ AnalyzeDir(path string, ignore ShouldDirBeIgnored) *Dir
+ GetProgressChan() chan CurrentProgress
+ GetDoneChan() chan struct{}
+ ResetProgress()
+}
+
+// ParallelAnalyzer implements Analyzer
+type ParallelAnalyzer struct {
+ progress *CurrentProgress
+ progressChan chan CurrentProgress
+ progressOutChan chan CurrentProgress
+ doneChan chan struct{}
+ wait *WaitGroup
+ ignoreDir ShouldDirBeIgnored
+}
+
+// CreateAnalyzer returns Analyzer
+func CreateAnalyzer() Analyzer {
+ return &ParallelAnalyzer{
+ progress: &CurrentProgress{
+ ItemCount: 0,
+ TotalSize: int64(0),
+ },
+ progressChan: make(chan CurrentProgress, 1),
+ progressOutChan: make(chan CurrentProgress, 1),
+ doneChan: make(chan struct{}, 1),
+ wait: (&WaitGroup{}).Init(),
+ }
+}
+
+// GetProgressChan returns channel for getting progress
+func (a *ParallelAnalyzer) GetProgressChan() chan CurrentProgress {
+ return a.progressOutChan
+}
+
+// GetDoneChan returns channel for checking when analysis is done
+func (a *ParallelAnalyzer) GetDoneChan() chan struct{} {
+ return a.doneChan
+}
+
+// ResetProgress returns progress
+func (a *ParallelAnalyzer) ResetProgress() {
+ a.progress.ItemCount = 0
+ a.progress.TotalSize = int64(0)
+ a.progress.CurrentItemName = ""
+}
+
+// AnalyzeDir analyzes given path
+func (a *ParallelAnalyzer) AnalyzeDir(path string, ignore ShouldDirBeIgnored) *Dir {
+ a.ignoreDir = ignore
+
+ go a.updateProgress()
+ dir := a.processDir(path)
+
+ dir.BasePath = filepath.Dir(path)
+ a.wait.Wait()
+
+ a.doneChan <- struct{}{} // finish updateProgress here
+ a.doneChan <- struct{}{} // and there
+
+ return dir
+}
+
+func (a *ParallelAnalyzer) processDir(path string) *Dir {
+ var (
+ file *File
+ err error
+ totalSize int64
+ info os.FileInfo
+ subDirChan = make(chan *Dir)
+ dirCount int
+ )
+
+ a.wait.Add(1)
+
+ files, err := os.ReadDir(path)
+ if err != nil {
+ log.Print(err.Error())
+ }
+
+ dir := &Dir{
+ File: &File{
+ Name: filepath.Base(path),
+ Flag: getDirFlag(err, len(files)),
+ },
+ ItemCount: 1,
+ Files: make([]Item, 0, len(files)),
+ }
+ setDirPlatformSpecificAttrs(dir, path)
+
+ for _, f := range files {
+ name := f.Name()
+ entryPath := filepath.Join(path, name)
+ if f.IsDir() {
+ if a.ignoreDir(name, entryPath) {
+ continue
+ }
+ dirCount++
+
+ go func(entryPath string) {
+ concurrencyLimit <- struct{}{}
+ subdir := a.processDir(entryPath)
+ subdir.Parent = dir
+
+ subDirChan <- subdir
+ <-concurrencyLimit
+ }(entryPath)
+ } else {
+ info, err = f.Info()
+ if err != nil {
+ log.Print(err.Error())
+ continue
+ }
+ file = &File{
+ Name: name,
+ Flag: getFlag(info),
+ Size: info.Size(),
+ Parent: dir,
+ }
+ setPlatformSpecificAttrs(file, info)
+
+ totalSize += info.Size()
+
+ dir.Files.Append(file)
+ }
+ }
+
+ go func() {
+ var sub *Dir
+
+ for i := 0; i < dirCount; i++ {
+ sub = <-subDirChan
+ dir.Files.Append(sub)
+ }
+
+ a.wait.Done()
+ }()
+
+ a.progressChan <- CurrentProgress{path, len(files), totalSize}
+ return dir
+}
+
+func (a *ParallelAnalyzer) updateProgress() {
+ for {
+ select {
+ case <-a.doneChan:
+ return
+ case progress := <-a.progressChan:
+ a.progress.CurrentItemName = progress.CurrentItemName
+ a.progress.ItemCount += progress.ItemCount
+ a.progress.TotalSize += progress.TotalSize
+ }
+
+ select {
+ case a.progressOutChan <- *a.progress:
+ default:
+ }
+ }
+}
+
+func getDirFlag(err error, items int) rune {
+ switch {
+ case err != nil:
+ return '!'
+ case items == 0:
+ return 'e'
+ default:
+ return ' '
+ }
+}
+
+func getFlag(f os.FileInfo) rune {
+ switch {
+ case f.Mode()&os.ModeSymlink != 0:
+ fallthrough
+ case f.Mode()&os.ModeSocket != 0:
+ return '@'
+ default:
+ return ' '
+ }
+}
--- /dev/null
+//go:build linux || openbsd
+// +build linux openbsd
+
+package analyze
+
+import (
+ "os"
+ "syscall"
+ "time"
+)
+
+const devBSize = 512
+
+func setPlatformSpecificAttrs(file *File, f os.FileInfo) {
+ switch stat := f.Sys().(type) {
+ case *syscall.Stat_t:
+ file.Usage = stat.Blocks * devBSize
+ file.Mtime = time.Unix(int64(stat.Mtim.Sec), int64(stat.Mtim.Nsec))
+
+ if stat.Nlink > 1 {
+ file.Mli = stat.Ino
+ }
+ }
+}
+
+func setDirPlatformSpecificAttrs(dir *Dir, path string) {
+ var stat syscall.Stat_t
+ if err := syscall.Stat(path, &stat); err != nil {
+ return
+ }
+
+ dir.Mtime = time.Unix(int64(stat.Mtim.Sec), int64(stat.Mtim.Nsec))
+}
--- /dev/null
+//go:build windows || plan9
+// +build windows plan9
+
+package analyze
+
+import (
+ "os"
+)
+
+func setPlatformSpecificAttrs(file *File, f os.FileInfo) {}
+
+func setDirPlatformSpecificAttrs(dir *Dir, path string) {}
--- /dev/null
+package analyze
+
+import (
+ "os"
+ "sort"
+ "testing"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/dundee/gdu/v5/internal/testdir"
+ "github.com/stretchr/testify/assert"
+)
+
+func init() {
+ log.SetLevel(log.WarnLevel)
+}
+
+func TestAnalyzeDir(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ analyzer := CreateAnalyzer()
+ dir := analyzer.AnalyzeDir("test_dir", func(_, _ string) bool { return false })
+
+ progress := <-analyzer.GetProgressChan()
+ assert.GreaterOrEqual(t, progress.TotalSize, int64(0))
+ analyzer.ResetProgress()
+
+ <-analyzer.GetDoneChan()
+ dir.UpdateStats(make(HardLinkedItems))
+
+ // 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].GetName())
+ assert.Equal(t, "subnested", dir.Files[0].(*Dir).Files[1].GetName())
+
+ // test file
+ assert.Equal(t, "file2", dir.Files[0].(*Dir).Files[0].GetName())
+ assert.Equal(t, int64(2), dir.Files[0].(*Dir).Files[0].GetSize())
+
+ assert.Equal(t, "file", dir.Files[0].(*Dir).Files[1].(*Dir).Files[0].GetName())
+ assert.Equal(t, int64(5), dir.Files[0].(*Dir).Files[1].(*Dir).Files[0].GetSize())
+
+ // test parent link
+ assert.Equal(t, "test_dir", dir.Files[0].(*Dir).Files[1].(*Dir).Files[0].GetParent().GetParent().GetParent().Name)
+}
+
+func TestIgnoreDir(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ dir := CreateAnalyzer().AnalyzeDir("test_dir", 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()
+
+ err := os.Mkdir("test_dir/empty", 0644)
+ assert.Nil(t, err)
+
+ err = os.Symlink("test_dir/nested/file2", "test_dir/nested/file3")
+ assert.Nil(t, err)
+
+ analyzer := CreateAnalyzer()
+ dir := analyzer.AnalyzeDir("test_dir", func(_, _ string) bool { return false })
+ <-analyzer.GetDoneChan()
+ dir.UpdateStats(make(HardLinkedItems))
+
+ 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].GetName())
+ assert.Equal(t, "file3", dir.Files[0].(*Dir).Files[1].GetName())
+ assert.Equal(t, int64(21), dir.Files[0].(*Dir).Files[1].GetSize())
+ assert.Equal(t, '@', dir.Files[0].(*Dir).Files[1].GetFlag())
+
+ assert.Equal(t, 'e', dir.Files[1].GetFlag())
+}
+
+func TestHardlink(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ err := os.Link("test_dir/nested/file2", "test_dir/nested/file3")
+ assert.Nil(t, err)
+
+ analyzer := CreateAnalyzer()
+ dir := analyzer.AnalyzeDir("test_dir", func(_, _ string) bool { return false })
+ <-analyzer.GetDoneChan()
+ dir.UpdateStats(make(HardLinkedItems))
+
+ 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].(*Dir).Files[1].GetName())
+ assert.Equal(t, int64(2), dir.Files[0].(*Dir).Files[1].GetSize())
+ assert.Equal(t, 'H', dir.Files[0].(*Dir).Files[1].GetFlag())
+}
+
+func TestErr(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ err := os.Chmod("test_dir/nested", 0)
+ assert.Nil(t, err)
+ defer func() {
+ err = os.Chmod("test_dir/nested", 0755)
+ assert.Nil(t, err)
+ }()
+
+ analyzer := CreateAnalyzer()
+ dir := analyzer.AnalyzeDir("test_dir", func(_, _ string) bool { return false })
+ <-analyzer.GetDoneChan()
+ dir.UpdateStats(make(HardLinkedItems))
+
+ assert.Equal(t, "test_dir", dir.GetName())
+ assert.Equal(t, 2, dir.ItemCount)
+ assert.Equal(t, '.', dir.GetFlag())
+
+ assert.Equal(t, "nested", dir.Files[0].GetName())
+ assert.Equal(t, '!', dir.Files[0].GetFlag())
+}
+
+func BenchmarkAnalyzeDir(b *testing.B) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ b.ResetTimer()
+
+ analyzer := CreateAnalyzer()
+ dir := analyzer.AnalyzeDir("test_dir", func(_, _ string) bool { return false })
+ <-analyzer.GetDoneChan()
+ dir.UpdateStats(make(HardLinkedItems))
+}
--- /dev/null
+//go:build darwin || netbsd || freebsd
+// +build darwin netbsd freebsd
+
+package analyze
+
+import (
+ "os"
+ "syscall"
+ "time"
+)
+
+const devBSize = 512
+
+func setPlatformSpecificAttrs(file *File, f os.FileInfo) {
+ switch stat := f.Sys().(type) {
+ case *syscall.Stat_t:
+ file.Usage = stat.Blocks * devBSize
+ file.Mtime = time.Unix(int64(stat.Mtimespec.Sec), int64(stat.Mtimespec.Nsec))
+
+ if stat.Nlink > 1 {
+ file.Mli = stat.Ino
+ }
+ }
+}
+
+func setDirPlatformSpecificAttrs(dir *Dir, path string) {
+ var stat syscall.Stat_t
+ if err := syscall.Stat(path, &stat); err != nil {
+ return
+ }
+
+ dir.Mtime = time.Unix(int64(stat.Mtimespec.Sec), int64(stat.Mtimespec.Nsec))
+}
--- /dev/null
+package analyze
+
+import (
+ "encoding/json"
+ "io"
+ "strconv"
+)
+
+// EncodeJSON writes JSON representation of dir
+func (f *Dir) EncodeJSON(writer io.Writer, topLevel bool) error {
+ buff := make([]byte, 0, 20)
+
+ buff = append(buff, []byte(`[{"name":`)...)
+
+ if topLevel {
+ if err := addString(&buff, f.GetPath()); err != nil {
+ return err
+ }
+ } else {
+ if err := addString(&buff, f.GetName()); err != nil {
+ return err
+ }
+ }
+
+ if !f.GetMtime().IsZero() {
+ buff = append(buff, []byte(`,"mtime":`)...)
+ buff = append(buff, []byte(strconv.FormatInt(f.GetMtime().Unix(), 10))...)
+ }
+
+ buff = append(buff, '}')
+ if f.Files.Len() > 0 {
+ buff = append(buff, ',')
+ }
+ buff = append(buff, '\n')
+
+ if _, err := writer.Write(buff); err != nil {
+ return err
+ }
+
+ for i, item := range f.Files {
+ if i > 0 {
+ if _, err := writer.Write([]byte(",\n")); err != nil {
+ return err
+ }
+ }
+ err := item.EncodeJSON(writer, false)
+ if err != nil {
+ return err
+ }
+ }
+
+ if _, err := writer.Write([]byte("]")); err != nil {
+ return err
+ }
+ return nil
+}
+
+// EncodeJSON writes JSON representation of file
+func (f *File) EncodeJSON(writer io.Writer, topLevel bool) error {
+ buff := make([]byte, 0, 20)
+
+ buff = append(buff, []byte(`{"name":`)...)
+ if err := addString(&buff, f.GetName()); err != nil {
+ return err
+ }
+ if f.GetSize() > 0 {
+ buff = append(buff, []byte(`,"asize":`)...)
+ buff = append(buff, []byte(strconv.FormatInt(f.GetSize(), 10))...)
+ }
+ if f.GetUsage() > 0 {
+ buff = append(buff, []byte(`,"dsize":`)...)
+ buff = append(buff, []byte(strconv.FormatInt(f.GetUsage(), 10))...)
+ }
+ if !f.GetMtime().IsZero() {
+ buff = append(buff, []byte(`,"mtime":`)...)
+ buff = append(buff, []byte(strconv.FormatInt(f.GetMtime().Unix(), 10))...)
+ }
+
+ if f.Flag == '@' {
+ buff = append(buff, []byte(`,"notreg":true`)...)
+ }
+ if f.Flag == 'H' {
+ buff = append(buff, []byte(`,"ino":`+strconv.FormatUint(f.Mli, 10)+`,"hlnkc":true`)...)
+ }
+
+ buff = append(buff, '}')
+
+ if _, err := writer.Write(buff); err != nil {
+ return err
+ }
+ return nil
+}
+
+func addString(buff *[]byte, val string) error {
+ b, err := json.Marshal(val)
+ if err != nil {
+ return err
+ }
+ *buff = append(*buff, b...)
+ return err
+}
--- /dev/null
+package analyze
+
+import (
+ "bytes"
+ "testing"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+ "github.com/stretchr/testify/assert"
+)
+
+func init() {
+ log.SetLevel(log.WarnLevel)
+}
+
+func TestEncode(t *testing.T) {
+ dir := &Dir{
+ File: &File{
+ Name: "test_dir",
+ Size: 10,
+ Usage: 18,
+ Mtime: time.Date(2021, 8, 19, 0, 40, 0, 0, time.UTC),
+ },
+ ItemCount: 4,
+ BasePath: ".",
+ }
+
+ subdir := &Dir{
+ File: &File{
+ Name: "nested",
+ Size: 9,
+ Usage: 14,
+ Parent: dir,
+ },
+ ItemCount: 3,
+ }
+ file := &File{
+ Name: "file2",
+ Size: 3,
+ Usage: 4,
+ Parent: subdir,
+ }
+ file2 := &File{
+ Name: "file",
+ Size: 5,
+ Usage: 6,
+ Parent: subdir,
+ Flag: '@',
+ Mtime: time.Date(2021, 8, 19, 0, 40, 0, 0, time.UTC),
+ }
+ file3 := &File{
+ Name: "file3",
+ Mli: 1234,
+ Flag: 'H',
+ }
+ dir.Files = Files{subdir}
+ subdir.Files = Files{file, file2, file3}
+
+ var buff bytes.Buffer
+ err := dir.EncodeJSON(&buff, true)
+
+ assert.Nil(t, err)
+ assert.Contains(t, buff.String(), `"name":"nested"`)
+ assert.Contains(t, buff.String(), `"mtime":1629333600`)
+ assert.Contains(t, buff.String(), `"ino":1234`)
+ assert.Contains(t, buff.String(), `"hlnkc":true`)
+}
--- /dev/null
+package analyze
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+ "time"
+)
+
+// HardLinkedItems maps inode number to array of all hard linked items
+type HardLinkedItems map[uint64]Files
+
+// Item is fs item (file or dir)
+type Item interface {
+ GetPath() string
+ GetName() string
+ GetFlag() rune
+ IsDir() bool
+ GetSize() int64
+ GetType() string
+ GetUsage() int64
+ GetMtime() time.Time
+ GetItemCount() int
+ GetParent() *Dir
+ GetMultiLinkedInode() uint64
+ EncodeJSON(writer io.Writer, topLevel bool) error
+ getItemStats(linkedItems HardLinkedItems) (int, int64, int64)
+}
+
+// File struct
+type File struct {
+ Mtime time.Time
+ Parent *Dir
+ Name string
+ Size int64
+ Usage int64
+ Mli uint64
+ Flag rune
+}
+
+// GetName returns name of dir
+func (f *File) GetName() string {
+ return f.Name
+}
+
+// IsDir returns false for file
+func (f *File) IsDir() bool {
+ return false
+}
+
+// GetParent retruns parent dir
+func (f *File) GetParent() *Dir {
+ return f.Parent
+}
+
+// GetPath retruns absolute Get of the file
+func (f *File) GetPath() string {
+ return filepath.Join(f.Parent.GetPath(), f.Name)
+}
+
+// GetFlag returns flag of the file
+func (f *File) GetFlag() rune {
+ return f.Flag
+}
+
+// GetSize returns size of the file
+func (f *File) GetSize() int64 {
+ return f.Size
+}
+
+// GetUsage returns usage of the file
+func (f *File) GetUsage() int64 {
+ return f.Usage
+}
+
+// GetMtime returns mtime of the file
+func (f *File) GetMtime() time.Time {
+ return f.Mtime
+}
+
+// GetType returns name type of item
+func (f *File) GetType() string {
+ switch f.Flag {
+ case '@':
+ return "Other"
+ }
+ return "File"
+}
+
+// GetItemCount returns 1 for file
+func (f *File) GetItemCount() int {
+ return 1
+}
+
+// GetMultiLinkedInode returns inode number of multilinked file
+func (f *File) GetMultiLinkedInode() uint64 {
+ return f.Mli
+}
+
+func (f *File) alreadyCounted(linkedItems HardLinkedItems) bool {
+ mli := f.Mli
+ counted := false
+ if mli > 0 {
+ if _, ok := linkedItems[mli]; ok {
+ f.Flag = 'H'
+ counted = true
+ }
+ linkedItems[mli] = append(linkedItems[mli], f)
+ }
+ return counted
+}
+
+func (f *File) getItemStats(linkedItems HardLinkedItems) (int, int64, int64) {
+ if f.alreadyCounted(linkedItems) {
+ return 1, 0, 0
+ }
+ return 1, f.GetSize(), f.GetUsage()
+}
+
+// Dir struct
+type Dir struct {
+ *File
+ BasePath string
+ Files Files
+ ItemCount int
+}
+
+// GetType returns name type of item
+func (f *Dir) GetType() string {
+ return "Directory"
+}
+
+// GetItemCount returns number of files in dir
+func (f *Dir) GetItemCount() int {
+ return f.ItemCount
+}
+
+// IsDir returns true for dir
+func (f *Dir) IsDir() bool {
+ return true
+}
+
+// GetPath retruns absolute path of the file
+func (f *Dir) GetPath() string {
+ if f.BasePath != "" {
+ return filepath.Join(f.BasePath, f.Name)
+ }
+ return filepath.Join(f.Parent.GetPath(), f.Name)
+}
+
+func (f *Dir) getItemStats(linkedItems HardLinkedItems) (int, int64, int64) {
+ f.UpdateStats(linkedItems)
+ return f.ItemCount, f.GetSize(), f.GetUsage()
+}
+
+// UpdateStats recursively updates size and item count
+func (f *Dir) UpdateStats(linkedItems HardLinkedItems) {
+ totalSize := int64(4096)
+ totalUsage := int64(4096)
+ var itemCount int
+ for _, entry := range f.Files {
+ count, size, usage := entry.getItemStats(linkedItems)
+ totalSize += size
+ totalUsage += usage
+ itemCount += count
+
+ if entry.GetMtime().After(f.Mtime) {
+ f.Mtime = entry.GetMtime()
+ }
+
+ switch entry.GetFlag() {
+ case '!', '.':
+ if f.Flag != '!' {
+ f.Flag = '.'
+ }
+ }
+ }
+ f.ItemCount = itemCount + 1
+ f.Size = totalSize
+ f.Usage = totalUsage
+}
+
+// Files - slice of pointers to File
+type Files []Item
+
+// Append addes one item to Files
+func (f *Files) Append(file Item) {
+ slice := *f
+ slice = append(slice, file)
+ *f = slice
+}
+
+// IndexOf searches File in Files and returns its index
+func (f Files) IndexOf(file Item) (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.GetName() == name {
+ return i, true
+ }
+ }
+ return 0, false
+}
+
+// Remove removes File from Files
+func (f Files) Remove(file Item) 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].GetUsage() > f[j].GetUsage() }
+
+// 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].GetSize() > f[j].GetSize() }
+
+// 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].GetItemCount() > f[j].GetItemCount() }
+
+// 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].GetName() > f[j].GetName() }
+
+// ByMtime sorts files by name
+type ByMtime Files
+
+func (f ByMtime) Len() int { return len(f) }
+func (f ByMtime) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
+func (f ByMtime) Less(i, j int) bool { return f[i].GetMtime().After(f[j].GetMtime()) }
+
+// RemoveItemFromDir removes item from dir
+func RemoveItemFromDir(dir *Dir, item Item) error {
+ err := os.RemoveAll(item.GetPath())
+ if err != nil {
+ return err
+ }
+
+ dir.Files = dir.Files.Remove(item)
+
+ cur := dir
+ for {
+ cur.ItemCount -= item.GetItemCount()
+ cur.Size -= item.GetSize()
+ cur.Usage -= item.GetUsage()
+
+ if cur.Parent == nil {
+ break
+ }
+ cur = cur.Parent
+ }
+ return nil
+}
+
+// EmptyFileFromDir empty file from dir
+func EmptyFileFromDir(dir *Dir, file Item) error {
+ err := os.Truncate(file.GetPath(), 0)
+ if err != nil {
+ return err
+ }
+
+ cur := dir
+ for {
+ cur.Size -= file.GetSize()
+ cur.Usage -= file.GetUsage()
+
+ if cur.Parent == nil {
+ break
+ }
+ cur = cur.Parent
+ }
+
+ dir.Files = dir.Files.Remove(file)
+ newFile := &File{
+ Name: file.GetName(),
+ Flag: file.GetFlag(),
+ Size: 0,
+ Parent: dir,
+ }
+ dir.Files.Append(newFile)
+
+ return nil
+}
--- /dev/null
+package analyze
+
+import (
+ "os"
+ "testing"
+ "time"
+
+ "github.com/dundee/gdu/v5/internal/testdir"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestIsDir(t *testing.T) {
+ dir := Dir{
+ File: &File{
+ Name: "xxx",
+ Size: 5,
+ },
+ ItemCount: 2,
+ }
+ file := &File{
+ Name: "yyy",
+ Size: 2,
+ Parent: &dir,
+ }
+ dir.Files = Files{file}
+
+ assert.True(t, dir.IsDir())
+ assert.False(t, file.IsDir())
+}
+
+func TestGetType(t *testing.T) {
+ dir := Dir{
+ File: &File{
+ Name: "xxx",
+ Size: 5,
+ },
+ ItemCount: 2,
+ }
+ file := &File{
+ Name: "yyy",
+ Size: 2,
+ Parent: &dir,
+ Flag: ' ',
+ }
+ file2 := &File{
+ Name: "yyy",
+ Size: 2,
+ Parent: &dir,
+ Flag: '@',
+ }
+ dir.Files = Files{file, file2}
+
+ assert.Equal(t, "Directory", dir.GetType())
+ assert.Equal(t, "File", file.GetType())
+ assert.Equal(t, "Other", file2.GetType())
+}
+
+func TestFind(t *testing.T) {
+ dir := Dir{
+ File: &File{
+ Name: "xxx",
+ Size: 5,
+ },
+ ItemCount: 2,
+ }
+
+ file := &File{
+ Name: "yyy",
+ Size: 2,
+ Parent: &dir,
+ }
+ file2 := &File{
+ Name: "zzz",
+ Size: 3,
+ Parent: &dir,
+ }
+ dir.Files = Files{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 := Dir{
+ File: &File{
+ Name: "xxx",
+ Size: 5,
+ },
+ ItemCount: 2,
+ }
+
+ file := &File{
+ Name: "yyy",
+ Size: 2,
+ Parent: &dir,
+ }
+ file2 := &File{
+ Name: "zzz",
+ Size: 3,
+ Parent: &dir,
+ }
+ dir.Files = Files{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 := Dir{
+ File: &File{
+ Name: "xxx",
+ Size: 5,
+ Usage: 8,
+ },
+ ItemCount: 2,
+ }
+
+ file := &File{
+ Name: "yyy",
+ Size: 2,
+ Usage: 4,
+ Parent: &dir,
+ }
+ file2 := &File{
+ Name: "zzz",
+ Size: 3,
+ Usage: 4,
+ Parent: &dir,
+ }
+ dir.Files = Files{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 := Dir{
+ File: &File{
+ Name: "xxx",
+ Size: 5,
+ Usage: 8,
+ },
+ ItemCount: 2,
+ }
+
+ file := &File{
+ Name: "yyy",
+ Size: 2,
+ Usage: 4,
+ Parent: &dir,
+ }
+ file2 := &File{
+ Name: "zzz",
+ Size: 3,
+ Usage: 4,
+ }
+ dir.Files = Files{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 := Dir{
+ File: &File{
+ Name: "xxx",
+ Size: 5,
+ Usage: 8,
+ },
+ ItemCount: 2,
+ }
+
+ file := &File{
+ Name: "yyy",
+ Size: 2,
+ Usage: 4,
+ Parent: &dir,
+ }
+ file2 := &File{
+ Name: "zzz",
+ Size: 3,
+ Usage: 4,
+ }
+ dir.Files = Files{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 := &Dir{
+ File: &File{
+ Name: "xxx",
+ Size: 5,
+ Usage: 12,
+ },
+ ItemCount: 3,
+ BasePath: ".",
+ }
+
+ subdir := &Dir{
+ File: &File{
+ Name: "yyy",
+ Size: 4,
+ Usage: 8,
+ Parent: dir,
+ },
+ ItemCount: 2,
+ }
+ file := &File{
+ Name: "zzz",
+ Size: 3,
+ Usage: 4,
+ Parent: subdir,
+ }
+ dir.Files = Files{subdir}
+ subdir.Files = Files{file}
+
+ err := RemoveItemFromDir(subdir, file)
+ assert.Nil(t, err)
+
+ 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()
+
+ err := os.Chmod("test_dir/nested", 0)
+ assert.Nil(t, err)
+ defer func() {
+ err = os.Chmod("test_dir/nested", 0755)
+ assert.Nil(t, err)
+ }()
+
+ dir := &Dir{
+ File: &File{
+ Name: "test_dir",
+ },
+ BasePath: ".",
+ }
+
+ subdir := &Dir{
+ File: &File{
+ Name: "nested",
+ Parent: dir,
+ },
+ }
+
+ err = RemoveItemFromDir(dir, subdir)
+ assert.Contains(t, err.Error(), "permission denied")
+}
+
+func TestTruncateFile(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ dir := &Dir{
+ File: &File{
+ Name: "test_dir",
+ Size: 5,
+ Usage: 12,
+ },
+ ItemCount: 3,
+ BasePath: ".",
+ }
+
+ subdir := &Dir{
+ File: &File{
+ Name: "nested",
+ Size: 4,
+ Usage: 8,
+ Parent: dir,
+ },
+ ItemCount: 2,
+ }
+ file := &File{
+ Name: "file2",
+ Size: 3,
+ Usage: 4,
+ Parent: subdir,
+ }
+ dir.Files = Files{subdir}
+ subdir.Files = Files{file}
+
+ err := EmptyFileFromDir(subdir, file)
+
+ assert.Nil(t, err)
+ assert.Equal(t, 1, len(subdir.Files))
+ assert.Equal(t, 2, 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, 3, dir.ItemCount)
+ assert.Equal(t, int64(2), dir.Size)
+}
+
+func TestTruncateFileWithErr(t *testing.T) {
+ dir := &Dir{
+ File: &File{
+ Name: "xxx",
+ Size: 5,
+ Usage: 12,
+ },
+ ItemCount: 3,
+ BasePath: ".",
+ }
+
+ subdir := &Dir{
+ File: &File{
+ Name: "yyy",
+ Size: 4,
+ Usage: 8,
+ Parent: dir,
+ },
+ ItemCount: 2,
+ }
+ file := &File{
+ Name: "zzz",
+ Size: 3,
+ Usage: 4,
+ Parent: subdir,
+ }
+ dir.Files = Files{subdir}
+ subdir.Files = Files{file}
+
+ err := EmptyFileFromDir(subdir, file)
+
+ assert.Contains(t, err.Error(), "no such file or directory")
+}
+
+func TestUpdateStats(t *testing.T) {
+ dir := Dir{
+ File: &File{
+ Name: "xxx",
+ Size: 1,
+ Mtime: time.Date(2021, 8, 19, 0, 40, 0, 0, time.UTC),
+ },
+ ItemCount: 1,
+ }
+
+ file := &File{
+ Name: "yyy",
+ Size: 2,
+ Mtime: time.Date(2021, 8, 19, 0, 41, 0, 0, time.UTC),
+ Parent: &dir,
+ }
+ file2 := &File{
+ Name: "zzz",
+ Size: 3,
+ Mtime: time.Date(2021, 8, 19, 0, 42, 0, 0, time.UTC),
+ Parent: &dir,
+ }
+ dir.Files = Files{file, file2}
+
+ dir.UpdateStats(nil)
+
+ assert.Equal(t, int64(4096+5), dir.Size)
+ assert.Equal(t, 42, dir.GetMtime().Minute())
+}
+
+func TestGetMultiLinkedInode(t *testing.T) {
+ file := &File{
+ Name: "xxx",
+ Mli: 5,
+ }
+
+ assert.Equal(t, uint64(5), file.GetMultiLinkedInode())
+
+}
--- /dev/null
+package analyze
+
+import (
+ "sort"
+ "testing"
+ "time"
+
+ "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].GetUsage())
+ assert.Equal(t, int64(2), files[1].GetUsage())
+ assert.Equal(t, int64(1), files[2].GetUsage())
+}
+
+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].GetSize())
+ assert.Equal(t, int64(2), files[1].GetSize())
+ assert.Equal(t, int64(3), files[2].GetSize())
+}
+
+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].GetSize())
+ assert.Equal(t, int64(2), files[1].GetSize())
+ assert.Equal(t, int64(1), files[2].GetSize())
+}
+
+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].GetSize())
+ assert.Equal(t, int64(2), files[1].GetSize())
+ assert.Equal(t, int64(3), files[2].GetSize())
+}
+
+func TestSortByItemCount(t *testing.T) {
+ files := Files{
+ &Dir{
+ ItemCount: 1,
+ },
+ &Dir{
+ ItemCount: 2,
+ },
+ &Dir{
+ ItemCount: 3,
+ },
+ }
+
+ sort.Sort(ByItemCount(files))
+
+ assert.Equal(t, 3, files[0].GetItemCount())
+ assert.Equal(t, 2, files[1].GetItemCount())
+ assert.Equal(t, 1, files[2].GetItemCount())
+}
+
+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].GetName())
+ assert.Equal(t, "bb", files[1].GetName())
+ assert.Equal(t, "aa", files[2].GetName())
+}
+
+func TestSortByMtime(t *testing.T) {
+ files := Files{
+ &File{
+ Mtime: time.Date(2021, 8, 19, 0, 40, 0, 0, time.UTC),
+ },
+ &File{
+ Mtime: time.Date(2021, 8, 19, 0, 41, 0, 0, time.UTC),
+ },
+ &File{
+ Mtime: time.Date(2021, 8, 19, 0, 42, 0, 0, time.UTC),
+ },
+ }
+
+ sort.Sort(ByMtime(files))
+
+ assert.Equal(t, 42, files[0].GetMtime().Minute())
+ assert.Equal(t, 41, files[1].GetMtime().Minute())
+ assert.Equal(t, 40, files[2].GetMtime().Minute())
+}
--- /dev/null
+package analyze
+
+import "sync"
+
+// A WaitGroup waits for a collection of goroutines to finish.
+// In contrast to sync.WaitGroup Add method can be called from a goroutine.
+type WaitGroup struct {
+ wait sync.Mutex
+ value int
+ access sync.Mutex
+}
+
+// Init prepares the WaitGroup for usage, locks
+func (s *WaitGroup) Init() *WaitGroup {
+ s.wait.Lock()
+ return s
+}
+
+// Add increments value
+func (s *WaitGroup) Add(value int) {
+ s.access.Lock()
+ s.value = s.value + value
+ s.access.Unlock()
+}
+
+// Done decrements the value by one, if value is 0, lock is released
+func (s *WaitGroup) Done() {
+ s.access.Lock()
+ s.value--
+ s.check()
+ s.access.Unlock()
+}
+
+// Wait blocks until value is 0
+func (s *WaitGroup) Wait() {
+ s.wait.Lock()
+}
+
+func (s *WaitGroup) check() {
+ if s.value == 0 {
+ s.wait.Unlock()
+ }
+}
--- /dev/null
+package device
+
+import "strings"
+
+// Device struct
+type Device struct {
+ Name string
+ MountPoint string
+ Fstype string
+ Size int64
+ Free int64
+}
+
+// GetUsage returns used size of device
+func (d Device) GetUsage() int64 {
+ return d.Size - d.Free
+}
+
+// DevicesInfoGetter is type for GetDevicesInfo function
+type DevicesInfoGetter interface {
+ GetMounts() (Devices, error)
+ GetDevicesInfo() (Devices, error)
+}
+
+// Devices if slice of Device items
+type Devices []*Device
+
+// ByUsedSize sorts devices by used size
+type ByUsedSize Devices
+
+func (f ByUsedSize) Len() int { return len(f) }
+func (f ByUsedSize) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
+func (f ByUsedSize) Less(i, j int) bool {
+ return f[i].GetUsage() > f[j].GetUsage()
+}
+
+// ByName sorts devices by device name
+type ByName Devices
+
+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
+}
+
+// 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
+}
--- /dev/null
+//go:build freebsd || darwin
+// +build freebsd darwin
+
+package device
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "io"
+ "os/exec"
+ "regexp"
+ "strings"
+
+ "golang.org/x/sys/unix"
+)
+
+// BSDDevicesInfoGetter returns info for Darwin devices
+type BSDDevicesInfoGetter struct {
+ MountCmd string
+}
+
+// Getter is current instance of DevicesInfoGetter
+var Getter DevicesInfoGetter = BSDDevicesInfoGetter{MountCmd: "/sbin/mount"}
+
+// GetMounts returns all mounted filesystems from output of /sbin/mount
+func (t BSDDevicesInfoGetter) GetMounts() (Devices, error) {
+ out, err := exec.Command(t.MountCmd).Output()
+ if err != nil {
+ return nil, err
+ }
+
+ rdr := bytes.NewReader(out)
+
+ return readMountOutput(rdr)
+}
+
+// GetDevicesInfo returns result of GetMounts with usage info about mounted devices (by calling Statfs syscall)
+func (t BSDDevicesInfoGetter) GetDevicesInfo() (Devices, error) {
+ mounts, err := t.GetMounts()
+ if err != nil {
+ return nil, err
+ }
+
+ return processMounts(mounts, false)
+}
+
+func readMountOutput(rdr io.Reader) (Devices, error) {
+ mounts := Devices{}
+
+ scanner := bufio.NewScanner(rdr)
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ re := regexp.MustCompile("^(.*) on (/.*) \\(([^)]+)\\)$")
+ parts := re.FindAllStringSubmatch(line, -1)
+
+ if len(parts) < 1 {
+ return nil, errors.New("Cannot parse mount output")
+ }
+
+ fstype := strings.TrimSpace(strings.Split(parts[0][3], ",")[0])
+
+ device := &Device{
+ Name: parts[0][1],
+ MountPoint: parts[0][2],
+ Fstype: fstype,
+ }
+ mounts = append(mounts, device)
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil, err
+ }
+
+ return mounts, nil
+}
+
+func processMounts(mounts Devices, ignoreErrors bool) (Devices, error) {
+ devices := Devices{}
+
+ for _, mount := range mounts {
+ if strings.HasPrefix(mount.Name, "/dev") || mount.Fstype == "zfs" {
+ info := &unix.Statfs_t{}
+ err := unix.Statfs(mount.MountPoint, info)
+ if err != nil && !ignoreErrors {
+ return nil, err
+ }
+
+ mount.Size = int64(info.Bsize) * int64(info.Blocks)
+ mount.Free = int64(info.Bsize) * int64(info.Bavail)
+
+ devices = append(devices, mount)
+ }
+ }
+
+ return devices, nil
+}
--- /dev/null
+//go:build freebsd || openbsd || netbsd || darwin
+// +build freebsd openbsd netbsd darwin
+
+package device
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetDevicesInfo(t *testing.T) {
+ getter := BSDDevicesInfoGetter{MountCmd: "/sbin/mount"}
+ devices, _ := getter.GetDevicesInfo()
+ assert.IsType(t, Devices{}, devices)
+}
+
+func TestGetDevicesInfoFail(t *testing.T) {
+ getter := BSDDevicesInfoGetter{MountCmd: "/nonexistent"}
+ _, err := getter.GetDevicesInfo()
+ assert.Equal(t, "fork/exec /nonexistent: no such file or directory", err.Error())
+}
+
+func TestZfsMountsShown(t *testing.T) {
+ mounts, _ := readMountOutput(strings.NewReader(`/dev/ada0p2 on / (ufs, local, soft-updates)
+devfs on /dev (devfs)
+tmpfs on /tmp (tmpfs, local)
+fdescfs on /dev/fd (fdescfs)
+procfs on /proc (procfs, local)
+t on /t (zfs, local, nfsv4acls)
+t/db on /t/db (zfs, local, nfsv4acls)
+t/vm on /t/vm (zfs, local, nfsv4acls)
+t/log/pflog on /var/log/pflog (zfs, local, nfsv4acls)
+t/log on /t/log (zfs, local, nfsv4acls)
+devfs on /compat/linux/dev (devfs)
+fdescfs on /compat/linux/dev/fd (fdescfs)
+tmpfs on /compat/linux/dev/shm (tmpfs, local)
+map -hosts on /net (autofs)
+argon:/usr/src on /usr/src (nfs)
+argon:/usr/obj on /usr/obj (nfs)`))
+
+ devices, err := processMounts(mounts, true)
+ assert.Len(t, devices, 6)
+ assert.Nil(t, err)
+}
+
+func TestMountsWithSpace(t *testing.T) {
+ mounts, err := readMountOutput(strings.NewReader(`//inglor@vault.lan/volatile on /Users/inglor/Mountpoints/volatile (vault.lan) (smbfs, nodev, nosuid, mounted by inglor)`))
+ assert.Equal(t, "//inglor@vault.lan/volatile", mounts[0].Name)
+ assert.Equal(t, "/Users/inglor/Mountpoints/volatile (vault.lan)", mounts[0].MountPoint)
+ assert.Equal(t, "smbfs", mounts[0].Fstype)
+ assert.Nil(t, err)
+}
--- /dev/null
+package device
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "golang.org/x/sys/unix"
+)
+
+// 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
+ }
+
+ devices, err := readMountsFile(file)
+ if err != nil {
+ if cerr := file.Close(); cerr != nil {
+ return nil, fmt.Errorf("%w; %s", err, cerr)
+ }
+ return nil, err
+ }
+ if err := file.Close(); err != nil {
+ return nil, err
+ }
+ return devices, nil
+}
+
+// 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, false)
+}
+
+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: unescapeString(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, ignoreErrors bool) (Devices, error) {
+ devices := Devices{}
+
+ for _, mount := range mounts {
+ if strings.Contains(mount.MountPoint, "/snap/") {
+ continue
+ }
+
+ if strings.HasPrefix(mount.Name, "/dev") ||
+ mount.Fstype == "zfs" ||
+ mount.Fstype == "nfs" ||
+ mount.Fstype == "nfs4" {
+ info := &unix.Statfs_t{}
+ err := unix.Statfs(mount.MountPoint, info)
+ if err != nil && !ignoreErrors {
+ return nil, err
+ }
+
+ mount.Size = int64(info.Bsize) * int64(info.Blocks)
+ mount.Free = int64(info.Bsize) * int64(info.Bavail)
+
+ devices = append(devices, mount)
+ }
+ }
+
+ return devices, nil
+}
+
+func unescapeString(str string) string {
+ return strings.ReplaceAll(str, "\\040", " ")
+}
--- /dev/null
+//go:build linux
+// +build linux
+
+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, true)
+ 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, true)
+ assert.Len(t, devices, 6)
+ assert.Nil(t, err)
+}
+
+func TestNfsMountsShown(t *testing.T) {
+ mounts, _ := readMountsFile(strings.NewReader(`host1:/dir1/ /mnt/dir1 nfs4 rw,nosuid,nodev,noatime,nodiratime,vers=4.2,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.1.1,fsc,local_lock=none,addr=192.168.1.2 0 0
+host2:/dir2/ /mnt/dir2 nfs rw,relatime,vers=3,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,mountaddr=192.168.1.3,mountvers=3,mountport=38081,mountproto=udp,fsc,local_lock=none,addr=192.168.1.4 0 0`))
+
+ devices, err := processMounts(mounts, true)
+ assert.Len(t, devices, 2)
+ assert.Equal(t, "host1:/dir1/", devices[0].Name)
+ assert.Equal(t, "/mnt/dir1", devices[0].MountPoint)
+ assert.Nil(t, err)
+}
+
+func TestMountsWithSpaces(t *testing.T) {
+ mounts, _ := readMountsFile(strings.NewReader(`host1:/dir1/ /mnt/dir\040with\040spaces nfs4 rw,nosuid,nodev,noatime,nodiratime,vers=4.2,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.1.1,fsc,local_lock=none,addr=192.168.1.2 0 0
+host2:/dir2/ /mnt/dir2 nfs rw,relatime,vers=3,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,mountaddr=192.168.1.3,mountvers=3,mountport=38081,mountproto=udp,fsc,local_lock=none,addr=192.168.1.4 0 0`))
+
+ devices, err := processMounts(mounts, true)
+ assert.Len(t, devices, 2)
+ assert.Equal(t, "host1:/dir1/", devices[0].Name)
+ assert.Equal(t, "/mnt/dir with spaces", devices[0].MountPoint)
+ assert.Nil(t, err)
+}
--- /dev/null
+//go:build netbsd
+// +build netbsd
+
+package device
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "golang.org/x/sys/unix"
+ "io"
+ "os/exec"
+ "regexp"
+ "strings"
+)
+
+// OpenBSDDevicesInfoGetter returns info for Darwin devices
+type OpenBSDDevicesInfoGetter struct {
+ MountCmd string
+}
+
+// Getter is current instance of DevicesInfoGetter
+var Getter DevicesInfoGetter = OpenBSDDevicesInfoGetter{MountCmd: "/sbin/mount"}
+
+// GetMounts returns all mounted filesystems from output of /sbin/mount
+func (t OpenBSDDevicesInfoGetter) GetMounts() (Devices, error) {
+ out, err := exec.Command(t.MountCmd).Output()
+ if err != nil {
+ return nil, err
+ }
+
+ rdr := bytes.NewReader(out)
+
+ return readMountOutput(rdr)
+}
+
+// GetDevicesInfo returns result of GetMounts with usage info about mounted devices (by calling Statfs syscall)
+func (t OpenBSDDevicesInfoGetter) GetDevicesInfo() (Devices, error) {
+ mounts, err := t.GetMounts()
+ if err != nil {
+ return nil, err
+ }
+
+ return processMounts(mounts, false)
+}
+
+func readMountOutput(rdr io.Reader) (Devices, error) {
+ mounts := Devices{}
+
+ scanner := bufio.NewScanner(rdr)
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ re := regexp.MustCompile("^(.*) on (/.*) \\(([^)]+)\\)$")
+ parts := re.FindAllStringSubmatch(line, -1)
+
+ if len(parts) < 1 {
+ return nil, errors.New("Cannot parse mount output")
+ }
+
+ fstype := strings.TrimSpace(strings.Split(parts[0][3], ",")[0])
+
+ device := &Device{
+ Name: parts[0][1],
+ MountPoint: parts[0][2],
+ Fstype: fstype,
+ }
+ mounts = append(mounts, device)
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil, err
+ }
+
+ return mounts, nil
+}
+
+func processMounts(mounts Devices, ignoreErrors bool) (Devices, error) {
+ devices := Devices{}
+
+ for _, mount := range mounts {
+ if strings.HasPrefix(mount.Name, "/dev") || mount.Fstype == "zfs" {
+ info := &unix.Statvfs_t{}
+ err := unix.Statvfs(mount.MountPoint, info)
+ if err != nil && !ignoreErrors {
+ return nil, err
+ }
+
+ mount.Size = int64(info.Bsize) * int64(info.Blocks)
+ mount.Free = int64(info.Bsize) * int64(info.Bavail)
+
+ devices = append(devices, mount)
+ }
+ }
+
+ return devices, nil
+}
--- /dev/null
+//go:build openbsd
+// +build openbsd
+
+package device
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "io"
+ "os/exec"
+ "regexp"
+ "strings"
+
+ "golang.org/x/sys/unix"
+)
+
+// OpenBSDDevicesInfoGetter returns info for Darwin devices
+type OpenBSDDevicesInfoGetter struct {
+ MountCmd string
+}
+
+// Getter is current instance of DevicesInfoGetter
+var Getter DevicesInfoGetter = OpenBSDDevicesInfoGetter{MountCmd: "/sbin/mount"}
+
+// GetMounts returns all mounted filesystems from output of /sbin/mount
+func (t OpenBSDDevicesInfoGetter) GetMounts() (Devices, error) {
+ out, err := exec.Command(t.MountCmd).Output()
+ if err != nil {
+ return nil, err
+ }
+
+ rdr := bytes.NewReader(out)
+
+ return readMountOutput(rdr)
+}
+
+// GetDevicesInfo returns result of GetMounts with usage info about mounted devices (by calling Statfs syscall)
+func (t OpenBSDDevicesInfoGetter) GetDevicesInfo() (Devices, error) {
+ mounts, err := t.GetMounts()
+ if err != nil {
+ return nil, err
+ }
+
+ return processMounts(mounts, false)
+}
+
+func readMountOutput(rdr io.Reader) (Devices, error) {
+ mounts := Devices{}
+
+ scanner := bufio.NewScanner(rdr)
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ re := regexp.MustCompile("^(.*) on (/.*) \\(([^)]+)\\)$")
+ parts := re.FindAllStringSubmatch(line, -1)
+
+ if len(parts) < 1 {
+ return nil, errors.New("Cannot parse mount output")
+ }
+
+ fstype := strings.TrimSpace(strings.Split(parts[0][3], ",")[0])
+
+ device := &Device{
+ Name: parts[0][1],
+ MountPoint: parts[0][2],
+ Fstype: fstype,
+ }
+ mounts = append(mounts, device)
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil, err
+ }
+
+ return mounts, nil
+}
+
+func processMounts(mounts Devices, ignoreErrors bool) (Devices, error) {
+ devices := Devices{}
+
+ for _, mount := range mounts {
+ if strings.HasPrefix(mount.Name, "/dev") || mount.Fstype == "zfs" {
+ info := &unix.Statfs_t{}
+ err := unix.Statfs(mount.MountPoint, info)
+ if err != nil && !ignoreErrors {
+ return nil, err
+ }
+
+ mount.Size = int64(info.F_bsize) * int64(info.F_blocks)
+ mount.Free = int64(info.F_bsize) * int64(info.F_bavail)
+
+ devices = append(devices, mount)
+ }
+ }
+
+ return devices, nil
+}
--- /dev/null
+//go:build windows || plan9
+// +build windows 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")
+}
--- /dev/null
+package device
+
+import (
+ "sort"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+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])
+}
+
+func TestSortByName(t *testing.T) {
+ item := &Device{
+ Name: "/xxx",
+ }
+ nested := &Device{
+ Name: "/xxx/yyy",
+ }
+ notNested := &Device{
+ Name: "/zzz/yyy",
+ }
+
+ devices := Devices{item, nested, notNested}
+
+ sort.Sort(ByName(devices))
+
+ assert.Equal(t, "/zzz/yyy", devices[0].Name)
+ assert.Equal(t, "/xxx/yyy", devices[1].Name)
+ assert.Equal(t, "/xxx", devices[2].Name)
+}
+
+func TestSortByUsedSize(t *testing.T) {
+ item := &Device{
+ Name: "xxx",
+ Size: 1e12,
+ Free: 1e3,
+ }
+ nested := &Device{
+ Name: "yyy",
+ Size: 1e12,
+ Free: 1e6,
+ }
+ notNested := &Device{
+ Name: "zzz",
+ Size: 1e12,
+ Free: 1e12,
+ }
+
+ devices := Devices{item, nested, notNested}
+
+ sort.Sort(sort.Reverse(ByUsedSize(devices)))
+
+ assert.Equal(t, "zzz", devices[0].Name)
+ assert.Equal(t, "yyy", devices[1].Name)
+ assert.Equal(t, "xxx", devices[2].Name)
+}
--- /dev/null
+package report
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "runtime/debug"
+ "sort"
+ "strconv"
+ "sync"
+ "time"
+
+ "github.com/dundee/gdu/v5/build"
+ "github.com/dundee/gdu/v5/internal/common"
+ "github.com/dundee/gdu/v5/pkg/analyze"
+ "github.com/dundee/gdu/v5/pkg/device"
+ "github.com/fatih/color"
+)
+
+// UI struct
+type UI struct {
+ *common.UI
+ output io.Writer
+ exportOutput io.Writer
+ red *color.Color
+ orange *color.Color
+ writtenChan chan struct{}
+}
+
+// CreateExportUI creates UI for stdout
+func CreateExportUI(output io.Writer, exportOutput io.Writer, useColors bool, showProgress bool, enableGC bool) *UI {
+ ui := &UI{
+ UI: &common.UI{
+ ShowProgress: showProgress,
+ Analyzer: analyze.CreateAnalyzer(),
+ EnableGC: enableGC,
+ },
+ output: output,
+ exportOutput: exportOutput,
+ writtenChan: make(chan struct{}),
+ }
+ ui.red = color.New(color.FgRed).Add(color.Bold)
+ ui.orange = color.New(color.FgYellow).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 {
+ return errors.New("Exporting devices list is not supported")
+}
+
+// ReadAnalysis reads analysis report from JSON file
+func (ui *UI) ReadAnalysis(input io.Reader) error {
+ return errors.New("Reading analysis is not possible while exporting")
+}
+
+// AnalyzePath analyzes recursively disk usage in given path
+func (ui *UI) AnalyzePath(path string, _ *analyze.Dir) error {
+ var (
+ dir *analyze.Dir
+ wait sync.WaitGroup
+ waitWritten sync.WaitGroup
+ err error
+ )
+
+ if ui.ShowProgress {
+ waitWritten.Add(1)
+ go func() {
+ defer waitWritten.Done()
+ ui.updateProgress()
+ }()
+ }
+
+ wait.Add(1)
+ go func() {
+ defer wait.Done()
+ if !ui.EnableGC {
+ defer debug.SetGCPercent(debug.SetGCPercent(-1))
+ }
+ dir = ui.Analyzer.AnalyzeDir(path, ui.CreateIgnoreFunc())
+ dir.UpdateStats(make(analyze.HardLinkedItems, 10))
+ }()
+
+ wait.Wait()
+
+ sort.Sort(dir.Files)
+
+ var buff bytes.Buffer
+
+ buff.Write([]byte(`[1,2,{"progname":"gdu","progver":"`))
+ buff.Write([]byte(build.Version))
+ buff.Write([]byte(`","timestamp":`))
+ buff.Write([]byte(strconv.FormatInt(time.Now().Unix(), 10)))
+ buff.Write([]byte("},\n"))
+
+ if err = dir.EncodeJSON(&buff, true); err != nil {
+ return err
+ }
+ if _, err = buff.Write([]byte("]\n")); err != nil {
+ return err
+ }
+ if _, err = buff.WriteTo(ui.exportOutput); err != nil {
+ return err
+ }
+
+ switch f := ui.exportOutput.(type) {
+ case *os.File:
+ err = f.Close()
+ if err != nil {
+ return err
+ }
+ }
+
+ if ui.ShowProgress {
+ ui.writtenChan <- struct{}{}
+ waitWritten.Wait()
+ }
+
+ return nil
+}
+
+func (ui *UI) updateProgress() {
+ waitingForWrite := false
+
+ emptyRow := "\r"
+ for j := 0; j < 100; j++ {
+ emptyRow += " "
+ }
+
+ progressRunes := []rune(`â â â â â šâ ¸â ŧâ ´â Ļâ §`)
+
+ progressChan := ui.Analyzer.GetProgressChan()
+ doneChan := ui.Analyzer.GetDoneChan()
+
+ var progress analyze.CurrentProgress
+
+ i := 0
+ for {
+ fmt.Fprint(ui.output, emptyRow)
+
+ select {
+ case progress = <-progressChan:
+ case <-doneChan:
+ fmt.Fprint(ui.output, "\r")
+ waitingForWrite = true
+ case <-ui.writtenChan:
+ fmt.Fprint(ui.output, "\r")
+ return
+ default:
+ }
+
+ fmt.Fprintf(ui.output, "\r %s ", string(progressRunes[i]))
+
+ if waitingForWrite {
+ fmt.Fprint(ui.output, "Writing output file...")
+ } else {
+ fmt.Fprint(ui.output, "Scanning... Total items: "+
+ ui.red.Sprint(common.FormatNumber(int64(progress.ItemCount)))+
+ " size: "+
+ ui.formatSize(progress.TotalSize))
+ }
+
+ time.Sleep(100 * time.Millisecond)
+ i++
+ i %= 10
+ }
+}
+
+func (ui *UI) formatSize(size int64) string {
+ fsize := float64(size)
+
+ switch {
+ case fsize >= common.EB:
+ return ui.orange.Sprintf("%.1f", fsize/common.EB) + " EiB"
+ case fsize >= common.PB:
+ return ui.orange.Sprintf("%.1f", fsize/common.PB) + " PiB"
+ case fsize >= common.TB:
+ return ui.orange.Sprintf("%.1f", fsize/common.TB) + " TiB"
+ case fsize >= common.GB:
+ return ui.orange.Sprintf("%.1f", fsize/common.GB) + " GiB"
+ case fsize >= common.MB:
+ return ui.orange.Sprintf("%.1f", fsize/common.MB) + " MiB"
+ case fsize >= common.KB:
+ return ui.orange.Sprintf("%.1f", fsize/common.KB) + " KiB"
+ default:
+ return ui.orange.Sprintf("%d", size) + " B"
+ }
+}
--- /dev/null
+package report
+
+import (
+ "bytes"
+ "os"
+ "testing"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/dundee/gdu/v5/internal/testdir"
+ "github.com/dundee/gdu/v5/pkg/device"
+ "github.com/stretchr/testify/assert"
+)
+
+func init() {
+ log.SetLevel(log.WarnLevel)
+}
+
+func TestAnalyzePath(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ output := bytes.NewBuffer(make([]byte, 10))
+ reportOutput := bytes.NewBuffer(make([]byte, 10))
+
+ ui := CreateExportUI(output, reportOutput, false, false, false)
+ ui.SetIgnoreDirPaths([]string{"/xxx"})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+ err = ui.StartUILoop()
+
+ assert.Nil(t, err)
+ assert.Contains(t, reportOutput.String(), `"name":"nested"`)
+}
+
+func TestAnalyzePathWithProgress(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ output := bytes.NewBuffer(make([]byte, 10))
+ reportOutput := bytes.NewBuffer(make([]byte, 10))
+
+ ui := CreateExportUI(output, reportOutput, true, true, true)
+ ui.SetIgnoreDirPaths([]string{"/xxx"})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+ err = ui.StartUILoop()
+
+ assert.Nil(t, err)
+ assert.Contains(t, reportOutput.String(), `"name":"nested"`)
+}
+
+func TestShowDevices(t *testing.T) {
+ output := bytes.NewBuffer(make([]byte, 10))
+ reportOutput := bytes.NewBuffer(make([]byte, 10))
+
+ ui := CreateExportUI(output, reportOutput, false, true, false)
+ err := ui.ListDevices(device.Getter)
+
+ assert.Contains(t, err.Error(), "not supported")
+}
+
+func TestReadAnalysisWhileExporting(t *testing.T) {
+ output := bytes.NewBuffer(make([]byte, 10))
+ reportOutput := bytes.NewBuffer(make([]byte, 10))
+
+ ui := CreateExportUI(output, reportOutput, false, true, false)
+ err := ui.ReadAnalysis(output)
+
+ assert.Contains(t, err.Error(), "not possible while exporting")
+}
+
+func TestExportToFile(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ reportOutput, err := os.OpenFile("output.json", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+ assert.Nil(t, err)
+ defer func() {
+ os.Remove("output.json")
+ }()
+
+ output := bytes.NewBuffer(make([]byte, 10))
+
+ ui := CreateExportUI(output, reportOutput, false, true, false)
+ ui.SetIgnoreDirPaths([]string{"/xxx"})
+ err = ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+ err = ui.StartUILoop()
+ assert.Nil(t, err)
+
+ reportOutput, err = os.OpenFile("output.json", os.O_RDONLY, 0644)
+ assert.Nil(t, err)
+ _, err = reportOutput.Seek(0, 0)
+ assert.Nil(t, err)
+ buff := make([]byte, 200)
+ _, err = reportOutput.Read(buff)
+ assert.Nil(t, err)
+
+ assert.Contains(t, string(buff), `"name":"nested"`)
+}
+
+func TestFormatSize(t *testing.T) {
+ output := bytes.NewBuffer(make([]byte, 10))
+ reportOutput := bytes.NewBuffer(make([]byte, 10))
+
+ ui := CreateExportUI(output, reportOutput, false, true, false)
+
+ assert.Contains(t, ui.formatSize(1), "B")
+ assert.Contains(t, ui.formatSize(1<<10+1), "KiB")
+ assert.Contains(t, ui.formatSize(1<<20+1), "MiB")
+ assert.Contains(t, ui.formatSize(1<<30+1), "GiB")
+ assert.Contains(t, ui.formatSize(1<<40+1), "TiB")
+ assert.Contains(t, ui.formatSize(1<<50+1), "PiB")
+ assert.Contains(t, ui.formatSize(1<<60+1), "EiB")
+}
--- /dev/null
+package report
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "io"
+ "strings"
+ "time"
+
+ "github.com/dundee/gdu/v5/pkg/analyze"
+)
+
+// ReadAnalysis reads analysis report from JSON file and returns directory item
+func ReadAnalysis(input io.Reader) (*analyze.Dir, error) {
+ var data interface{}
+
+ var buff bytes.Buffer
+ if _, err := buff.ReadFrom(input); err != nil {
+ return nil, err
+ }
+ if err := json.Unmarshal(buff.Bytes(), &data); err != nil {
+ return nil, err
+ }
+
+ dataArray, ok := data.([]interface{})
+ if !ok {
+ return nil, errors.New("JSON file does not contain top level array")
+ }
+ if len(dataArray) < 4 {
+ return nil, errors.New("Top level array must have at least 4 items")
+ }
+
+ items, ok := dataArray[3].([]interface{})
+ if !ok {
+ return nil, errors.New("Array of maps not found in the top level array on 4th position")
+ }
+
+ return processDir(items)
+}
+
+func processDir(items []interface{}) (*analyze.Dir, error) {
+ dir := &analyze.Dir{
+ File: &analyze.File{
+ Flag: ' ',
+ },
+ }
+ dirMap, ok := items[0].(map[string]interface{})
+ if !ok {
+ return nil, errors.New("Directory item is not a map")
+ }
+ name, ok := dirMap["name"].(string)
+ if !ok {
+ return nil, errors.New("Directory name is not a string")
+ }
+ if mtime, ok := dirMap["mtime"].(float64); ok {
+ dir.Mtime = time.Unix(int64(mtime), 0)
+ }
+
+ slashPos := strings.LastIndex(name, "/")
+ if slashPos > -1 {
+ dir.Name = name[slashPos+1:]
+ dir.BasePath = name[:slashPos+1]
+ } else {
+ dir.Name = name
+ }
+
+ for _, v := range items[1:] {
+ switch item := v.(type) {
+ case map[string]interface{}:
+ file := &analyze.File{}
+ file.Name = item["name"].(string)
+
+ if asize, ok := item["asize"].(float64); ok {
+ file.Size = int64(asize)
+ }
+ if dsize, ok := item["dsize"].(float64); ok {
+ file.Usage = int64(dsize)
+ }
+ if mtime, ok := item["mtime"].(float64); ok {
+ file.Mtime = time.Unix(int64(mtime), 0)
+ }
+ if _, ok := item["notreg"].(bool); ok {
+ file.Flag = '@'
+ } else {
+ file.Flag = ' '
+ }
+ if mli, ok := item["ino"].(float64); ok {
+ file.Mli = uint64(mli)
+ }
+ if _, ok := item["hlnkc"].(bool); ok {
+ file.Flag = 'H'
+ }
+
+ file.Parent = dir
+
+ dir.Files.Append(file)
+ case []interface{}:
+ subdir, err := processDir(item)
+ if err != nil {
+ return nil, err
+ }
+ subdir.Parent = dir
+ dir.Files.Append(subdir)
+ }
+ }
+
+ return dir, nil
+}
--- /dev/null
+package report
+
+import (
+ "bytes"
+ "errors"
+ "testing"
+
+ "github.com/dundee/gdu/v5/pkg/analyze"
+ log "github.com/sirupsen/logrus"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func init() {
+ log.SetLevel(log.WarnLevel)
+}
+
+func TestReadAnalysis(t *testing.T) {
+ buff := bytes.NewBuffer([]byte(`
+ [1,2,{"progname":"gdu","progver":"development","timestamp":1626806293},
+ [{"name":"/home/xxx","mtime":1629333600},
+ {"name":"gdu.json","asize":33805233,"dsize":33808384},
+ {"name":"sock","notreg":true},
+ [{"name":"app"},
+ {"name":"app.go","asize":4638,"dsize":8192},
+ {"name":"app_linux_test.go","asize":1410,"dsize":4096},
+ {"name":"app_linux_test2.go","ino":1234,"hlnkc":true,"asize":1410,"dsize":4096},
+ {"name":"app_test.go","asize":4974,"dsize":8192}],
+ {"name":"main.go","asize":3205,"dsize":4096,"mtime":1629333600}]]
+ `))
+
+ dir, err := ReadAnalysis(buff)
+
+ assert.Nil(t, err)
+ assert.Equal(t, "xxx", dir.GetName())
+ assert.Equal(t, "/home/xxx", dir.GetPath())
+ assert.Equal(t, 2021, dir.GetMtime().Year())
+ assert.Equal(t, 2021, dir.Files[3].GetMtime().Year())
+ alt2 := dir.Files[2].(*analyze.Dir).Files[2].(*analyze.File)
+ assert.Equal(t, "app_linux_test2.go", alt2.Name)
+ assert.Equal(t, uint64(1234), alt2.Mli)
+ assert.Equal(t, 'H', alt2.Flag)
+}
+
+func TestReadAnalysisWithEmptyInput(t *testing.T) {
+ buff := bytes.NewBuffer([]byte(``))
+
+ _, err := ReadAnalysis(buff)
+
+ assert.Equal(t, "unexpected end of JSON input", err.Error())
+}
+
+func TestReadAnalysisWithEmptyDict(t *testing.T) {
+ buff := bytes.NewBuffer([]byte(`{}`))
+
+ _, err := ReadAnalysis(buff)
+
+ assert.Equal(t, "JSON file does not contain top level array", err.Error())
+}
+
+func TestReadFromBrokenInput(t *testing.T) {
+ _, err := ReadAnalysis(&BrokenInput{})
+
+ assert.Equal(t, "IO error", err.Error())
+}
+
+func TestReadAnalysisWithEmptyArray(t *testing.T) {
+ buff := bytes.NewBuffer([]byte(`[]`))
+
+ _, err := ReadAnalysis(buff)
+
+ assert.Equal(t, "Top level array must have at least 4 items", err.Error())
+}
+
+func TestReadAnalysisWithWrongContent(t *testing.T) {
+ buff := bytes.NewBuffer([]byte(`[1,2,3,4]`))
+
+ _, err := ReadAnalysis(buff)
+
+ assert.Equal(t, "Array of maps not found in the top level array on 4th position", err.Error())
+}
+
+func TestReadAnalysisWithEmptyDirContent(t *testing.T) {
+ buff := bytes.NewBuffer([]byte(`[1,2,3,[{}]]`))
+
+ _, err := ReadAnalysis(buff)
+
+ assert.Equal(t, "Directory name is not a string", err.Error())
+}
+
+func TestReadAnalysisWithWrongDirItem(t *testing.T) {
+ buff := bytes.NewBuffer([]byte(`[1,2,3,[1, 2, 3]]`))
+
+ _, err := ReadAnalysis(buff)
+
+ assert.Equal(t, "Directory item is not a map", err.Error())
+}
+
+func TestReadAnalysisWithWrongSubdirItem(t *testing.T) {
+ buff := bytes.NewBuffer([]byte(`[1,2,3,[{"name":"xxx"}, [1,2,3]]]`))
+
+ _, err := ReadAnalysis(buff)
+
+ assert.Equal(t, "Directory item is not a map", err.Error())
+}
+
+type BrokenInput struct{}
+
+func (i *BrokenInput) Read(p []byte) (n int, err error) {
+ return 0, errors.New("IO error")
+}
--- /dev/null
+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 \
+ -buildmode=pie -trimpath -mod=readonly -modcacherw \
+ -ldflags \
+ "-s -w \
+ -X 'github.com/dundee/gdu/v5/build.Version=$(git describe)' \
+ -X 'github.com/dundee/gdu/v5/build.User=$(id -u -n)' \
+ -X 'github.com/dundee/gdu/v5/build.Time=$(LC_ALL=en_US.UTF-8 date)' \
+ -X 'github.com/dundee/gdu/v5/build.RootPathPrefix=/var/lib/snapd/hostfs'" \
+ -o $SNAPCRAFT_PART_INSTALL/gdu \
+ github.com/dundee/gdu/v5/cmd/gdu
+ $SNAPCRAFT_PART_INSTALL/gdu -v
+apps:
+ gdu:
+ command: gdu
+ plugs:
+ - mount-observe
+ - system-backup
--- /dev/null
+package stdout
+
+import (
+ "fmt"
+ "io"
+ "math"
+ "runtime"
+ "runtime/debug"
+ "sort"
+ "sync"
+ "time"
+
+ "github.com/dundee/gdu/v5/internal/common"
+ "github.com/dundee/gdu/v5/pkg/analyze"
+ "github.com/dundee/gdu/v5/pkg/device"
+ "github.com/dundee/gdu/v5/report"
+ "github.com/fatih/color"
+)
+
+// UI struct
+type UI struct {
+ *common.UI
+ output io.Writer
+ red *color.Color
+ orange *color.Color
+ blue *color.Color
+ summarize bool
+}
+
+var progressRunes = []rune(`â â â â â šâ ¸â ŧâ ´â Ļâ §`)
+
+// CreateStdoutUI creates UI for stdout
+func CreateStdoutUI(
+ output io.Writer,
+ useColors bool,
+ showProgress bool,
+ showApparentSize bool,
+ summarize bool,
+ enableGC bool,
+) *UI {
+ ui := &UI{
+ UI: &common.UI{
+ UseColors: useColors,
+ ShowProgress: showProgress,
+ ShowApparentSize: showApparentSize,
+ Analyzer: analyze.CreateAnalyzer(),
+ EnableGC: enableGC,
+ },
+ output: output,
+ summarize: summarize,
+ }
+
+ 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, _ *analyze.Dir) error {
+ var (
+ dir *analyze.Dir
+ wait sync.WaitGroup
+ )
+
+ if ui.ShowProgress {
+ wait.Add(1)
+ go func() {
+ defer wait.Done()
+ ui.updateProgress()
+ }()
+ }
+
+ wait.Add(1)
+ go func() {
+ defer wait.Done()
+ if !ui.EnableGC {
+ defer debug.SetGCPercent(debug.SetGCPercent(-1))
+ }
+ dir = ui.Analyzer.AnalyzeDir(path, ui.CreateIgnoreFunc())
+ dir.UpdateStats(make(analyze.HardLinkedItems, 10))
+ }()
+
+ wait.Wait()
+
+ if ui.summarize {
+ ui.printTotalItem(dir)
+ } else {
+ ui.showDir(dir)
+ }
+
+ return nil
+}
+
+func (ui *UI) showDir(dir *analyze.Dir) {
+ sort.Sort(dir.Files)
+
+ for _, file := range dir.Files {
+ ui.printItem(file)
+ }
+}
+
+func (ui *UI) printTotalItem(file analyze.Item) {
+ var lineFormat string
+ if ui.UseColors {
+ lineFormat = "%20s %s\n"
+ } else {
+ lineFormat = "%9s %s\n"
+ }
+
+ var size int64
+ if ui.ShowApparentSize {
+ size = file.GetSize()
+ } else {
+ size = file.GetUsage()
+ }
+
+ fmt.Fprintf(
+ ui.output,
+ lineFormat,
+ ui.formatSize(size),
+ file.GetName(),
+ )
+}
+
+func (ui *UI) printItem(file analyze.Item) {
+ var lineFormat string
+ if ui.UseColors {
+ lineFormat = "%s %20s %s\n"
+ } else {
+ lineFormat = "%s %9s %s\n"
+ }
+
+ var size int64
+ if ui.ShowApparentSize {
+ size = file.GetSize()
+ } else {
+ size = file.GetUsage()
+ }
+
+ if file.IsDir() {
+ fmt.Fprintf(ui.output,
+ lineFormat,
+ string(file.GetFlag()),
+ ui.formatSize(size),
+ ui.blue.Sprintf("/"+file.GetName()))
+ } else {
+ fmt.Fprintf(ui.output,
+ lineFormat,
+ string(file.GetFlag()),
+ ui.formatSize(size),
+ file.GetName())
+ }
+}
+
+// ReadAnalysis reads analysis report from JSON file
+func (ui *UI) ReadAnalysis(input io.Reader) error {
+ var (
+ dir *analyze.Dir
+ wait sync.WaitGroup
+ err error
+ doneChan chan struct{}
+ )
+
+ if ui.ShowProgress {
+ wait.Add(1)
+ doneChan = make(chan struct{})
+ go func() {
+ defer wait.Done()
+ ui.showReadingProgress(doneChan)
+ }()
+ }
+
+ wait.Add(1)
+ go func() {
+ defer wait.Done()
+ dir, err = report.ReadAnalysis(input)
+ if err != nil {
+ if ui.ShowProgress {
+ doneChan <- struct{}{}
+ }
+ return
+ }
+ runtime.GC()
+
+ dir.UpdateStats(make(analyze.HardLinkedItems, 10))
+
+ if ui.ShowProgress {
+ doneChan <- struct{}{}
+ }
+ }()
+
+ wait.Wait()
+
+ if err != nil {
+ return err
+ }
+
+ ui.showDir(dir)
+
+ return nil
+}
+
+func (ui *UI) showReadingProgress(doneChan chan struct{}) {
+ emptyRow := "\r"
+ for j := 0; j < 40; j++ {
+ emptyRow += " "
+ }
+
+ i := 0
+ for {
+ fmt.Fprint(ui.output, emptyRow)
+
+ select {
+ case <-doneChan:
+ fmt.Fprint(ui.output, "\r")
+ return
+ default:
+ }
+
+ fmt.Fprintf(ui.output, "\r %s ", string(progressRunes[i]))
+ fmt.Fprint(ui.output, "Reading analysis from file...")
+
+ time.Sleep(100 * time.Millisecond)
+ i++
+ i %= 10
+ }
+}
+
+func (ui *UI) updateProgress() {
+ emptyRow := "\r"
+ for j := 0; j < 100; j++ {
+ emptyRow += " "
+ }
+
+ progressChan := ui.Analyzer.GetProgressChan()
+ doneChan := ui.Analyzer.GetDoneChan()
+
+ var progress analyze.CurrentProgress
+
+ i := 0
+ for {
+ fmt.Fprint(ui.output, emptyRow)
+
+ select {
+ case progress = <-progressChan:
+ case <-doneChan:
+ 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(common.FormatNumber(int64(progress.ItemCount)))+
+ " size: "+
+ ui.formatSize(progress.TotalSize))
+
+ time.Sleep(100 * time.Millisecond)
+ i++
+ i %= 10
+ }
+}
+
+func (ui *UI) formatSize(size int64) string {
+ fsize := float64(size)
+
+ switch {
+ case fsize >= common.EB:
+ return ui.orange.Sprintf("%.1f", fsize/common.EB) + " EiB"
+ case fsize >= common.PB:
+ return ui.orange.Sprintf("%.1f", fsize/common.PB) + " PiB"
+ case fsize >= common.TB:
+ return ui.orange.Sprintf("%.1f", fsize/common.TB) + " TiB"
+ case fsize >= common.GB:
+ return ui.orange.Sprintf("%.1f", fsize/common.GB) + " GiB"
+ case fsize >= common.MB:
+ return ui.orange.Sprintf("%.1f", fsize/common.MB) + " MiB"
+ case fsize >= common.KB:
+ return ui.orange.Sprintf("%.1f", fsize/common.KB) + " 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
+}
--- /dev/null
+//go:build linux
+// +build linux
+
+package stdout
+
+import (
+ "bytes"
+ "testing"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/dundee/gdu/v5/pkg/device"
+ "github.com/stretchr/testify/assert"
+)
+
+func init() {
+ log.SetLevel(log.WarnLevel)
+}
+
+func TestShowDevicesWithErr(t *testing.T) {
+ output := bytes.NewBuffer(make([]byte, 10))
+
+ getter := device.LinuxDevicesInfoGetter{MountsPath: "/xyzxyz"}
+ ui := CreateStdoutUI(output, false, true, false, false, false)
+ err := ui.ListDevices(getter)
+
+ assert.Contains(t, err.Error(), "no such file")
+}
--- /dev/null
+package stdout
+
+import (
+ "bytes"
+ "os"
+ "testing"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/dundee/gdu/v5/internal/testanalyze"
+ "github.com/dundee/gdu/v5/internal/testdev"
+ "github.com/dundee/gdu/v5/internal/testdir"
+ "github.com/dundee/gdu/v5/pkg/device"
+ "github.com/stretchr/testify/assert"
+)
+
+func init() {
+ log.SetLevel(log.WarnLevel)
+}
+
+func TestAnalyzePath(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ buff := make([]byte, 10)
+ output := bytes.NewBuffer(buff)
+
+ ui := CreateStdoutUI(output, false, false, false, false, true)
+ ui.SetIgnoreDirPaths([]string{"/xxx"})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+ err = ui.StartUILoop()
+
+ assert.Nil(t, err)
+ assert.Contains(t, output.String(), "nested")
+}
+
+func TestShowSummary(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ buff := make([]byte, 10)
+ output := bytes.NewBuffer(buff)
+
+ ui := CreateStdoutUI(output, true, false, true, true, false)
+ ui.SetIgnoreDirPaths([]string{"/xxx"})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+ err = ui.StartUILoop()
+
+ assert.Nil(t, err)
+ assert.Contains(t, output.String(), "test_dir")
+}
+
+func TestShowSummaryBw(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ buff := make([]byte, 10)
+ output := bytes.NewBuffer(buff)
+
+ ui := CreateStdoutUI(output, false, false, false, true, false)
+ ui.SetIgnoreDirPaths([]string{"/xxx"})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+ err = ui.StartUILoop()
+
+ assert.Nil(t, err)
+ assert.Contains(t, output.String(), "test_dir")
+}
+
+func TestAnalyzeSubdir(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ buff := make([]byte, 10)
+ output := bytes.NewBuffer(buff)
+
+ ui := CreateStdoutUI(output, false, false, false, false, false)
+ ui.SetIgnoreDirPaths([]string{"/xxx"})
+ err := ui.AnalyzePath("test_dir/nested", nil)
+ assert.Nil(t, err)
+ err = ui.StartUILoop()
+
+ assert.Nil(t, err)
+ assert.Contains(t, output.String(), "file2")
+}
+
+func TestAnalyzePathWithColors(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ buff := make([]byte, 10)
+ output := bytes.NewBuffer(buff)
+
+ ui := CreateStdoutUI(output, true, false, true, false, false)
+ ui.SetIgnoreDirPaths([]string{"/xxx"})
+ err := ui.AnalyzePath("test_dir/nested", nil)
+
+ assert.Nil(t, err)
+ assert.Contains(t, output.String(), "subnested")
+}
+
+func TestItemRows(t *testing.T) {
+ output := bytes.NewBuffer(make([]byte, 10))
+
+ ui := CreateStdoutUI(output, false, true, false, false, false)
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ err := ui.AnalyzePath("test_dir", nil)
+
+ assert.Nil(t, err)
+ 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, false, false)
+ ui.SetIgnoreDirPaths([]string{"/xxx"})
+ err := ui.AnalyzePath("test_dir", nil)
+
+ assert.Nil(t, err)
+ assert.Contains(t, output.String(), "nested")
+}
+
+func TestShowDevices(t *testing.T) {
+ output := bytes.NewBuffer(make([]byte, 10))
+
+ ui := CreateStdoutUI(output, false, true, false, false, false)
+ err := ui.ListDevices(getDevicesInfoMock())
+
+ assert.Nil(t, err)
+ 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, false, false)
+ err := ui.ListDevices(getDevicesInfoMock())
+
+ assert.Nil(t, err)
+ assert.Contains(t, output.String(), "Device")
+ assert.Contains(t, output.String(), "xxx")
+}
+
+func TestReadAnalysisWithColor(t *testing.T) {
+ input, err := os.OpenFile("../internal/testdata/test.json", os.O_RDONLY, 0644)
+ assert.Nil(t, err)
+
+ output := bytes.NewBuffer(make([]byte, 10))
+
+ ui := CreateStdoutUI(output, true, true, true, false, false)
+ err = ui.ReadAnalysis(input)
+
+ assert.Nil(t, err)
+ assert.Contains(t, output.String(), "main.go")
+}
+
+func TestReadAnalysisBw(t *testing.T) {
+ input, err := os.OpenFile("../internal/testdata/test.json", os.O_RDONLY, 0644)
+ assert.Nil(t, err)
+
+ output := bytes.NewBuffer(make([]byte, 10))
+
+ ui := CreateStdoutUI(output, false, false, false, false, false)
+ err = ui.ReadAnalysis(input)
+
+ assert.Nil(t, err)
+ assert.Contains(t, output.String(), "main.go")
+}
+
+func TestReadAnalysisWithWrongFile(t *testing.T) {
+ input, err := os.OpenFile("../internal/testdata/wrong.json", os.O_RDONLY, 0644)
+ assert.Nil(t, err)
+
+ output := bytes.NewBuffer(make([]byte, 10))
+
+ ui := CreateStdoutUI(output, true, true, true, false, false)
+ err = ui.ReadAnalysis(input)
+
+ assert.NotNil(t, err)
+}
+
+func TestMaxInt(t *testing.T) {
+ assert.Equal(t, 5, maxInt(2, 5))
+ assert.Equal(t, 4, maxInt(4, 2))
+}
+
+func TestFormatSize(t *testing.T) {
+ output := bytes.NewBuffer(make([]byte, 10))
+
+ ui := CreateStdoutUI(output, true, true, true, false, false)
+
+ assert.Contains(t, ui.formatSize(1), "B")
+ assert.Contains(t, ui.formatSize(1<<10+1), "KiB")
+ assert.Contains(t, ui.formatSize(1<<20+1), "MiB")
+ assert.Contains(t, ui.formatSize(1<<30+1), "GiB")
+ assert.Contains(t, ui.formatSize(1<<40+1), "TiB")
+ assert.Contains(t, ui.formatSize(1<<50+1), "PiB")
+ assert.Contains(t, ui.formatSize(1<<60+1), "EiB")
+}
+
+// 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
+}
--- /dev/null
+package tui
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "os"
+ "runtime"
+ "runtime/debug"
+ "strings"
+
+ "github.com/dundee/gdu/v5/build"
+ "github.com/dundee/gdu/v5/pkg/analyze"
+ "github.com/dundee/gdu/v5/pkg/device"
+ "github.com/dundee/gdu/v5/report"
+ "github.com/gdamore/tcell/v2"
+ "github.com/rivo/tview"
+)
+
+const defaultLinesCount = 500
+const linesTreshold = 20
+
+// 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.showDevices()
+
+ return nil
+}
+
+// AnalyzePath analyzes recursively disk usage for given path
+func (ui *UI) AnalyzePath(path string, parentDir *analyze.Dir) error {
+ ui.progress = tview.NewTextView().SetText("Scanning...")
+ ui.progress.SetBorder(true).SetBorderPadding(2, 2, 2, 2)
+ 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)
+
+ go ui.updateProgress()
+
+ go func() {
+ if !ui.EnableGC {
+ defer debug.SetGCPercent(debug.SetGCPercent(-1))
+ defer debug.FreeOSMemory()
+ }
+ currentDir := ui.Analyzer.AnalyzeDir(path, ui.CreateIgnoreFunc())
+
+ if parentDir != nil {
+ currentDir.Parent = parentDir
+ parentDir.Files = parentDir.Files.RemoveByName(currentDir.Name)
+ parentDir.Files.Append(currentDir)
+ } else {
+ ui.topDirPath = path
+ ui.topDir = currentDir
+ }
+
+ ui.topDir.UpdateStats(ui.linkedItems)
+
+ ui.app.QueueUpdateDraw(func() {
+ ui.currentDir = currentDir
+ ui.showDir()
+ ui.pages.RemovePage("progress")
+ })
+
+ if ui.done != nil {
+ ui.done <- struct{}{}
+ }
+ }()
+
+ return nil
+}
+
+// ReadAnalysis reads analysis report from JSON file
+func (ui *UI) ReadAnalysis(input io.Reader) error {
+ ui.progress = tview.NewTextView().SetText("Reading analysis from file...")
+ ui.progress.SetBorder(true).SetBorderPadding(2, 2, 2, 2)
+ ui.progress.SetTitle(" Reading... ")
+ 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)
+
+ go func() {
+ var err error
+ ui.currentDir, err = report.ReadAnalysis(input)
+ if err != nil {
+ ui.app.QueueUpdateDraw(func() {
+ ui.pages.RemovePage("progress")
+ ui.showErr("Error reading file", err)
+ })
+ if ui.done != nil {
+ ui.done <- struct{}{}
+ }
+ return
+ }
+ runtime.GC()
+
+ ui.topDirPath = ui.currentDir.GetPath()
+ ui.topDir = ui.currentDir
+
+ links := make(analyze.HardLinkedItems, 10)
+ ui.topDir.UpdateStats(links)
+
+ ui.app.QueueUpdateDraw(func() {
+ ui.showDir()
+ ui.pages.RemovePage("progress")
+ })
+
+ if ui.done != nil {
+ ui.done <- struct{}{}
+ }
+ }()
+
+ return nil
+}
+
+func (ui *UI) deleteSelected(shouldEmpty bool) {
+ row, column := ui.table.GetSelection()
+ selectedItem := ui.table.GetCell(row, column).GetReference().(analyze.Item)
+
+ var action, acting string
+ if shouldEmpty {
+ action = "empty "
+ acting = "emptying"
+ } else {
+ action = "delete "
+ acting = "deleting"
+ }
+ modal := tview.NewModal().SetText(
+ strings.Title(acting) +
+ " " +
+ tview.Escape(selectedItem.GetName()) +
+ "...",
+ )
+ ui.pages.AddPage(acting, modal, true, true)
+
+ var currentDir *analyze.Dir
+ var deleteItems []analyze.Item
+ if shouldEmpty && selectedItem.IsDir() {
+ currentDir = selectedItem.(*analyze.Dir)
+ for _, file := range currentDir.Files {
+ deleteItems = append(deleteItems, file)
+ }
+ } else {
+ currentDir = ui.currentDir
+ deleteItems = append(deleteItems, selectedItem)
+ }
+
+ var deleteFun func(*analyze.Dir, analyze.Item) error
+ if shouldEmpty && !selectedItem.IsDir() {
+ deleteFun = ui.emptier
+ } else {
+ deleteFun = ui.remover
+ }
+ go func() {
+ for _, item := range deleteItems {
+ if err := deleteFun(currentDir, item); err != nil {
+ msg := "Can't " + action + tview.Escape(selectedItem.GetName())
+ ui.app.QueueUpdateDraw(func() {
+ ui.pages.RemovePage(acting)
+ ui.showErr(msg, err)
+ })
+ if ui.done != nil {
+ ui.done <- struct{}{}
+ }
+ return
+ }
+ }
+
+ ui.app.QueueUpdateDraw(func() {
+ ui.pages.RemovePage(acting)
+ ui.showDir()
+ ui.table.Select(min(row, ui.table.GetRowCount()-1), 0)
+ })
+
+ if ui.done != nil {
+ ui.done <- struct{}{}
+ }
+ }()
+}
+
+func (ui *UI) showFile() *tview.TextView {
+ if ui.currentDir == nil {
+ return nil
+ }
+
+ row, column := ui.table.GetSelection()
+ selectedFile := ui.table.GetCell(row, column).GetReference().(analyze.Item)
+ if selectedFile.IsDir() {
+ return nil
+ }
+
+ f, err := os.Open(selectedFile.GetPath())
+ if err != nil {
+ ui.showErr("Error opening file", err)
+ return nil
+ }
+
+ totalLines := 0
+ scanner := bufio.NewScanner(f)
+
+ file := tview.NewTextView()
+ ui.currentDirLabel.SetText("[::b] --- " +
+ strings.TrimPrefix(selectedFile.GetPath(), build.RootPathPrefix) +
+ " ---").SetDynamicColors(true)
+
+ readNextPart := func(linesCount int) int {
+ var err error
+ readLines := 0
+ for scanner.Scan() && readLines <= linesCount {
+ _, err = file.Write(scanner.Bytes())
+ if err != nil {
+ ui.showErr("Error reading file", err)
+ return 0
+ }
+ _, err = file.Write([]byte("\n"))
+ if err != nil {
+ ui.showErr("Error reading file", err)
+ return 0
+ }
+ readLines++
+ }
+ return readLines
+ }
+ totalLines += readNextPart(defaultLinesCount)
+
+ file.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+ if event.Rune() == 'q' || event.Key() == tcell.KeyESC {
+ err = f.Close()
+ if err != nil {
+ ui.showErr("Error closing file", err)
+ return event
+ }
+ ui.currentDirLabel.SetText("[::b] --- " +
+ strings.TrimPrefix(ui.currentDirPath, build.RootPathPrefix) +
+ " ---").SetDynamicColors(true)
+ ui.pages.RemovePage("file")
+ ui.app.SetFocus(ui.table)
+ return event
+ }
+
+ switch {
+ case event.Rune() == 'j':
+ fallthrough
+ case event.Rune() == 'G':
+ fallthrough
+ case event.Key() == tcell.KeyDown:
+ fallthrough
+ case event.Key() == tcell.KeyPgDn:
+ _, _, _, height := file.GetInnerRect()
+ row, _ := file.GetScrollOffset()
+ if height+row > totalLines-linesTreshold {
+ totalLines += readNextPart(defaultLinesCount)
+ }
+ }
+ return event
+ })
+
+ 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(file, 2, 0, 1, 1, 0, 0, true).
+ AddItem(ui.footerLabel, 3, 0, 1, 1, 0, 0, false)
+
+ ui.pages.HidePage("background")
+ ui.pages.AddPage("file", grid, true, true)
+
+ return file
+}
+
+func (ui *UI) showInfo() {
+ if ui.currentDir == nil {
+ return
+ }
+
+ var content, numberColor string
+ row, column := ui.table.GetSelection()
+ selectedFile := ui.table.GetCell(row, column).GetReference().(analyze.Item)
+
+ if ui.UseColors {
+ numberColor = "[#e67100::b]"
+ } else {
+ numberColor = "[::b]"
+ }
+
+ linesCount := 12
+
+ text := tview.NewTextView().SetDynamicColors(true)
+ text.SetBorder(true).SetBorderPadding(2, 2, 2, 2)
+ text.SetBorderColor(tcell.ColorDefault)
+ text.SetTitle(" Item info ")
+
+ content += "[::b]Name:[::-] "
+ content += tview.Escape(selectedFile.GetName()) + "\n"
+ content += "[::b]Path:[::-] "
+ content += tview.Escape(
+ strings.TrimPrefix(selectedFile.GetPath(), build.RootPathPrefix),
+ ) + "\n"
+ content += "[::b]Type:[::-] " + selectedFile.GetType() + "\n\n"
+
+ content += " [::b]Disk usage:[::-] "
+ content += numberColor + ui.formatSize(selectedFile.GetUsage(), false, true)
+ content += fmt.Sprintf(" (%s%d[-::] B)", numberColor, selectedFile.GetUsage()) + "\n"
+ content += "[::b]Apparent size:[::-] "
+ content += numberColor + ui.formatSize(selectedFile.GetSize(), false, true)
+ content += fmt.Sprintf(" (%s%d[-::] B)", numberColor, selectedFile.GetSize()) + "\n"
+
+ if selectedFile.GetMultiLinkedInode() > 0 {
+ linkedItems := ui.linkedItems[selectedFile.GetMultiLinkedInode()]
+ linesCount += 2 + len(linkedItems)
+ content += "\nHard-linked files:\n"
+ for _, linkedItem := range linkedItems {
+ content += "\t" + linkedItem.GetPath() + "\n"
+ }
+ }
+
+ text.SetText(content)
+
+ flex := tview.NewFlex().
+ AddItem(nil, 0, 1, false).
+ AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
+ AddItem(nil, 0, 1, false).
+ AddItem(text, linesCount, 1, false).
+ AddItem(nil, 0, 1, false), 80, 1, false).
+ AddItem(nil, 0, 1, false)
+
+ ui.pages.AddPage("info", flex, true, true)
+}
--- /dev/null
+//go:build linux
+// +build linux
+
+package tui
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/dundee/gdu/v5/internal/testapp"
+ "github.com/dundee/gdu/v5/pkg/device"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestShowDevicesWithError(t *testing.T) {
+ app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ getter := device.LinuxDevicesInfoGetter{MountsPath: "/xyzxyz"}
+
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, false, false)
+ err := ui.ListDevices(getter)
+
+ assert.Contains(t, err.Error(), "no such file")
+}
--- /dev/null
+package tui
+
+import (
+ "bytes"
+ "os"
+ "testing"
+
+ "github.com/dundee/gdu/v5/internal/testanalyze"
+ "github.com/dundee/gdu/v5/internal/testapp"
+ "github.com/dundee/gdu/v5/internal/testdir"
+ "github.com/dundee/gdu/v5/pkg/analyze"
+ "github.com/gdamore/tcell/v2"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestShowDevices(t *testing.T) {
+ app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+ err := ui.ListDevices(getDevicesInfoMock())
+
+ assert.Nil(t, err)
+
+ 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) {
+ app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, false, false)
+ err := ui.ListDevices(getDevicesInfoMock())
+
+ assert.Nil(t, err)
+
+ 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 TestDeviceSelected(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.done = make(chan struct{})
+ ui.SetIgnoreDirPaths([]string{"/xxx"})
+ err := ui.ListDevices(getDevicesInfoMock())
+
+ assert.Nil(t, err)
+ assert.Equal(t, 3, ui.table.GetRowCount())
+
+ ui.deviceItemSelected(1, 0)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ assert.Equal(t, 5, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "/..")
+ assert.Contains(t, ui.table.GetCell(1, 0).Text, "aaa")
+}
+
+func TestAnalyzePath(t *testing.T) {
+ ui := getAnalyzedPathMockedApp(t, true, true, true)
+
+ assert.Equal(t, 4, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "aaa")
+}
+
+func TestAnalyzePathBW(t *testing.T) {
+ ui := getAnalyzedPathMockedApp(t, false, true, true)
+
+ assert.Equal(t, 4, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "aaa")
+}
+
+func TestAnalyzePathWithParentDir(t *testing.T) {
+ parentDir := &analyze.Dir{
+ File: &analyze.File{
+ Name: "parent",
+ },
+ Files: make([]analyze.Item, 0, 1),
+ }
+
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, true)
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.topDir = parentDir
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", parentDir)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+ assert.Equal(t, parentDir, ui.currentDir.Parent)
+
+ assert.Equal(t, 5, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "/..")
+ assert.Contains(t, ui.table.GetCell(1, 0).Text, "aaa")
+}
+
+func TestReadAnalysis(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ input, err := os.OpenFile("../internal/testdata/test.json", os.O_RDONLY, 0644)
+ assert.Nil(t, err)
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+ ui.done = make(chan struct{})
+
+ err = ui.ReadAnalysis(input)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for reading
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "gdu", ui.currentDir.Name)
+}
+
+func TestReadAnalysisWithWrongFile(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ input, err := os.OpenFile("../internal/testdata/wrong.json", os.O_RDONLY, 0644)
+ assert.Nil(t, err)
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+ ui.done = make(chan struct{})
+
+ err = ui.ReadAnalysis(input)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for reading
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.True(t, ui.pages.HasPage("error"))
+}
+
+func TestViewDirContents(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ res := ui.showFile() // selected item is dir, do nothing
+ assert.Nil(t, res)
+}
+
+func TestViewFileWithoutCurrentDir(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.done = make(chan struct{})
+
+ res := ui.showFile() // no current directory
+ assert.Nil(t, res)
+}
+
+func TestViewContentsOfNotExistingFile(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ ui.table.Select(3, 0)
+
+ selectedFile := ui.table.GetCell(3, 0).GetReference().(analyze.Item)
+ assert.Equal(t, "ddd", selectedFile.GetName())
+
+ res := ui.showFile()
+ assert.Nil(t, res)
+}
+
+func TestViewFile(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ ui.table.Select(0, 0)
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0))
+ ui.table.Select(2, 0)
+
+ file := ui.showFile()
+ assert.True(t, ui.pages.HasPage("file"))
+
+ event := file.GetInputCapture()(tcell.NewEventKey(tcell.KeyRune, 'j', 0))
+ assert.Equal(t, 'j', event.Rune())
+}
+
+func TestShowInfo(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0))
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'i', 0))
+
+ assert.True(t, ui.pages.HasPage("info"))
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'q', 0))
+
+ assert.False(t, ui.pages.HasPage("info"))
+}
+
+func TestShowInfoBW(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, false, false)
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0))
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'i', 0))
+
+ assert.True(t, ui.pages.HasPage("info"))
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'i', 0))
+
+ assert.False(t, ui.pages.HasPage("info"))
+}
+
+func TestShowInfoWithHardlinks(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ nested := ui.currentDir.Files[0].(*analyze.Dir)
+ subnested := nested.Files[1].(*analyze.Dir)
+ file := subnested.Files[0].(*analyze.File)
+ file2 := nested.Files[0].(*analyze.File)
+ file.Mli = 1
+ file2.Mli = 1
+
+ ui.currentDir.UpdateStats(ui.linkedItems)
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0))
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'i', 0))
+
+ assert.True(t, ui.pages.HasPage("info"))
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'q', 0))
+
+ assert.False(t, ui.pages.HasPage("info"))
+}
+
+func TestShowInfoWithoutCurrentDir(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.done = make(chan struct{})
+
+ // pressing `i` will do nothing
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'i', 0))
+ assert.False(t, ui.pages.HasPage("info"))
+}
+
+func TestExitViewFile(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ ui.table.Select(0, 0)
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0))
+ ui.table.Select(2, 0)
+
+ file := ui.showFile()
+
+ assert.True(t, ui.pages.HasPage("file"))
+
+ file.GetInputCapture()(tcell.NewEventKey(tcell.KeyRune, 'q', 0))
+
+ assert.False(t, ui.pages.HasPage("file"))
+}
--- /dev/null
+package tui
+
+import (
+ "os"
+ "os/exec"
+)
+
+// Execute runs given bin path via exec.Command call
+func Execute(argv0 string, argv []string, envv []string) error {
+ cmd := exec.Command(argv0, argv...)
+
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ cmd.Stdin = os.Stdin
+ cmd.Env = envv
+
+ return cmd.Run()
+}
--- /dev/null
+//go:build !windows\r
+// +build !windows\r
+\r
+package tui\r
+\r
+import (\r
+ "os"\r
+)\r
+\r
+func getShellBin() string {\r
+ shellbin, ok := os.LookupEnv("SHELL")\r
+ if !ok {\r
+ shellbin = "/bin/bash"\r
+ }\r
+ return shellbin\r
+}\r
+\r
+func (ui *UI) spawnShell() {\r
+ if ui.currentDir == nil {\r
+ return\r
+ }\r
+\r
+ ui.app.Suspend(func() {\r
+ if err := os.Chdir(ui.currentDirPath); err != nil {\r
+ ui.showErr("Error changing directory", err)\r
+ return\r
+ }\r
+\r
+ if err := ui.exec(getShellBin(), nil, os.Environ()); err != nil {\r
+ ui.showErr("Error executing shell", err)\r
+ }\r
+ })\r
+}\r
--- /dev/null
+package tui
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestExecute(t *testing.T) {
+ err := Execute("true", []string{}, []string{})
+
+ assert.Nil(t, err)
+}
--- /dev/null
+package tui\r
+\r
+import (\r
+ "os"\r
+)\r
+\r
+func getShellBin() string {\r
+ shellbin, ok := os.LookupEnv("COMSPEC")\r
+ if !ok {\r
+ shellbin = "C:\\WINDOWS\\System32\\cmd.exe"\r
+ }\r
+ return shellbin\r
+}\r
+\r
+func (ui *UI) spawnShell() {\r
+ if ui.currentDir == nil {\r
+ return\r
+ }\r
+\r
+ ui.app.Stop()\r
+\r
+ if err := os.Chdir(ui.currentDirPath); err != nil {\r
+ ui.showErr("Error changing directory", err)\r
+ return\r
+ }\r
+ if err := ui.exec(getShellBin(), nil, os.Environ()); err != nil {\r
+ ui.showErr("Error executing shell", err)\r
+ }\r
+}\r
--- /dev/null
+package tui
+
+import (
+ "github.com/gdamore/tcell/v2"
+ "github.com/rivo/tview"
+)
+
+func (ui *UI) hideFilterInput() {
+ ui.filterValue = ""
+ ui.footer.Clear()
+ ui.footer.AddItem(ui.footerLabel, 0, 1, false)
+ ui.app.SetFocus(ui.table)
+ ui.filteringInput = nil
+ ui.filtering = false
+}
+
+func (ui *UI) showFilterInput() {
+ if ui.currentDir == nil {
+ return
+ }
+
+ if ui.filteringInput == nil {
+ ui.filteringInput = tview.NewInputField()
+
+ if !ui.UseColors {
+ ui.filteringInput.SetFieldBackgroundColor(
+ tcell.NewRGBColor(100, 100, 100),
+ )
+ ui.filteringInput.SetFieldTextColor(
+ tcell.NewRGBColor(255, 255, 255),
+ )
+ }
+
+ ui.filteringInput.SetChangedFunc(func(text string) {
+ ui.filterValue = text
+ ui.showDir()
+ })
+ ui.filteringInput.SetDoneFunc(func(key tcell.Key) {
+ if key == tcell.KeyESC {
+ ui.hideFilterInput()
+ ui.showDir()
+ } else {
+ ui.app.SetFocus(ui.table)
+ ui.filtering = false
+ }
+ })
+
+ ui.footer.Clear()
+ ui.footer.AddItem(ui.filteringInput, 0, 1, true)
+ ui.footer.AddItem(ui.footerLabel, 0, 5, false)
+ }
+ ui.app.SetFocus(ui.filteringInput)
+ ui.filtering = true
+}
--- /dev/null
+package tui
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/dundee/gdu/v5/internal/testanalyze"
+ "github.com/dundee/gdu/v5/internal/testapp"
+ "github.com/dundee/gdu/v5/internal/testdir"
+ "github.com/gdamore/tcell/v2"
+ "github.com/rivo/tview"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestFiltering(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ ui.showFilterInput()
+ ui.filterValue = ""
+ ui.showDir()
+
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "aaa") // nothing is filtered
+
+ ui.filterValue = "cc"
+ ui.showDir()
+
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "ccc") // shows only cccc
+
+ ui.hideFilterInput()
+ ui.showDir()
+
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "aaa") // filtering reset
+}
+
+func TestFilteringWithoutCurrentDir(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.done = make(chan struct{})
+
+ ui.showFilterInput()
+
+ assert.False(t, ui.filtering)
+}
+
+func TestSwitchToTable(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, '/', 0)) // open filtering input
+ handler := ui.filteringInput.InputHandler()
+ handler(tcell.NewEventKey(tcell.KeyRune, 'n', 0), func(p tview.Primitive) {})
+ handler(tcell.NewEventKey(tcell.KeyRune, 'e', 0), func(p tview.Primitive) {})
+ handler(tcell.NewEventKey(tcell.KeyRune, 's', 0), func(p tview.Primitive) {})
+
+ ui.table.Select(0, 0)
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0)) // we are filtering, should do nothing
+
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "nested")
+
+ handler(
+ tcell.NewEventKey(tcell.KeyTAB, ' ', 0), func(p tview.Primitive) {},
+ ) // switch focus to table
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyTAB, ' ', 0)) // switch back to input
+ handler(
+ tcell.NewEventKey(tcell.KeyEnter, ' ', 0), func(p tview.Primitive) {},
+ ) // switch back to table
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0)) // open nested dir
+
+ assert.Contains(t, ui.table.GetCell(1, 0).Text, "subnested")
+ assert.Empty(t, ui.filterValue) // filtering reset
+}
+
+func TestExitFiltering(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, '/', 0)) // open filtering input
+ handler := ui.filteringInput.InputHandler()
+ ui.filterValue = "xxx"
+ ui.showDir()
+
+ assert.Equal(t, ui.table.GetCell(0, 0).Text, "") // nothing is filtered
+
+ handler(
+ tcell.NewEventKey(tcell.KeyEsc, ' ', 0), func(p tview.Primitive) {},
+ ) // exit filtering
+
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "nested")
+ assert.Empty(t, ui.filterValue) // filtering reset
+}
--- /dev/null
+package tui
+
+import (
+ "fmt"
+
+ "github.com/dundee/gdu/v5/internal/common"
+ "github.com/dundee/gdu/v5/pkg/analyze"
+ "github.com/rivo/tview"
+)
+
+func (ui *UI) formatFileRow(item analyze.Item) string {
+ var part int
+
+ if ui.ShowApparentSize {
+ part = int(float64(item.GetSize()) / float64(item.GetParent().GetSize()) * 10.0)
+ } else {
+ part = int(float64(item.GetUsage()) / float64(item.GetParent().GetUsage()) * 10.0)
+ }
+
+ row := string(item.GetFlag())
+
+ if ui.UseColors {
+ row += "[#e67100::b]"
+ } else {
+ row += "[::b]"
+ }
+
+ if ui.ShowApparentSize {
+ row += fmt.Sprintf("%15s", ui.formatSize(item.GetSize(), false, true))
+ } else {
+ row += fmt.Sprintf("%15s", ui.formatSize(item.GetUsage(), false, true))
+ }
+
+ row += getUsageGraph(part)
+
+ if ui.showItemCount {
+ if ui.UseColors {
+ row += "[#e67100::b]"
+ } else {
+ row += "[::b]"
+ }
+ row += fmt.Sprintf("%11s ", ui.formatCount(item.GetItemCount()))
+ }
+
+ if ui.showMtime {
+ if ui.UseColors {
+ row += "[#e67100::b]"
+ } else {
+ row += "[::b]"
+ }
+ row += fmt.Sprintf(
+ "%s [-::]",
+ item.GetMtime().Format("2006-01-02 15:04:05"),
+ )
+ }
+
+ if item.IsDir() {
+ if ui.UseColors {
+ row += "[#3498db::b]/"
+ } else {
+ row += "[::b]/"
+ }
+ }
+ row += tview.Escape(item.GetName())
+ return row
+}
+
+func (ui *UI) formatSize(size int64, reverseColor bool, transparentBg bool) string {
+ var color string
+ if reverseColor {
+ if ui.UseColors {
+ color = "[black:#2479d0:-]"
+ } else {
+ color = "[black:white:-]"
+ }
+ } else {
+ if transparentBg {
+ color = "[-::]"
+ } else {
+ color = "[white:black:-]"
+ }
+ }
+
+ fsize := float64(size)
+
+ switch {
+ case fsize >= common.EB:
+ return fmt.Sprintf("%.1f%s EiB", fsize/common.EB, color)
+ case fsize >= common.PB:
+ return fmt.Sprintf("%.1f%s PiB", fsize/common.PB, color)
+ case fsize >= common.TB:
+ return fmt.Sprintf("%.1f%s TiB", fsize/common.TB, color)
+ case fsize >= common.GB:
+ return fmt.Sprintf("%.1f%s GiB", fsize/common.GB, color)
+ case fsize >= common.MB:
+ return fmt.Sprintf("%.1f%s MiB", fsize/common.MB, color)
+ case fsize >= common.KB:
+ return fmt.Sprintf("%.1f%s KiB", fsize/common.KB, color)
+ default:
+ return fmt.Sprintf("%d%s B", size, color)
+ }
+}
+
+func (ui *UI) formatCount(count int) string {
+ row := ""
+ color := "[-::]"
+
+ switch {
+ case count >= common.G:
+ row += fmt.Sprintf("%.1f%sG", float64(count)/float64(common.G), color)
+ case count >= common.M:
+ row += fmt.Sprintf("%.1f%sM", float64(count)/float64(common.M), color)
+ case count >= common.K:
+ row += fmt.Sprintf("%.1f%sk", float64(count)/float64(common.K), color)
+ default:
+ row += fmt.Sprintf("%d%s", count, color)
+ }
+ return row
+}
--- /dev/null
+package tui
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/dundee/gdu/v5/internal/testapp"
+ "github.com/dundee/gdu/v5/pkg/analyze"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestFormatSize(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, false, false)
+
+ assert.Equal(t, "1[white:black:-] B", ui.formatSize(1, false, false))
+ assert.Equal(t, "1.0[white:black:-] KiB", ui.formatSize(1<<10, false, false))
+ assert.Equal(t, "1.0[white:black:-] MiB", ui.formatSize(1<<20, false, false))
+ assert.Equal(t, "1.0[white:black:-] GiB", ui.formatSize(1<<30, false, false))
+ assert.Equal(t, "1.0[white:black:-] TiB", ui.formatSize(1<<40, false, false))
+ assert.Equal(t, "1.0[white:black:-] PiB", ui.formatSize(1<<50, false, false))
+ assert.Equal(t, "1.0[white:black:-] EiB", ui.formatSize(1<<60, false, false))
+}
+
+func TestFormatCount(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, false, false)
+
+ assert.Equal(t, "1[-::]", ui.formatCount(1))
+ assert.Equal(t, "1.0[-::]k", ui.formatCount(1<<10))
+ assert.Equal(t, "1.0[-::]M", ui.formatCount(1<<20))
+ assert.Equal(t, "1.1[-::]G", ui.formatCount(1<<30))
+}
+
+func TestEscapeName(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, false, false)
+
+ dir := &analyze.Dir{
+ File: &analyze.File{
+ Usage: 10,
+ },
+ }
+
+ file := &analyze.File{
+ Name: "Aaa [red] bbb",
+ Parent: dir,
+ Usage: 10,
+ }
+
+ assert.Contains(t, ui.formatFileRow(file), "Aaa [red[] bbb")
+}
--- /dev/null
+package tui
+
+import (
+ "fmt"
+
+ "github.com/dundee/gdu/v5/pkg/analyze"
+ "github.com/gdamore/tcell/v2"
+)
+
+func (ui *UI) keyPressed(key *tcell.EventKey) *tcell.EventKey {
+ if ui.pages.HasPage("file") {
+ return key // send event to primitive
+ }
+ if ui.filtering {
+ return key
+ }
+
+ if key.Key() == tcell.KeyEsc || key.Rune() == 'q' {
+ if ui.pages.HasPage("help") {
+ ui.pages.RemovePage("help")
+ ui.app.SetFocus(ui.table)
+ return nil
+ }
+ if ui.pages.HasPage("info") {
+ ui.pages.RemovePage("info")
+ ui.app.SetFocus(ui.table)
+ return nil
+ }
+ }
+
+ if ui.pages.HasPage("info") {
+ switch key.Rune() {
+ case 'i':
+ ui.pages.RemovePage("info")
+ ui.app.SetFocus(ui.table)
+ return nil
+ case '?':
+ return nil
+ }
+
+ if key.Key() == tcell.KeyUp ||
+ key.Key() == tcell.KeyDown ||
+ key.Rune() == 'j' ||
+ key.Rune() == 'k' {
+ row, column := ui.table.GetSelection()
+ if (key.Key() == tcell.KeyUp || key.Rune() == 'k') && row > 0 {
+ row--
+ } else if (key.Key() == tcell.KeyDown || key.Rune() == 'j') &&
+ row+1 < ui.table.GetRowCount() {
+ row++
+ }
+ ui.table.Select(row, column)
+ }
+ defer ui.showInfo() // refresh file info after any change
+ }
+
+ switch key.Rune() {
+ case 'Q':
+ ui.app.Stop()
+ fmt.Fprintf(ui.output, "%s\n", ui.currentDirPath)
+ return nil
+ case 'q':
+ ui.app.Stop()
+ return nil
+ case 'b':
+ ui.spawnShell()
+ return nil
+ case '?':
+ if ui.pages.HasPage("help") {
+ ui.pages.RemovePage("help")
+ ui.app.SetFocus(ui.table)
+ return nil
+ }
+ ui.showHelp()
+ }
+
+ if ui.pages.HasPage("confirm") ||
+ ui.pages.HasPage("progress") ||
+ ui.pages.HasPage("deleting") ||
+ ui.pages.HasPage("emptying") ||
+ ui.pages.HasPage("help") {
+ return key
+ }
+
+ if key.Rune() == 'h' || key.Key() == tcell.KeyLeft {
+ ui.handleLeft()
+ return nil
+ }
+
+ if key.Rune() == 'l' || key.Key() == tcell.KeyRight {
+ ui.handleRight()
+ return nil
+ }
+
+ if key.Key() == tcell.KeyTab && ui.filteringInput != nil {
+ ui.filtering = true
+ ui.app.SetFocus(ui.filteringInput)
+ return nil
+ }
+
+ switch key.Rune() {
+ case 'd':
+ ui.handleDelete(false)
+ case 'e':
+ ui.handleDelete(true)
+ case 'v':
+ ui.showFile()
+ case 'i':
+ ui.showInfo()
+ case 'a':
+ ui.ShowApparentSize = !ui.ShowApparentSize
+ if ui.currentDir != nil {
+ row, column := ui.table.GetSelection()
+ ui.showDir()
+ ui.table.Select(row, column)
+ }
+ case 'c':
+ ui.showItemCount = !ui.showItemCount
+ if ui.currentDir != nil {
+ row, column := ui.table.GetSelection()
+ ui.showDir()
+ ui.table.Select(row, column)
+ }
+ case 'm':
+ ui.showMtime = !ui.showMtime
+ if ui.currentDir != nil {
+ row, column := ui.table.GetSelection()
+ ui.showDir()
+ ui.table.Select(row, column)
+ }
+ case 'r':
+ if ui.currentDir != nil {
+ ui.rescanDir()
+ }
+ case 's':
+ ui.setSorting("size")
+ case 'C':
+ ui.setSorting("itemCount")
+ case 'n':
+ ui.setSorting("name")
+ case 'M':
+ ui.setSorting("mtime")
+ case '/':
+ ui.showFilterInput()
+ return nil
+ }
+ return key
+}
+
+func (ui *UI) handleLeft() {
+ if ui.currentDirPath == ui.topDirPath {
+ return
+ }
+ if ui.currentDir != nil {
+ ui.fileItemSelected(0, 0)
+ }
+}
+
+func (ui *UI) handleRight() {
+ row, column := ui.table.GetSelection()
+ if ui.currentDirPath != ui.topDirPath && row == 0 { // do not select /..
+ return
+ }
+
+ if ui.currentDir != nil {
+ ui.fileItemSelected(row, column)
+ } else {
+ ui.deviceItemSelected(row, column)
+ }
+}
+
+func (ui *UI) handleDelete(shouldEmpty bool) {
+ if ui.currentDir == nil {
+ return
+ }
+ // do not allow deleting parent dir
+ row, column := ui.table.GetSelection()
+ selectedFile := ui.table.GetCell(row, column).GetReference().(analyze.Item)
+ if selectedFile == ui.currentDir.Parent {
+ return
+ }
+
+ if ui.askBeforeDelete {
+ ui.confirmDeletion(shouldEmpty)
+ } else {
+ ui.deleteSelected(shouldEmpty)
+ }
+}
--- /dev/null
+package tui
+
+import (
+ "bytes"
+ "errors"
+ "testing"
+
+ "github.com/dundee/gdu/v5/internal/testanalyze"
+ "github.com/dundee/gdu/v5/internal/testapp"
+ "github.com/dundee/gdu/v5/internal/testdir"
+ "github.com/dundee/gdu/v5/pkg/analyze"
+ "github.com/gdamore/tcell/v2"
+ "github.com/rivo/tview"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestShowHelp(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, '?', 0))
+
+ assert.True(t, ui.pages.HasPage("help"))
+}
+
+func TestCloseHelp(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+ ui.showHelp()
+
+ assert.True(t, ui.pages.HasPage("help"))
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyEsc, 'q', 0))
+
+ assert.False(t, ui.pages.HasPage("help"))
+}
+
+func TestKeyWhileDeleting(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+
+ modal := tview.NewModal().SetText("Deleting...")
+ ui.pages.AddPage("deleting", modal, true, true)
+
+ key := ui.keyPressed(tcell.NewEventKey(tcell.KeyEnter, ' ', 0))
+ assert.Equal(t, tcell.KeyEnter, key.Key())
+}
+
+func TestLeftRightKeyWhileConfirm(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+
+ modal := tview.NewModal().SetText("Really?")
+ ui.pages.AddPage("confirm", modal, true, true)
+
+ key := ui.keyPressed(tcell.NewEventKey(tcell.KeyLeft, 'h', 0))
+ assert.Equal(t, tcell.KeyLeft, key.Key())
+ key = ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0))
+ assert.Equal(t, tcell.KeyRight, key.Key())
+}
+
+func TestMoveLeftRight(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ ui.table.Select(0, 0)
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0))
+
+ assert.Equal(t, "nested", ui.currentDir.Name)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0)) // try /.. first
+
+ assert.Equal(t, "nested", ui.currentDir.Name)
+
+ ui.table.Select(1, 0)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0))
+
+ assert.Equal(t, "subnested", ui.currentDir.Name)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyLeft, 'h', 0))
+
+ assert.Equal(t, "nested", ui.currentDir.Name)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyLeft, 'h', 0))
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyLeft, 'h', 0))
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+}
+
+func TestMoveRightOnDevice(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.done = make(chan struct{})
+ ui.SetIgnoreDirPaths([]string{})
+ err := ui.ListDevices(getDevicesInfoMock())
+ assert.Nil(t, err)
+
+ ui.table.Select(1, 0)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0))
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+}
+
+func TestStop(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+
+ key := ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'q', 0))
+ assert.Nil(t, key)
+}
+
+func TestStopWithPrintingPath(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ buff := &bytes.Buffer{}
+ ui := CreateUI(app, simScreen, buff, true, true, false)
+
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ key := ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'Q', 0))
+ assert.Nil(t, key)
+
+ assert.Equal(t, "test_dir\n", buff.String())
+}
+
+func TestSpawnShell(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ buff := &bytes.Buffer{}
+ ui := CreateUI(app, simScreen, buff, true, true, false)
+ var called = false
+ ui.exec = func(argv0 string, argv, envv []string) error {
+ called = true
+ return nil
+ }
+
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ key := ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'b', 0))
+ assert.Nil(t, key)
+ assert.True(t, called)
+}
+
+func TestSpawnShellWithoutDir(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ buff := &bytes.Buffer{}
+ ui := CreateUI(app, simScreen, buff, true, true, false)
+ var called = false
+ ui.exec = func(argv0 string, argv, envv []string) error {
+ called = true
+ return nil
+ }
+
+ ui.done = make(chan struct{})
+
+ key := ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'b', 0))
+ assert.Nil(t, key)
+ assert.False(t, called)
+}
+
+func TestSpawnShellWithWrongDir(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ buff := &bytes.Buffer{}
+ ui := CreateUI(app, simScreen, buff, true, true, false)
+ var called = false
+ ui.exec = func(argv0 string, argv, envv []string) error {
+ called = true
+ return nil
+ }
+
+ ui.done = make(chan struct{})
+ ui.currentDir = &analyze.Dir{}
+ ui.currentDirPath = "/xxxxx"
+
+ key := ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'b', 0))
+ assert.Nil(t, key)
+ assert.False(t, called)
+ assert.True(t, ui.pages.HasPage("error"))
+}
+
+func TestSpawnShellWithError(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ buff := &bytes.Buffer{}
+ ui := CreateUI(app, simScreen, buff, true, true, false)
+ var called = false
+ ui.exec = func(argv0 string, argv, envv []string) error {
+ called = true
+ return errors.New("wrong shell")
+ }
+
+ ui.done = make(chan struct{})
+ ui.currentDir = &analyze.Dir{}
+ ui.currentDirPath = "."
+
+ key := ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'b', 0))
+ assert.Nil(t, key)
+ assert.True(t, called)
+ assert.True(t, ui.pages.HasPage("error"))
+}
+
+func TestShowConfirm(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ ui.table.Select(1, 0)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'd', 0))
+
+ assert.True(t, ui.pages.HasPage("confirm"))
+}
+
+func TestDeleteEmpty(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.done = make(chan struct{})
+
+ key := ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'd', 0))
+ assert.NotNil(t, key)
+}
+
+func TestDelete(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.done = make(chan struct{})
+ ui.askBeforeDelete = false
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ assert.Equal(t, 1, ui.table.GetRowCount())
+
+ ui.table.Select(0, 0)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'd', 0))
+
+ <-ui.done
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.NoDirExists(t, "test_dir/nested")
+}
+
+func TestDeleteParent(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.done = make(chan struct{})
+ ui.askBeforeDelete = false
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+ assert.Equal(t, 1, ui.table.GetRowCount())
+
+ ui.table.Select(0, 0)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0))
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'd', 0))
+
+ assert.DirExists(t, "test_dir/nested")
+}
+
+func TestEmptyDir(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.done = make(chan struct{})
+ ui.askBeforeDelete = false
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ assert.Equal(t, 1, ui.table.GetRowCount())
+
+ ui.table.Select(0, 0)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'e', 0))
+
+ <-ui.done
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.DirExists(t, "test_dir/nested")
+ assert.NoDirExists(t, "test_dir/nested/subnested")
+}
+
+func TestEmptyFile(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.done = make(chan struct{})
+ ui.askBeforeDelete = false
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ assert.Equal(t, 1, ui.table.GetRowCount())
+
+ ui.table.Select(0, 0)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0)) // into nested
+
+ ui.table.Select(2, 0) // file2
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'e', 0))
+
+ <-ui.done
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.DirExists(t, "test_dir/nested")
+ assert.DirExists(t, "test_dir/nested/subnested")
+}
+
+func TestSortByApparentSize(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, false, false)
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'a', 0))
+
+ assert.True(t, ui.ShowApparentSize)
+}
+
+func TestShowFileCount(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, false, false)
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'c', 0))
+
+ assert.True(t, ui.showItemCount)
+}
+
+func TestShowFileCountBW(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, false, false)
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'c', 0))
+
+ assert.True(t, ui.showItemCount)
+}
+
+func TestShowMtime(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, false, false)
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'm', 0))
+
+ assert.True(t, ui.showMtime)
+}
+
+func TestShowMtimeBW(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, false, false)
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'm', 0))
+
+ assert.True(t, ui.showMtime)
+}
+
+func TestRescan(t *testing.T) {
+ parentDir := &analyze.Dir{
+ File: &analyze.File{
+ Name: "parent",
+ },
+ Files: make([]analyze.Item, 0, 1),
+ }
+ currentDir := &analyze.Dir{
+ File: &analyze.File{
+ Name: "sub",
+ Parent: parentDir,
+ },
+ }
+
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.done = make(chan struct{})
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.currentDir = currentDir
+ ui.topDir = parentDir
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'r', 0))
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+ assert.Equal(t, parentDir, ui.currentDir.Parent)
+
+ assert.Equal(t, 5, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "/..")
+ assert.Contains(t, ui.table.GetCell(1, 0).Text, "aaa")
+}
+
+func TestSorting(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 's', 0))
+ assert.Equal(t, "size", ui.sortBy)
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'C', 0))
+ assert.Equal(t, "itemCount", ui.sortBy)
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'n', 0))
+ assert.Equal(t, "name", ui.sortBy)
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'M', 0))
+ assert.Equal(t, "mtime", ui.sortBy)
+}
+
+func TestShowFile(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ ui.table.Select(0, 0)
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0))
+ ui.table.Select(2, 0)
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'v', 0))
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'q', 0))
+}
+
+func TestShowInfoAndMoveAround(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0))
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'i', 0))
+
+ assert.True(t, ui.pages.HasPage("info"))
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'k', 0)) // move up
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'j', 0)) // move down
+
+ ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'q', 0))
+
+ assert.False(t, ui.pages.HasPage("info"))
+}
--- /dev/null
+package tui
+
+import (
+ "time"
+
+ "github.com/dundee/gdu/v5/internal/common"
+ "github.com/dundee/gdu/v5/pkg/analyze"
+)
+
+func (ui *UI) updateProgress() {
+ color := "[white:black:b]"
+ if ui.UseColors {
+ color = "[red:black:b]"
+ }
+
+ progressChan := ui.Analyzer.GetProgressChan()
+ doneChan := ui.Analyzer.GetDoneChan()
+
+ var progress analyze.CurrentProgress
+
+ for {
+ select {
+ case progress = <-progressChan:
+ case <-doneChan:
+ return
+ }
+
+ func(itemCount int, totalSize int64, currentItem string) {
+ ui.app.QueueUpdateDraw(func() {
+ ui.progress.SetText("Total items: " +
+ color +
+ common.FormatNumber(int64(itemCount)) +
+ "[white:black:-] size: " +
+ color +
+ ui.formatSize(totalSize, false, false) +
+ "[white:black:-]\nCurrent item: [white:black:b]" +
+ currentItem)
+ })
+ }(progress.ItemCount, progress.TotalSize, progress.CurrentItemName)
+
+ time.Sleep(100 * time.Millisecond)
+ }
+}
--- /dev/null
+package tui
+
+import (
+ "strconv"
+ "strings"
+
+ "github.com/dundee/gdu/v5/build"
+ "github.com/gdamore/tcell/v2"
+ "github.com/rivo/tview"
+)
+
+func (ui *UI) showDir() {
+ var (
+ totalUsage int64
+ totalSize int64
+ itemCount int
+ )
+
+ ui.currentDirPath = ui.currentDir.GetPath()
+ ui.currentDirLabel.SetText("[::b] --- " +
+ tview.Escape(
+ strings.TrimPrefix(ui.currentDirPath, build.RootPathPrefix),
+ ) +
+ " ---").SetDynamicColors(true)
+
+ ui.table.Clear()
+
+ rowIndex := 0
+ if ui.currentDirPath != ui.topDirPath {
+ cell := tview.NewTableCell(" [::b]/..")
+ cell.SetReference(ui.currentDir.Parent)
+ cell.SetStyle(tcell.Style{}.Foreground(tcell.ColorDefault))
+ ui.table.SetCell(0, 0, cell)
+ rowIndex++
+ }
+
+ ui.sortItems()
+
+ for i, item := range ui.currentDir.Files {
+ if ui.filterValue != "" && !strings.Contains(
+ strings.ToLower(item.GetName()),
+ strings.ToLower(ui.filterValue),
+ ) {
+ continue
+ }
+
+ totalUsage += item.GetUsage()
+ totalSize += item.GetSize()
+ itemCount += item.GetItemCount()
+
+ cell := tview.NewTableCell(ui.formatFileRow(item))
+ cell.SetStyle(tcell.Style{}.Foreground(tcell.ColorDefault))
+ cell.SetReference(ui.currentDir.Files[i])
+
+ ui.table.SetCell(rowIndex, 0, cell)
+ rowIndex++
+ }
+
+ var footerNumberColor, footerTextColor string
+ if ui.UseColors {
+ footerNumberColor = "[#ffffff:#2479d0:b]"
+ footerTextColor = "[black:#2479d0:-]"
+ } else {
+ footerNumberColor = "[black:white:b]"
+ footerTextColor = "[black:white:-]"
+ }
+
+ ui.footerLabel.SetText(
+ " Total disk usage: " +
+ footerNumberColor +
+ ui.formatSize(totalUsage, true, false) +
+ " Apparent size: " +
+ footerNumberColor +
+ ui.formatSize(totalSize, true, false) +
+ " Items: " + footerNumberColor + strconv.Itoa(itemCount) +
+ footerTextColor +
+ " Sorting by: " + ui.sortBy + " " + ui.sortOrder)
+
+ ui.table.Select(0, 0)
+ ui.table.ScrollToBeginning()
+
+ if !ui.filtering {
+ ui.app.SetFocus(ui.table)
+ }
+}
+
+func (ui *UI) showDevices() {
+ var totalUsage int64
+
+ 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]"
+ }
+
+ ui.sortDevices()
+
+ for i, device := range ui.devices {
+ totalUsage += device.GetUsage()
+ 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, true)))
+ ui.table.SetCell(i+1, 2, tview.NewTableCell(sizeColor+ui.formatSize(device.Size-device.Free, false, true)))
+ ui.table.SetCell(i+1, 3, tview.NewTableCell(getDeviceUsagePart(device)))
+ ui.table.SetCell(i+1, 4, tview.NewTableCell(ui.formatSize(device.Free, false, true)))
+ ui.table.SetCell(i+1, 5, tview.NewTableCell(textColor+device.MountPoint))
+ }
+
+ var footerNumberColor, footerTextColor string
+ if ui.UseColors {
+ footerNumberColor = "[#ffffff:#2479d0:b]"
+ footerTextColor = "[black:#2479d0:-]"
+ } else {
+ footerNumberColor = "[black:white:b]"
+ footerTextColor = "[black:white:-]"
+ }
+
+ ui.footerLabel.SetText(
+ " Total usage: " +
+ footerNumberColor +
+ ui.formatSize(totalUsage, true, false) +
+ footerTextColor +
+ " Sorting by: " + ui.sortBy + " " + ui.sortOrder)
+
+ ui.table.Select(1, 0)
+ ui.table.SetSelectedFunc(ui.deviceItemSelected)
+}
+
+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) showHelp() {
+ text := tview.NewTextView().SetDynamicColors(true)
+ text.SetBorder(true).SetBorderPadding(2, 2, 2, 2)
+ text.SetBorderColor(tcell.ColorDefault)
+ text.SetTitle(" gdu help ")
+ text.SetScrollable(true)
+
+ if ui.UseColors {
+ text.SetText(
+ strings.ReplaceAll(
+ strings.ReplaceAll(helpText, "[::b]", "[red]"),
+ "[white:black:-]",
+ "[white]",
+ ),
+ )
+ } else {
+ text.SetText(helpText)
+ }
+
+ maxHeight := strings.Count(helpText, "\n") + 7
+ _, height := ui.screen.Size()
+ if height > maxHeight {
+ height = maxHeight
+ }
+
+ flex := tview.NewFlex().
+ AddItem(nil, 0, 1, false).
+ AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
+ AddItem(nil, 0, 1, false).
+ AddItem(text, height, 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)
+ ui.app.SetFocus(text)
+}
--- /dev/null
+package tui
+
+import (
+ "sort"
+
+ "github.com/dundee/gdu/v5/pkg/analyze"
+ "github.com/dundee/gdu/v5/pkg/device"
+)
+
+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"
+ }
+
+ if ui.currentDir != nil {
+ ui.showDir()
+ } else if ui.devices != nil && (newOrder == "size" || newOrder == "name") {
+ ui.showDevices()
+ }
+}
+
+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)))
+ }
+ }
+ if ui.sortBy == "mtime" {
+ if ui.sortOrder == "desc" {
+ sort.Sort(analyze.ByMtime(ui.currentDir.Files))
+ } else {
+ sort.Sort(sort.Reverse(analyze.ByMtime(ui.currentDir.Files)))
+ }
+ }
+}
+
+func (ui *UI) sortDevices() {
+ if ui.sortBy == "size" {
+ if ui.sortOrder == "desc" {
+ sort.Sort(device.ByUsedSize(ui.devices))
+ } else {
+ sort.Sort(sort.Reverse(device.ByUsedSize(ui.devices)))
+ }
+ }
+ if ui.sortBy == "name" {
+ if ui.sortOrder == "desc" {
+ sort.Sort(device.ByName(ui.devices))
+ } else {
+ sort.Sort(sort.Reverse(device.ByName(ui.devices)))
+ }
+ }
+}
--- /dev/null
+package tui
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/dundee/gdu/v5/internal/testanalyze"
+ "github.com/dundee/gdu/v5/internal/testapp"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAnalyzeByApparentSize(t *testing.T) {
+ ui := getAnalyzedPathWithSorting("size", "desc", true)
+
+ assert.Equal(t, 4, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "aaa")
+ assert.Contains(t, ui.table.GetCell(1, 0).Text, "bbb")
+ assert.Contains(t, ui.table.GetCell(2, 0).Text, "ccc")
+ assert.Contains(t, ui.table.GetCell(3, 0).Text, "ddd")
+}
+
+func TestSortByApparentSizeAsc(t *testing.T) {
+ ui := getAnalyzedPathWithSorting("size", "asc", true)
+
+ assert.Equal(t, 4, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "ddd")
+ assert.Contains(t, ui.table.GetCell(1, 0).Text, "aaa")
+ assert.Contains(t, ui.table.GetCell(2, 0).Text, "bbb")
+ assert.Contains(t, ui.table.GetCell(3, 0).Text, "ccc")
+}
+
+func TestAnalyzeBySize(t *testing.T) {
+ ui := getAnalyzedPathWithSorting("size", "desc", false)
+
+ assert.Equal(t, 4, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "aaa")
+ assert.Contains(t, ui.table.GetCell(1, 0).Text, "bbb")
+ assert.Contains(t, ui.table.GetCell(2, 0).Text, "ccc")
+ assert.Contains(t, ui.table.GetCell(3, 0).Text, "ddd")
+}
+
+func TestSortBySizeAsc(t *testing.T) {
+ ui := getAnalyzedPathWithSorting("size", "asc", false)
+
+ assert.Equal(t, 4, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "ddd")
+ assert.Contains(t, ui.table.GetCell(1, 0).Text, "aaa")
+ assert.Contains(t, ui.table.GetCell(2, 0).Text, "bbb")
+ assert.Contains(t, ui.table.GetCell(3, 0).Text, "ccc")
+}
+
+func TestAnalyzeByName(t *testing.T) {
+ ui := getAnalyzedPathWithSorting("name", "desc", false)
+
+ assert.Equal(t, 4, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "ddd")
+ assert.Contains(t, ui.table.GetCell(1, 0).Text, "ccc")
+ assert.Contains(t, ui.table.GetCell(2, 0).Text, "bbb")
+ assert.Contains(t, ui.table.GetCell(3, 0).Text, "aaa")
+}
+
+func TestAnalyzeByNameAsc(t *testing.T) {
+ ui := getAnalyzedPathWithSorting("name", "asc", false)
+
+ assert.Equal(t, 4, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "aaa")
+ assert.Contains(t, ui.table.GetCell(1, 0).Text, "bbb")
+ assert.Contains(t, ui.table.GetCell(2, 0).Text, "ccc")
+ assert.Contains(t, ui.table.GetCell(3, 0).Text, "ddd")
+}
+
+func TestAnalyzeByItemCount(t *testing.T) {
+ ui := getAnalyzedPathWithSorting("itemCount", "desc", false)
+
+ assert.Equal(t, 4, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "aaa")
+ assert.Contains(t, ui.table.GetCell(1, 0).Text, "bbb")
+ assert.Contains(t, ui.table.GetCell(2, 0).Text, "ccc")
+ assert.Contains(t, ui.table.GetCell(3, 0).Text, "ddd")
+}
+
+func TestAnalyzeByItemCountAsc(t *testing.T) {
+ ui := getAnalyzedPathWithSorting("itemCount", "asc", false)
+
+ assert.Equal(t, 4, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "aaa")
+ assert.Contains(t, ui.table.GetCell(1, 0).Text, "bbb")
+ assert.Contains(t, ui.table.GetCell(2, 0).Text, "ccc")
+ assert.Contains(t, ui.table.GetCell(3, 0).Text, "ddd")
+}
+
+func TestAnalyzeByMtime(t *testing.T) {
+ ui := getAnalyzedPathWithSorting("mtime", "desc", false)
+
+ assert.Equal(t, 4, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "aaa")
+ assert.Contains(t, ui.table.GetCell(1, 0).Text, "bbb")
+ assert.Contains(t, ui.table.GetCell(2, 0).Text, "ccc")
+ assert.Contains(t, ui.table.GetCell(3, 0).Text, "ddd")
+}
+
+func TestAnalyzeByMtimeAsc(t *testing.T) {
+ ui := getAnalyzedPathWithSorting("mtime", "asc", false)
+
+ assert.Equal(t, 4, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "ddd")
+ assert.Contains(t, ui.table.GetCell(1, 0).Text, "ccc")
+ assert.Contains(t, ui.table.GetCell(2, 0).Text, "bbb")
+ assert.Contains(t, ui.table.GetCell(3, 0).Text, "aaa")
+}
+
+func TestSetSorting(t *testing.T) {
+ ui := getAnalyzedPathWithSorting("itemCount", "asc", false)
+
+ ui.setSorting("name")
+ assert.Equal(t, "name", ui.sortBy)
+ assert.Equal(t, "asc", ui.sortOrder)
+ ui.setSorting("name")
+ assert.Equal(t, "name", ui.sortBy)
+ assert.Equal(t, "desc", ui.sortOrder)
+ ui.setSorting("name")
+ assert.Equal(t, "name", ui.sortBy)
+ assert.Equal(t, "asc", ui.sortOrder)
+}
+
+func TestSortDevicesByName(t *testing.T) {
+ app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+ err := ui.ListDevices(getDevicesInfoMock())
+
+ assert.Nil(t, err)
+
+ ui.setSorting("name") // sort by name asc
+ assert.Equal(t, "/dev/boot", ui.devices[0].Name)
+
+ ui.setSorting("name") // sort by name desc
+ assert.Equal(t, "/dev/root", ui.devices[0].Name)
+}
+
+func TestSortDevicesByUsedSize(t *testing.T) {
+ app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+ err := ui.ListDevices(getDevicesInfoMock())
+
+ assert.Nil(t, err)
+
+ ui.setSorting("size") // sort by used size asc
+ assert.Equal(t, "/dev/boot", ui.devices[0].Name)
+
+ ui.setSorting("size") // sort by used size desc
+ assert.Equal(t, "/dev/root", ui.devices[0].Name)
+}
+
+func getAnalyzedPathWithSorting(sortBy string, sortOrder string, apparentSize bool) *UI {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, apparentSize, false)
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.done = make(chan struct{})
+ ui.sortBy = sortBy
+ ui.sortOrder = sortOrder
+ if err := ui.AnalyzePath("test_dir", nil); err != nil {
+ panic(err)
+ }
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ return ui
+}
--- /dev/null
+package tui
+
+import (
+ "io"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/dundee/gdu/v5/internal/common"
+ "github.com/dundee/gdu/v5/pkg/analyze"
+ "github.com/dundee/gdu/v5/pkg/device"
+ "github.com/gdamore/tcell/v2"
+ "github.com/rivo/tview"
+)
+
+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:-]Go to directory/device
+ [::b]left, h [white:black:-]Go to parent directory
+
+ [::b]r [white:black:-]Rescan current directory
+ [::b]/ [white:black:-]Search items by name
+ [::b]a [white:black:-]Toggle between showing disk usage and apparent size
+ [::b]c [white:black:-]Show/hide file count
+ [::b]m [white:black:-]Show/hide latest mtime
+ [::b]b [white:black:-]Spawn shell in current directory
+ [::b]q [white:black:-]Quit gdu
+ [::b]Q [white:black:-]Quit gdu and print current directory path
+
+Item under cursor:
+ [::b]d [white:black:-]Delete file or directory
+ [::b]e [white:black:-]Empty file or directory
+ [::b]v [white:black:-]Show content of file
+ [::b]i [white:black:-]Show info about item
+
+Sort by (twice toggles asc/desc):
+ [::b]n [white:black:-]Sort by name (asc/desc)
+ [::b]s [white:black:-]Sort by size (asc/desc)
+ [::b]C [white:black:-]Sort by file count (asc/desc)
+ [::b]M [white:black:-]Sort by mtime (asc/desc)`
+
+// UI struct
+type UI struct {
+ *common.UI
+ app common.TermApplication
+ screen tcell.Screen
+ output io.Writer
+ header *tview.TextView
+ footer *tview.Flex
+ footerLabel *tview.TextView
+ currentDirLabel *tview.TextView
+ pages *tview.Pages
+ progress *tview.TextView
+ help *tview.Flex
+ table *tview.Table
+ filteringInput *tview.InputField
+ currentDir *analyze.Dir
+ devices []*device.Device
+ topDir *analyze.Dir
+ topDirPath string
+ currentDirPath string
+ askBeforeDelete bool
+ showItemCount bool
+ showMtime bool
+ filtering bool
+ filterValue string
+ sortBy string
+ sortOrder string
+ done chan struct{}
+ remover func(*analyze.Dir, analyze.Item) error
+ emptier func(*analyze.Dir, analyze.Item) error
+ exec func(argv0 string, argv []string, envv []string) error
+ linkedItems analyze.HardLinkedItems
+}
+
+// CreateUI creates the whole UI app
+func CreateUI(
+ app common.TermApplication, screen tcell.Screen, output io.Writer, useColors bool, showApparentSize bool, enableGC bool,
+) *UI {
+ ui := &UI{
+ UI: &common.UI{
+ UseColors: useColors,
+ ShowApparentSize: showApparentSize,
+ Analyzer: analyze.CreateAnalyzer(),
+ EnableGC: enableGC,
+ },
+ app: app,
+ screen: screen,
+ output: output,
+ askBeforeDelete: true,
+ showItemCount: false,
+ remover: analyze.RemoveItemFromDir,
+ emptier: analyze.EmptyFileFromDir,
+ exec: Execute,
+ linkedItems: make(analyze.HardLinkedItems, 10),
+ }
+ ui.resetSorting()
+
+ app.SetBeforeDrawFunc(func(screen tcell.Screen) bool {
+ screen.Clear()
+ return false
+ })
+
+ ui.app.SetInputCapture(ui.keyPressed)
+
+ var textColor, textBgColor tcell.Color
+ if ui.UseColors {
+ textColor = tcell.NewRGBColor(0, 0, 0)
+ textBgColor = tcell.NewRGBColor(36, 121, 208)
+ } else {
+ textColor = tcell.NewRGBColor(0, 0, 0)
+ textBgColor = tcell.NewRGBColor(255, 255, 255)
+ }
+
+ ui.header = tview.NewTextView()
+ ui.header.SetText(" gdu ~ Use arrow keys to navigate, press ? for help ")
+ ui.header.SetTextColor(textColor)
+ ui.header.SetBackgroundColor(textBgColor)
+
+ ui.currentDirLabel = tview.NewTextView()
+ ui.currentDirLabel.SetTextColor(tcell.ColorDefault)
+ ui.currentDirLabel.SetBackgroundColor(tcell.ColorDefault)
+
+ ui.table = tview.NewTable().SetSelectable(true, false)
+ ui.table.SetBackgroundColor(tcell.ColorDefault)
+
+ if ui.UseColors {
+ ui.table.SetSelectedStyle(tcell.Style{}.
+ Foreground(tview.Styles.TitleColor).
+ Background(tview.Styles.MoreContrastBackgroundColor).Bold(true))
+ } else {
+ ui.table.SetSelectedStyle(tcell.Style{}.
+ Foreground(tcell.ColorWhite).
+ Background(tcell.ColorGray).Bold(true))
+ }
+
+ ui.footerLabel = tview.NewTextView().SetDynamicColors(true)
+ ui.footerLabel.SetTextColor(textColor)
+ ui.footerLabel.SetBackgroundColor(textBgColor)
+ ui.footerLabel.SetText(" No items to display. ")
+
+ ui.footer = tview.NewFlex()
+ ui.footer.AddItem(ui.footerLabel, 0, 1, false)
+
+ 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
+}
+
+// StartUILoop starts tview application
+func (ui *UI) StartUILoop() error {
+ return ui.app.Run()
+}
+
+func (ui *UI) resetSorting() {
+ ui.sortBy = "size"
+ ui.sortOrder = "desc"
+}
+
+func (ui *UI) rescanDir() {
+ ui.Analyzer.ResetProgress()
+ err := ui.AnalyzePath(ui.currentDirPath, ui.currentDir.Parent)
+ if err != nil {
+ ui.showErr("Error rescanning path", err)
+ }
+}
+
+func (ui *UI) fileItemSelected(row, column int) {
+ if ui.currentDir == nil {
+ return
+ }
+
+ origDir := ui.currentDir
+ selectedDir := ui.table.GetCell(row, column).GetReference().(analyze.Item)
+ if !selectedDir.IsDir() {
+ return
+ }
+
+ ui.currentDir = selectedDir.(*analyze.Dir)
+ ui.hideFilterInput()
+ ui.showDir()
+
+ if selectedDir == origDir.Parent {
+ index, _ := ui.currentDir.Files.IndexOf(origDir)
+ if ui.currentDir != ui.topDir {
+ index++
+ }
+ ui.table.Select(index, 0)
+ }
+}
+
+func (ui *UI) deviceItemSelected(row, column int) {
+ var err error
+ selectedDevice := ui.table.GetCell(row, column).GetReference().(*device.Device)
+
+ paths := device.GetNestedMountpointsPaths(selectedDevice.MountPoint, ui.devices)
+ ui.IgnoreDirPathPatterns, err = common.CreateIgnorePattern(paths)
+
+ if err != nil {
+ log.Printf("Creating path patterns for other devices failed: %s", paths)
+ }
+
+ ui.resetSorting()
+
+ err = ui.AnalyzePath(selectedDevice.MountPoint, nil)
+ if err != nil {
+ ui.showErr("Error analyzing device", err)
+ }
+}
+
+func (ui *UI) confirmDeletion(shouldEmpty bool) {
+ row, column := ui.table.GetSelection()
+ selectedFile := ui.table.GetCell(row, column).GetReference().(analyze.Item)
+ var action string
+ if shouldEmpty {
+ action = "empty"
+ } else {
+ action = "delete"
+ }
+ modal := tview.NewModal().
+ SetText(
+ "Are you sure you want to " +
+ action +
+ " \"" +
+ tview.Escape(selectedFile.GetName()) +
+ "\"?",
+ ).
+ 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(shouldEmpty)
+ }
+ ui.pages.RemovePage("confirm")
+ })
+
+ if !ui.UseColors {
+ modal.SetBackgroundColor(tcell.ColorGray)
+ } else {
+ modal.SetBackgroundColor(tcell.ColorBlack)
+ }
+ modal.SetBorderColor(tcell.ColorDefault)
+
+ ui.pages.AddPage("confirm", modal, true, true)
+}
--- /dev/null
+package tui
+
+import (
+ "bytes"
+ "errors"
+ "testing"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/dundee/gdu/v5/internal/testanalyze"
+ "github.com/dundee/gdu/v5/internal/testapp"
+ "github.com/dundee/gdu/v5/internal/testdev"
+ "github.com/dundee/gdu/v5/internal/testdir"
+ "github.com/dundee/gdu/v5/pkg/analyze"
+ "github.com/dundee/gdu/v5/pkg/device"
+ "github.com/gdamore/tcell/v2"
+ "github.com/stretchr/testify/assert"
+)
+
+func init() {
+ log.SetLevel(log.WarnLevel)
+}
+
+func TestFooter(t *testing.T) {
+ app, simScreen := testapp.CreateTestAppWithSimScreen(15, 15)
+ defer simScreen.Fini()
+
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+
+ dir := &analyze.Dir{
+ File: &analyze.File{
+ Name: "xxx",
+ Size: 5,
+ Usage: 4096,
+ },
+ BasePath: ".",
+ ItemCount: 2,
+ }
+
+ file := &analyze.File{
+ Name: "yyy",
+ Size: 2,
+ Usage: 4096,
+ Parent: dir,
+ }
+ dir.Files = analyze.Files{file}
+
+ ui.currentDir = dir
+ ui.showDir()
+ ui.pages.HidePage("progress")
+
+ ui.footerLabel.Draw(simScreen)
+ simScreen.Show()
+
+ b, _, _ := simScreen.GetContents()
+
+ text := []byte(" Total disk usage: 4.0 KiB Apparent size: 2 B Items: 1")
+ 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()
+
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, false, false)
+ done := ui.Analyzer.GetDoneChan()
+ done <- struct{}{}
+ ui.updateProgress()
+ assert.True(t, true)
+}
+
+func TestHelp(t *testing.T) {
+ app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+ ui.showHelp()
+
+ assert.True(t, ui.pages.HasPage("help"))
+
+ ui.help.Draw(simScreen)
+ simScreen.Show()
+
+ // printScreen(simScreen)
+
+ b, _, _ := simScreen.GetContents()
+
+ cells := b[406 : 406+9]
+
+ text := []byte("directory")
+ for i, r := range cells {
+ assert.Equal(t, text[i], r.Bytes[0])
+ }
+}
+
+func TestHelpBw(t *testing.T) {
+ app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.showHelp()
+ ui.help.Draw(simScreen)
+ simScreen.Show()
+
+ // printScreen(simScreen)
+
+ b, _, _ := simScreen.GetContents()
+
+ cells := b[406 : 406+9]
+
+ text := []byte("directory")
+ for i, r := range cells {
+ assert.Equal(t, text[i], r.Bytes[0])
+ }
+}
+
+func TestAppRun(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(false)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+
+ err := ui.StartUILoop()
+
+ assert.Nil(t, err)
+}
+
+func TestAppRunWithErr(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+
+ err := ui.StartUILoop()
+
+ assert.Equal(t, "Fail", err.Error())
+}
+
+func TestRescanDir(t *testing.T) {
+ parentDir := &analyze.Dir{
+ File: &analyze.File{
+ Name: "parent",
+ },
+ Files: make([]analyze.Item, 0, 1),
+ }
+ currentDir := &analyze.Dir{
+ File: &analyze.File{
+ Name: "sub",
+ Parent: parentDir,
+ },
+ }
+
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+ ui.done = make(chan struct{})
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ ui.currentDir = currentDir
+ ui.topDir = parentDir
+ ui.rescanDir()
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+ assert.Equal(t, parentDir, ui.currentDir.Parent)
+
+ assert.Equal(t, 5, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "/..")
+ assert.Contains(t, ui.table.GetCell(1, 0).Text, "aaa")
+}
+
+func TestDirSelected(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ ui := getAnalyzedPathMockedApp(t, true, true, false)
+ ui.done = make(chan struct{})
+
+ ui.fileItemSelected(0, 0)
+
+ assert.Equal(t, 3, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "/..")
+ assert.Contains(t, ui.table.GetCell(1, 0).Text, "subnested")
+}
+
+func TestFileSelected(t *testing.T) {
+ ui := getAnalyzedPathMockedApp(t, true, true, true)
+
+ ui.fileItemSelected(3, 0)
+
+ assert.Equal(t, 4, ui.table.GetRowCount())
+ assert.Contains(t, ui.table.GetCell(0, 0).Text, "aaa")
+}
+
+func TestSelectedWithoutCurrentDir(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+
+ ui.fileItemSelected(1, 0)
+
+ assert.Nil(t, ui.currentDir)
+}
+
+func TestBeforeDraw(t *testing.T) {
+ screen := tcell.NewSimulationScreen("UTF-8")
+ err := screen.Init()
+
+ assert.Nil(t, err)
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, screen, &bytes.Buffer{}, false, true, false)
+
+ for _, f := range ui.app.(*testapp.MockedApp).BeforeDraws {
+ assert.False(t, f(screen))
+ }
+}
+
+func TestIgnorePaths(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+
+ ui.SetIgnoreDirPaths([]string{"/aaa", "/bbb"})
+
+ assert.True(t, ui.ShouldDirBeIgnored("aaa", "/aaa"))
+ assert.True(t, ui.ShouldDirBeIgnored("bbb", "/bbb"))
+ assert.False(t, ui.ShouldDirBeIgnored("ccc", "/ccc"))
+}
+
+func TestConfirmDeletion(t *testing.T) {
+ ui := getAnalyzedPathMockedApp(t, true, true, true)
+
+ ui.table.Select(1, 0)
+ ui.confirmDeletion(false)
+
+ assert.True(t, ui.pages.HasPage("confirm"))
+}
+
+func TestConfirmDeletionBW(t *testing.T) {
+ ui := getAnalyzedPathMockedApp(t, false, true, true)
+
+ ui.table.Select(1, 0)
+ ui.confirmDeletion(false)
+
+ assert.True(t, ui.pages.HasPage("confirm"))
+}
+
+func TestConfirmEmpty(t *testing.T) {
+ ui := getAnalyzedPathMockedApp(t, false, true, true)
+
+ ui.table.Select(1, 0)
+ ui.confirmDeletion(true)
+
+ assert.True(t, ui.pages.HasPage("confirm"))
+}
+
+func TestDeleteSelected(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ ui := getAnalyzedPathMockedApp(t, false, true, false)
+ ui.done = make(chan struct{})
+
+ assert.Equal(t, 1, ui.table.GetRowCount())
+
+ ui.table.Select(0, 0)
+
+ ui.deleteSelected(false)
+
+ <-ui.done
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.NoDirExists(t, "test_dir/nested")
+}
+
+func TestDeleteSelectedWithErr(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ ui := getAnalyzedPathMockedApp(t, false, true, false)
+ ui.remover = testanalyze.RemoveItemFromDirWithErr
+
+ assert.Equal(t, 1, ui.table.GetRowCount())
+
+ ui.table.Select(0, 0)
+
+ ui.deleteSelected(false)
+
+ <-ui.done
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.True(t, ui.pages.HasPage("error"))
+ assert.DirExists(t, "test_dir/nested")
+}
+
+func TestShowErr(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false)
+
+ ui.showErr("Something went wrong", errors.New("error"))
+
+ assert.True(t, ui.pages.HasPage("error"))
+}
+
+func TestShowErrBW(t *testing.T) {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false)
+
+ ui.showErr("Something went wrong", errors.New("error"))
+
+ assert.True(t, ui.pages.HasPage("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 getDevicesInfoMock() device.DevicesInfoGetter {
+ item := &device.Device{
+ Name: "/dev/root",
+ MountPoint: "/",
+ Size: 1e12,
+ Free: 1e6,
+ }
+ item2 := &device.Device{
+ Name: "/dev/boot",
+ MountPoint: "/boot",
+ Size: 1e6,
+ Free: 1e3,
+ }
+
+ mock := testdev.DevicesInfoGetterMock{}
+ mock.Devices = []*device.Device{item, item2}
+ return mock
+}
+
+func getAnalyzedPathMockedApp(t *testing.T, useColors, apparentSize bool, mockedAnalyzer bool) *UI {
+ simScreen := testapp.CreateSimScreen(50, 50)
+ defer simScreen.Fini()
+
+ app := testapp.CreateMockedApp(true)
+ ui := CreateUI(app, simScreen, &bytes.Buffer{}, useColors, apparentSize, false)
+
+ if mockedAnalyzer {
+ ui.Analyzer = &testanalyze.MockedAnalyzer{}
+ }
+ ui.done = make(chan struct{})
+ err := ui.AnalyzePath("test_dir", nil)
+ assert.Nil(t, err)
+
+ <-ui.done // wait for analyzer
+
+ for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws {
+ f()
+ }
+
+ assert.Equal(t, "test_dir", ui.currentDir.Name)
+
+ return ui
+}
--- /dev/null
+package tui
+
+import (
+ "github.com/dundee/gdu/v5/pkg/device"
+)
+
+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 getUsageGraph(part int) string {
+ graph := " ["
+ for i := 0; i < 10; i++ {
+ if part > i {
+ graph += "#"
+ } else {
+ graph += " "
+ }
+ }
+ graph += "] "
+ return graph
+}
+
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}