pax_global_header00006660000000000000000000000064144376254070014526gustar00rootroot0000000000000052 comment=28796bae38105d65feb25c8d401c1557bb6f6cef gdu-5.25.0/000077500000000000000000000000001443762540700123765ustar00rootroot00000000000000gdu-5.25.0/.github/000077500000000000000000000000001443762540700137365ustar00rootroot00000000000000gdu-5.25.0/.github/dependabot.yml000066400000000000000000000003151443762540700165650ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" gdu-5.25.0/.github/workflows/000077500000000000000000000000001443762540700157735ustar00rootroot00000000000000gdu-5.25.0/.github/workflows/codeql-analysis.yml000066400000000000000000000044641443762540700216160ustar00rootroot00000000000000# 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@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 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@v2 # â„šī¸ 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@v2 gdu-5.25.0/.github/workflows/test.yml000066400000000000000000000024771443762540700175070ustar00rootroot00000000000000on: push: branches: - master pull_request: branches: - master name: run tests jobs: lint: runs-on: ubuntu-latest steps: - name: Install Go uses: actions/setup-go@v4 with: go-version: 1.18.x - name: Checkout code uses: actions/checkout@v3 - name: Run linters uses: golangci/golangci-lint-action@v3 with: version: v1.45 test: strategy: matrix: go-version: [1.18.x, 1.19.x] platform: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.platform }} steps: - name: Install Go if: success() uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v3 - name: Run tests run: go test -v -covermode=count ./... coverage: runs-on: ubuntu-latest steps: - name: Install Go if: success() uses: actions/setup-go@v4 with: go-version: 1.18.x - name: Checkout code uses: actions/checkout@v3 - name: Calc coverage run: | go test -v -race -covermode=atomic -coverprofile=coverage.out ./... - name: Upload coverage report uses: codecov/codecov-action@v3 with: files: ./coverage.out fail_ci_if_error: true verbose: true gdu-5.25.0/.github/workflows/winget.yml000066400000000000000000000005311443762540700200120ustar00rootroot00000000000000name: Publish to Winget on: release: types: [released] jobs: publish: runs-on: windows-latest # Action can only run on Windows steps: - uses: vedantmgoyal2009/winget-releaser@v2 with: identifier: dundee.gdu installers-regex: '_windows_[\w.]+\.zip$' token: ${{ secrets.WINGET_TOKEN }} gdu-5.25.0/.gitignore000066400000000000000000000000651443762540700143670ustar00rootroot00000000000000/.vscode /.idea /coverage.txt /dist /test_dir /vendorgdu-5.25.0/.tool-versions000066400000000000000000000000161443762540700152170ustar00rootroot00000000000000golang 1.20.4 gdu-5.25.0/LICENSE.md000066400000000000000000000020641443762540700140040ustar00rootroot00000000000000Copyright 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.25.0/Makefile000066400000000000000000000105541443762540700140430ustar00rootroot00000000000000NAME := gdu MAJOR_VER := v5 PACKAGE := github.com/dundee/$(NAME)/$(MAJOR_VER) CMD_GDU := cmd/gdu VERSION := $(shell git describe --tags 2>/dev/null) NAMEVER := $(NAME)-$(subst v,,$(VERSION)) DATE := $(shell date +'%Y-%m-%d') GOFLAGS ?= -buildmode=pie -trimpath -mod=readonly -modcacherw GOFLAGS_STATIC ?= -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)' TAR := tar ifeq ($(shell uname -s),Darwin) TAR := gtar # brew install gnu-tar endif all: clean tarball build-all man clean-uncompressed-dist shasums run: go run $(PACKAGE)/$(CMD_GDU) vendor: go.mod go.sum go mod vendor tarball: vendor -mkdir dist $(TAR) czf dist/$(NAMEVER).tgz --transform "s,^,$(NAMEVER)/," --exclude dist --exclude test_dir --exclude coverage.txt * build: @echo "Version: " $(VERSION) mkdir -p dist GOFLAGS="$(GOFLAGS)" CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o dist/$(NAME) $(PACKAGE)/$(CMD_GDU) build-static: @echo "Version: " $(VERSION) mkdir -p dist GOFLAGS="$(GOFLAGS_STATIC)" 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" \ -arch="amd64 arm64" \ -output="dist/gdu_{{.OS}}_{{.Arch}}" \ -ldflags="$(LDFLAGS)" \ $(PACKAGE)/$(CMD_GDU) -CGO_ENABLED=0 gox \ -os="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; GOFLAGS="$(GOFLAGS_STATIC)" CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o gdu_linux_amd64_static $(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; CGO_ENABLED=0 GOOS=android GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o gdu_android_arm64 $(PACKAGE)/$(CMD_GDU) cd dist; for file in gdu_linux_* gdu_darwin_* gdu_netbsd_* gdu_openbsd_* gdu_freebsd_* gdu_android_*; 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: gotestsum coverage: gotestsum -- -race -coverprofile=coverage.txt -covermode=atomic ./... coverage-html: coverage go tool cover -html=coverage.txt gobench: go test -bench=. $(PACKAGE)/pkg/analyze benchmark: sudo cpupower frequency-set -g performance hyperfine --export-markdown=bench-cold.md \ --prepare 'sync; echo 3 | sudo tee /proc/sys/vm/drop_caches' \ --ignore-failure \ 'gdu -npc ~' 'gdu -gnpc ~' 'dua ~' 'duc index ~' 'ncdu -0 -o /dev/null ~' \ 'diskus ~' 'du -hs ~' 'dust -d0 ~' 'pdu ~' hyperfine --export-markdown=bench-warm.md \ --warmup 5 \ --ignore-failure \ 'gdu -npc ~' 'gdu -gnpc ~' 'dua ~' 'duc index ~' 'ncdu -0 -o /dev/null ~' \ 'diskus ~' 'du -hs ~' 'dust -d0 ~' 'pdu ~' sudo cpupower frequency-set -g schedutil clean: go mod tidy -rm coverage.txt -rm -r test_dir -rm -r vendor -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/* install-dev-dependencies: go install gotest.tools/gotestsum@latest .PHONY: run build build-static build-all test gobench benchmark coverage coverage-html clean clean-uncompressed-dist man show-man release gdu-5.25.0/README.md000066400000000000000000000264471443762540700136720ustar00rootroot00000000000000# 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://archlinux.org/packages/community/x86_64/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 [Winget](https://github.com/microsoft/winget-pkgs/tree/master/manifests/d/dundee/gdu): winget install gdu ## Usage ``` gdu [flags] [directory_to_scan] Flags: --config-file string Read config from file (default is $HOME/.gdu.yaml) -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/ -L, --follow-symlinks Follow symlinks for files, i.e. show the size of the file to which symlink points to (symlinks to directories are not followed) -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) --no-mouse Do not use mouse --no-prefix Show sizes as raw numbers without any prefixes (SI or binary) in non-interactive mode -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 -B, --show-relative-size Show relative size --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 --write-config Write current configuration to file (default is $HOME/.gdu.yaml) ``` ## 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 automatically 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. ## Configuration file Gdu can read (and write) YAML configuration file. `$HOME/.config/gdu/gdu.yaml` and `$HOME/.gdu.yaml` are checked for the presense of the config file by default. ### Examples * To configure gdu to permanently run in gray-scale color mode: ``` echo "no-color: true" >> ~/.gdu.yaml ``` * To set default sorting in configuration file: ``` sorting: by: name // size, name, itemCount, mtime order: desc ``` * To configure gdu to set CWD variable when browsing directories: ``` echo "change-cwd: true" >> ~/.gdu.yaml ``` * To save the current configuration ``` gdu --write-config ``` ## Styling There are wast ways how terminals can be colored. Some gdu primitives (like basic text) addapt to different color schemas, but the selected/highlighted row does not. If the default look is not sufficient, it can be changed in configuration file, e.g.: ``` style: selected-row: text-color: black background-color: "#ff0000" ``` ## 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 aggressive as default: ``` GOGC=200 gdu -g / ``` ## Running tests make install-dev-dependencies 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 embedded 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 ~` | 4.995 Âą 0.032 | 4.964 | 5.083 | 1.00 | | `gdu -gnpc ~` | 5.080 Âą 0.132 | 5.000 | 5.339 | 1.02 Âą 0.03 | | `diskus ~` | 5.174 Âą 0.042 | 5.113 | 5.231 | 1.04 Âą 0.01 | | `pdu ~` | 5.940 Âą 0.011 | 5.918 | 5.956 | 1.19 Âą 0.01 | | `dua ~` | 6.176 Âą 0.012 | 6.160 | 6.195 | 1.24 Âą 0.01 | | `dust -d0 ~` | 6.556 Âą 0.497 | 6.217 | 7.319 | 1.31 Âą 0.10 | | `du -hs ~` | 24.105 Âą 0.061 | 24.045 | 24.220 | 4.83 Âą 0.03 | | `ncdu -0 -o /dev/null ~` | 25.065 Âą 0.071 | 24.970 | 25.180 | 5.02 Âą 0.04 | | `duc index ~` | 25.711 Âą 3.168 | 24.550 | 34.723 | 5.15 Âą 0.64 | ### Warm cache | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `pdu ~` | 354.9 Âą 3.9 | 350.1 | 363.2 | 1.00 | | `diskus ~` | 406.5 Âą 6.9 | 395.2 | 419.2 | 1.15 Âą 0.02 | | `dua ~` | 525.0 Âą 10.0 | 506.8 | 536.7 | 1.48 Âą 0.03 | | `dust -d0 ~` | 592.1 Âą 15.5 | 567.8 | 626.7 | 1.67 Âą 0.05 | | `gdu -npc ~` | 711.4 Âą 9.8 | 702.9 | 734.8 | 2.00 Âą 0.04 | | `gdu -gnpc ~` | 847.7 Âą 11.1 | 827.2 | 861.2 | 2.39 Âą 0.04 | | `du -hs ~` | 1387.0 Âą 6.5 | 1379.9 | 1398.3 | 3.91 Âą 0.05 | | `duc index ~` | 1638.3 Âą 5.1 | 1630.7 | 1646.7 | 4.62 Âą 0.05 | | `ncdu -0 -o /dev/null ~` | 2348.9 Âą 9.1 | 2330.5 | 2364.5 | 6.62 Âą 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 * [pdu](https://github.com/KSXGitHub/parallel-disk-usage) - Tool written in Rust showing tree like structures of disk usage gdu-5.25.0/build/000077500000000000000000000000001443762540700134755ustar00rootroot00000000000000gdu-5.25.0/build/build.go000066400000000000000000000004551443762540700151270ustar00rootroot00000000000000package 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.25.0/cmd/000077500000000000000000000000001443762540700131415ustar00rootroot00000000000000gdu-5.25.0/cmd/gdu/000077500000000000000000000000001443762540700137205ustar00rootroot00000000000000gdu-5.25.0/cmd/gdu/app/000077500000000000000000000000001443762540700145005ustar00rootroot00000000000000gdu-5.25.0/cmd/gdu/app/app.go000066400000000000000000000211611443762540700156100ustar00rootroot00000000000000package 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) SetFollowSymlinks(value bool) StartUILoop() error } // Flags define flags accepted by Run type Flags struct { CfgFile string `yaml:"-"` LogFile string `yaml:"log-file"` InputFile string `yaml:"input-file"` OutputFile string `yaml:"output-file"` IgnoreDirs []string `yaml:"ignore-dirs"` IgnoreDirPatterns []string `yaml:"ignore-dir-patterns"` IgnoreFromFile string `yaml:"ignore-from-file"` MaxCores int `yaml:"max-cores"` ShowDisks bool `yaml:"-"` ShowApparentSize bool `yaml:"show-apparent-size"` ShowRelativeSize bool `yaml:"show-relative-size"` ShowVersion bool `yaml:"-"` NoColor bool `yaml:"no-color"` NoMouse bool `yaml:"no-mouse"` NonInteractive bool `yaml:"non-interactive"` NoProgress bool `yaml:"no-progress"` NoCross bool `yaml:"no-cross"` NoHidden bool `yaml:"no-hidden"` FollowSymlinks bool `yaml:"follow-symlinks"` Profiling bool `yaml:"profiling"` ConstGC bool `yaml:"const-gc"` Summarize bool `yaml:"summarize"` UseSIPrefix bool `yaml:"use-si-prefix"` NoPrefix bool `yaml:"no-prefix"` WriteConfig bool `yaml:"-"` ChangeCwd bool `yaml:"change-cwd"` Style Style `yaml:"style"` Sorting Sorting `yaml:"sorting"` } // Style define style config type Style struct { SelectedRow ColorStyle `yaml:"selected-row"` ProgressModal ProgressModalOpts `yaml:"progress-modal"` UseOldSizeBar bool `yaml:"use-old-size-bar"` } // ProgressModalOpts defines options for progress modal type ProgressModalOpts struct { CurrentItemNameMaxLen int `yaml:"current-item-path-max-len"` } // ColorStyle defines styling of some item type ColorStyle struct { TextColor string `yaml:"text-color"` BackgroundColor string `yaml:"background-color"` } // Sorting defines default sorting of items type Sorting struct { By string `yaml:"by"` Order string `yaml:"order"` } // 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 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 } log.Printf("Runtime flags: %+v", *a.Flags) if a.Flags.NoPrefix && a.Flags.UseSIPrefix { return fmt.Errorf("--no-prefix and --si cannot be used at once") } 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, ) } else 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, a.Flags.NoPrefix, ) } else { var opts []tui.Option if a.Flags.Style.SelectedRow.TextColor != "" { opts = append(opts, func(ui *tui.UI) { ui.SetSelectedTextColor(tcell.GetColor(a.Flags.Style.SelectedRow.TextColor)) }) } if a.Flags.Style.SelectedRow.BackgroundColor != "" { opts = append(opts, func(ui *tui.UI) { ui.SetSelectedBackgroundColor(tcell.GetColor(a.Flags.Style.SelectedRow.BackgroundColor)) }) } if a.Flags.Style.ProgressModal.CurrentItemNameMaxLen > 0 { opts = append(opts, func(ui *tui.UI) { ui.SetCurrentItemNameMaxLen(a.Flags.Style.ProgressModal.CurrentItemNameMaxLen) }) } if a.Flags.Style.UseOldSizeBar { opts = append(opts, func(ui *tui.UI) { ui.UseOldSizeBar() }) } if a.Flags.Sorting.Order != "" || a.Flags.Sorting.By != "" { opts = append(opts, func(ui *tui.UI) { ui.SetDefaultSorting(a.Flags.Sorting.By, a.Flags.Sorting.Order) }) } if a.Flags.ChangeCwd != false { opts = append(opts, func(ui *tui.UI) { ui.SetChangeCwdFn(os.Chdir) }) } ui = tui.CreateUI( a.TermApp, a.Screen, os.Stdout, !a.Flags.NoColor, a.Flags.ShowApparentSize, a.Flags.ShowRelativeSize, a.Flags.ConstGC, a.Flags.UseSIPrefix, opts..., ) if !a.Flags.NoColor { tview.Styles.TitleColor = tcell.NewRGBColor(27, 161, 227) } else { tview.Styles.ContrastBackgroundColor = tcell.NewRGBColor(150, 150, 150) } tview.Styles.BorderColor = tcell.ColorDefault } if a.Flags.FollowSymlinks { ui.SetFollowSymlinks(true) } 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.25.0/cmd/gdu/app/app_linux_test.go000066400000000000000000000022611443762540700200660ustar00rootroot00000000000000//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 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.25.0/cmd/gdu/app/app_test.go000066400000000000000000000205011443762540700166440ustar00rootroot00000000000000package 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 TestFollowSymlinks(t *testing.T) { fin := testdir.CreateTestDir() defer fin() out, err := runApp( &Flags{LogFile: "/dev/null", FollowSymlinks: true}, []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 TestAnalyzePathWithDefaultSorting(t *testing.T) { fin := testdir.CreateTestDir() defer fin() out, err := runApp( &Flags{ LogFile: "/dev/null", Sorting: Sorting{ By: "name", Order: "asc", }, }, []string{"test_dir"}, true, testdev.DevicesInfoGetterMock{}, ) assert.Empty(t, out) assert.Nil(t, err) } func TestAnalyzePathWithStyle(t *testing.T) { fin := testdir.CreateTestDir() defer fin() out, err := runApp( &Flags{ LogFile: "/dev/null", Style: Style{ SelectedRow: ColorStyle{ TextColor: "black", BackgroundColor: "red", }, ProgressModal: ProgressModalOpts{ CurrentItemNameMaxLen: 10, }, UseOldSizeBar: true, }, }, []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 TestAnalyzePathWithChdir(t *testing.T) { fin := testdir.CreateTestDir() defer fin() out, err := runApp( &Flags{ LogFile: "/dev/null", ChangeCwd: true, }, []string{"test_dir"}, true, testdev.DevicesInfoGetterMock{}, ) assert.Empty(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 TestWrongCombinationOfPrefixes(t *testing.T) { out, err := runApp( &Flags{NoPrefix: true, UseSIPrefix: true}, []string{"test_dir"}, false, testdev.DevicesInfoGetterMock{}, ) assert.Empty(t, out) assert.Contains(t, err.Error(), "cannot be used at once") } 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.25.0/cmd/gdu/main.go000066400000000000000000000143001443762540700151710ustar00rootroot00000000000000package main import ( "fmt" "os" "path/filepath" "regexp" "runtime" "strings" "github.com/gdamore/tcell/v2" "github.com/mattn/go-isatty" "github.com/rivo/tview" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "gopkg.in/yaml.v3" "github.com/dundee/gdu/v5/cmd/gdu/app" "github.com/dundee/gdu/v5/pkg/device" ) var af *app.Flags var configErr error 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.StringVar(&af.CfgFile, "config-file", "", "Read config from file (default is $HOME/.gdu.yaml)") 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.FollowSymlinks, "follow-symlinks", "L", false, "Follow symlinks for files, i.e. show the size of the file to which symlink points to (symlinks to directories are not followed)", ) 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)") flags.BoolVar(&af.NoPrefix, "no-prefix", false, "Show sizes as raw numbers without any prefixes (SI or binary) in non-interactive mode") flags.BoolVar(&af.NoMouse, "no-mouse", false, "Do not use mouse") flags.BoolVar(&af.WriteConfig, "write-config", false, "Write current configuration to file (default is $HOME/.gdu.yaml)") initConfig() } func initConfig() { setConfigFilePath() data, err := os.ReadFile(af.CfgFile) if err != nil { configErr = err return // config file does not exist, return } configErr = yaml.Unmarshal(data, &af) } func setConfigFilePath() { command := strings.Join(os.Args, " ") if strings.Contains(command, "--config-file") { re := regexp.MustCompile("--config-file[= ]([^ ]+)") parts := re.FindStringSubmatch(command) if len(parts) > 1 { af.CfgFile = parts[1] return } } setDefaultConfigFilePath() } func setDefaultConfigFilePath() { home, err := os.UserHomeDir() if err != nil { configErr = err return } path := filepath.Join(home, ".config", "gdu", "gdu.yaml") if _, err := os.Stat(path); err == nil { af.CfgFile = path return } af.CfgFile = filepath.Join(home, ".gdu.yaml") } func runE(command *cobra.Command, args []string) error { var ( termApp *tview.Application screen tcell.Screen err error ) if af.WriteConfig { data, err := yaml.Marshal(af) if err != nil { return fmt.Errorf("Error marshaling config file: %w", err) } if af.CfgFile == "" { setDefaultConfigFilePath() } err = os.WriteFile(af.CfgFile, data, 0600) if err != nil { return fmt.Errorf("Error writing config file %s: %w", af.CfgFile, err) } } if runtime.GOOS == "windows" && af.LogFile == "/dev/null" { af.LogFile = "nul" } var f *os.File if af.LogFile == "-" { f = os.Stdout } else { f, err = os.OpenFile(af.LogFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return fmt.Errorf("opening log file: %w", err) } defer func() { cerr := f.Close() if cerr != nil { panic(cerr) } }() } log.SetOutput(f) if configErr != nil { log.Printf("Error reading config file: %s", configErr.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 !af.ShowVersion && !af.NonInteractive && istty && af.OutputFile == "" { screen, err = tcell.NewScreen() if err != nil { return fmt.Errorf("Error creating screen: %w", err) } defer screen.Clear() defer screen.Fini() termApp = tview.NewApplication() termApp.SetScreen(screen) if !af.NoMouse { termApp.EnableMouse(true) } } 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.25.0/codecov.yml000066400000000000000000000002541443762540700145440ustar00rootroot00000000000000coverage: status: project: default: target: auto threshold: 2% informational: true patch: default: informational: truegdu-5.25.0/gdu.1000066400000000000000000000066571443762540700132550ustar00rootroot00000000000000.\" Automatically generated by Pandoc 3.0 .\" .\" Define V font for inline verbatim, using C font in formats .\" that render this, and otherwise B font. .ie "\f[CB]x\f[]"x" \{\ . ftr V B . ftr VI BI . ftr VB B . ftr VBI BI .\} .el \{\ . ftr V CR . ftr VI CI . ftr VB CB . ftr VBI CBI .\} .TH "gdu" "1" "2023-02-06" "" "" .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]-L\f[R], \f[B]--follow-symlinks\f[R][=false] Follow symlinks for files, i.e.\ show the size of the file to which symlink points to (symlinks to directories are not followed) .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]--no-prefix\f[R][=false] Show sizes as raw numbers without any prefixes (SI or binary) in non-interactive mode .PP \f[B]--no-mouse\f[R][=false] Do not use mouse .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]--config-file\f[R]=\[dq]$HOME/.gdu.yaml\[dq] Read config from file .PP \f[B]--write-config\f[R][=false] Write current configuration to file (default is $HOME/.gdu.yaml) .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.25.0/gdu.1.md000066400000000000000000000055001443762540700136360ustar00rootroot00000000000000--- 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) **-L**, **\--follow-symlinks**\[=false\] Follow symlinks for files, i.e. show the size of the file to which symlink points to (symlinks to directories are not followed) **-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) **\--no-prefix**\[=false\] Show sizes as raw numbers without any prefixes (SI or binary) in non-interactive mode **\--no-mouse**\[=false\] Do not use mouse **-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. **\--config-file**=\"$HOME/.gdu.yaml\" Read config from file **\--write-config**\[=false\] Write current configuration to file (default is $HOME/.gdu.yaml) **-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.25.0/gdu.spec000066400000000000000000000125551443762540700140410ustar00rootroot00000000000000Name: gdu Version: 5.22.0 Release: 1 Summary: Pretty fast disk usage analyzer written in Go ExclusiveArch: x86_64 License: MIT URL: https://github.com/dundee/gdu Source0: %{name}-%{version}.tar.gz #BuildRequires: golang Requires: bash Provides: %{name} = %{version} %description Pretty fast disk usage analyzer written in Go. %global debug_package %{nil} %prep %autosetup %build GO111MODULE=on CGO_ENABLED=0 go build \ -trimpath \ -buildmode=pie \ -mod=readonly \ -modcacherw \ -ldflags \ %if 0%{?fedora} "-linkmode=external \ -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)'" \ -o %{name} github.com/dundee/gdu/v5/cmd/gdu %endif %if 0%{?rhel} "-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)'" \ -o %{name} github.com/dundee/gdu/v5/cmd/gdu %endif %install rm -rf $RPM_BUILD_ROOT install -Dpm 0755 %{name} %{buildroot}%{_bindir}/%{name} install -Dpm 0755 %{name}.1 $RPM_BUILD_ROOT%{_mandir}/man1/gdu.1 %check %post %preun %files %{_bindir}/gdu %{_mandir}/man1/gdu.1.gz %changelog * Mon Feb 6 2023 Danie de Jager - 5.22.0-1 - feat: added option to follow symlinks in #206 - fix: ignore mouse events when modal is opened in #205 - Updated SPEC file used for rpm creation by @daniejstriata in #198 * Mon Jan 9 2023 Danie de Jager - 5.21.1-2 - updated SPEC file to support builds on Fedora * Mon Jan 9 2023 Danie de Jager - 5.21.1-1 - fix: correct open command for Win * Wed Jan 4 2023 Danie de Jager - 5.21.0-1 - feat: mark multiple items for deletion by @dundee in #193 - feat: move cursor to next row when marked by @dundee in #194 - Use GNU tar on Darwin to fix build error by @sryze in #188 * Mon Oct 24 2022 Danie de Jager - 5.20.0-1 - feat: set default sorting using config option - feat: open file or directory in external program - fix: check reference type * Wed Sep 28 2022 Danie de Jager - 5.19.0-1 - feat: upgrade all dependencies - feat: bump go version to 1.18 - feat: format negative numbers correctly - feat: try to read config from ~/.config/gdu/gdu.yaml first - test: export formatting - docs: config file default locations * Sun Sep 18 2022 Danie de Jager - 5.18.1-1 - fix: correct config file option regex - fix: read non-default config file properly in #175 - feat: crop current item path to 70 chars in #173 - feat: show elapsed time in progress modal - feat: configuration option for setting maximum length of the path for current item in the progress modal in #174 * Tue Sep 13 2022 Danie de Jager - 5.17.1-1 - fix: nul log file for Windows (#171) - fix: increase the vertical size of the progress modal (#172) - feat: added possibility to change text and background color of the selected row by @dundee in #170 * Thu Sep 8 2022 Danie de Jager - 5.16.0-1 - feat: support for reading (and writing) configuration to YAML file - feat: initial mouse support by @dundee in #165 - add mtime for Windows by @mcoret in #157 - openbsd fixes by @dundee in #164 * Wed Aug 10 2022 Danie de Jager - 5.15.0-1 - feat: show sizes as raw numbers without prefixes by @dundee in #147 - feat: natural sorting by @dundee in #156 - fix: honor --summarize when reading analysis by @Riatre in #149 - fix: upgrade dependencies by @phanirithvij in #153 - ci: generate release tarballs with vendor directory by @CyberTailor in #148 * Mon Jul 18 2022 Danie de Jager - 5.14.0-2 * Thu May 26 2022 Danie de Jager - 5.14.0-1 - sort items by name if usage/size/count is the same (#143) * Mon Feb 21 2022 Danie de Jager - 5.13.2 - able to go back to devices list from analyzed directory * Thu Feb 10 2022 Danie de Jager - 5.13.1 - properly count only the first hard link size on a rescan - do not panic if path does not start with a slash * Sat Jan 29 2022 Danie de Jager - 5.13.0-1 - lower memory usage - possibility to toggle between bar graph relative to the size of the directory or the biggest file - added option --si for showing sizes with decimal SI prefixes - fixed freeze when r key binding is being hold * Tue Dec 14 2021 Danie de Jager - 5.12.1-1 - Bump to 5.12.1-1 - fixed listing devices on NetBSD - escape file names (#111) - fixed filtering * Fri Dec 3 2021 Danie de Jager - 5.12.0-1 - Bump to 5.12.0-1 * Fri Dec 3 2021 Danie de Jager - 5.11.0-2 - Compile with go 1.17.4 * Sun Nov 28 2021 Danie de Jager - 5.11.0-1 - Bump to 5.11.0 * Tue Nov 23 2021 Danie de Jager - 5.10.1-1 - Bump to 5.10.1 * Wed Nov 10 2021 Danie de Jager - 5.10.0-1 - Bump to 5.10.01 * Mon Oct 25 2021 Danie de Jager - 5.9.0-1 - Bump to 5.9.0 * Mon Sep 27 2021 Danie de Jager - 5.8.1-2 - Remove pandoc requirement. * Sun Sep 26 2021 Danie de Jager - 5.8.1-1 - Bump to 5.8.1 * Thu Sep 23 2021 Danie de Jager - 5.8.0-2 - Bump to 5.8.0 * Tue Sep 7 2021 Danie de Jager - 5.7.0-1 - Bump to 5.7.0 * Sat Aug 28 2021 Danie de Jager - 5.6.2-1 - Bump to 5.6.2 - Compiled with go 1.17 * Fri Aug 27 2021 Danie de Jager - 5.6.1-1 - Bump to 5.6.1 * Mon Aug 23 2021 Danie de Jager - 5.6.0-1 - Bump to 5.6.0 * Fri Aug 13 2021 Danie de Jager - 5.5.0-2 - Compiled with go 1.16.7 * Mon Aug 2 2021 Danie de Jager - 5.5.0-1 - Bump to 5.5.0 * Mon Jul 26 2021 Danie de Jager - 5.4.0-1 - Bump to 5.4.0 * Thu Jul 22 2021 Danie de Jager - 5.3.0-2 - First release gdu-5.25.0/go.mod000066400000000000000000000021201443762540700134770ustar00rootroot00000000000000module github.com/dundee/gdu/v5 go 1.18 require ( github.com/fatih/color v1.15.0 github.com/gdamore/tcell/v2 v2.6.0 github.com/maruel/natural v1.1.0 github.com/mattn/go-isatty v0.0.19 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/rivo/tview v0.0.0-20230530133550-8bd761dda819 github.com/sirupsen/logrus v1.9.2 github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.8.4 golang.org/x/sys v0.8.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/pretty v0.3.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/term v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) gdu-5.25.0/go.sum000066400000000000000000000212371443762540700135360ustar00rootroot00000000000000github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 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.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg= github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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/maruel/natural v1.1.0 h1:2z1NgP/Vae+gYrtC0VuvrTJ6U35OuyUqDdfluLqMWuQ= github.com/maruel/natural v1.1.0/go.mod h1:eFVhYCcUOfZFxXoDZam8Ktya72wa79fNC3lc/leA0DQ= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/tview v0.0.0-20230530133550-8bd761dda819 h1:qRMCGgwKl66uWe7Hnzl5bCvZlfrLNIxOx7K00j5XeNc= github.com/rivo/tview v0.0.0-20230530133550-8bd761dda819/go.mod h1:nVwGv4MP47T0jvlk7KuTTjjuSmrGO4JF0iaiNt4bufE= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gdu-5.25.0/internal/000077500000000000000000000000001443762540700142125ustar00rootroot00000000000000gdu-5.25.0/internal/common/000077500000000000000000000000001443762540700155025ustar00rootroot00000000000000gdu-5.25.0/internal/common/analyze.go000066400000000000000000000010421443762540700174710ustar00rootroot00000000000000package 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 SetFollowSymlinks(bool) GetProgressChan() chan CurrentProgress GetDone() SignalGroup ResetProgress() } gdu-5.25.0/internal/common/app.go000066400000000000000000000012561443762540700166150ustar00rootroot00000000000000package 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 SetMouseCapture( capture func(event *tcell.EventMouse, action tview.MouseAction) (*tcell.EventMouse, tview.MouseAction), ) *tview.Application QueueUpdateDraw(f func()) *tview.Application SetBeforeDrawFunc(func(screen tcell.Screen) bool) *tview.Application } gdu-5.25.0/internal/common/ignore.go000066400000000000000000000076611443762540700173260ustar00rootroot00000000000000package 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.25.0/internal/common/ignore_test.go000066400000000000000000000101241443762540700203510ustar00rootroot00000000000000package 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.25.0/internal/common/signal.go000066400000000000000000000002051443762540700173030ustar00rootroot00000000000000package common type SignalGroup chan struct{} func (s SignalGroup) Wait() { <-s } func (s SignalGroup) Broadcast() { close(s) } gdu-5.25.0/internal/common/ui.go000066400000000000000000000022721443762540700164510ustar00rootroot00000000000000package 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 } // SetFollowSymlinks sets whether symlinks to files should be followed func (ui *UI) SetFollowSymlinks(v bool) { ui.Analyzer.SetFollowSymlinks(v) } // binary multiplies prefixes (IEC) const ( _ = iota Ki float64 = 1 << (10 * iota) Mi Gi Ti Pi Ei ) // SI prefixes const ( K float64 = 1e3 M float64 = 1e6 G float64 = 1e9 T float64 = 1e12 P float64 = 1e15 E float64 = 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.25.0/internal/common/ui_test.go000066400000000000000000000021421443762540700175040ustar00rootroot00000000000000package common import ( "testing" "github.com/dundee/gdu/v5/pkg/fs" "github.com/stretchr/testify/assert" ) func TestFormatNumber(t *testing.T) { res := FormatNumber(1234567890) assert.Equal(t, "1,234,567,890", res) } func TestSetFollowSymlinks(t *testing.T) { ui := UI{ Analyzer: &MockedAnalyzer{}, } ui.SetFollowSymlinks(true) assert.Equal(t, true, ui.Analyzer.(*MockedAnalyzer).FollowSymlinks) } type MockedAnalyzer struct { FollowSymlinks bool } // AnalyzeDir returns dir with files with different size exponents func (a *MockedAnalyzer) AnalyzeDir( path string, ignore ShouldDirBeIgnored, enableGC bool, ) fs.Item { return nil } // GetProgressChan returns always Done func (a *MockedAnalyzer) GetProgressChan() chan CurrentProgress { return make(chan CurrentProgress) } // GetDone returns always Done func (a *MockedAnalyzer) GetDone() SignalGroup { c := make(SignalGroup) defer c.Broadcast() return c } // ResetProgress does nothing func (a *MockedAnalyzer) ResetProgress() {} // SetFollowSymlinks does nothing func (a *MockedAnalyzer) SetFollowSymlinks(v bool) { a.FollowSymlinks = v } gdu-5.25.0/internal/testanalyze/000077500000000000000000000000001443762540700165555ustar00rootroot00000000000000gdu-5.25.0/internal/testanalyze/analyze.go000066400000000000000000000040071443762540700205500ustar00rootroot00000000000000package 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) } // GetDone returns always Done func (a *MockedAnalyzer) GetDone() common.SignalGroup { c := make(common.SignalGroup) defer c.Broadcast() return c } // ResetProgress does nothing func (a *MockedAnalyzer) ResetProgress() {} // SetFollowSymlinks does nothing func (a *MockedAnalyzer) SetFollowSymlinks(v bool) {} // RemoveItemFromDirWithErr returns error func RemoveItemFromDirWithErr(dir fs.Item, file fs.Item) error { return errors.New("Failed") } gdu-5.25.0/internal/testapp/000077500000000000000000000000001443762540700156725ustar00rootroot00000000000000gdu-5.25.0/internal/testapp/app.go000066400000000000000000000047731443762540700170140ustar00rootroot00000000000000package 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() tcell.SimulationScreen { screen := tcell.NewSimulationScreen("UTF-8") return screen } // CreateTestAppWithSimScreen returns app with simulation screen for tests func CreateTestAppWithSimScreen(width, height int) (*tview.Application, tcell.SimulationScreen) { app := tview.NewApplication() screen := CreateSimScreen() app.SetScreen(screen) screen.SetSize(width, height) 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 } // SetMouseCapture does nothing func (app *MockedApp) SetMouseCapture( capture func(event *tcell.EventMouse, action tview.MouseAction) (*tcell.EventMouse, tview.MouseAction), ) *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 } // QueueUpdateDraw does nothing func (app *MockedApp) GetUpdateDraws() []func() { app.mutex.Lock() defer app.mutex.Unlock() return app.updateDraws } // 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.25.0/internal/testdata/000077500000000000000000000000001443762540700160235ustar00rootroot00000000000000gdu-5.25.0/internal/testdata/test.json000066400000000000000000000004671443762540700177040ustar00rootroot00000000000000[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.25.0/internal/testdata/wrong.json000066400000000000000000000000121443762540700200430ustar00rootroot00000000000000[1,2,3,4] gdu-5.25.0/internal/testdev/000077500000000000000000000000001443762540700156705ustar00rootroot00000000000000gdu-5.25.0/internal/testdev/dev.go000066400000000000000000000007361443762540700170030ustar00rootroot00000000000000package 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.25.0/internal/testdir/000077500000000000000000000000001443762540700156705ustar00rootroot00000000000000gdu-5.25.0/internal/testdir/test_dir.go000066400000000000000000000012151443762540700200330ustar00rootroot00000000000000package 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.25.0/pkg/000077500000000000000000000000001443762540700131575ustar00rootroot00000000000000gdu-5.25.0/pkg/analyze/000077500000000000000000000000001443762540700146225ustar00rootroot00000000000000gdu-5.25.0/pkg/analyze/dir.go000066400000000000000000000114771443762540700157410ustar00rootroot00000000000000package 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 progressDoneChan chan struct{} doneChan common.SignalGroup wait *WaitGroup ignoreDir common.ShouldDirBeIgnored followSymlinks bool } // 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), progressDoneChan: make(chan struct{}), doneChan: make(common.SignalGroup), wait: (&WaitGroup{}).Init(), } } // SetFollowSymlinks sets whether symlink to files should be followed func (a *ParallelAnalyzer) SetFollowSymlinks(v bool) { a.followSymlinks = v } // GetProgressChan returns channel for getting progress func (a *ParallelAnalyzer) GetProgressChan() chan common.CurrentProgress { return a.progressOutChan } // GetDone returns channel for checking when analysis is done func (a *ParallelAnalyzer) GetDone() common.SignalGroup { 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.progressDoneChan = make(chan struct{}) a.doneChan = make(common.SignalGroup) 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.progressDoneChan <- struct{}{} a.doneChan.Broadcast() 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()) dir.Flag = '!' continue } if a.followSymlinks && info.Mode()&os.ModeSymlink != 0 { err = followSymlink(entryPath, &info) if err != nil { log.Print(err.Error()) dir.Flag = '!' 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.progressDoneChan: 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 ' ' } } func followSymlink(path string, f *os.FileInfo) error { target, err := filepath.EvalSymlinks(path) if err != nil { return err } tInfo, err := os.Lstat(target) if err != nil { return err } if !tInfo.IsDir() { *f = tInfo } return nil } gdu-5.25.0/pkg/analyze/dir_linux-openbsd.go000066400000000000000000000011611443762540700205750ustar00rootroot00000000000000//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.25.0/pkg/analyze/dir_linux_test.go000066400000000000000000000015041443762540700202050ustar00rootroot00000000000000//go:build linux // +build linux package analyze import ( "os" "testing" "github.com/dundee/gdu/v5/internal/testdir" "github.com/dundee/gdu/v5/pkg/fs" "github.com/stretchr/testify/assert" ) 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.GetDone().Wait() 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()) } gdu-5.25.0/pkg/analyze/dir_other.go000066400000000000000000000006521443762540700171330ustar00rootroot00000000000000//go:build windows || plan9 // +build windows plan9 package analyze import ( "os" "syscall" "time" ) func setPlatformSpecificAttrs(file *File, f os.FileInfo) { stat := f.Sys().(*syscall.Win32FileAttributeData) file.Mtime = time.Unix(0, stat.LastWriteTime.Nanoseconds()) } func setDirPlatformSpecificAttrs(dir *Dir, path string) { stat, err := os.Stat(path) if err != nil { return } dir.Mtime = stat.ModTime() } gdu-5.25.0/pkg/analyze/dir_test.go000066400000000000000000000120531443762540700167670ustar00rootroot00000000000000package 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.GetDone().Wait() 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.GetDone().Wait() dir.UpdateStats(make(fs.HardLinkedItems)) sort.Sort(sort.Reverse(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.GetDone().Wait() 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 TestFollowSymlink(t *testing.T) { fin := testdir.CreateTestDir() defer fin() err := os.Mkdir("test_dir/empty", 0644) assert.Nil(t, err) err = os.Symlink("./file2", "test_dir/nested/file3") assert.Nil(t, err) analyzer := CreateAnalyzer() analyzer.SetFollowSymlinks(true) dir := analyzer.AnalyzeDir( "test_dir", func(_, _ string) bool { return false }, false, ).(*Dir) analyzer.GetDone().Wait() dir.UpdateStats(make(fs.HardLinkedItems)) sort.Sort(sort.Reverse(dir.Files)) assert.Equal(t, int64(9+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(2), 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 TestBrokenSymlinkSkipped(t *testing.T) { fin := testdir.CreateTestDir() defer fin() err := os.Mkdir("test_dir/empty", 0644) assert.Nil(t, err) err = os.Symlink("xxx", "test_dir/nested/file3") assert.Nil(t, err) analyzer := CreateAnalyzer() analyzer.SetFollowSymlinks(true) dir := analyzer.AnalyzeDir( "test_dir", func(_, _ string) bool { return false }, false, ).(*Dir) analyzer.GetDone().Wait() dir.UpdateStats(make(fs.HardLinkedItems)) sort.Sort(sort.Reverse(dir.Files)) assert.Equal(t, int64(7+4096*4), dir.Size) assert.Equal(t, 6, dir.ItemCount) 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.GetDone().Wait() dir.UpdateStats(make(fs.HardLinkedItems)) } gdu-5.25.0/pkg/analyze/dir_unix.go000066400000000000000000000012301443762540700167660ustar00rootroot00000000000000//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.25.0/pkg/analyze/encode.go000066400000000000000000000042241443762540700164100ustar00rootroot00000000000000package 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.25.0/pkg/analyze/encode_test.go000066400000000000000000000022751443762540700174530ustar00rootroot00000000000000package 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.25.0/pkg/analyze/file.go000066400000000000000000000116621443762540700160760ustar00rootroot00000000000000package 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 returns 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 returns 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 returns 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.25.0/pkg/analyze/file_linux_test.go000066400000000000000000000012051443762540700203440ustar00rootroot00000000000000//go:build linux // +build linux package analyze import ( "os" "testing" "github.com/dundee/gdu/v5/internal/testdir" "github.com/stretchr/testify/assert" ) 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") } gdu-5.25.0/pkg/analyze/file_test.go000066400000000000000000000154061443762540700171350ustar00rootroot00000000000000package analyze import ( "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 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.25.0/pkg/analyze/memory.go000066400000000000000000000025561443762540700164710ustar00rootroot00000000000000package 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 aggressive 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.25.0/pkg/analyze/memory_test.go000066400000000000000000000007771443762540700175330ustar00rootroot00000000000000package 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.25.0/pkg/analyze/sort_test.go000066400000000000000000000062631443762540700172060ustar00rootroot00000000000000package 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(sort.Reverse(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 TestStableSortByUsage(t *testing.T) { files := fs.Files{ &File{ Name: "aaa", Usage: 1, }, &File{ Name: "bbb", Usage: 1, }, &File{ Name: "ccc", Usage: 3, }, } sort.Sort(sort.Reverse(files)) assert.Equal(t, "ccc", files[0].GetName()) assert.Equal(t, "bbb", files[1].GetName()) assert.Equal(t, "aaa", files[2].GetName()) } func TestSortByUsageAsc(t *testing.T) { files := fs.Files{ &File{ Size: 1, }, &File{ Size: 2, }, &File{ Size: 3, }, } sort.Sort(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(sort.Reverse(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(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(sort.Reverse(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(sort.Reverse(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 TestNaturalSortByNameAsc(t *testing.T) { files := fs.Files{ &File{ Name: "aa3", }, &File{ Name: "aa20", }, &File{ Name: "aa100", }, } sort.Sort(fs.ByName(files)) assert.Equal(t, "aa3", files[0].GetName()) assert.Equal(t, "aa20", files[1].GetName()) assert.Equal(t, "aa100", 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(sort.Reverse(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.25.0/pkg/analyze/wait.go000066400000000000000000000015571443762540700161250ustar00rootroot00000000000000package 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.25.0/pkg/device/000077500000000000000000000000001443762540700144165ustar00rootroot00000000000000gdu-5.25.0/pkg/device/dev.go000066400000000000000000000025101443762540700155210ustar00rootroot00000000000000package 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.25.0/pkg/device/dev_bsd.go000066400000000000000000000031071443762540700163540ustar00rootroot00000000000000//go:build netbsd || openbsd // +build netbsd openbsd package device import ( "bufio" "bytes" "errors" "io" "os/exec" "regexp" "strings" ) // 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 (/.*) type (.*) \\(([^)]+)\\)$") 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 } gdu-5.25.0/pkg/device/dev_bsd_test.go000066400000000000000000000010701443762540700174100ustar00rootroot00000000000000//go:build freebsd || openbsd || netbsd || darwin // +build freebsd openbsd netbsd darwin package device import ( "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()) } gdu-5.25.0/pkg/device/dev_freebsd_darwin_other.go000066400000000000000000000041301443762540700217600ustar00rootroot00000000000000//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.25.0/pkg/device/dev_freebsd_darwin_test.go000066400000000000000000000024751443762540700216300ustar00rootroot00000000000000//go:build freebsd || darwin // +build freebsd darwin package device import ( "strings" "testing" "github.com/stretchr/testify/assert" ) 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.25.0/pkg/device/dev_linux.go000066400000000000000000000043051443762540700167440ustar00rootroot00000000000000package device import ( "bufio" "fmt" "io" "os" "strings" "golang.org/x/sys/unix" ) // LinuxDevicesInfoGetter returns 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.25.0/pkg/device/dev_linux_test.go000066400000000000000000000063321443762540700200050ustar00rootroot00000000000000//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.25.0/pkg/device/dev_netbsd.go000066400000000000000000000011471443762540700170650ustar00rootroot00000000000000//go:build netbsd // +build netbsd package device import ( "strings" "golang.org/x/sys/unix" ) 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.25.0/pkg/device/dev_openbsd.go000066400000000000000000000013011443762540700172300ustar00rootroot00000000000000//go:build openbsd // +build openbsd package device import ( "fmt" "strings" "golang.org/x/sys/unix" ) 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, fmt.Errorf("getting stats for mount point: \"%s\", %w", mount.MountPoint, 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.25.0/pkg/device/dev_other.go000066400000000000000000000013171443762540700167260ustar00rootroot00000000000000//go:build windows || plan9 // +build windows plan9 package device import "errors" // OtherDevicesInfoGetter returns 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.25.0/pkg/device/dev_test.go000066400000000000000000000024001443762540700165560ustar00rootroot00000000000000package 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(sort.Reverse(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(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.25.0/pkg/fs/000077500000000000000000000000001443762540700135675ustar00rootroot00000000000000gdu-5.25.0/pkg/fs/file.go000066400000000000000000000064031443762540700150400ustar00rootroot00000000000000package fs import ( "io" "time" "github.com/maruel/natural" ) // 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 { if f[i].GetUsage() != f[j].GetUsage() { return f[i].GetUsage() < f[j].GetUsage() } // if usage is the same, sort by name return natural.Less(f[i].GetName(), f[j].GetName()) } // 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 { if f[i].GetSize() != f[j].GetSize() { return f[i].GetSize() < f[j].GetSize() } // if size is the same, sort by name return natural.Less(f[i].GetName(), f[j].GetName()) } // 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 { if f[i].GetItemCount() != f[j].GetItemCount() { return f[i].GetItemCount() < f[j].GetItemCount() } // if item count is the same, sort by name return natural.Less(f[i].GetName(), f[j].GetName()) } // 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 natural.Less(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 { if !f[i].GetMtime().Equal(f[j].GetMtime()) { return f[i].GetMtime().Before(f[j].GetMtime()) } // if item count is the same, sort by name return natural.Less(f[i].GetName(), f[j].GetName()) } gdu-5.25.0/pkg/path/000077500000000000000000000000001443762540700141135ustar00rootroot00000000000000gdu-5.25.0/pkg/path/path.go000066400000000000000000000007751443762540700154070ustar00rootroot00000000000000package path import "strings" // ShortenPath removes the last but one path components to fit into maxLen func ShortenPath(path string, maxLen int) string { if len(path) <= maxLen { return path } res := "" parts := strings.SplitAfter(path, "/") curLen := len(parts[len(parts)-1]) // count lenght of last part for start for _, part := range parts[:len(parts)-1] { curLen += len(part) if curLen > maxLen { res += ".../" break } res += part } res += parts[len(parts)-1] return res } gdu-5.25.0/pkg/path/path_test.go000066400000000000000000000007451443762540700164430ustar00rootroot00000000000000package path import ( "testing" "github.com/stretchr/testify/assert" ) func TestShortenPath(t *testing.T) { assert.Equal(t, "/root", ShortenPath("/root", 10)) assert.Equal(t, "/home/.../foo", ShortenPath("/home/dundee/foo", 10)) assert.Equal(t, "/home/dundee/foo", ShortenPath("/home/dundee/foo", 50)) assert.Equal(t, "/home/dundee/.../bar.txt", ShortenPath("/home/dundee/foo/bar.txt", 20)) assert.Equal(t, "/home/.../bar.txt", ShortenPath("/home/dundee/foo/bar.txt", 15)) } gdu-5.25.0/report/000077500000000000000000000000001443762540700137115ustar00rootroot00000000000000gdu-5.25.0/report/export.go000066400000000000000000000122261443762540700155640ustar00rootroot00000000000000package report import ( "bytes" "errors" "fmt" "io" "math" "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(sort.Reverse(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.GetDone() 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) asize := math.Abs(fsize) switch { case asize >= common.Ei: return ui.orange.Sprintf("%.1f", fsize/common.Ei) + " EiB" case asize >= common.Pi: return ui.orange.Sprintf("%.1f", fsize/common.Pi) + " PiB" case asize >= common.Ti: return ui.orange.Sprintf("%.1f", fsize/common.Ti) + " TiB" case asize >= common.Gi: return ui.orange.Sprintf("%.1f", fsize/common.Gi) + " GiB" case asize >= common.Mi: return ui.orange.Sprintf("%.1f", fsize/common.Mi) + " MiB" case asize >= 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) asize := math.Abs(fsize) switch { case asize >= common.E: return ui.orange.Sprintf("%.1f", fsize/common.E) + " EB" case asize >= common.P: return ui.orange.Sprintf("%.1f", fsize/common.P) + " PB" case asize >= common.T: return ui.orange.Sprintf("%.1f", fsize/common.T) + " TB" case asize >= common.G: return ui.orange.Sprintf("%.1f", fsize/common.G) + " GB" case asize >= common.M: return ui.orange.Sprintf("%.1f", fsize/common.M) + " MB" case asize >= common.K: return ui.orange.Sprintf("%.1f", fsize/common.K) + " kB" default: return ui.orange.Sprintf("%d", size) + " B" } } gdu-5.25.0/report/export_test.go000066400000000000000000000073341443762540700166270ustar00rootroot00000000000000package 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") assert.Contains(t, ui.formatSize(-1<<10-1), "KiB") } 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") assert.Contains(t, ui.formatSize(-1<<10-1), "kB") } gdu-5.25.0/report/import.go000066400000000000000000000044671443762540700155650ustar00rootroot00000000000000package 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.25.0/report/import_test.go000066400000000000000000000056231443762540700166170ustar00rootroot00000000000000package 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.25.0/snapcraft.yaml000066400000000000000000000020501443762540700152400ustar00rootroot00000000000000name: 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.25.0/stdout/000077500000000000000000000000001443762540700137205ustar00rootroot00000000000000gdu-5.25.0/stdout/stdout.go000066400000000000000000000174611443762540700156020ustar00rootroot00000000000000package 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 noPrefix 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, noPrefix 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, noPrefix: noPrefix, } 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(sort.Reverse(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 } if ui.summarize { ui.printTotalItem(dir) } else { 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.GetDone() 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.noPrefix { return ui.orange.Sprintf("%d", size) } if ui.UseSIPrefix { return ui.formatWithDecPrefix(size) } return ui.formatWithBinPrefix(size) } func (ui *UI) formatWithBinPrefix(size int64) string { fsize := float64(size) asize := math.Abs(fsize) switch { case asize >= common.Ei: return ui.orange.Sprintf("%.1f", fsize/common.Ei) + " EiB" case asize >= common.Pi: return ui.orange.Sprintf("%.1f", fsize/common.Pi) + " PiB" case asize >= common.Ti: return ui.orange.Sprintf("%.1f", fsize/common.Ti) + " TiB" case asize >= common.Gi: return ui.orange.Sprintf("%.1f", fsize/common.Gi) + " GiB" case asize >= common.Mi: return ui.orange.Sprintf("%.1f", fsize/common.Mi) + " MiB" case asize >= 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) asize := math.Abs(fsize) switch { case asize >= common.E: return ui.orange.Sprintf("%.1f", fsize/common.E) + " EB" case asize >= common.P: return ui.orange.Sprintf("%.1f", fsize/common.P) + " PB" case asize >= common.T: return ui.orange.Sprintf("%.1f", fsize/common.T) + " TB" case asize >= common.G: return ui.orange.Sprintf("%.1f", fsize/common.G) + " GB" case asize >= common.M: return ui.orange.Sprintf("%.1f", fsize/common.M) + " MB" case asize >= common.K: return ui.orange.Sprintf("%.1f", fsize/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.25.0/stdout/stdout_linux_test.go000066400000000000000000000010631443762540700200470ustar00rootroot00000000000000//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, false) err := ui.ListDevices(getter) assert.Contains(t, err.Error(), "no such file") } gdu-5.25.0/stdout/stdout_test.go000066400000000000000000000161641443762540700166400ustar00rootroot00000000000000package 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, false) err = ui.ReadAnalysis(input) assert.NotNil(t, err) } func TestReadAnalysisWithSummarize(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, true, false, false, false) err = ui.ReadAnalysis(input) assert.Nil(t, err) assert.Contains(t, output.String(), " gdu\n") } 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, 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, false) 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 TestFormatSizeRaw(t *testing.T) { output := bytes.NewBuffer(make([]byte, 10)) ui := CreateStdoutUI(output, true, true, true, false, false, false, true, true) assert.Equal(t, ui.formatSize(1), "1") assert.Equal(t, ui.formatSize(1<<10+1), "1025") assert.Equal(t, ui.formatSize(1<<20+1), "1048577") assert.Equal(t, ui.formatSize(1<<30+1), "1073741825") assert.Equal(t, ui.formatSize(1<<40+1), "1099511627777") assert.Equal(t, ui.formatSize(1<<50+1), "1125899906842625") assert.Equal(t, ui.formatSize(1<<60+1), "1152921504606846977") } // 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.25.0/tui/000077500000000000000000000000001443762540700131775ustar00rootroot00000000000000gdu-5.25.0/tui/actions.go000066400000000000000000000222771443762540700152000ustar00rootroot00000000000000package tui import ( "bufio" "fmt" "io" "os" "os/exec" "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, 0, 1, false). AddItem(ui.progress, 8, 1, false). AddItem(nil, 0, 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) delete(shouldEmpty bool) { if len(ui.markedRows) > 0 { ui.deleteMarked(shouldEmpty) } else { ui.deleteSelected(shouldEmpty) } } 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( // nolint: staticcheck // Why: fixed string 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) } func (ui *UI) openItem() { row, column := ui.table.GetSelection() selectedFile, ok := ui.table.GetCell(row, column).GetReference().(fs.Item) if !ok || selectedFile == ui.currentDir.GetParent() { return } openBinary := "xdg-open" switch runtime.GOOS { case "darwin": openBinary = "open" case "windows": openBinary = "explorer" } cmd := exec.Command(openBinary, selectedFile.GetPath()) err := cmd.Start() if err != nil { ui.showErr("Error opening", err) } } gdu-5.25.0/tui/actions_linux_test.go000066400000000000000000000010671443762540700174500ustar00rootroot00000000000000//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.25.0/tui/actions_test.go000066400000000000000000000307271443762540700162360ustar00rootroot00000000000000package tui import ( "bytes" "errors" "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() 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.UseOldSizeBar() 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).GetUpdateDraws() { 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, "ccc") 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, "ccc") } 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, "ccc") } func TestAnalyzePathWithParentDir(t *testing.T) { parentDir := &analyze.Dir{ File: &analyze.File{ Name: "parent", }, Files: make([]fs.Item, 0, 1), } simScreen := testapp.CreateSimScreen() 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).GetUpdateDraws() { 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, "ccc") } func TestReadAnalysis(t *testing.T) { simScreen := testapp.CreateSimScreen() 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).GetUpdateDraws() { f() } assert.Equal(t, "gdu", ui.currentDir.GetName()) } func TestReadAnalysisWithWrongFile(t *testing.T) { simScreen := testapp.CreateSimScreen() 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).GetUpdateDraws() { f() } assert.True(t, ui.pages.HasPage("error")) } func TestViewDirContents(t *testing.T) { simScreen := testapp.CreateSimScreen() 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).GetUpdateDraws() { 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() 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() 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).GetUpdateDraws() { 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() 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).GetUpdateDraws() { 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 TestChangeCwd(t *testing.T) { fin := testdir.CreateTestDir() defer fin() simScreen := testapp.CreateSimScreen() defer simScreen.Fini() cwd := "" opt := func(ui *UI) { ui.SetChangeCwdFn(func(p string) error { cwd = p return nil }) } app := testapp.CreateMockedApp(true) ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false, false, false, opt) 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).GetUpdateDraws() { 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(1, 0) ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0)) assert.Equal(t, cwd, "test_dir/nested/subnested") } func TestChangeCwdWithErr(t *testing.T) { fin := testdir.CreateTestDir() defer fin() simScreen := testapp.CreateSimScreen() defer simScreen.Fini() cwd := "" opt := func(ui *UI) { ui.SetChangeCwdFn(func(p string) error { cwd = p return errors.New("failed") }) } app := testapp.CreateMockedApp(true) ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false, false, false, opt) 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).GetUpdateDraws() { 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(1, 0) ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0)) assert.Equal(t, cwd, "test_dir/nested/subnested") } func TestShowInfo(t *testing.T) { fin := testdir.CreateTestDir() defer fin() simScreen := testapp.CreateSimScreen() 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).GetUpdateDraws() { 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() 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).GetUpdateDraws() { 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() 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).GetUpdateDraws() { 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() 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() 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).GetUpdateDraws() { 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.25.0/tui/exec.go000066400000000000000000000004611443762540700144530ustar00rootroot00000000000000package 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.25.0/tui/exec_other.go000066400000000000000000000012271443762540700156550ustar00rootroot00000000000000//go:build !windows // +build !windows package tui import ( "os" "syscall" ) 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) } }) } func stopProcess() error { return syscall.Kill(syscall.Getpid(), syscall.SIGTSTP) } gdu-5.25.0/tui/exec_test.go000066400000000000000000000002631443762540700155120ustar00rootroot00000000000000package tui import ( "testing" "github.com/stretchr/testify/assert" ) func TestExecute(t *testing.T) { err := Execute("true", []string{}, []string{}) assert.Nil(t, err) } gdu-5.25.0/tui/exec_windows.go000066400000000000000000000010651443762540700162260ustar00rootroot00000000000000package 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) } } func stopProcess() error { return nil } gdu-5.25.0/tui/filter.go000066400000000000000000000021311443762540700150100ustar00rootroot00000000000000package 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.25.0/tui/filter_test.go000066400000000000000000000075231443762540700160610ustar00rootroot00000000000000package 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() 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).GetUpdateDraws() { f() } ui.showFilterInput() ui.filterValue = "" ui.showDir() assert.Contains(t, ui.table.GetCell(0, 0).Text, "ccc") // nothing is filtered ui.filterValue = "aa" ui.showDir() assert.Contains(t, ui.table.GetCell(0, 0).Text, "aaa") // shows only cccc ui.hideFilterInput() ui.showDir() assert.Contains(t, ui.table.GetCell(0, 0).Text, "ccc") // filtering reset } func TestFilteringWithoutCurrentDir(t *testing.T) { simScreen := testapp.CreateSimScreen() 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() 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).GetUpdateDraws() { 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() 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).GetUpdateDraws() { 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.25.0/tui/format.go000066400000000000000000000072741443762540700150300ustar00rootroot00000000000000package tui import ( "fmt" "math" "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, marked bool) string { var part int if ui.ShowApparentSize { part = int(float64(item.GetSize()) / float64(maxSize) * 100.0) } else { part = int(float64(item.GetUsage()) / float64(maxUsage) * 100.0) } row := string(item.GetFlag()) if ui.UseColors && !marked { 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)) } if ui.useOldSizeBar { row += " " + getUsageGraphOld(part) + " " } else { row += getUsageGraph(part) } if ui.showItemCount { if ui.UseColors && !marked { row += "[#e67100::b]" } else { row += "[::b]" } row += fmt.Sprintf("%11s ", ui.formatCount(item.GetItemCount())) } if ui.showMtime { if ui.UseColors && !marked { row += "[#e67100::b]" } else { row += "[::b]" } row += fmt.Sprintf( "%s [-::]", item.GetMtime().Format("2006-01-02 15:04:05"), ) } if len(ui.markedRows) > 0 { if marked { row += string('✓') } else { row += " " } row += " " } if item.IsDir() { if ui.UseColors && !marked { 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 := float64(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 { asize := math.Abs(fsize) switch { case asize >= common.Ei: return fmt.Sprintf("%.1f%s EiB", fsize/common.Ei, color) case asize >= common.Pi: return fmt.Sprintf("%.1f%s PiB", fsize/common.Pi, color) case asize >= common.Ti: return fmt.Sprintf("%.1f%s TiB", fsize/common.Ti, color) case asize >= common.Gi: return fmt.Sprintf("%.1f%s GiB", fsize/common.Gi, color) case asize >= common.Mi: return fmt.Sprintf("%.1f%s MiB", fsize/common.Mi, color) case asize >= 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) asize := math.Abs(fsize) switch { case asize >= common.E: return fmt.Sprintf("%.1f%s EB", fsize/common.E, color) case asize >= common.P: return fmt.Sprintf("%.1f%s PB", fsize/common.P, color) case asize >= common.T: return fmt.Sprintf("%.1f%s TB", fsize/common.T, color) case asize >= common.G: return fmt.Sprintf("%.1f%s GB", fsize/common.G, color) case asize >= common.M: return fmt.Sprintf("%.1f%s MB", fsize/common.M, color) case asize >= common.K: return fmt.Sprintf("%.1f%s kB", fsize/common.K, color) default: return fmt.Sprintf("%d%s B", size, color) } } gdu-5.25.0/tui/format_test.go000066400000000000000000000105071443762540700160600ustar00rootroot00000000000000package 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() 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)) assert.Equal(t, "-1.0[white:black:-] KiB", ui.formatSize(-1<<10, false, false)) } func TestFormatSizeDec(t *testing.T) { simScreen := testapp.CreateSimScreen() 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)) assert.Equal(t, "-1.0[white:black:-] kB", ui.formatSize(-1<<10, false, false)) } func TestFormatCount(t *testing.T) { simScreen := testapp.CreateSimScreen() 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() 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(), false), "Aaa [red[] bbb") } func TestMarked(t *testing.T) { simScreen := testapp.CreateSimScreen() defer simScreen.Fini() app := testapp.CreateMockedApp(true) ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, false, false, false, false) ui.markedRows[0] = struct{}{} ui.useOldSizeBar = true dir := &analyze.Dir{ File: &analyze.File{ Usage: 10, }, } file := &analyze.File{ Name: "Aaa", Parent: dir, Usage: 10, } assert.Contains(t, ui.formatFileRow(file, file.GetUsage(), file.GetSize(), true), "✓ Aaa") assert.Contains(t, ui.formatFileRow(file, file.GetUsage(), file.GetSize(), false), "[##########] Aaa") } func TestSizeBar(t *testing.T) { simScreen := testapp.CreateSimScreen() 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", Parent: dir, Usage: 10, } assert.Contains(t, ui.formatFileRow(file, file.GetUsage(), file.GetSize(), false), "██████████▏Aaa") } func TestOldSizeBar(t *testing.T) { simScreen := testapp.CreateSimScreen() defer simScreen.Fini() app := testapp.CreateMockedApp(true) ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, false, false, false, false) ui.markedRows[0] = struct{}{} ui.useOldSizeBar = true dir := &analyze.Dir{ File: &analyze.File{ Usage: 20, }, } file := &analyze.File{ Name: "Aaa", Parent: dir, Usage: 10, } assert.Contains(t, ui.formatFileRow(file, dir.GetUsage(), dir.GetSize(), false), "[##### ] Aaa") } gdu-5.25.0/tui/keys.go000066400000000000000000000137321443762540700145070ustar00rootroot00000000000000package tui import ( "fmt" "github.com/dundee/gdu/v5/pkg/fs" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) func (ui *UI) keyPressed(key *tcell.EventKey) *tcell.EventKey { if ui.handleCtrlZ(key) == nil { return nil } if ui.pages.HasPage("file") { return key // send event to primitive } if ui.filtering { return key } key = ui.handleClosingModals(key) if key == nil { return nil } key = ui.handleInfoPageEvents(key) if key == nil { return nil } key = ui.handleQuit(key) if key == nil { return nil } if ui.pages.HasPage("confirm") || ui.pages.HasPage("progress") || ui.pages.HasPage("deleting") || ui.pages.HasPage("emptying") { return key } key = ui.handleHelp(key) if key == nil { return nil } if ui.pages.HasPage("help") { return key } key = ui.handleShell(key) if key == nil { return nil } key = ui.handleLeftRight(key) if key == nil { return nil } key = ui.handleFiltering(key) if key == nil { return nil } return ui.handleMainActions(key) } func (ui *UI) handleClosingModals(key *tcell.EventKey) *tcell.EventKey { 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 } } return key } func (ui *UI) handleInfoPageEvents(key *tcell.EventKey) *tcell.EventKey { 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) } ui.showInfo() // refresh file info after any change } return key } // handle ctrl+z job control func (ui *UI) handleCtrlZ(key *tcell.EventKey) *tcell.EventKey { if key.Key() == tcell.KeyCtrlZ { ui.app.Suspend(func() { termApp := ui.app.(*tview.Application) termApp.Lock() defer termApp.Unlock() err := stopProcess() if err != nil { ui.showErr("Error sending STOP signal", err) } }) return nil } return key } func (ui *UI) handleQuit(key *tcell.EventKey) *tcell.EventKey { 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 } return key } func (ui *UI) handleHelp(key *tcell.EventKey) *tcell.EventKey { if key.Rune() == '?' { if ui.pages.HasPage("help") { ui.pages.RemovePage("help") ui.app.SetFocus(ui.table) return nil } ui.showHelp() return nil } return key } func (ui *UI) handleShell(key *tcell.EventKey) *tcell.EventKey { if key.Rune() == 'b' { ui.spawnShell() return nil } return key } func (ui *UI) handleLeftRight(key *tcell.EventKey) *tcell.EventKey { if key.Rune() == 'h' || key.Key() == tcell.KeyLeft { ui.handleLeft() return nil } if key.Rune() == 'l' || key.Key() == tcell.KeyRight { ui.handleRight() return nil } return key } func (ui *UI) handleFiltering(key *tcell.EventKey) *tcell.EventKey { if key.Key() == tcell.KeyTab && ui.filteringInput != nil { ui.filtering = true ui.app.SetFocus(ui.filteringInput) return nil } return key } func (ui *UI) handleMainActions(key *tcell.EventKey) *tcell.EventKey { switch key.Rune() { case 'd': ui.handleDelete(false) case 'e': ui.handleDelete(true) case 'v': ui.showFile() case 'o': ui.openItem() 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 case ' ': ui.handleMark() } 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, ok := ui.table.GetCell(row, column).GetReference().(fs.Item) if !ok || selectedFile == ui.currentDir.GetParent() { return } if ui.askBeforeDelete { ui.confirmDeletion(shouldEmpty) } else { ui.delete(shouldEmpty) } } func (ui *UI) handleMark() { if ui.currentDir == nil { return } // do not allow deleting parent dir row, column := ui.table.GetSelection() selectedFile, ok := ui.table.GetCell(row, column).GetReference().(fs.Item) if !ok || selectedFile == ui.currentDir.GetParent() { return } ui.fileItemMarked(row) } gdu-5.25.0/tui/keys_test.go000066400000000000000000000571111443762540700155450ustar00rootroot00000000000000package 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() 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() 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() 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() 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() 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() 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).GetUpdateDraws() { 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() 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).GetUpdateDraws() { 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() 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() 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).GetUpdateDraws() { 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() 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).GetUpdateDraws() { 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() 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() 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() 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() 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).GetUpdateDraws() { 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")) ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, '?', 0)) assert.False(t, ui.pages.HasPage("help")) } func TestDeleteEmpty(t *testing.T) { simScreen := testapp.CreateSimScreen() 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 TestMarkEmpty(t *testing.T) { simScreen := testapp.CreateSimScreen() 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, ' ', 0)) assert.NotNil(t, key) } func TestDelete(t *testing.T) { fin := testdir.CreateTestDir() defer fin() simScreen := testapp.CreateSimScreen() 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).GetUpdateDraws() { 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).GetUpdateDraws() { f() } assert.NoDirExists(t, "test_dir/nested") } func TestDeleteMarked(t *testing.T) { fin := testdir.CreateTestDir() defer fin() simScreen := testapp.CreateSimScreen() 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).GetUpdateDraws() { 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, ' ', 0)) ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'd', 0)) <-ui.done for _, f := range ui.app.(*testapp.MockedApp).GetUpdateDraws() { f() } assert.NoDirExists(t, "test_dir/nested") } func TestDeleteParent(t *testing.T) { fin := testdir.CreateTestDir() defer fin() simScreen := testapp.CreateSimScreen() 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).GetUpdateDraws() { 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 TestMarkParent(t *testing.T) { fin := testdir.CreateTestDir() defer fin() simScreen := testapp.CreateSimScreen() 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).GetUpdateDraws() { 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, ' ', 0)) assert.Equal(t, len(ui.markedRows), 0) } func TestEmptyDir(t *testing.T) { fin := testdir.CreateTestDir() defer fin() simScreen := testapp.CreateSimScreen() 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).GetUpdateDraws() { 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).GetUpdateDraws() { f() } assert.DirExists(t, "test_dir/nested") assert.NoDirExists(t, "test_dir/nested/subnested") } func TestMarkedEmptyDir(t *testing.T) { fin := testdir.CreateTestDir() defer fin() simScreen := testapp.CreateSimScreen() 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).GetUpdateDraws() { 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, ' ', 0)) ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'e', 0)) <-ui.done for _, f := range ui.app.(*testapp.MockedApp).GetUpdateDraws() { 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() 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).GetUpdateDraws() { 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).GetUpdateDraws() { f() } assert.DirExists(t, "test_dir/nested") assert.DirExists(t, "test_dir/nested/subnested") } func TestMarkedEmptyFile(t *testing.T) { fin := testdir.CreateTestDir() defer fin() simScreen := testapp.CreateSimScreen() 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).GetUpdateDraws() { 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, ' ', 0)) ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'e', 0)) <-ui.done for _, f := range ui.app.(*testapp.MockedApp).GetUpdateDraws() { f() } assert.DirExists(t, "test_dir/nested") assert.DirExists(t, "test_dir/nested/subnested") } func TestSortByApparentSize(t *testing.T) { simScreen := testapp.CreateSimScreen() 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).GetUpdateDraws() { 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() 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).GetUpdateDraws() { 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() 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).GetUpdateDraws() { 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() 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).GetUpdateDraws() { 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() 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).GetUpdateDraws() { 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() 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).GetUpdateDraws() { 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() 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).GetUpdateDraws() { 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, "ccc") } func TestSorting(t *testing.T) { simScreen := testapp.CreateSimScreen() 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).GetUpdateDraws() { 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() 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).GetUpdateDraws() { 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() 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).GetUpdateDraws() { 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.25.0/tui/marked.go000066400000000000000000000057511443762540700150010ustar00rootroot00000000000000package tui import ( "strconv" "strings" "github.com/dundee/gdu/v5/pkg/analyze" "github.com/dundee/gdu/v5/pkg/fs" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) func (ui *UI) fileItemMarked(row int) { if _, ok := ui.markedRows[row]; ok { delete(ui.markedRows, row) } else { ui.markedRows[row] = struct{}{} } ui.showDir() // select next row if possible ui.table.Select(min(row+1, ui.table.GetRowCount()-1), 0) } func (ui *UI) deleteMarked(shouldEmpty bool) { var action, acting string if shouldEmpty { action = "empty " acting = "emptying" } else { action = "delete " acting = "deleting" } modal := tview.NewModal() ui.pages.AddPage(acting, modal, true, true) var currentDir fs.Item var markedItems []fs.Item for row := range ui.markedRows { item := ui.table.GetCell(row, 0).GetReference().(fs.Item) markedItems = append(markedItems, item) } currentRow, _ := ui.table.GetSelection() var deleteFun func(fs.Item, fs.Item) error go func() { for _, one := range markedItems { ui.app.QueueUpdateDraw(func() { modal.SetText( // nolint: staticcheck // Why: fixed string strings.Title(acting) + " " + tview.Escape(one.GetName()) + "...", ) }) if shouldEmpty && !one.IsDir() { deleteFun = ui.emptier } else { deleteFun = ui.remover } var deleteItems []fs.Item if shouldEmpty && one.IsDir() { currentDir = one.(*analyze.Dir) for _, file := range currentDir.GetFiles() { deleteItems = append(deleteItems, file) } } else { currentDir = ui.currentDir deleteItems = append(deleteItems, one) } for _, item := range deleteItems { if err := deleteFun(currentDir, item); err != nil { msg := "Can't " + action + tview.Escape(one.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.markedRows = make(map[int]struct{}) ui.showDir() ui.table.Select(min(currentRow, ui.table.GetRowCount()-1), 0) }) if ui.done != nil { ui.done <- struct{}{} } }() } func (ui *UI) confirmDeletionMarked(shouldEmpty bool) { var action string if shouldEmpty { action = "empty" } else { action = "delete" } modal := tview.NewModal(). SetText( "Are you sure you want to " + action + " [::b]" + strconv.Itoa(len(ui.markedRows)) + "[::-] items?", ). 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.deleteMarked(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.25.0/tui/marked_test.go000066400000000000000000000006751443762540700160400ustar00rootroot00000000000000package tui import ( "testing" "github.com/dundee/gdu/v5/internal/testdir" "github.com/stretchr/testify/assert" ) func TestItemMarked(t *testing.T) { fin := testdir.CreateTestDir() defer fin() ui := getAnalyzedPathMockedApp(t, false, true, false) ui.done = make(chan struct{}) ui.fileItemMarked(1) assert.Equal(t, ui.markedRows, map[int]struct{}{1: {}}) ui.fileItemMarked(1) assert.Equal(t, ui.markedRows, map[int]struct{}{}) } gdu-5.25.0/tui/mouse.go000066400000000000000000000022161443762540700146570ustar00rootroot00000000000000package tui import ( "github.com/dundee/gdu/v5/pkg/fs" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) func (ui *UI) onMouse(event *tcell.EventMouse, action tview.MouseAction) (*tcell.EventMouse, tview.MouseAction) { if event == nil { return nil, action } if ui.pages.HasPage("confirm") || ui.pages.HasPage("progress") || ui.pages.HasPage("deleting") || ui.pages.HasPage("emptying") || ui.pages.HasPage("help") { return nil, action } switch action { case tview.MouseLeftDoubleClick: row, column := ui.table.GetSelection() if ui.currentDirPath != ui.topDirPath && row == 0 { ui.handleLeft() } else { selectedFile := ui.table.GetCell(row, column).GetReference().(fs.Item) if selectedFile.IsDir() { ui.handleRight() } else { ui.showFile() } } return nil, action case tview.MouseScrollUp: fallthrough case tview.MouseScrollDown: row, column := ui.table.GetSelection() if action == tview.MouseScrollUp && row > 0 { row-- } else if action == tview.MouseScrollDown && row+1 < ui.table.GetRowCount() { row++ } ui.table.Select(row, column) return nil, action } return event, action } gdu-5.25.0/tui/mouse_test.go000066400000000000000000000070321443762540700157170ustar00rootroot00000000000000package 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/dundee/gdu/v5/pkg/fs" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" "github.com/stretchr/testify/assert" ) func TestDoubleClick(t *testing.T) { fin := testdir.CreateTestDir() defer fin() simScreen := testapp.CreateSimScreen() 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).GetUpdateDraws() { f() } ui.table.Select(0, 0) assert.Equal(t, "test_dir", ui.currentDir.GetName()) ui.onMouse(tcell.NewEventMouse(0, 0, 0, 0), tview.MouseLeftDoubleClick) assert.Equal(t, "nested", ui.currentDir.GetName()) ui.onMouse(tcell.NewEventMouse(0, 0, 0, 0), tview.MouseLeftDoubleClick) assert.Equal(t, "test_dir", ui.currentDir.GetName()) // show file content ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0)) ui.table.Select(2, 0) selectedFile := ui.table.GetCell(2, 0).GetReference().(fs.Item) assert.Equal(t, selectedFile.GetName(), "file2") ui.onMouse(tcell.NewEventMouse(0, 0, 0, 0), tview.MouseLeftDoubleClick) assert.True(t, ui.pages.HasPage("file")) } func TestScroll(t *testing.T) { simScreen := testapp.CreateSimScreen() 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).GetUpdateDraws() { f() } ui.onMouse(tcell.NewEventMouse(0, 0, 0, 0), tview.MouseScrollDown) row, _ := ui.table.GetSelection() assert.Equal(t, row, 1) ui.onMouse(tcell.NewEventMouse(0, 0, 0, 0), tview.MouseScrollUp) row, _ = ui.table.GetSelection() assert.Equal(t, row, 0) } func TestScrollWhenPageOpened(t *testing.T) { simScreen := testapp.CreateSimScreen() 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).GetUpdateDraws() { f() } // open confirm dialog ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'd', 0)) ui.onMouse(tcell.NewEventMouse(0, 0, 0, 0), tview.MouseScrollDown) row, _ := ui.table.GetSelection() // scrolling does nothing assert.Equal(t, 0, row) } func TestEmptyEvent(t *testing.T) { simScreen := testapp.CreateSimScreen() defer simScreen.Fini() app := testapp.CreateMockedApp(true) ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false, false, false) event, action := ui.onMouse(nil, tview.MouseMove) assert.True(t, event == nil) assert.Equal(t, action, tview.MouseMove) } func TestMouseMove(t *testing.T) { simScreen := testapp.CreateSimScreen() defer simScreen.Fini() app := testapp.CreateMockedApp(true) ui := CreateUI(app, simScreen, &bytes.Buffer{}, true, true, false, false, false) event, action := ui.onMouse(tcell.NewEventMouse(0, 0, 0, 0), tview.MouseMove) assert.True(t, event != nil) assert.Equal(t, action, tview.MouseMove) } gdu-5.25.0/tui/progress.go000066400000000000000000000021201443762540700153650ustar00rootroot00000000000000package tui import ( "time" "github.com/dundee/gdu/v5/internal/common" "github.com/dundee/gdu/v5/pkg/path" ) func (ui *UI) updateProgress() { color := "[white:black:b]" if ui.UseColors { color = "[red:black:b]" } progressChan := ui.Analyzer.GetProgressChan() doneChan := ui.Analyzer.GetDone() var progress common.CurrentProgress start := time.Now() for { select { case progress = <-progressChan: case <-doneChan: return } func(itemCount int, totalSize int64, currentItem string) { delta := time.Since(start).Round(time.Second) 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:-], elapsed time: " + color + delta.String() + "[white:black:-]\nCurrent item: [white:black:b]" + path.ShortenPath(currentItem, ui.currentItemNameMaxLen)) }) }(progress.ItemCount, progress.TotalSize, progress.CurrentItemName) time.Sleep(100 * time.Millisecond) } } gdu-5.25.0/tui/show.go000066400000000000000000000174021443762540700145120ustar00rootroot00000000000000package tui import ( "strconv" "strings" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" log "github.com/sirupsen/logrus" "github.com/dundee/gdu/v5/build" ) 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]space [white:black:-]Mark file or directory for deletion [::b]v [white:black:-]Show content of file [::b]o [white:black:-]Open file or directory in external program [::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)` func (ui *UI) showDir() { var ( totalUsage int64 totalSize int64 maxUsage int64 maxSize int64 itemCount int ) ui.currentDirPath = ui.currentDir.GetPath() if ui.changeCwdFn != nil { err := ui.changeCwdFn(ui.currentDirPath) if err != nil { log.Printf("error setting cwd: %s", err.Error()) } log.Printf("changing cwd to %s", ui.currentDirPath) } ui.currentDirLabel.SetText("[::b] --- " + tview.Escape( strings.TrimPrefix(ui.currentDirPath, build.RootPathPrefix), ) + " ---").SetDynamicColors(true) ui.table.Clear() rowIndex := 0 if ui.currentDirPath != ui.topDirPath { prefix := " " if len(ui.markedRows) > 0 { prefix += " " } cell := tview.NewTableCell(prefix + "[::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() _, marked := ui.markedRows[rowIndex] cell := tview.NewTableCell(ui.formatFileRow(item, maxUsage, maxSize, marked)) cell.SetReference(ui.currentDir.GetFiles()[i]) if marked { cell.SetStyle(tcell.Style{}.Foreground(tview.Styles.PrimaryTextColor)) cell.SetBackgroundColor(tview.Styles.ContrastBackgroundColor) } else { cell.SetStyle(tcell.Style{}.Foreground(tcell.ColorDefault)) } 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:-]" } selected := "" if len(ui.markedRows) > 0 { selected = " Selected items: " + footerNumberColor + strconv.Itoa(len(ui.markedRows)) + footerTextColor } ui.footerLabel.SetText( selected + " 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.useOldSizeBar))) 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.25.0/tui/sort.go000066400000000000000000000040331443762540700145150ustar00rootroot00000000000000package tui import ( "sort" "github.com/dundee/gdu/v5/pkg/device" "github.com/dundee/gdu/v5/pkg/fs" ) // SetDefaultSorting sets the default sorting func (ui *UI) SetDefaultSorting(by, order string) { if by != "" { ui.defaultSortBy = by } if order == "asc" || order == "desc" { ui.defaultSortOrder = order } } 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(sort.Reverse(fs.ByApparentSize(ui.currentDir.GetFiles()))) } else { sort.Sort(fs.ByApparentSize(ui.currentDir.GetFiles())) } } else { if ui.sortOrder == "desc" { sort.Sort(sort.Reverse(ui.currentDir.GetFiles())) } else { sort.Sort(ui.currentDir.GetFiles()) } } } if ui.sortBy == "itemCount" { if ui.sortOrder == "desc" { sort.Sort(sort.Reverse(fs.ByItemCount(ui.currentDir.GetFiles()))) } else { sort.Sort(fs.ByItemCount(ui.currentDir.GetFiles())) } } if ui.sortBy == "name" { if ui.sortOrder == "desc" { sort.Sort(sort.Reverse(fs.ByName(ui.currentDir.GetFiles()))) } else { sort.Sort(fs.ByName(ui.currentDir.GetFiles())) } } if ui.sortBy == "mtime" { if ui.sortOrder == "desc" { sort.Sort(sort.Reverse(fs.ByMtime(ui.currentDir.GetFiles()))) } else { sort.Sort(fs.ByMtime(ui.currentDir.GetFiles())) } } } func (ui *UI) sortDevices() { if ui.sortBy == "size" { if ui.sortOrder == "desc" { sort.Sort(sort.Reverse(device.ByUsedSize(ui.devices))) } else { sort.Sort(device.ByUsedSize(ui.devices)) } } if ui.sortBy == "name" { if ui.sortOrder == "desc" { sort.Sort(sort.Reverse(device.ByName(ui.devices))) } else { sort.Sort(device.ByName(ui.devices)) } } } gdu-5.25.0/tui/sort_test.go000066400000000000000000000145711443762540700155640ustar00rootroot00000000000000package 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, "ccc") assert.Contains(t, ui.table.GetCell(1, 0).Text, "bbb") assert.Contains(t, ui.table.GetCell(2, 0).Text, "aaa") 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, "ccc") assert.Contains(t, ui.table.GetCell(1, 0).Text, "bbb") assert.Contains(t, ui.table.GetCell(2, 0).Text, "aaa") 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, "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 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 TestSetDEfaultSorting(t *testing.T) { simScreen := testapp.CreateSimScreen() defer simScreen.Fini() var opts []Option opts = append(opts, func(ui *UI) { ui.SetDefaultSorting("name", "asc") }) app := testapp.CreateMockedApp(true) ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, false, false, false, false, opts...) ui.Analyzer = &testanalyze.MockedAnalyzer{} ui.done = make(chan struct{}) if err := ui.AnalyzePath("test_dir", nil); err != nil { panic(err) } <-ui.done // wait for analyzer for _, f := range ui.app.(*testapp.MockedApp).GetUpdateDraws() { f() } 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() 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).GetUpdateDraws() { f() } return ui } gdu-5.25.0/tui/tui.go000066400000000000000000000205021443762540700143260ustar00rootroot00000000000000package 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" ) // 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 changeCwdFn func(string) error linkedItems fs.HardLinkedItems selectedTextColor tcell.Color selectedBackgroundColor tcell.Color currentItemNameMaxLen int useOldSizeBar bool defaultSortBy string defaultSortOrder string markedRows map[int]struct{} } // Option is optional function customizing the bahaviour of UI type Option func(ui *UI) // 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, opts ...Option, ) *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), selectedTextColor: tview.Styles.TitleColor, selectedBackgroundColor: tview.Styles.MoreContrastBackgroundColor, currentItemNameMaxLen: 70, defaultSortBy: "size", defaultSortOrder: "desc", markedRows: make(map[int]struct{}), } for _, o := range opts { o(ui) } ui.resetSorting() app.SetBeforeDrawFunc(func(screen tcell.Screen) bool { screen.Clear() return false }) ui.app.SetInputCapture(ui.keyPressed) ui.app.SetMouseCapture(ui.onMouse) 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(ui.selectedTextColor). Background(ui.selectedBackgroundColor).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 } // SetSelectedTextColor sets the color for the highighted selected text func (ui *UI) SetSelectedTextColor(color tcell.Color) { ui.selectedTextColor = color } // SetSelectedBackgroundColor sets the color for the highighted selected text func (ui *UI) SetSelectedBackgroundColor(color tcell.Color) { ui.selectedBackgroundColor = color } // SetCurrentItemNameMaxLen sets the maximum length of the path of the currently processed item // to be shown in the progress modal func (ui *UI) SetCurrentItemNameMaxLen(len int) { ui.currentItemNameMaxLen = len } // UseOldSizeBar uses the old size bar (# chars) instead of the new one (unicode block elements) func (ui *UI) UseOldSizeBar() { ui.useOldSizeBar = true } // SetChangeCwdFn sets function that can be used to change current working dir // during dir browsing func (ui *UI) SetChangeCwdFn(fn func(string) error) { ui.changeCwdFn = fn } // StartUILoop starts tview application func (ui *UI) StartUILoop() error { return ui.app.Run() } func (ui *UI) resetSorting() { ui.sortBy = ui.defaultSortBy ui.sortOrder = ui.defaultSortOrder } 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.markedRows = make(map[int]struct{}) 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() ui.Analyzer.ResetProgress() ui.linkedItems = make(fs.HardLinkedItems) err = ui.AnalyzePath(selectedDevice.MountPoint, nil) if err != nil { ui.showErr("Error analyzing device", err) } } func (ui *UI) confirmDeletion(shouldEmpty bool) { if len(ui.markedRows) > 0 { ui.confirmDeletionMarked(shouldEmpty) } else { ui.confirmDeletionSelected(shouldEmpty) } } func (ui *UI) confirmDeletionSelected(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.25.0/tui/tui_test.go000066400000000000000000000266651443762540700154050ustar00rootroot00000000000000package 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() printScreen(simScreen) 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.GetDone() done.Broadcast() 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() 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() 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() 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).GetUpdateDraws() { 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, "ccc") } 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, "ccc") } func TestSelectedWithoutCurrentDir(t *testing.T) { simScreen := testapp.CreateSimScreen() 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() 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 TestConfirmEmptyMarked(t *testing.T) { ui := getAnalyzedPathMockedApp(t, false, true, true) ui.table.Select(1, 0) ui.markedRows[1] = struct{}{} ui.confirmDeletion(true) assert.True(t, ui.pages.HasPage("confirm")) } func TestConfirmDeletionMarked(t *testing.T) { ui := getAnalyzedPathMockedApp(t, true, true, true) ui.table.Select(1, 0) ui.markedRows[1] = struct{}{} ui.confirmDeletion(false) assert.True(t, ui.pages.HasPage("confirm")) } func TestConfirmDeletionMarkedBW(t *testing.T) { ui := getAnalyzedPathMockedApp(t, false, true, true) ui.table.Select(1, 0) ui.markedRows[1] = struct{}{} ui.confirmDeletion(false) 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).GetUpdateDraws() { 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.delete(false) <-ui.done for _, f := range ui.app.(*testapp.MockedApp).GetUpdateDraws() { f() } assert.True(t, ui.pages.HasPage("error")) assert.DirExists(t, "test_dir/nested") } func TestDeleteMarkedWithErr(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.markedRows[0] = struct{}{} ui.deleteMarked(false) <-ui.done for _, f := range ui.app.(*testapp.MockedApp).GetUpdateDraws() { f() } assert.True(t, ui.pages.HasPage("error")) assert.DirExists(t, "test_dir/nested") } func TestShowErr(t *testing.T) { simScreen := testapp.CreateSimScreen() 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() 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 TestSetSelectedBackgroundColor(t *testing.T) { simScreen := testapp.CreateSimScreen() defer simScreen.Fini() app := testapp.CreateMockedApp(true) ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false, false, false) ui.SetSelectedBackgroundColor(tcell.ColorRed) assert.Equal(t, ui.selectedBackgroundColor, tcell.ColorRed) } func TestSetSelectedTextColor(t *testing.T) { simScreen := testapp.CreateSimScreen() defer simScreen.Fini() app := testapp.CreateMockedApp(true) ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false, false, false) ui.SetSelectedTextColor(tcell.ColorRed) assert.Equal(t, ui.selectedTextColor, tcell.ColorRed) } func TestSetCurrentItemNameMaxLen(t *testing.T) { simScreen := testapp.CreateSimScreen() defer simScreen.Fini() app := testapp.CreateMockedApp(true) ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false, false, false) ui.SetCurrentItemNameMaxLen(5) assert.Equal(t, ui.currentItemNameMaxLen, 5) } func TestUseOldSizeBar(t *testing.T) { simScreen := testapp.CreateSimScreen() defer simScreen.Fini() app := testapp.CreateMockedApp(true) ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false, false, false) ui.UseOldSizeBar() assert.Equal(t, ui.useOldSizeBar, true) } // nolint: deadcode,unused // Why: for debugging func printScreen(simScreen tcell.SimulationScreen) { b, _, _ := simScreen.GetContents() for i, r := range b { if string(r.Bytes) != " " { 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() 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).GetUpdateDraws() { f() } assert.Equal(t, "test_dir", ui.currentDir.GetName()) return ui } gdu-5.25.0/tui/utils.go000066400000000000000000000020131443762540700146620ustar00rootroot00000000000000package tui import ( "github.com/dundee/gdu/v5/pkg/device" ) var ( barFullRune = "\u2588" barPartRunes = map[int]string{ 0: " ", 1: "\u258F", 2: "\u258E", 3: "\u258D", 4: "\u258C", 5: "\u258B", 6: "\u258A", 7: "\u2589", } ) func getDeviceUsagePart(item *device.Device, useOld bool) string { part := int(float64(item.Size-item.Free) / float64(item.Size) * 100.0) if useOld { return getUsageGraphOld(part) } return getUsageGraph(part) } func getUsageGraph(part int) string { graph := " " whole := part / 10 for i := 0; i < whole; i++ { graph += barFullRune } partWidth := (part % 10) * 8 / 10 if part < 100 { graph += barPartRunes[partWidth] } for i := 0; i < 10-whole-1; i++ { graph += " " } graph += "\u258F" return graph } func getUsageGraphOld(part int) string { part = part / 10 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 } gdu-5.25.0/tui/utils_test.go000066400000000000000000000025711443762540700157320ustar00rootroot00000000000000package tui import ( "testing" "github.com/stretchr/testify/assert" ) func TestGetUsageGraph(t *testing.T) { assert.Equal(t, " \u258F", getUsageGraph(0)) assert.Equal(t, " █ \u258F", getUsageGraph(10)) assert.Equal(t, " ██ \u258F", getUsageGraph(20)) assert.Equal(t, " ███ \u258F", getUsageGraph(30)) assert.Equal(t, " ████ \u258F", getUsageGraph(40)) assert.Equal(t, " █████ \u258F", getUsageGraph(50)) assert.Equal(t, " ██████ \u258F", getUsageGraph(60)) assert.Equal(t, " ███████ \u258F", getUsageGraph(70)) assert.Equal(t, " ████████ \u258F", getUsageGraph(80)) assert.Equal(t, " █████████ \u258F", getUsageGraph(90)) assert.Equal(t, " ██████████\u258F", getUsageGraph(100)) assert.Equal(t, " █ \u258F", getUsageGraph(11)) assert.Equal(t, " █▏ \u258F", getUsageGraph(12)) assert.Equal(t, " █▎ \u258F", getUsageGraph(13)) assert.Equal(t, " █▍ \u258F", getUsageGraph(14)) assert.Equal(t, " █▌ \u258F", getUsageGraph(15)) assert.Equal(t, " █▌ \u258F", getUsageGraph(16)) assert.Equal(t, " █▋ \u258F", getUsageGraph(17)) assert.Equal(t, " █▊ \u258F", getUsageGraph(18)) assert.Equal(t, " █▉ \u258F", getUsageGraph(19)) }