cmd/gdu/app/app.go
package app
import (
"fmt"
"io"
"io/fs"
"net/http"
"net/http/pprof"
"os"
"path/filepath"
"runtime"
"strings"
log "github.com/sirupsen/logrus"
"github.com/dundee/gdu/v5/build"
"github.com/dundee/gdu/v5/internal/common"
"github.com/dundee/gdu/v5/pkg/analyze"
"github.com/dundee/gdu/v5/pkg/device"
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
ReadFromStorage(storagePath, path string) error
SetIgnoreDirPaths(paths []string)
SetIgnoreDirPatterns(paths []string) error
SetIgnoreFromFile(ignoreFile string) error
SetIgnoreHidden(value bool)
SetFollowSymlinks(value bool)
SetAnalyzer(analyzer common.Analyzer)
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"`
SequentialScanning bool `yaml:"sequential-scanning"`
ShowDisks bool `yaml:"-"`
ShowApparentSize bool `yaml:"show-apparent-size"`
ShowRelativeSize bool `yaml:"show-relative-size"`
ShowVersion bool `yaml:"-"`
ShowItemCount bool `yaml:"show-item-count"`
ShowMTime bool `yaml:"show-mtime"`
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"`
NoDelete bool `yaml:"no-delete"`
FollowSymlinks bool `yaml:"follow-symlinks"`
Profiling bool `yaml:"profiling"`
ConstGC bool `yaml:"const-gc"`
UseStorage bool `yaml:"use-storage"`
StoragePath string `yaml:"storage-path"`
ReadFromStorage bool `yaml:"read-from-storage"`
Summarize bool `yaml:"summarize"`
UseSIPrefix bool `yaml:"use-si-prefix"`
NoPrefix bool `yaml:"no-prefix"`
WriteConfig bool `yaml:"-"`
ChangeCwd bool `yaml:"change-cwd"`
DeleteInBackground bool `yaml:"delete-in-background"`
DeleteInParallel bool `yaml:"delete-in-parallel"`
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() 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 nil
}
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, err := filepath.Abs(path)
if err != nil {
return err
}
ui, err = a.createUI()
if err != nil {
return err
}
if a.Flags.UseStorage {
ui.SetAnalyzer(analyze.CreateStoredAnalyzer(a.Flags.StoragePath))
}
if a.Flags.SequentialScanning {
ui.SetAnalyzer(analyze.CreateSeqAnalyzer())
}
if a.Flags.FollowSymlinks {
ui.SetFollowSymlinks(true)
}
if err := a.setNoCross(path); err != nil {
return err
}
ui.SetIgnoreDirPaths(a.Flags.IgnoreDirs)
if len(a.Flags.IgnoreDirPatterns) > 0 {
if err := ui.SetIgnoreDirPatterns(a.Flags.IgnoreDirPatterns); err != nil {
return err
}
}
if a.Flags.IgnoreFromFile != "" {
if err := ui.SetIgnoreFromFile(a.Flags.IgnoreFromFile); err != nil {
return err
}
}
if a.Flags.NoHidden {
ui.SetIgnoreHidden(true)
}
a.setMaxProcs()
if err := a.runAction(ui, path); err != nil {
return err
}
return ui.StartUILoop()
}
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
switch {
case 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, 0o600)
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,
)
case 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,
)
default:
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 {
opts = append(opts, func(ui *tui.UI) {
ui.SetChangeCwdFn(os.Chdir)
})
}
if a.Flags.ShowItemCount {
opts = append(opts, func(ui *tui.UI) {
ui.SetShowItemCount()
})
}
if a.Flags.ShowMTime {
opts = append(opts, func(ui *tui.UI) {
ui.SetShowMTime()
})
}
if a.Flags.NoDelete {
opts = append(opts, func(ui *tui.UI) {
ui.SetNoDelete()
})
}
if a.Flags.DeleteInBackground {
opts = append(opts, func(ui *tui.UI) {
ui.SetDeleteInBackground()
})
}
if a.Flags.DeleteInParallel {
opts = append(opts, func(ui *tui.UI) {
ui.SetDeleteInParallel()
})
}
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
}
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))
}()
}
switch {
case a.Flags.ShowDisks:
if err := ui.ListDevices(a.Getter); err != nil {
return fmt.Errorf("loading mount points: %w", err)
}
case 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, 0o600)
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)
}
case a.Flags.ReadFromStorage:
ui.SetAnalyzer(analyze.CreateStoredAnalyzer(a.Flags.StoragePath))
if err := ui.ReadFromStorage(a.Flags.StoragePath, path); err != nil {
return fmt.Errorf("reading from storage (%s): %w", a.Flags.StoragePath, err)
}
default:
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
}