Skip to content

Commit

Permalink
feat: delete/empty items in background
Browse files Browse the repository at this point in the history
  • Loading branch information
dundee committed Mar 30, 2024
1 parent 7bfeb98 commit 755b20d
Show file tree
Hide file tree
Showing 10 changed files with 415 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
golang 1.21.5
golang 1.22.1
6 changes: 6 additions & 0 deletions cmd/gdu/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type Flags struct {
NoPrefix bool `yaml:"no-prefix"`
WriteConfig bool `yaml:"-"`
ChangeCwd bool `yaml:"change-cwd"`
DeleteInBackground bool `yaml:"delete-in-background"`
Style Style `yaml:"style"`
Sorting Sorting `yaml:"sorting"`
}
Expand Down Expand Up @@ -280,6 +281,11 @@ func (a *App) createUI() (UI, error) {
ui.SetNoDelete()
})
}
if a.Flags.DeleteInBackground {
opts = append(opts, func(ui *tui.UI) {
ui.SetDeleteInBackground()
})
}

ui = tui.CreateUI(
a.TermApp,
Expand Down
15 changes: 15 additions & 0 deletions cmd/gdu/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,21 @@ func TestAnalyzePathWithGui(t *testing.T) {
assert.Nil(t, err)
}

func TestAnalyzePathWithGuiBackgroundDeletion(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()

out, err := runApp(
&Flags{LogFile: "/dev/null", DeleteInBackground: true},
[]string{"test_dir"},
true,
testdev.DevicesInfoGetterMock{},
)

assert.Empty(t, out)
assert.Nil(t, err)
}

func TestAnalyzePathWithDefaultSorting(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
Expand Down
12 changes: 12 additions & 0 deletions internal/testanalyze/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,15 @@ func (a *MockedAnalyzer) SetFollowSymlinks(v bool) {}
func RemoveItemFromDirWithErr(dir fs.Item, file fs.Item) error {
return errors.New("Failed")
}

// RemoveItemFromDirWithSleep returns error
func RemoveItemFromDirWithSleep(dir fs.Item, file fs.Item) error {
time.Sleep(time.Millisecond * 600)
return analyze.RemoveItemFromDir(dir, file)
}

// RemoveItemFromDirWithSleepAndErr returns error
func RemoveItemFromDirWithSleepAndErr(dir fs.Item, file fs.Item) error {
time.Sleep(time.Millisecond * 600)
return errors.New("Failed")
}
5 changes: 5 additions & 0 deletions tui/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ func (ui *UI) deleteSelected(shouldEmpty bool) {
row, column := ui.table.GetSelection()
selectedItem := ui.table.GetCell(row, column).GetReference().(fs.Item)

if ui.deleteInBackground {
ui.queueForDeletion([]fs.Item{selectedItem}, shouldEmpty)
return
}

var action, acting string
if shouldEmpty {
action = "empty "
Expand Down
93 changes: 93 additions & 0 deletions tui/background.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package tui

import (
"github.com/dundee/gdu/v5/pkg/analyze"
"github.com/dundee/gdu/v5/pkg/fs"
"github.com/rivo/tview"
)

func (ui *UI) queueForDeletion(items []fs.Item, shouldEmpty bool) {
go func() {
for _, item := range items {
ui.deleteQueue <- deleteQueueItem{item: item, shouldEmpty: shouldEmpty}
}
}()

ui.markedRows = make(map[int]struct{})
}

func (ui *UI) deleteWorker() {
for item := range ui.deleteQueue {
ui.deleteItem(item.item, item.shouldEmpty)
}
}

func (ui *UI) deleteItem(item fs.Item, shouldEmpty bool) {
ui.increaseActiveWorkers()
defer ui.decreaseActiveWorkers()

var action, acting string
if shouldEmpty {
action = "empty "
} else {
action = "delete "
}

var deleteFun func(fs.Item, fs.Item) error
if shouldEmpty && !item.IsDir() {
deleteFun = ui.emptier
} else {
deleteFun = ui.remover
}

var parentDir fs.Item
var deleteItems []fs.Item
if shouldEmpty && item.IsDir() {
parentDir = item.(*analyze.Dir)
for _, file := range item.GetFiles() {
deleteItems = append(deleteItems, file)
}
} else {
parentDir = item.GetParent()
deleteItems = append(deleteItems, item)
}

for _, toDelete := range deleteItems {
if err := deleteFun(parentDir, toDelete); err != nil {
msg := "Can't " + action + tview.Escape(toDelete.GetName())
ui.app.QueueUpdateDraw(func() {
ui.pages.RemovePage(acting)
ui.showErr(msg, err)
})
if ui.done != nil {
ui.done <- struct{}{}
}
return
}
}

if item.GetParent() == ui.currentDir {
ui.app.QueueUpdateDraw(func() {
row, _ := ui.table.GetSelection()
x, y := ui.table.GetOffset()
ui.showDir()
ui.table.Select(min(row, ui.table.GetRowCount()-1), 0)
ui.table.SetOffset(min(x, ui.table.GetRowCount()-1), y)
})
}
if ui.done != nil {
ui.done <- struct{}{}
}
}

func (ui *UI) increaseActiveWorkers() {
ui.workersMut.Lock()
defer ui.workersMut.Unlock()
ui.activeWorkers++
}

func (ui *UI) decreaseActiveWorkers() {
ui.workersMut.Lock()
defer ui.workersMut.Unlock()
ui.activeWorkers--
}
11 changes: 8 additions & 3 deletions tui/marked.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,21 @@ func (ui *UI) deleteMarked(shouldEmpty bool) {
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)
}

if ui.deleteInBackground {
ui.queueForDeletion(markedItems, shouldEmpty)
return
}

modal := tview.NewModal()
ui.pages.AddPage(acting, modal, true, true)

currentRow, _ := ui.table.GetSelection()

var deleteFun func(fs.Item, fs.Item) error
Expand Down
84 changes: 84 additions & 0 deletions tui/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package tui

import (
"fmt"
"time"

"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)

func (ui *UI) toggleStatusBar(show bool) {
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.grid.Clear()

ui.statusMut.Lock()
defer ui.statusMut.Unlock()

if show {
ui.status = tview.NewTextView().SetDynamicColors(true)
ui.status.SetTextColor(textColor)
ui.status.SetBackgroundColor(textBgColor)

ui.grid.SetRows(1, 1, 0, 1, 1)
ui.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.status, 3, 0, 1, 1, 0, 0, false).
AddItem(ui.footer, 4, 0, 1, 1, 0, 0, false)
return
}
ui.status = nil
ui.grid.SetRows(1, 1, 0, 1)
ui.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)
}

func (ui *UI) updateStatusWorker() {
for {
ui.updateStatus()
time.Sleep(500 * time.Millisecond)
}
}

func (ui *UI) updateStatus() {
ui.workersMut.Lock()
cnt := ui.activeWorkers
ui.workersMut.Unlock()

ui.statusMut.RLock()
status := ui.status
ui.statusMut.RUnlock()

if cnt == 0 && status == nil {
return
}

if cnt > 0 && status == nil {
ui.app.QueueUpdateDraw(func() {
ui.toggleStatusBar(true)
})
} else if cnt == 0 {
ui.app.QueueUpdateDraw(func() {
ui.toggleStatusBar(false)
})
return
}

ui.app.QueueUpdateDraw(func() {
msg := fmt.Sprintf(" Active background deletions: %d", cnt)
ui.statusMut.RLock()
ui.status.SetText(msg)
ui.statusMut.RUnlock()
})
}
33 changes: 30 additions & 3 deletions tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package tui

import (
"io"
"runtime"
"sync"
"time"

"golang.org/x/exp/slices"
Expand All @@ -22,12 +24,14 @@ type UI struct {
app common.TermApplication
screen tcell.Screen
output io.Writer
grid *tview.Grid
header *tview.TextView
footer *tview.Flex
footerLabel *tview.TextView
currentDirLabel *tview.TextView
pages *tview.Pages
progress *tview.TextView
status *tview.TextView
help *tview.Flex
table *tview.Table
filteringInput *tview.InputField
Expand Down Expand Up @@ -59,6 +63,16 @@ type UI struct {
markedRows map[int]struct{}
exportName string
noDelete bool
deleteInBackground bool
deleteQueue chan deleteQueueItem
activeWorkers int
workersMut sync.Mutex
statusMut sync.RWMutex
}

type deleteQueueItem struct {
item fs.Item
shouldEmpty bool
}

// Option is optional function customizing the bahaviour of UI
Expand Down Expand Up @@ -102,6 +116,7 @@ func CreateUI(
markedRows: make(map[int]struct{}),
exportName: "export.json",
noDelete: false,
deleteQueue: make(chan deleteQueueItem, 1000),
}
for _, o := range opts {
o(ui)
Expand Down Expand Up @@ -157,14 +172,14 @@ func CreateUI(
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).
ui.grid = tview.NewGrid().SetRows(1, 1, 0, 1).SetColumns(0)
ui.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)
AddPage("background", ui.grid, true, true)
ui.pages.SetBackgroundColor(tcell.ColorDefault)

ui.app.SetRoot(ui.pages, true)
Expand Down Expand Up @@ -204,14 +219,26 @@ func (ui *UI) StartUILoop() error {
return ui.app.Run()
}

// SetShowItemCount sets the flag to show number of items in directory
func (ui *UI) SetShowItemCount() {
ui.showItemCount = true
}

// SetNoDelete disables all write operations
func (ui *UI) SetNoDelete() {
ui.noDelete = true
}

// SetDeleteInBackground sets the flag to delete files in background
func (ui *UI) SetDeleteInBackground() {
ui.deleteInBackground = true

for i := 0; i < 3*runtime.GOMAXPROCS(0); i++ {
go ui.deleteWorker()
}
go ui.updateStatusWorker()
}

func (ui *UI) resetSorting() {
ui.sortBy = ui.defaultSortBy
ui.sortOrder = ui.defaultSortOrder
Expand Down
Loading

0 comments on commit 755b20d

Please sign in to comment.