summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Milde <daniel@milde.cz>2024-03-30 21:10:12 +0100
committerDaniel Milde <daniel@milde.cz>2024-03-30 23:17:37 +0100
commit755b20d5f0b830f5ea3823112dd91f429598f687 (patch)
tree3ede6ca8f63488f82e5827b58a08c72a4b779929
parent7bfeb985f4d687a05781d4f870a128990b0c6638 (diff)
feat: delete/empty items in background
-rw-r--r--.tool-versions2
-rw-r--r--cmd/gdu/app/app.go6
-rw-r--r--cmd/gdu/app/app_test.go15
-rw-r--r--internal/testanalyze/analyze.go12
-rw-r--r--tui/actions.go5
-rw-r--r--tui/background.go93
-rw-r--r--tui/marked.go11
-rw-r--r--tui/status.go84
-rw-r--r--tui/tui.go33
-rw-r--r--tui/tui_test.go161
10 files changed, 415 insertions, 7 deletions
diff --git a/.tool-versions b/.tool-versions
index 3eaf48e..6bc74c5 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1 +1 @@
-golang 1.21.5
+golang 1.22.1
diff --git a/cmd/gdu/app/app.go b/cmd/gdu/app/app.go
index 8b55571..4ac6696 100644
--- a/cmd/gdu/app/app.go
+++ b/cmd/gdu/app/app.go
@@ -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"`
}
@@ -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,
diff --git a/cmd/gdu/app/app_test.go b/cmd/gdu/app/app_test.go
index 10240a9..7fa34e0 100644
--- a/cmd/gdu/app/app_test.go
+++ b/cmd/gdu/app/app_test.go
@@ -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()
diff --git a/internal/testanalyze/analyze.go b/internal/testanalyze/analyze.go
index 89ca4cf..9911a98 100644
--- a/internal/testanalyze/analyze.go
+++ b/internal/testanalyze/analyze.go
@@ -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")
+}
diff --git a/tui/actions.go b/tui/actions.go
index 613cfe8..5ca16ee 100644
--- a/tui/actions.go
+++ b/tui/actions.go
@@ -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 "
diff --git a/tui/background.go b/tui/background.go
new file mode 100644
index 0000000..813c3b9
--- /dev/null
+++ b/tui/background.go
@@ -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--
+}
diff --git a/tui/marked.go b/tui/marked.go
index f41ebc6..fb5ac91 100644
--- a/tui/marked.go
+++ b/tui/marked.go
@@ -31,9 +31,6 @@ 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 {
@@ -41,6 +38,14 @@ func (ui *UI) deleteMarked(shouldEmpty bool) {
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
diff --git a/tui/status.go b/tui/status.go
new file mode 100644
index 0000000..ff4c062
--- /dev/null
+++ b/tui/status.go
@@ -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()
+ })
+}
diff --git a/tui/tui.go b/tui/tui.go
index e912696..a0facec 100644
--- a/tui/tui.go
+++ b/tui/tui.go
@@ -2,6 +2,8 @@ package tui
import (
"io"
+ "runtime"
+ "sync"
"time"
"golang.org/x/exp/slices"
@@ -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
@@ -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
@@ -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)
@@ -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)
@@ -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
diff --git a/tui/tui_test.go b/tui/tui_test.go
index 2176586..ecd0c8f 100644
--- a/tui/tui_test.go
+++ b/tui/tui_test.go
@@ -4,7 +4,9 @@ import (
"bytes"
"errors"
"fmt"
+ "os"
"testing"
+ "time"
log "github.com/sirupsen/logrus"
@@ -328,6 +330,108 @@ func TestDeleteSelected(t *testing.T) {
assert.NoDirExists(t, "test_dir/nested")
}
+func TestDeleteSelectedInBackground(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ ui := getAnalyzedPathMockedApp(t, true, true, false)
+ ui.remover = testanalyze.RemoveItemFromDirWithSleep
+ ui.done = make(chan struct{})
+ ui.SetDeleteInBackground()
+
+ 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 TestDeleteSelectedInBackgroundBW(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ ui := getAnalyzedPathMockedApp(t, false, true, false)
+ ui.done = make(chan struct{})
+ ui.SetDeleteInBackground()
+
+ 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 TestEmptyDirInBackground(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ ui := getAnalyzedPathMockedApp(t, true, true, false)
+ ui.done = make(chan struct{})
+ ui.SetDeleteInBackground()
+
+ assert.Equal(t, 1, ui.table.GetRowCount())
+
+ ui.table.Select(0, 0)
+
+ ui.deleteSelected(true)
+
+ <-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 TestEmptyFileInBackground(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ ui := getAnalyzedPathMockedApp(t, true, true, false)
+ ui.done = make(chan struct{})
+ ui.SetDeleteInBackground()
+
+ assert.Equal(t, 1, ui.table.GetRowCount())
+
+ ui.fileItemSelected(0, 0) // nested
+ ui.table.Select(2, 0)
+
+ ui.deleteSelected(true)
+
+ <-ui.done
+
+ for _, f := range ui.app.(*testapp.MockedApp).GetUpdateDraws() {
+ f()
+ }
+
+ assert.DirExists(t, "test_dir/nested")
+ assert.FileExists(t, "test_dir/nested/file2")
+
+ f, err := os.Open("test_dir/nested/file2")
+ assert.Nil(t, err)
+ info, err := f.Stat()
+ assert.Nil(t, err)
+ assert.Equal(t, int64(0), info.Size())
+}
+
func TestDeleteSelectedWithErr(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
@@ -351,6 +455,38 @@ func TestDeleteSelectedWithErr(t *testing.T) {
assert.DirExists(t, "test_dir/nested")
}
+func TestDeleteSelectedInBackgroundWithErr(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ ui := getAnalyzedPathMockedApp(t, false, true, false)
+ ui.SetDeleteInBackground()
+ ui.remover = testanalyze.RemoveItemFromDirWithSleepAndErr
+
+ assert.Equal(t, 1, ui.table.GetRowCount())
+
+ ui.table.Select(0, 0)
+
+ ui.delete(false)
+
+ <-ui.done
+
+ // change the status
+ for _, f := range ui.app.(*testapp.MockedApp).GetUpdateDraws() {
+ f()
+ }
+
+ // wait for status to be removed
+ time.Sleep(500 * time.Millisecond)
+
+ 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()
@@ -375,6 +511,31 @@ func TestDeleteMarkedWithErr(t *testing.T) {
assert.DirExists(t, "test_dir/nested")
}
+func TestDeleteMarkedInBackgroundWithErr(t *testing.T) {
+ fin := testdir.CreateTestDir()
+ defer fin()
+
+ ui := getAnalyzedPathMockedApp(t, false, true, false)
+ ui.SetDeleteInBackground()
+ 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()