pax_global_header00006660000000000000000000000064142020274250014510gustar00rootroot0000000000000052 comment=ab4e2fac6d118fe93e8bdbc7bf8e78e5c693d4ff gdu-5.13.2/000077500000000000000000000000001420202742500123575ustar00rootroot00000000000000gdu-5.13.2/.github/000077500000000000000000000000001420202742500137175ustar00rootroot00000000000000gdu-5.13.2/.github/workflows/000077500000000000000000000000001420202742500157545ustar00rootroot00000000000000gdu-5.13.2/.github/workflows/codeql-analysis.yml000066400000000000000000000044641420202742500215770ustar00rootroot00000000000000# 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 gdu-5.13.2/.github/workflows/test.yml000066400000000000000000000031021420202742500174520ustar00rootroot00000000000000on: 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 gdu-5.13.2/.gitignore000066400000000000000000000000571420202742500143510ustar00rootroot00000000000000/.vscode /.idea /coverage.txt /dist /test_dir gdu-5.13.2/LICENSE.md000066400000000000000000000020641420202742500137650ustar00rootroot00000000000000Copyright 2020-2021 Daniel Milde Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gdu-5.13.2/Makefile000066400000000000000000000061631420202742500140250ustar00rootroot00000000000000NAME := 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 gdu-5.13.2/README.md000066400000000000000000000225631420202742500136460ustar00rootroot00000000000000# go DiskUsage() [![Codecov](https://codecov.io/gh/dundee/gdu/branch/master/graph/badge.svg)](https://codecov.io/gh/dundee/gdu) [![Go Report Card](https://goreportcard.com/badge/github.com/dundee/gdu)](https://goreportcard.com/report/github.com/dundee/gdu) [![Maintainability](https://api.codeclimate.com/v1/badges/30d793274607f599e658/maintainability)](https://codeclimate.com/github/dundee/gdu/maintainability) [![CodeScene Code Health](https://codescene.io/projects/13129/status-badges/code-health)](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. [![asciicast](https://asciinema.org/a/382738.svg)](https://asciinema.org/a/382738) Packaging status ## 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/): pacman -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, --const-gc Enable memory garbage collection during analysis with constant level set by GOGC --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 --si Show sizes with decimal SI prefixes (kB, MB, GB) instead of binary prefixes (KiB, MiB, GiB) -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 # analyze given dir gdu -d # show all mounted disks gdu -l ./gdu.log # write errors to log file gdu -i /sys,/proc / # ignore some paths gdu -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. ## Memory usage ### Automatic balancing Gdu tries to balance performance and memory usage. When less memory is used by gdu than the total free memory of the host, then Garbage Collection is disabled during the analysis phase completely to gain maximum speed. Otherwise GC is enabled. The more memory is used and the less memory is free, the more often will the GC happen. ### Manual memory usage control If you want manual control over Garbage Collection, you can use `--const-gc` / `-g` flag. It will run Garbage Collection during the analysis phase with constant level of aggressiveness. As a result, the analysis will be about 25% slower and will consume about 30% less memory. To change the level, you can set the `GOGC` environment variable to specify how often the garbage collection will happen. Lower value (than 100) means GC will run more often. Higher means less often. Negative number will stop GC. Example running gdu with constant GC, 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 gdu-5.13.2/build/000077500000000000000000000000001420202742500134565ustar00rootroot00000000000000gdu-5.13.2/build/build.go000066400000000000000000000004551420202742500151100ustar00rootroot00000000000000package 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 = "" gdu-5.13.2/cmd/000077500000000000000000000000001420202742500131225ustar00rootroot00000000000000gdu-5.13.2/cmd/gdu/000077500000000000000000000000001420202742500137015ustar00rootroot00000000000000gdu-5.13.2/cmd/gdu/app/000077500000000000000000000000001420202742500144615ustar00rootroot00000000000000gdu-5.13.2/cmd/gdu/app/app.go000066400000000000000000000141631420202742500155750ustar00rootroot00000000000000package app import ( "fmt" "io" "io/fs" "os" "path/filepath" "runtime" "strings" "net/http" "net/http/pprof" log "github.com/sirupsen/logrus" "github.com/dundee/gdu/v5/build" "github.com/dundee/gdu/v5/internal/common" "github.com/dundee/gdu/v5/pkg/device" gfs "github.com/dundee/gdu/v5/pkg/fs" "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 gfs.Item) 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 ShowRelativeSize bool ShowVersion bool NoColor bool NonInteractive bool NoProgress bool NoCross bool NoHidden bool Profiling bool ConstGC bool Summarize bool UseSIPrefix 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) } func init() { http.DefaultServeMux = http.NewServeMux() } // 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.ConstGC, a.Flags.UseSIPrefix, ) 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.ShowRelativeSize, a.Flags.Summarize, a.Flags.ConstGC, a.Flags.UseSIPrefix, ) } else { ui = tui.CreateUI( a.TermApp, a.Screen, os.Stdout, !a.Flags.NoColor, a.Flags.ShowApparentSize, a.Flags.ShowRelativeSize, a.Flags.ConstGC, a.Flags.UseSIPrefix, ) 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() { http.HandleFunc("/debug/pprof/", pprof.Index) http.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) http.HandleFunc("/debug/pprof/profile", pprof.Profile) http.HandleFunc("/debug/pprof/symbol", pprof.Symbol) http.HandleFunc("/debug/pprof/trace", pprof.Trace) 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 } gdu-5.13.2/cmd/gdu/app/app_linux_test.go000066400000000000000000000026231420202742500200510ustar00rootroot00000000000000//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") } gdu-5.13.2/cmd/gdu/app/app_test.go000066400000000000000000000152511420202742500166330ustar00rootroot00000000000000package 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 } gdu-5.13.2/cmd/gdu/main.go000066400000000000000000000077261420202742500151700ustar00rootroot00000000000000package 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.ConstGC, "const-gc", "g", false, "Enable memory garbage collection during analysis with constant level set by GOGC") 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.ShowRelativeSize, "show-relative-size", "B", false, "Show relative 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") flags.BoolVar(&af.UseSIPrefix, "si", false, "Show sizes with decimal SI prefixes (kB, MB, GB) instead of binary prefixes (KiB, MiB, GiB)") } 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) } } gdu-5.13.2/codecov.yml000066400000000000000000000002541420202742500145250ustar00rootroot00000000000000coverage: status: project: default: target: auto threshold: 2% informational: true patch: default: informational: truegdu-5.13.2/gdu.1000066400000000000000000000052551420202742500132270ustar00rootroot00000000000000.\" Automatically generated by Pandoc 2.14.2 .\" .TH "gdu" "1" "2022-01-18" "" "" .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]--si\f[R][=false] Show sizes with decimal SI prefixes (kB, MB, GB) instead of binary prefixes (KiB, MiB, GiB) .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]--const-gc\f[R][=false] Enable memory garbage collection during analysis with constant level set by GOGC .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. gdu-5.13.2/gdu.1.md000066400000000000000000000045161420202742500136250ustar00rootroot00000000000000--- 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 **\--si**\[=false\] Show sizes with decimal SI prefixes (kB, MB, GB) instead of binary prefixes (KiB, MiB, GiB) **-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**, **\--const-gc**\[=false\] Enable memory garbage collection during analysis with constant level set by GOGC **\--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. gdu-5.13.2/go.mod000066400000000000000000000011701420202742500134640ustar00rootroot00000000000000module 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/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 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 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) gdu-5.13.2/go.sum000066400000000000000000001672361420202742500135310ustar00rootroot00000000000000cloud.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/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= 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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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= gdu-5.13.2/internal/000077500000000000000000000000001420202742500141735ustar00rootroot00000000000000gdu-5.13.2/internal/common/000077500000000000000000000000001420202742500154635ustar00rootroot00000000000000gdu-5.13.2/internal/common/analyze.go000066400000000000000000000010171420202742500174540ustar00rootroot00000000000000package common import "github.com/dundee/gdu/v5/pkg/fs" // CurrentProgress struct type CurrentProgress struct { CurrentItemName string ItemCount int TotalSize int64 } // 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, constGC bool) fs.Item GetProgressChan() chan CurrentProgress GetDoneChan() chan struct{} ResetProgress() } gdu-5.13.2/internal/common/app.go000066400000000000000000000010341420202742500165700ustar00rootroot00000000000000package 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 } gdu-5.13.2/internal/common/ignore.go000066400000000000000000000076611420202742500173070ustar00rootroot00000000000000package common import ( "bufio" "os" "regexp" "strings" log "github.com/sirupsen/logrus" ) // 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() 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 } } } gdu-5.13.2/internal/common/ignore_test.go000066400000000000000000000101241420202742500203320ustar00rootroot00000000000000package 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")) } gdu-5.13.2/internal/common/ui.go000066400000000000000000000020301420202742500164220ustar00rootroot00000000000000package common import ( "regexp" "strconv" ) // UI struct type UI struct { Analyzer Analyzer IgnoreDirPaths map[string]struct{} IgnoreDirPathPatterns *regexp.Regexp IgnoreHidden bool UseColors bool UseSIPrefix bool ShowProgress bool ShowApparentSize bool ShowRelativeSize bool ConstGC bool } // binary multiplies prefixes (IEC) const ( _ = iota Ki float64 = 1 << (10 * iota) Mi Gi Ti Pi Ei ) // SI prefixes const ( K int64 = 1e3 M int64 = 1e6 G int64 = 1e9 T int64 = 1e12 P int64 = 1e15 E int64 = 1e18 ) // 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) } gdu-5.13.2/internal/common/ui_test.go000066400000000000000000000002761420202742500174730ustar00rootroot00000000000000package common import ( "testing" "github.com/stretchr/testify/assert" ) func TestFormatNumber(t *testing.T) { res := FormatNumber(1234567890) assert.Equal(t, "1,234,567,890", res) } gdu-5.13.2/internal/testanalyze/000077500000000000000000000000001420202742500165365ustar00rootroot00000000000000gdu-5.13.2/internal/testanalyze/analyze.go000066400000000000000000000036761420202742500205440ustar00rootroot00000000000000package testanalyze import ( "errors" "time" "github.com/dundee/gdu/v5/internal/common" "github.com/dundee/gdu/v5/pkg/analyze" "github.com/dundee/gdu/v5/pkg/fs" ) // 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 common.ShouldDirBeIgnored, enableGC bool, ) fs.Item { 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 = fs.Files{dir2, dir3, dir4, file} return dir } // GetProgressChan returns always Done func (a *MockedAnalyzer) GetProgressChan() chan common.CurrentProgress { return make(chan common.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 fs.Item, file fs.Item) error { return errors.New("Failed") } gdu-5.13.2/internal/testapp/000077500000000000000000000000001420202742500156535ustar00rootroot00000000000000gdu-5.13.2/internal/testapp/app.go000066400000000000000000000043421420202742500167650ustar00rootroot00000000000000package 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 } gdu-5.13.2/internal/testdata/000077500000000000000000000000001420202742500160045ustar00rootroot00000000000000gdu-5.13.2/internal/testdata/test.json000066400000000000000000000004671420202742500176650ustar00rootroot00000000000000[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}]] gdu-5.13.2/internal/testdata/wrong.json000066400000000000000000000000121420202742500200240ustar00rootroot00000000000000[1,2,3,4] gdu-5.13.2/internal/testdev/000077500000000000000000000000001420202742500156515ustar00rootroot00000000000000gdu-5.13.2/internal/testdev/dev.go000066400000000000000000000007361420202742500167640ustar00rootroot00000000000000package 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 } gdu-5.13.2/internal/testdir/000077500000000000000000000000001420202742500156515ustar00rootroot00000000000000gdu-5.13.2/internal/testdir/test_dir.go000066400000000000000000000012151420202742500200140ustar00rootroot00000000000000package 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 } gdu-5.13.2/pkg/000077500000000000000000000000001420202742500131405ustar00rootroot00000000000000gdu-5.13.2/pkg/analyze/000077500000000000000000000000001420202742500146035ustar00rootroot00000000000000gdu-5.13.2/pkg/analyze/dir.go000066400000000000000000000102161420202742500157100ustar00rootroot00000000000000package analyze import ( "os" "path/filepath" "runtime" "runtime/debug" "github.com/dundee/gdu/v5/internal/common" "github.com/dundee/gdu/v5/pkg/fs" log "github.com/sirupsen/logrus" ) var concurrencyLimit = make(chan struct{}, 3*runtime.GOMAXPROCS(0)) // ParallelAnalyzer implements Analyzer type ParallelAnalyzer struct { progress *common.CurrentProgress progressChan chan common.CurrentProgress progressOutChan chan common.CurrentProgress doneChan chan struct{} wait *WaitGroup ignoreDir common.ShouldDirBeIgnored } // CreateAnalyzer returns Analyzer func CreateAnalyzer() *ParallelAnalyzer { return &ParallelAnalyzer{ progress: &common.CurrentProgress{ ItemCount: 0, TotalSize: int64(0), }, progressChan: make(chan common.CurrentProgress, 1), progressOutChan: make(chan common.CurrentProgress, 1), doneChan: make(chan struct{}, 2), wait: (&WaitGroup{}).Init(), } } // GetProgressChan returns channel for getting progress func (a *ParallelAnalyzer) GetProgressChan() chan common.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 = &common.CurrentProgress{} a.progressChan = make(chan common.CurrentProgress, 1) a.progressOutChan = make(chan common.CurrentProgress, 1) a.doneChan = make(chan struct{}, 2) a.wait = (&WaitGroup{}).Init() } // AnalyzeDir analyzes given path func (a *ParallelAnalyzer) AnalyzeDir( path string, ignore common.ShouldDirBeIgnored, constGC bool, ) fs.Item { if !constGC { defer debug.SetGCPercent(debug.SetGCPercent(-1)) go manageMemoryUsage(a.doneChan) } 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 a.doneChan <- struct{}{} // and manageMemoryUsage 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(fs.Files, 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.AddFile(file) } } go func() { var sub *Dir for i := 0; i < dirCount; i++ { sub = <-subDirChan dir.AddFile(sub) } a.wait.Done() }() a.progressChan <- common.CurrentProgress{ CurrentItemName: path, ItemCount: len(files), TotalSize: 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 ' ' } } gdu-5.13.2/pkg/analyze/dir_linux-openbsd.go000066400000000000000000000011611420202742500205560ustar00rootroot00000000000000//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)) } gdu-5.13.2/pkg/analyze/dir_other.go000066400000000000000000000003201420202742500171040ustar00rootroot00000000000000//go:build windows || plan9 // +build windows plan9 package analyze import ( "os" ) func setPlatformSpecificAttrs(file *File, f os.FileInfo) {} func setDirPlatformSpecificAttrs(dir *Dir, path string) {} gdu-5.13.2/pkg/analyze/dir_test.go000066400000000000000000000102551420202742500167520ustar00rootroot00000000000000package analyze import ( "os" "sort" "testing" log "github.com/sirupsen/logrus" "github.com/dundee/gdu/v5/internal/testdir" "github.com/dundee/gdu/v5/pkg/fs" "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 }, false, ).(*Dir) progress := <-analyzer.GetProgressChan() assert.GreaterOrEqual(t, progress.TotalSize, int64(0)) <-analyzer.GetDoneChan() analyzer.ResetProgress() dir.UpdateStats(make(fs.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(). GetName(), ) } func TestIgnoreDir(t *testing.T) { fin := testdir.CreateTestDir() defer fin() dir := CreateAnalyzer().AnalyzeDir( "test_dir", func(_, _ string) bool { return true }, false, ).(*Dir) 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 }, false, ).(*Dir) <-analyzer.GetDoneChan() dir.UpdateStats(make(fs.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 }, false, ).(*Dir) <-analyzer.GetDoneChan() dir.UpdateStats(make(fs.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 }, false, ).(*Dir) <-analyzer.GetDoneChan() dir.UpdateStats(make(fs.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 }, false, ) <-analyzer.GetDoneChan() dir.UpdateStats(make(fs.HardLinkedItems)) } gdu-5.13.2/pkg/analyze/dir_unix.go000066400000000000000000000012301420202742500167470ustar00rootroot00000000000000//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)) } gdu-5.13.2/pkg/analyze/encode.go000066400000000000000000000042241420202742500163710ustar00rootroot00000000000000package 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 } gdu-5.13.2/pkg/analyze/encode_test.go000066400000000000000000000022751420202742500174340ustar00rootroot00000000000000package analyze import ( "bytes" "testing" "time" "github.com/dundee/gdu/v5/pkg/fs" 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 = fs.Files{subdir} subdir.Files = fs.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`) } gdu-5.13.2/pkg/analyze/file.go000066400000000000000000000116621420202742500160570ustar00rootroot00000000000000package analyze import ( "os" "path/filepath" "time" "github.com/dundee/gdu/v5/pkg/fs" ) // File struct type File struct { Mtime time.Time Parent fs.Item 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() fs.Item { return f.Parent } // SetParent sets parent dir func (f *File) SetParent(parent fs.Item) { f.Parent = 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 fs.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 } // GetItemStats returns 1 as count of items, apparent usage and real usage of this file func (f *File) GetItemStats(linkedItems fs.HardLinkedItems) (int, int64, int64) { if f.alreadyCounted(linkedItems) { return 1, 0, 0 } return 1, f.GetSize(), f.GetUsage() } // UpdateStats does nothing on file func (f *File) UpdateStats(linkedItems fs.HardLinkedItems) {} // GetFiles returns all files in directory func (f *File) GetFiles() fs.Files { return fs.Files{} } // SetFiles panics on file func (f *File) SetFiles(files fs.Files) { panic("SetFiles should not be called on file") } // AddFile panics on file func (f *File) AddFile(item fs.Item) { panic("AddFile should not be called on file") } // Dir struct type Dir struct { *File BasePath string Files fs.Files ItemCount int } // AddFile add item fo files func (f *Dir) AddFile(item fs.Item) { f.Files = append(f.Files, item) } // GetFiles returns all files in directory func (f *Dir) GetFiles() fs.Files { return f.Files } // SetFiles sets files in directory func (f *Dir) SetFiles(files fs.Files) { f.Files = files } // 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) } if f.Parent != nil { return filepath.Join(f.Parent.GetPath(), f.Name) } return f.Name } // GetItemStats returns item count, apparent usage and real usage of this dir func (f *Dir) GetItemStats(linkedItems fs.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 fs.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 } // RemoveItemFromDir removes item from dir func RemoveItemFromDir(dir fs.Item, item fs.Item) error { err := os.RemoveAll(item.GetPath()) if err != nil { return err } dir.SetFiles(dir.GetFiles().Remove(item)) cur := dir.(*Dir) for { cur.ItemCount -= item.GetItemCount() cur.Size -= item.GetSize() cur.Usage -= item.GetUsage() if cur.Parent == nil { break } cur = cur.Parent.(*Dir) } return nil } // EmptyFileFromDir empty file from dir func EmptyFileFromDir(dir fs.Item, file fs.Item) error { err := os.Truncate(file.GetPath(), 0) if err != nil { return err } cur := dir.(*Dir) for { cur.Size -= file.GetSize() cur.Usage -= file.GetUsage() if cur.Parent == nil { break } cur = cur.Parent.(*Dir) } dir.SetFiles(dir.GetFiles().Remove(file)) newFile := &File{ Name: file.GetName(), Flag: file.GetFlag(), Size: 0, Parent: dir, } dir.AddFile(newFile) return nil } gdu-5.13.2/pkg/analyze/file_test.go000066400000000000000000000163561420202742500171230ustar00rootroot00000000000000package analyze import ( "os" "testing" "time" "github.com/dundee/gdu/v5/internal/testdir" "github.com/dundee/gdu/v5/pkg/fs" "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 = fs.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 = fs.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 = fs.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 = fs.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 = fs.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 = fs.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 = fs.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 = fs.Files{subdir} subdir.Files = fs.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 = fs.Files{subdir} subdir.Files = fs.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 = fs.Files{subdir} subdir.Files = fs.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 = fs.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()) } func TestGetPathWithoutLeadingSlash(t *testing.T) { dir := &Dir{ File: &File{ Name: "C:\\", Size: 5, Usage: 12, }, ItemCount: 3, BasePath: "", } assert.Equal(t, "C:\\", dir.GetPath()) } func TestSetParent(t *testing.T) { dir := &Dir{ File: &File{ Name: "root", Size: 5, Usage: 12, }, ItemCount: 3, BasePath: "/", } file := &File{ Name: "xxx", Mli: 5, } file.SetParent(dir) assert.Equal(t, "root", file.GetParent().GetName()) } func TestGetFiles(t *testing.T) { file := &File{ Name: "xxx", Mli: 5, } dir := &Dir{ File: &File{ Name: "root", Size: 5, Usage: 12, }, ItemCount: 3, BasePath: "/", Files: fs.Files{file}, } assert.Equal(t, file.Name, dir.GetFiles()[0].GetName()) assert.Equal(t, fs.Files{}, file.GetFiles()) } func TestSetFilesPanicsOnFile(t *testing.T) { file := &File{ Name: "xxx", Mli: 5, } assert.Panics(t, func() { file.SetFiles(fs.Files{file}) }) } func TestAddFilePanicsOnFile(t *testing.T) { file := &File{ Name: "xxx", Mli: 5, } assert.Panics(t, func() { file.AddFile(file) }) } gdu-5.13.2/pkg/analyze/memory.go000066400000000000000000000025551420202742500164510ustar00rootroot00000000000000package analyze import ( "runtime" "runtime/debug" "time" "github.com/pbnjay/memory" log "github.com/sirupsen/logrus" ) // set GC percentage according to memory usage and system free memory func manageMemoryUsage(c <-chan struct{}) { disabledGC := true for { select { case <-c: return default: } time.Sleep(time.Second) rebalanceGC(&disabledGC) } } /* Try to balance performance and memory consumption. When less memory is used by gdu than the total free memory of the host, Garbage Collection is disabled during the analysis phase at all. Otherwise GC is enabled. The more memory is used and the less memory is free, the more often will the GC happen. */ func rebalanceGC(disabledGC *bool) { memStats := runtime.MemStats{} runtime.ReadMemStats(&memStats) free := memory.FreeMemory() // we use less memory than is free, disable GC if memStats.Alloc < free { if !*disabledGC { log.Printf( "disabling GC, alloc: %d, free: %d", memStats.Alloc, free, ) debug.SetGCPercent(-1) *disabledGC = true } } else { // the more memory we use and the less memory is free, the more aggresive the GC will be gcPercent := int(100 / float64(memStats.Alloc) * float64(free)) log.Printf( "setting GC percent to %d, alloc: %d, free: %d", gcPercent, memStats.Alloc, free, ) debug.SetGCPercent(gcPercent) *disabledGC = false } } gdu-5.13.2/pkg/analyze/memory_test.go000066400000000000000000000007771420202742500175140ustar00rootroot00000000000000package analyze import ( "runtime" "runtime/debug" "testing" "github.com/pbnjay/memory" "github.com/stretchr/testify/assert" ) func TestRebalanceGC(t *testing.T) { memStats := runtime.MemStats{} runtime.ReadMemStats(&memStats) free := memory.FreeMemory() disabledGC := false rebalanceGC(&disabledGC) if free > memStats.Alloc { assert.True(t, disabledGC) assert.Equal(t, -1, debug.SetGCPercent(100)) } else { assert.False(t, disabledGC) assert.Greater(t, 0, debug.SetGCPercent(-1)) } } gdu-5.13.2/pkg/analyze/sort_test.go000066400000000000000000000047171420202742500171710ustar00rootroot00000000000000package analyze import ( "sort" "testing" "time" "github.com/dundee/gdu/v5/pkg/fs" "github.com/stretchr/testify/assert" ) func TestSortByUsage(t *testing.T) { files := fs.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 := fs.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 := fs.Files{ &File{ Size: 1, }, &File{ Size: 2, }, &File{ Size: 3, }, } sort.Sort(fs.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 := fs.Files{ &File{ Size: 1, }, &File{ Size: 2, }, &File{ Size: 3, }, } sort.Sort(sort.Reverse(fs.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 := fs.Files{ &Dir{ ItemCount: 1, }, &Dir{ ItemCount: 2, }, &Dir{ ItemCount: 3, }, } sort.Sort(fs.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 := fs.Files{ &File{ Name: "aa", }, &File{ Name: "bb", }, &File{ Name: "cc", }, } sort.Sort(fs.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 := fs.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(fs.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()) } gdu-5.13.2/pkg/analyze/wait.go000066400000000000000000000015571420202742500161060ustar00rootroot00000000000000package 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.access.Lock() isValue := s.value > 0 s.access.Unlock() if isValue { s.wait.Lock() } } func (s *WaitGroup) check() { if s.value == 0 { s.wait.Unlock() } } gdu-5.13.2/pkg/device/000077500000000000000000000000001420202742500143775ustar00rootroot00000000000000gdu-5.13.2/pkg/device/dev.go000066400000000000000000000025101420202742500155020ustar00rootroot00000000000000package 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 } gdu-5.13.2/pkg/device/dev_bsd.go000066400000000000000000000041301420202742500163320ustar00rootroot00000000000000//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 } gdu-5.13.2/pkg/device/dev_bsd_test.go000066400000000000000000000033611420202742500173760ustar00rootroot00000000000000//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) } gdu-5.13.2/pkg/device/dev_linux.go000066400000000000000000000043051420202742500167250ustar00rootroot00000000000000package 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", " ") } gdu-5.13.2/pkg/device/dev_linux_test.go000066400000000000000000000063321420202742500177660ustar00rootroot00000000000000//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) } gdu-5.13.2/pkg/device/dev_netbsd.go000066400000000000000000000041321420202742500170430ustar00rootroot00000000000000//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 } gdu-5.13.2/pkg/device/dev_openbsd.go000066400000000000000000000041431420202742500172200ustar00rootroot00000000000000//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 } gdu-5.13.2/pkg/device/dev_other.go000066400000000000000000000013171420202742500167070ustar00rootroot00000000000000//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") } gdu-5.13.2/pkg/device/dev_test.go000066400000000000000000000024001420202742500165370ustar00rootroot00000000000000package 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) } gdu-5.13.2/pkg/fs/000077500000000000000000000000001420202742500135505ustar00rootroot00000000000000gdu-5.13.2/pkg/fs/file.go000066400000000000000000000053031420202742500150170ustar00rootroot00000000000000package fs import ( "io" "time" ) // Item is a 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() Item SetParent(Item) GetMultiLinkedInode() uint64 EncodeJSON(writer io.Writer, topLevel bool) error GetItemStats(linkedItems HardLinkedItems) (int, int64, int64) UpdateStats(linkedItems HardLinkedItems) AddFile(Item) GetFiles() Files SetFiles(Files) } // Files - slice of pointers to File type Files []Item // HardLinkedItems maps inode number to array of all hard linked items type HardLinkedItems map[uint64]Files // 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()) } gdu-5.13.2/report/000077500000000000000000000000001420202742500136725ustar00rootroot00000000000000gdu-5.13.2/report/export.go000066400000000000000000000122001420202742500155350ustar00rootroot00000000000000package report import ( "bytes" "errors" "fmt" "io" "os" "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/dundee/gdu/v5/pkg/fs" "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, constGC bool, useSIPrefix bool, ) *UI { ui := &UI{ UI: &common.UI{ ShowProgress: showProgress, Analyzer: analyze.CreateAnalyzer(), ConstGC: constGC, UseSIPrefix: useSIPrefix, }, 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, _ fs.Item) error { var ( dir fs.Item 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() dir = ui.Analyzer.AnalyzeDir(path, ui.CreateIgnoreFunc(), ui.ConstGC) dir.UpdateStats(make(fs.HardLinkedItems, 10)) }() wait.Wait() sort.Sort(dir.GetFiles()) 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 common.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 { if ui.UseSIPrefix { return ui.formatWithDecPrefix(size) } return ui.formatWithBinPrefix(size) } func (ui *UI) formatWithBinPrefix(size int64) string { fsize := float64(size) switch { case fsize >= common.Ei: return ui.orange.Sprintf("%.1f", fsize/common.Ei) + " EiB" case fsize >= common.Pi: return ui.orange.Sprintf("%.1f", fsize/common.Pi) + " PiB" case fsize >= common.Ti: return ui.orange.Sprintf("%.1f", fsize/common.Ti) + " TiB" case fsize >= common.Gi: return ui.orange.Sprintf("%.1f", fsize/common.Gi) + " GiB" case fsize >= common.Mi: return ui.orange.Sprintf("%.1f", fsize/common.Mi) + " MiB" case fsize >= common.Ki: return ui.orange.Sprintf("%.1f", fsize/common.Ki) + " KiB" default: return ui.orange.Sprintf("%d", size) + " B" } } func (ui *UI) formatWithDecPrefix(size int64) string { fsize := float64(size) switch { case size >= common.E: return ui.orange.Sprintf("%.1f", fsize/float64(common.E)) + " EB" case size >= common.P: return ui.orange.Sprintf("%.1f", fsize/float64(common.P)) + " PB" case size >= common.T: return ui.orange.Sprintf("%.1f", fsize/float64(common.T)) + " TB" case size >= common.G: return ui.orange.Sprintf("%.1f", fsize/float64(common.G)) + " GB" case size >= common.M: return ui.orange.Sprintf("%.1f", fsize/float64(common.M)) + " MB" case size >= common.K: return ui.orange.Sprintf("%.1f", fsize/float64(common.K)) + " kB" default: return ui.orange.Sprintf("%d", size) + " B" } } gdu-5.13.2/report/export_test.go000066400000000000000000000071651420202742500166120ustar00rootroot00000000000000package 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, 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, 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, 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, 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, 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, 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 TestFormatSizeDec(t *testing.T) { output := bytes.NewBuffer(make([]byte, 10)) reportOutput := bytes.NewBuffer(make([]byte, 10)) ui := CreateExportUI(output, reportOutput, false, true, false, true) assert.Contains(t, ui.formatSize(1), "B") assert.Contains(t, ui.formatSize(1<<10+1), "kB") assert.Contains(t, ui.formatSize(1<<20+1), "MB") assert.Contains(t, ui.formatSize(1<<30+1), "GB") assert.Contains(t, ui.formatSize(1<<40+1), "TB") assert.Contains(t, ui.formatSize(1<<50+1), "PB") assert.Contains(t, ui.formatSize(1<<60+1), "EB") } gdu-5.13.2/report/import.go000066400000000000000000000044671420202742500155460ustar00rootroot00000000000000package 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.AddFile(file) case []interface{}: subdir, err := processDir(item) if err != nil { return nil, err } subdir.Parent = dir dir.AddFile(subdir) } } return dir, nil } gdu-5.13.2/report/import_test.go000066400000000000000000000056231420202742500166000ustar00rootroot00000000000000package 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") } gdu-5.13.2/snapcraft.yaml000066400000000000000000000020501420202742500152210ustar00rootroot00000000000000name: 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 gdu-5.13.2/stdout/000077500000000000000000000000001420202742500137015ustar00rootroot00000000000000gdu-5.13.2/stdout/stdout.go000066400000000000000000000171661420202742500155650ustar00rootroot00000000000000package stdout import ( "fmt" "io" "math" "runtime" "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/pkg/fs" "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, showRelativeSize bool, summarize bool, constGC bool, useSIPrefix bool, ) *UI { ui := &UI{ UI: &common.UI{ UseColors: useColors, ShowProgress: showProgress, ShowApparentSize: showApparentSize, ShowRelativeSize: showRelativeSize, Analyzer: analyze.CreateAnalyzer(), ConstGC: constGC, UseSIPrefix: useSIPrefix, }, 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, _ fs.Item) error { var ( dir fs.Item wait sync.WaitGroup ) if ui.ShowProgress { wait.Add(1) go func() { defer wait.Done() ui.updateProgress() }() } wait.Add(1) go func() { defer wait.Done() dir = ui.Analyzer.AnalyzeDir(path, ui.CreateIgnoreFunc(), ui.ConstGC) dir.UpdateStats(make(fs.HardLinkedItems, 10)) }() wait.Wait() if ui.summarize { ui.printTotalItem(dir) } else { ui.showDir(dir) } return nil } func (ui *UI) showDir(dir fs.Item) { sort.Sort(dir.GetFiles()) for _, file := range dir.GetFiles() { ui.printItem(file) } } func (ui *UI) printTotalItem(file fs.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 fs.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(fs.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 common.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 { if ui.UseSIPrefix { return ui.formatWithDecPrefix(size) } return ui.formatWithBinPrefix(size) } func (ui *UI) formatWithBinPrefix(size int64) string { fsize := float64(size) switch { case fsize >= common.Ei: return ui.orange.Sprintf("%.1f", fsize/common.Ei) + " EiB" case fsize >= common.Pi: return ui.orange.Sprintf("%.1f", fsize/common.Pi) + " PiB" case fsize >= common.Ti: return ui.orange.Sprintf("%.1f", fsize/common.Ti) + " TiB" case fsize >= common.Gi: return ui.orange.Sprintf("%.1f", fsize/common.Gi) + " GiB" case fsize >= common.Mi: return ui.orange.Sprintf("%.1f", fsize/common.Mi) + " MiB" case fsize >= common.Ki: return ui.orange.Sprintf("%.1f", fsize/common.Ki) + " KiB" default: return ui.orange.Sprintf("%d", size) + " B" } } func (ui *UI) formatWithDecPrefix(size int64) string { fsize := float64(size) switch { case size >= common.E: return ui.orange.Sprintf("%.1f", fsize/float64(common.E)) + " EB" case size >= common.P: return ui.orange.Sprintf("%.1f", fsize/float64(common.P)) + " PB" case size >= common.T: return ui.orange.Sprintf("%.1f", fsize/float64(common.T)) + " TB" case size >= common.G: return ui.orange.Sprintf("%.1f", fsize/float64(common.G)) + " GB" case size >= common.M: return ui.orange.Sprintf("%.1f", fsize/float64(common.M)) + " MB" case size >= common.K: return ui.orange.Sprintf("%.1f", fsize/float64(common.K)) + " kB" 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 } gdu-5.13.2/stdout/stdout_linux_test.go000066400000000000000000000010541420202742500200300ustar00rootroot00000000000000//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, false, false) err := ui.ListDevices(getter) assert.Contains(t, err.Error(), "no such file") } gdu-5.13.2/stdout/stdout_test.go000066400000000000000000000141551420202742500166170ustar00rootroot00000000000000package 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, 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(), "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, false, true, 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, 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, false, true, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 TestFormatSizeDec(t *testing.T) { output := bytes.NewBuffer(make([]byte, 10)) ui := CreateStdoutUI(output, true, true, true, false, false, false, true) assert.Contains(t, ui.formatSize(1), "B") assert.Contains(t, ui.formatSize(1<<10+1), "kB") assert.Contains(t, ui.formatSize(1<<20+1), "MB") assert.Contains(t, ui.formatSize(1<<30+1), "GB") assert.Contains(t, ui.formatSize(1<<40+1), "TB") assert.Contains(t, ui.formatSize(1<<50+1), "PB") assert.Contains(t, ui.formatSize(1<<60+1), "EB") } // 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 } gdu-5.13.2/tui/000077500000000000000000000000001420202742500131605ustar00rootroot00000000000000gdu-5.13.2/tui/actions.go000066400000000000000000000210211420202742500151430ustar00rootroot00000000000000package 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/pkg/fs" "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.getter = getter 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 fs.Item) 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() { defer debug.FreeOSMemory() currentDir := ui.Analyzer.AnalyzeDir(path, ui.CreateIgnoreFunc(), ui.ConstGC) if parentDir != nil { currentDir.SetParent(parentDir) parentDir.SetFiles(parentDir.GetFiles().RemoveByName(currentDir.GetName())) parentDir.AddFile(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(fs.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().(fs.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 fs.Item var deleteItems []fs.Item if shouldEmpty && selectedItem.IsDir() { currentDir = selectedItem.(*analyze.Dir) for _, file := range currentDir.GetFiles() { deleteItems = append(deleteItems, file) } } else { currentDir = ui.currentDir deleteItems = append(deleteItems, selectedItem) } var deleteFun func(fs.Item, fs.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().(fs.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().(fs.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) } gdu-5.13.2/tui/actions_linux_test.go000066400000000000000000000010671420202742500174310ustar00rootroot00000000000000//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, false, false) err := ui.ListDevices(getter) assert.Contains(t, err.Error(), "no such file") } gdu-5.13.2/tui/actions_test.go000066400000000000000000000254141420202742500162140ustar00rootroot00000000000000package 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/dundee/gdu/v5/pkg/fs" "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, 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 TestShowDevicesBW(t *testing.T) { app, simScreen := testapp.CreateTestAppWithSimScreen(50, 50) defer simScreen.Fini() ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, false, 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, true, false, 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.GetName()) 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") } 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([]fs.Item, 0, 1), } simScreen := testapp.CreateSimScreen(50, 50) defer simScreen.Fini() app := testapp.CreateMockedApp(true) ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, true, true, false) 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.GetName()) assert.Equal(t, parentDir, ui.currentDir.GetParent()) 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, true, false, 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.GetName()) } 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, false, 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, 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.GetName()) 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, false, 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, 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.GetName()) ui.table.Select(3, 0) selectedFile := ui.table.GetCell(3, 0).GetReference().(fs.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, 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.GetName()) 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, 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.GetName()) 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, 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.GetName()) 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, 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() } nested := ui.currentDir.GetFiles()[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.GetName()) ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0)) ui.table.Select(1, 0) ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0)) ui.table.Select(1, 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, false, 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, 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.GetName()) 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")) } gdu-5.13.2/tui/exec.go000066400000000000000000000004611420202742500144340ustar00rootroot00000000000000package 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() } gdu-5.13.2/tui/exec_other.go000066400000000000000000000010611420202742500156320ustar00rootroot00000000000000//go:build !windows // +build !windows package tui import ( "os" ) func getShellBin() string { shellbin, ok := os.LookupEnv("SHELL") if !ok { shellbin = "/bin/bash" } return shellbin } func (ui *UI) spawnShell() { if ui.currentDir == nil { return } ui.app.Suspend(func() { if err := os.Chdir(ui.currentDirPath); err != nil { ui.showErr("Error changing directory", err) return } if err := ui.exec(getShellBin(), nil, os.Environ()); err != nil { ui.showErr("Error executing shell", err) } }) } gdu-5.13.2/tui/exec_test.go000066400000000000000000000002631420202742500154730ustar00rootroot00000000000000package tui import ( "testing" "github.com/stretchr/testify/assert" ) func TestExecute(t *testing.T) { err := Execute("true", []string{}, []string{}) assert.Nil(t, err) } gdu-5.13.2/tui/exec_windows.go000066400000000000000000000010071420202742500162030ustar00rootroot00000000000000package tui import ( "os" ) func getShellBin() string { shellbin, ok := os.LookupEnv("COMSPEC") if !ok { shellbin = "C:\\WINDOWS\\System32\\cmd.exe" } return shellbin } func (ui *UI) spawnShell() { if ui.currentDir == nil { return } ui.app.Stop() if err := os.Chdir(ui.currentDirPath); err != nil { ui.showErr("Error changing directory", err) return } if err := ui.exec(getShellBin(), nil, os.Environ()); err != nil { ui.showErr("Error executing shell", err) } } gdu-5.13.2/tui/filter.go000066400000000000000000000021311420202742500147710ustar00rootroot00000000000000package 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 } gdu-5.13.2/tui/filter_test.go000066400000000000000000000075341420202742500160440ustar00rootroot00000000000000package 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, 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() } 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, false, 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, 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() } 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, 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() } 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 } gdu-5.13.2/tui/format.go000066400000000000000000000066601420202742500150070ustar00rootroot00000000000000package tui import ( "fmt" "github.com/dundee/gdu/v5/internal/common" "github.com/dundee/gdu/v5/pkg/fs" "github.com/rivo/tview" ) func (ui *UI) formatFileRow(item fs.Item, maxUsage int64, maxSize int64) string { var part int if ui.ShowApparentSize { part = int(float64(item.GetSize()) / float64(maxSize) * 10.0) } else { part = int(float64(item.GetUsage()) / float64(maxUsage) * 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:-]" } } if ui.UseSIPrefix { return formatWithDecPrefix(size, color) } return formatWithBinPrefix(float64(size), color) } func (ui *UI) formatCount(count int) string { row := "" color := "[-::]" count64 := int64(count) switch { case count64 >= common.G: row += fmt.Sprintf("%.1f%sG", float64(count)/float64(common.G), color) case count64 >= common.M: row += fmt.Sprintf("%.1f%sM", float64(count)/float64(common.M), color) case count64 >= common.K: row += fmt.Sprintf("%.1f%sk", float64(count)/float64(common.K), color) default: row += fmt.Sprintf("%d%s", count, color) } return row } func formatWithBinPrefix(fsize float64, color string) string { switch { case fsize >= common.Ei: return fmt.Sprintf("%.1f%s EiB", fsize/common.Ei, color) case fsize >= common.Pi: return fmt.Sprintf("%.1f%s PiB", fsize/common.Pi, color) case fsize >= common.Ti: return fmt.Sprintf("%.1f%s TiB", fsize/common.Ti, color) case fsize >= common.Gi: return fmt.Sprintf("%.1f%s GiB", fsize/common.Gi, color) case fsize >= common.Mi: return fmt.Sprintf("%.1f%s MiB", fsize/common.Mi, color) case fsize >= common.Ki: return fmt.Sprintf("%.1f%s KiB", fsize/common.Ki, color) default: return fmt.Sprintf("%d%s B", int64(fsize), color) } } func formatWithDecPrefix(size int64, color string) string { fsize := float64(size) switch { case size >= common.E: return fmt.Sprintf("%.1f%s EB", fsize/float64(common.E), color) case size >= common.P: return fmt.Sprintf("%.1f%s PB", fsize/float64(common.P), color) case size >= common.T: return fmt.Sprintf("%.1f%s TB", fsize/float64(common.T), color) case size >= common.G: return fmt.Sprintf("%.1f%s GB", fsize/float64(common.G), color) case size >= common.M: return fmt.Sprintf("%.1f%s MB", fsize/float64(common.M), color) case size >= common.K: return fmt.Sprintf("%.1f%s kB", fsize/float64(common.K), color) default: return fmt.Sprintf("%d%s B", size, color) } } gdu-5.13.2/tui/format_test.go000066400000000000000000000051171420202742500160420ustar00rootroot00000000000000package 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, 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 TestFormatSizeDec(t *testing.T) { simScreen := testapp.CreateSimScreen(50, 50) defer simScreen.Fini() app := testapp.CreateMockedApp(true) ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, false, false, false, true) assert.Equal(t, "1[white:black:-] B", ui.formatSize(1, false, false)) assert.Equal(t, "1.0[white:black:-] kB", ui.formatSize(1<<10, false, false)) assert.Equal(t, "1.0[white:black:-] MB", ui.formatSize(1<<20, false, false)) assert.Equal(t, "1.1[white:black:-] GB", ui.formatSize(1<<30, false, false)) assert.Equal(t, "1.1[white:black:-] TB", ui.formatSize(1<<40, false, false)) assert.Equal(t, "1.1[white:black:-] PB", ui.formatSize(1<<50, false, false)) assert.Equal(t, "1.2[white:black:-] EB", 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, 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, 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, file.GetUsage(), file.GetSize()), "Aaa [red[] bbb") } gdu-5.13.2/tui/keys.go000066400000000000000000000077721420202742500144770ustar00rootroot00000000000000package tui import ( "fmt" "github.com/dundee/gdu/v5/pkg/fs" "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 'B': ui.ShowRelativeSize = !ui.ShowRelativeSize 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 { if ui.devices != nil { ui.currentDir = nil err := ui.ListDevices(ui.getter) if err != nil { ui.showErr("Error listing devices", err) } } 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().(fs.Item) if selectedFile == ui.currentDir.GetParent() { return } if ui.askBeforeDelete { ui.confirmDeletion(shouldEmpty) } else { ui.deleteSelected(shouldEmpty) } } gdu-5.13.2/tui/keys_test.go000066400000000000000000000472731420202742500155360ustar00rootroot00000000000000package 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/dundee/gdu/v5/pkg/device" "github.com/dundee/gdu/v5/pkg/fs" "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, false, 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, false, 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 TestCloseHelpWithQuestionMark(t *testing.T) { simScreen := testapp.CreateSimScreen(50, 50) defer simScreen.Fini() app := testapp.CreateMockedApp(false) ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false, false, false) ui.showHelp() assert.True(t, ui.pages.HasPage("help")) ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, '?', 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, false, 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, false, 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, 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() } ui.table.Select(0, 0) ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0)) assert.Equal(t, "nested", ui.currentDir.GetName()) ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0)) // try /.. first assert.Equal(t, "nested", ui.currentDir.GetName()) ui.table.Select(1, 0) ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0)) assert.Equal(t, "subnested", ui.currentDir.GetName()) ui.keyPressed(tcell.NewEventKey(tcell.KeyLeft, 'h', 0)) assert.Equal(t, "nested", ui.currentDir.GetName()) ui.keyPressed(tcell.NewEventKey(tcell.KeyLeft, 'h', 0)) assert.Equal(t, "test_dir", ui.currentDir.GetName()) ui.keyPressed(tcell.NewEventKey(tcell.KeyLeft, 'h', 0)) assert.Equal(t, "test_dir", ui.currentDir.GetName()) } 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, false, 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.GetName()) // go back to list of devices ui.keyPressed(tcell.NewEventKey(tcell.KeyLeft, 'h', 0)) assert.Nil(t, ui.currentDir) assert.Equal(t, "/dev/root", ui.table.GetCell(1, 0).GetReference().(*device.Device).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, false, 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, 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() } 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, false, 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, false, 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, false, 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, false, 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, 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.GetName()) 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, false, 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, false, 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.GetName()) 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, false, 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.GetName()) 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, false, 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.GetName()) 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, false, 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.GetName()) 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, 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.GetName()) 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, 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.GetName()) 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, 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.GetName()) 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, 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.GetName()) 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, 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.GetName()) ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'm', 0)) assert.True(t, ui.showMtime) } func TestShowRelativeBar(t *testing.T) { simScreen := testapp.CreateSimScreen(50, 50) defer simScreen.Fini() app := testapp.CreateMockedApp(true) ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, false, 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.GetName()) assert.False(t, ui.ShowRelativeSize) ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'B', 0)) assert.True(t, ui.ShowRelativeSize) } func TestRescan(t *testing.T) { parentDir := &analyze.Dir{ File: &analyze.File{ Name: "parent", }, Files: make([]fs.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, false, 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.GetName()) assert.Equal(t, parentDir, ui.currentDir.GetParent()) 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, 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.GetName()) 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, 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.GetName()) 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, 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.GetName()) 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, 'k', 0)) // move up ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, '?', 0)) // does nothing assert.True(t, ui.pages.HasPage("info")) // we can still see info page ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'q', 0)) assert.False(t, ui.pages.HasPage("info")) } gdu-5.13.2/tui/progress.go000066400000000000000000000015571420202742500153630ustar00rootroot00000000000000package tui import ( "time" "github.com/dundee/gdu/v5/internal/common" ) 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 common.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) } } gdu-5.13.2/tui/show.go000066400000000000000000000126771420202742500145040ustar00rootroot00000000000000package 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 maxUsage int64 maxSize 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.GetParent()) cell.SetStyle(tcell.Style{}.Foreground(tcell.ColorDefault)) ui.table.SetCell(0, 0, cell) rowIndex++ } ui.sortItems() if ui.ShowRelativeSize { for _, item := range ui.currentDir.GetFiles() { if item.GetUsage() > maxUsage { maxUsage = item.GetUsage() } if item.GetSize() > maxSize { maxSize = item.GetSize() } } } else { maxUsage = ui.currentDir.GetUsage() maxSize = ui.currentDir.GetSize() } for i, item := range ui.currentDir.GetFiles() { 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, maxUsage, maxSize)) cell.SetStyle(tcell.Style{}.Foreground(tcell.ColorDefault)) cell.SetReference(ui.currentDir.GetFiles()[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.Clear() 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) if ui.topDirPath != "" { for i, device := range ui.devices { if device.MountPoint == ui.topDirPath { ui.table.Select(i+1, 0) break } } } } 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) } gdu-5.13.2/tui/sort.go000066400000000000000000000035031420202742500144770ustar00rootroot00000000000000package tui import ( "sort" "github.com/dundee/gdu/v5/pkg/device" "github.com/dundee/gdu/v5/pkg/fs" ) 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(fs.ByApparentSize(ui.currentDir.GetFiles())) } else { sort.Sort(sort.Reverse(fs.ByApparentSize(ui.currentDir.GetFiles()))) } } else { if ui.sortOrder == "desc" { sort.Sort(ui.currentDir.GetFiles()) } else { sort.Sort(sort.Reverse(ui.currentDir.GetFiles())) } } } if ui.sortBy == "itemCount" { if ui.sortOrder == "desc" { sort.Sort(fs.ByItemCount(ui.currentDir.GetFiles())) } else { sort.Sort(sort.Reverse(fs.ByItemCount(ui.currentDir.GetFiles()))) } } if ui.sortBy == "name" { if ui.sortOrder == "desc" { sort.Sort(fs.ByName(ui.currentDir.GetFiles())) } else { sort.Sort(sort.Reverse(fs.ByName(ui.currentDir.GetFiles()))) } } if ui.sortBy == "mtime" { if ui.sortOrder == "desc" { sort.Sort(fs.ByMtime(ui.currentDir.GetFiles())) } else { sort.Sort(sort.Reverse(fs.ByMtime(ui.currentDir.GetFiles()))) } } } 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))) } } } gdu-5.13.2/tui/sort_test.go000066400000000000000000000133301420202742500155350ustar00rootroot00000000000000package 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, true, false, 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, true, false, 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, false, 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 } gdu-5.13.2/tui/tui.go000066400000000000000000000170761420202742500143230ustar00rootroot00000000000000package 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/dundee/gdu/v5/pkg/fs" "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]B [white:black:-]Toggle bar alignment to biggest file or directory [::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 fs.Item devices []*device.Device topDir fs.Item topDirPath string currentDirPath string askBeforeDelete bool showItemCount bool showMtime bool filtering bool filterValue string sortBy string sortOrder string done chan struct{} remover func(fs.Item, fs.Item) error emptier func(fs.Item, fs.Item) error getter device.DevicesInfoGetter exec func(argv0 string, argv []string, envv []string) error linkedItems fs.HardLinkedItems } // CreateUI creates the whole UI app func CreateUI( app common.TermApplication, screen tcell.Screen, output io.Writer, useColors bool, showApparentSize bool, showRelativeSize bool, constGC bool, useSIPrefix bool, ) *UI { ui := &UI{ UI: &common.UI{ UseColors: useColors, ShowApparentSize: showApparentSize, ShowRelativeSize: showRelativeSize, Analyzer: analyze.CreateAnalyzer(), ConstGC: constGC, UseSIPrefix: useSIPrefix, }, app: app, screen: screen, output: output, askBeforeDelete: true, showItemCount: false, remover: analyze.RemoveItemFromDir, emptier: analyze.EmptyFileFromDir, exec: Execute, linkedItems: make(fs.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() ui.linkedItems = make(fs.HardLinkedItems) err := ui.AnalyzePath(ui.currentDirPath, ui.currentDir.GetParent()) 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().(fs.Item) if !selectedDir.IsDir() { return } ui.currentDir = selectedDir.(*analyze.Dir) ui.hideFilterInput() ui.showDir() if selectedDir == origDir.GetParent() { index, _ := ui.currentDir.GetFiles().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().(fs.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) } gdu-5.13.2/tui/tui_test.go000066400000000000000000000217201420202742500153510ustar00rootroot00000000000000package 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/dundee/gdu/v5/pkg/fs" "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, false, 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 = fs.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, 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, false, 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, false, 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, false, 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, false, 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([]fs.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, false, 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.GetName()) assert.Equal(t, parentDir, ui.currentDir.GetParent()) 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, false, 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, false, 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, false, 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, false, 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, false, 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: "test_dir", 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, false, 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.GetName()) return ui } gdu-5.13.2/tui/utils.go000066400000000000000000000010561420202742500146510ustar00rootroot00000000000000package 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 }