Skip to content

Commit

Permalink
show info about all mounted disks when no path is given
Browse files Browse the repository at this point in the history
  • Loading branch information
Daniel Milde committed Dec 27, 2020
1 parent d6112a6 commit 764a662
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 64 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/dundee/gdu)](https://goreportcard.com/report/github.com/dundee/gdu)

Extremely fast disk usage analyzer.
Port of [ncdu](https://dev.yorhel.nl/ncdu) written in Go.
Port of [ncdu](https://dev.yorhel.nl/ncdu) written in Go with additional abilities of `df`.

<img src="/assets/demo.gif" width="100%" />

## Installation

Expand All @@ -21,7 +23,8 @@ Arch Linux:

## Usage

gdu some_dir_to_analyze
gdu # Show all mounted disks
gdu some_dir_to_analyze # analyze given dir


## Running tests
Expand Down
Binary file added assets/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
112 changes: 86 additions & 26 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,18 @@ type UI struct {
pages *tview.Pages
progress *tview.Modal
help *tview.Flex
dirContent *tview.Table
table *tview.Table
currentDir *File
topDirPath string
currentDirPath string
askBeforeDelete bool
}

// CreateUI creates the whole UI app
func CreateUI(topDirPath string, screen tcell.Screen) *UI {
func CreateUI(screen tcell.Screen) *UI {
ui := &UI{
askBeforeDelete: true,
}
ui.topDirPath, _ = filepath.Abs(topDirPath)

ui.app = tview.NewApplication()
ui.app.SetScreen(screen)
Expand All @@ -49,8 +48,7 @@ func CreateUI(topDirPath string, screen tcell.Screen) *UI {

ui.currentDirLabel = tview.NewTextView()

ui.dirContent = tview.NewTable().SetSelectable(true, false)
ui.dirContent.SetSelectedFunc(ui.itemSelected)
ui.table = tview.NewTable().SetSelectable(true, false)

ui.footer = tview.NewTextView()
ui.footer.SetTextColor(tcell.ColorBlack)
Expand All @@ -60,43 +58,86 @@ func CreateUI(topDirPath string, screen tcell.Screen) *UI {
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.dirContent, 2, 0, 1, 1, 0, 0, true).
AddItem(ui.table, 2, 0, 1, 1, 0, 0, true).
AddItem(ui.footer, 3, 0, 1, 1, 0, 0, false)

ui.progress = tview.NewModal().SetText("Scanning...")

ui.pages = tview.NewPages().
AddPage("background", grid, true, true).
AddPage("progress", ui.progress, true, true)
AddPage("background", grid, true, true)

ui.app.SetRoot(ui.pages, true)

return ui
}

// ShowDir shows content of the selected dir
func (ui *UI) ShowDir() {
// ListDevices lists mounted devices and shows their disk usage
func (ui *UI) ListDevices() {
devices := GetDevicesInfo()

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))

for i, device := range devices {
ui.table.SetCell(i+1, 0, tview.NewTableCell(device.name).SetReference(devices[i]))
ui.table.SetCell(i+1, 1, tview.NewTableCell(formatSize(device.size)))
ui.table.SetCell(i+1, 2, tview.NewTableCell(formatSize(device.size-device.free)))
ui.table.SetCell(i+1, 3, tview.NewTableCell(getDeviceUsagePart(device)))
ui.table.SetCell(i+1, 4, tview.NewTableCell(formatSize(device.free)))
ui.table.SetCell(i+1, 5, tview.NewTableCell(device.mountPoint))
}

ui.table.Select(1, 0)
ui.footer.SetText("")
ui.table.SetSelectedFunc(ui.deviceItemSelected)
}

// AnalyzePath analyzes recursively disk usage in given path
func (ui *UI) AnalyzePath(path string) {
ui.topDirPath, _ = filepath.Abs(path)

ui.progress = tview.NewModal().SetText("Scanning...")
ui.pages.AddPage("progress", ui.progress, true, true)
ui.table.SetSelectedFunc(ui.fileItemSelected)

statusChannel := make(chan CurrentProgress)
go ui.updateProgress(statusChannel)

go func() {
ui.currentDir = ProcessDir(ui.topDirPath, statusChannel)

ui.app.QueueUpdateDraw(func() {
ui.showDir()
ui.pages.HidePage("progress")
})
}()
}

// showDir shows content of the selected dir
func (ui *UI) showDir() {
ui.currentDirPath = ui.currentDir.path
ui.currentDirLabel.SetText("--- " + ui.currentDirPath + " ---")

ui.dirContent.Clear()
ui.table.Clear()

rowIndex := 0
if ui.currentDirPath != ui.topDirPath {
cell := tview.NewTableCell(" /..")
cell.SetReference(ui.currentDir.parent)
ui.dirContent.SetCell(0, 0, cell)
ui.table.SetCell(0, 0, cell)
rowIndex++
}

for i, item := range ui.currentDir.files {
cell := tview.NewTableCell(formatRow(item))
cell := tview.NewTableCell(formatFileRow(item))
cell.SetReference(ui.currentDir.files[i])
ui.dirContent.SetCell(rowIndex, 0, cell)
ui.table.SetCell(rowIndex, 0, cell)
rowIndex++
}

ui.dirContent.Select(0, 0)
ui.table.Select(0, 0)
ui.footer.SetText("Apparent size: " + formatSize(ui.currentDir.size) + " Items: " + fmt.Sprint(ui.currentDir.itemCount))
}

Expand All @@ -107,19 +148,24 @@ func (ui *UI) StartUILoop() {
}
}

func (ui *UI) itemSelected(row, column int) {
selectedDir := ui.dirContent.GetCell(row, column).GetReference().(*File)
func (ui *UI) fileItemSelected(row, column int) {
selectedDir := ui.table.GetCell(row, column).GetReference().(*File)
if !selectedDir.isDir {
return
}

ui.currentDir = selectedDir
ui.ShowDir()
ui.showDir()
}

func (ui *UI) deviceItemSelected(row, column int) {
selectedDevice := ui.table.GetCell(row, column).GetReference().(*Device)
ui.AnalyzePath(selectedDevice.mountPoint)
}

func (ui *UI) confirmDeletion() {
row, column := ui.dirContent.GetSelection()
selectedFile := ui.dirContent.GetCell(row, column).GetReference().(*File)
row, column := ui.table.GetSelection()
selectedFile := ui.table.GetCell(row, column).GetReference().(*File)
modal := tview.NewModal().
SetText("Are you sure you want to \"" + selectedFile.name + "\"").
AddButtons([]string{"yes", "no", "don't ask me again"}).
Expand All @@ -138,10 +184,10 @@ func (ui *UI) confirmDeletion() {
}

func (ui *UI) deleteSelected() {
row, column := ui.dirContent.GetSelection()
selectedFile := ui.dirContent.GetCell(row, column).GetReference().(*File)
row, column := ui.table.GetSelection()
selectedFile := ui.table.GetCell(row, column).GetReference().(*File)
ui.currentDir.RemoveFile(selectedFile)
ui.ShowDir()
ui.showDir()
}

func (ui *UI) keyPressed(key *tcell.EventKey) *tcell.EventKey {
Expand Down Expand Up @@ -212,7 +258,7 @@ func formatSize(size int64) string {
return fmt.Sprintf("%d B", size)
}

func formatRow(item *File) string {
func formatFileRow(item *File) string {
part := int(float64(item.size) / float64(item.parent.size) * 10.0)
row := fmt.Sprintf("%10s", formatSize(item.size))
row += " ["
Expand All @@ -231,3 +277,17 @@ func formatRow(item *File) string {
row += item.name
return row
}

func getDeviceUsagePart(item *Device) string {
part := int(float64(item.size-item.free) / float64(item.size) * 10.0)
row := "["
for i := 0; i < 10; i++ {
if part > i {
row += "#"
} else {
row += " "
}
}
row += "]"
return row
}
50 changes: 32 additions & 18 deletions cli_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"runtime"
"testing"
"time"

Expand All @@ -14,7 +15,7 @@ func TestFooter(t *testing.T) {
simScreen.Init()
simScreen.SetSize(15, 15)

ui := CreateUI(".", simScreen)
ui := CreateUI(simScreen)

dir := File{
name: "xxx",
Expand All @@ -31,7 +32,7 @@ func TestFooter(t *testing.T) {
dir.files = []*File{&file}

ui.currentDir = &dir
ui.ShowDir()
ui.showDir()
ui.pages.HidePage("progress")

ui.footer.Draw(simScreen)
Expand All @@ -56,7 +57,7 @@ func TestUpdateProgress(t *testing.T) {

statusChannel := make(chan CurrentProgress)

ui := CreateUI(".", simScreen)
ui := CreateUI(simScreen)
go func() {
ui.updateProgress(statusChannel)
}()
Expand All @@ -74,7 +75,7 @@ func TestHelp(t *testing.T) {
simScreen.Init()
simScreen.SetSize(50, 50)

ui := CreateUI(".", simScreen)
ui := CreateUI(simScreen)
ui.showHelp()
ui.help.Draw(simScreen)
simScreen.Show()
Expand All @@ -97,15 +98,10 @@ func TestDeleteDir(t *testing.T) {
simScreen.Init()
simScreen.SetSize(50, 50)

ui := CreateUI("test_dir", simScreen)
ui := CreateUI(simScreen)
ui.askBeforeDelete = false

statusChannel := make(chan CurrentProgress)
go ui.updateProgress(statusChannel)
ui.currentDir = ProcessDir("test_dir", statusChannel)

ui.ShowDir()
ui.pages.HidePage("progress")
ui.AnalyzePath("test_dir")

go func() {
time.Sleep(100 * time.Millisecond)
Expand Down Expand Up @@ -133,14 +129,9 @@ func TestShowConfirm(t *testing.T) {
simScreen.Init()
simScreen.SetSize(50, 50)

ui := CreateUI("test_dir", simScreen)
ui := CreateUI(simScreen)

statusChannel := make(chan CurrentProgress)
go ui.updateProgress(statusChannel)
ui.currentDir = ProcessDir("test_dir", statusChannel)

ui.ShowDir()
ui.pages.HidePage("progress")
ui.AnalyzePath("test_dir")

go func() {
time.Sleep(100 * time.Millisecond)
Expand All @@ -160,6 +151,29 @@ func TestShowConfirm(t *testing.T) {
assert.FileExists(t, "test_dir/nested/file2")
}

func TestShowDevices(t *testing.T) {
if runtime.GOOS != "linux" {
return
}

simScreen := tcell.NewSimulationScreen("UTF-8")
defer simScreen.Fini()
simScreen.Init()
simScreen.SetSize(50, 50)

ui := CreateUI(simScreen)
ui.ListDevices()
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 printScreen(simScreen tcell.SimulationScreen) {
b, _, _ := simScreen.GetContents()

Expand Down
9 changes: 9 additions & 0 deletions dev.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package main

// Device struct
type Device struct {
name string
mountPoint string
size int64
free int64
}
51 changes: 51 additions & 0 deletions dev_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// +build linux,amd64

package main

import (
"bufio"
"log"
"os"
"runtime"
"strings"
"syscall"
)

// GetDevicesInfo returns usage info about mounted devices (by calling Statfs syscall)
func GetDevicesInfo() []*Device {
if runtime.GOOS != "linux" {
panic("Listing devices is not yet supported on " + runtime.GOOS)
}

devices := []*Device{}

file, err := os.Open("/proc/mounts")
if err != nil {
log.Fatal(err)
}
defer file.Close()

scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if line[0:4] == "/dev" {
parts := strings.Fields(line)
info := &syscall.Statfs_t{}
syscall.Statfs(parts[1], info)

device := &Device{
name: parts[0],
mountPoint: parts[1],
size: info.Bsize * int64(info.Blocks),
free: info.Bsize * int64(info.Bavail),
}
devices = append(devices, device)
}
}

if err := scanner.Err(); err != nil {
log.Fatal(err)
}

return devices
}
Loading

0 comments on commit 764a662

Please sign in to comment.