diff --git a/cmd/gdu/app/app.go b/cmd/gdu/app/app.go index ca8879ea7..8b55571a1 100644 --- a/cmd/gdu/app/app.go +++ b/cmd/gdu/app/app.go @@ -63,6 +63,7 @@ type Flags struct { 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"` @@ -274,6 +275,11 @@ func (a *App) createUI() (UI, error) { ui.SetShowItemCount() }) } + if a.Flags.NoDelete { + opts = append(opts, func(ui *tui.UI) { + ui.SetNoDelete() + }) + } ui = tui.CreateUI( a.TermApp, diff --git a/cmd/gdu/main.go b/cmd/gdu/main.go index 885fd6d57..4b40fad58 100644 --- a/cmd/gdu/main.go +++ b/cmd/gdu/main.go @@ -73,6 +73,7 @@ func init() { flags.BoolVar(&af.UseSIPrefix, "si", false, "Show sizes with decimal SI prefixes (kB, MB, GB) instead of binary prefixes (KiB, MiB, GiB)") flags.BoolVar(&af.NoPrefix, "no-prefix", false, "Show sizes as raw numbers without any prefixes (SI or binary) in non-interactive mode") flags.BoolVar(&af.NoMouse, "no-mouse", false, "Do not use mouse") + flags.BoolVar(&af.NoDelete, "no-delete", false, "Do not allow deletions") flags.BoolVar(&af.WriteConfig, "write-config", false, "Write current configuration to file (default is $HOME/.gdu.yaml)") initConfig() diff --git a/tui/keys_test.go b/tui/keys_test.go index c0b3d4788..66889999d 100644 --- a/tui/keys_test.go +++ b/tui/keys_test.go @@ -400,6 +400,36 @@ func TestDelete(t *testing.T) { assert.NoDirExists(t, "test_dir/nested") } +func TestDeleteWithNoDelete(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + simScreen := testapp.CreateSimScreen() + defer simScreen.Fini() + + app := testapp.CreateMockedApp(true) + ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false, false, false) + ui.done = make(chan struct{}) + err := ui.AnalyzePath("test_dir", nil) + assert.Nil(t, err) + + <-ui.done // wait for analyzer + + for _, f := range ui.app.(*testapp.MockedApp).GetUpdateDraws() { + f() + } + + assert.Equal(t, "test_dir", ui.currentDir.GetName()) + + assert.Equal(t, 1, ui.table.GetRowCount()) + + ui.table.Select(0, 0) + + ui.SetNoDelete() + ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'd', 0)) + + assert.DirExists(t, "test_dir/nested") +} + func TestDeleteMarked(t *testing.T) { fin := testdir.CreateTestDir() defer fin() diff --git a/tui/show.go b/tui/show.go index bff6d5050..ce14e16c5 100644 --- a/tui/show.go +++ b/tui/show.go @@ -250,19 +250,10 @@ func (ui *UI) showHelp() { text.SetTitle(" gdu help ") text.SetScrollable(true) - if ui.UseColors { - text.SetText( - strings.ReplaceAll( - strings.ReplaceAll(helpText, "[::b]", "[red]"), - "[white:black:-]", - "[white]", - ), - ) - } else { - text.SetText(helpText) - } + formattedHelpText := ui.formatHelpTextFor() + text.SetText(formattedHelpText) - maxHeight := strings.Count(helpText, "\n") + 7 + maxHeight := strings.Count(formattedHelpText, "\n") + 7 _, height := ui.screen.Size() if height > maxHeight { height = maxHeight @@ -280,3 +271,24 @@ func (ui *UI) showHelp() { ui.pages.AddPage("help", flex, true, true) ui.app.SetFocus(text) } + +func (ui *UI) formatHelpTextFor() string { + lines := strings.Split(helpText, "\n") + + for i, line := range lines { + if ui.UseColors { + lines[i] = strings.ReplaceAll( + strings.ReplaceAll(line, "[::b]", "[red]"), + "[white:black:-]", + "[white]", + ) + } + + if ui.noDelete && (strings.Contains(line, "Empty file or directory") || + strings.Contains(line, "Delete file or directory")) { + lines[i] += " (disabled)" + } + } + + return strings.Join(lines, "\n") +} diff --git a/tui/tui.go b/tui/tui.go index 4e03aa343..e912696ac 100644 --- a/tui/tui.go +++ b/tui/tui.go @@ -2,6 +2,7 @@ package tui import ( "io" + "time" "golang.org/x/exp/slices" @@ -57,6 +58,7 @@ type UI struct { defaultSortOrder string markedRows map[int]struct{} exportName string + noDelete bool } // Option is optional function customizing the bahaviour of UI @@ -99,6 +101,7 @@ func CreateUI( defaultSortOrder: "desc", markedRows: make(map[int]struct{}), exportName: "export.json", + noDelete: false, } for _, o := range opts { o(ui) @@ -205,6 +208,10 @@ func (ui *UI) SetShowItemCount() { ui.showItemCount = true } +func (ui *UI) SetNoDelete() { + ui.noDelete = true +} + func (ui *UI) resetSorting() { ui.sortBy = ui.defaultSortBy ui.sortOrder = ui.defaultSortOrder @@ -280,6 +287,23 @@ func (ui *UI) deviceItemSelected(row, column int) { } func (ui *UI) confirmDeletion(shouldEmpty bool) { + if ui.noDelete { + previousHeaderText := ui.header.GetText(false) + + // show feedback to user + ui.header.SetText(" Deletion is disabled!") + + go func() { + time.Sleep(2 * time.Second) + ui.app.QueueUpdateDraw(func() { + ui.header.Clear() + ui.header.SetText(previousHeaderText) + }) + }() + + return + } + if len(ui.markedRows) > 0 { ui.confirmDeletionMarked(shouldEmpty) } else { diff --git a/tui/tui_test.go b/tui/tui_test.go index 4237e184a..21765866f 100644 --- a/tui/tui_test.go +++ b/tui/tui_test.go @@ -464,6 +464,18 @@ func TestSetShowItemCount(t *testing.T) { assert.Equal(t, ui.showItemCount, true) } +func TestNoDelete(t *testing.T) { + simScreen := testapp.CreateSimScreen() + defer simScreen.Fini() + + app := testapp.CreateMockedApp(true) + ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true, false, false, false) + + ui.SetNoDelete() + + assert.Equal(t, ui.noDelete, true) +} + // nolint: deadcode,unused // Why: for debugging func printScreen(simScreen tcell.SimulationScreen) { b, _, _ := simScreen.GetContents()