star/gdu/stdout/stdout.go
2025-04-26 19:33:14 +08:00

495 lines
9.6 KiB
Go

package stdout
import (
"fmt"
"io"
"math"
"runtime"
"sort"
"sync"
"time"
"b612.me/apps/b612/gdu/internal/common"
"b612.me/apps/b612/gdu/pkg/analyze"
"b612.me/apps/b612/gdu/pkg/device"
"b612.me/apps/b612/gdu/pkg/fs"
"b612.me/apps/b612/gdu/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
top int
}
var (
progressRunes = []rune(`⠇⠏⠋⠙⠹⠸⠼⠴⠦⠧`)
progressRunesOld = []rune(`-\\|/`)
progressRunesCount = len(progressRunes)
)
// 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,
top int,
) *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,
top: top,
}
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
}
func (ui *UI) UseOldProgressRunes() {
progressRunes = progressRunesOld
progressRunesCount = len(progressRunes)
}
// 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
}
maxDeviceNameLength := 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",
maxDeviceNameLength,
sizeLength,
sizeLength,
sizeLength,
percentLength,
)
fmt.Fprintf(
ui.output,
fmt.Sprintf("%%%ds %%9s %%9s %%9s %%5s %%s\n", maxDeviceNameLength),
"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
updateStatsDone chan struct{}
)
updateStatsDone = make(chan struct{}, 1)
if ui.ShowProgress {
wait.Add(1)
go func() {
defer wait.Done()
ui.updateProgress(updateStatsDone)
}()
}
wait.Add(1)
go func() {
defer wait.Done()
dir = ui.Analyzer.AnalyzeDir(path, ui.CreateIgnoreFunc(), ui.ConstGC)
dir.UpdateStats(make(fs.HardLinkedItems, 10))
updateStatsDone <- struct{}{}
}()
wait.Wait()
switch {
case ui.top > 0:
ui.printTopFiles(dir)
case ui.summarize:
ui.printTotalItem(dir)
default:
ui.showDir(dir)
}
return nil
}
// ReadFromStorage reads analysis data from persistent key-value storage
func (ui *UI) ReadFromStorage(storagePath, path string) error {
storage := analyze.NewStorage(storagePath, path)
closeFn := storage.Open()
defer closeFn()
dir, err := storage.GetDirForPath(path)
if err != nil {
return err
}
switch {
case ui.top > 0:
ui.printTopFiles(dir)
case ui.summarize:
ui.printTotalItem(dir)
default:
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) printTopFiles(file fs.Item) {
collected := analyze.CollectTopFiles(file, ui.top)
for _, file := range collected {
ui.printItemPath(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.Sprint("/"+file.GetName()))
} else {
fmt.Fprintf(ui.output,
lineFormat,
string(file.GetFlag()),
ui.formatSize(size),
file.GetName())
}
}
func (ui *UI) printItemPath(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.GetPath())
}
// 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 %= progressRunesCount
}
}
func (ui *UI) updateProgress(updateStatsDone <-chan struct{}) {
emptyRow := "\r"
for j := 0; j < 100; j++ {
emptyRow += " "
}
progressChan := ui.Analyzer.GetProgressChan()
analysisDoneChan := ui.Analyzer.GetDone()
var progress common.CurrentProgress
i := 0
for {
fmt.Fprint(ui.output, emptyRow)
select {
case progress = <-progressChan:
case <-analysisDoneChan:
for {
fmt.Fprint(ui.output, emptyRow)
fmt.Fprintf(ui.output, "\r %s ", string(progressRunes[i]))
fmt.Fprint(ui.output, "Calculating disk usage...")
time.Sleep(100 * time.Millisecond)
i++
i %= progressRunesCount
select {
case <-updateStatsDone:
fmt.Fprint(ui.output, emptyRow)
fmt.Fprint(ui.output, "\r")
return
default:
}
}
}
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 %= progressRunesCount
}
}
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, y int) int {
if x > y {
return x
}
return y
}