Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Flags:
--min-age string Include files with mtime at least DURATION old (e.g., 30d, 1w)
--mouse Use mouse
-c, --no-color Do not use colorized output
--no-confirm-quit Do not ask for confirmation before quitting after a long scan
-x, --no-cross Do not cross filesystem boundaries
--no-delete Do not allow deletions
-H, --no-hidden Ignore hidden directories (beginning with dot)
Expand Down
6 changes: 6 additions & 0 deletions cmd/gdu/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ type Flags struct {
NoDelete bool `yaml:"no-delete"`
NoViewFile bool `yaml:"no-view-file"`
NoSpawnShell bool `yaml:"no-spawn-shell"`
NoConfirmQuit bool `yaml:"no-confirm-quit"`
FollowSymlinks bool `yaml:"follow-symlinks"`
Profiling bool `yaml:"profiling"`
ReadFromStorage bool `yaml:"read-from-storage"`
Expand Down Expand Up @@ -546,6 +547,11 @@ func (a *App) getOptions() []tui.Option {
ui.SetNoSpawnShell()
})
}
if a.Flags.NoConfirmQuit {
opts = append(opts, func(ui *tui.UI) {
ui.SetConfirmQuit(false)
})
}
if a.Flags.DeleteInBackground {
opts = append(opts, func(ui *tui.UI) {
ui.SetDeleteInBackground()
Expand Down
1 change: 1 addition & 0 deletions cmd/gdu/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ func init() {
flags.BoolVar(&af.NoDelete, "no-delete", false, "Do not allow deletions")
flags.BoolVar(&af.NoViewFile, "no-view-file", false, "Do not allow viewing file contents")
flags.BoolVar(&af.NoSpawnShell, "no-spawn-shell", false, "Do not allow spawning shell")
flags.BoolVar(&af.NoConfirmQuit, "no-confirm-quit", false, "Do not ask for confirmation before quitting after a long scan")
flags.BoolVar(&af.WriteConfig, "write-config", false, "Write current configuration to file (default is $HOME/.gdu.yaml)")
flags.StringVar(
&af.Since, "since", "",
Expand Down
4 changes: 4 additions & 0 deletions configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ Do not allow deletions

Do not allow viewing file contents

#### `no-confirm-quit`

Do not ask for confirmation before quitting after a long scan. By default, pressing `q`/`Q` after a scan that took more than a few seconds shows a confirmation dialog so that results are not lost by an accidental key press.

#### `follow-symlinks`

Follow symlinks for files, i.e. show the size of the file to which symlink points to (symlinks to directories are not followed)
Expand Down
2 changes: 2 additions & 0 deletions gdu.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ non-interactive mode

**\--no-spawn-shell**\[=false\] Do not allow spawning shell

**\--no-confirm-quit**\[=false\] Do not ask for confirmation before quitting after a long scan

**\--no-delete**\[=false\] Do not allow deletions

**\--no-view-file**\[=false\] Do not allow viewing file contents
Expand Down
7 changes: 7 additions & 0 deletions tui/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ func (ui *UI) AnalyzePath(path string, parentDir fs.Item) error {
doneChan := analyzer.GetDone()
go ui.updateProgress(analyzer, doneChan)

ui.scanStart = time.Now()
ui.scanning = true
go func() {
defer debug.FreeOSMemory()
currentDir := ui.Analyzer.AnalyzeDir(path, ui.CreateIgnoreFunc(), ui.CreateFileTypeFilter())
Expand All @@ -103,6 +105,11 @@ func (ui *UI) AnalyzePath(path string, parentDir fs.Item) error {
}

ui.app.QueueUpdateDraw(func() {
ui.scanning = false
// remember the longest scan so we can guard against accidental quits
if d := time.Since(ui.scanStart); d > ui.scanDuration {
ui.scanDuration = d
}
ui.currentDir = currentDir
ui.showDir()
ui.pages.RemovePage("progress")
Expand Down
84 changes: 79 additions & 5 deletions tui/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,23 +150,97 @@ func (ui *UI) handleCtrlZ(key *tcell.EventKey) *tcell.EventKey {
return key
}

// confirmQuitMinScanDuration is the scan time above which quitting asks for
// confirmation, so a long scan is not lost by an accidental key press.
const confirmQuitMinScanDuration = 3 * time.Second

func (ui *UI) handleQuit(key *tcell.EventKey) *tcell.EventKey {
clearTerminalProgress()

// do not re-trigger quitting while a confirmation dialog is open
if ui.pages.HasPage("confirm") {
return key
}

switch key.Rune() {
case 'Q':
ui.app.Stop()
ui.printMarkedPaths()
fmt.Fprintf(ui.output, "%s\n", ui.currentDirPath)
ui.quit(true)
return nil
case 'q':
ui.app.Stop()
ui.printMarkedPaths()
ui.quit(false)
return nil
}
return key
}

// quit asks for confirmation when there are scan results worth protecting,
// otherwise it quits immediately.
func (ui *UI) quit(printCurrentDirPath bool) {
if ui.shouldConfirmQuit() {
ui.confirmQuitDialog(printCurrentDirPath)
return
}
ui.doQuit(printCurrentDirPath)
}

// shouldConfirmQuit returns true when quitting would discard the work of a scan
// that took a noticeable amount of time, whether it is still running or already
// finished.
func (ui *UI) shouldConfirmQuit() bool {
if !ui.confirmQuit {
return false
}
if ui.scanning {
return time.Since(ui.scanStart) >= confirmQuitMinScanDuration
}
return ui.currentDir != nil && ui.scanDuration >= confirmQuitMinScanDuration
}

func (ui *UI) doQuit(printCurrentDirPath bool) {
ui.app.Stop()
ui.printMarkedPaths()
if printCurrentDirPath {
fmt.Fprintf(ui.output, "%s\n", ui.currentDirPath)
}
}

func (ui *UI) confirmQuitDialog(printCurrentDirPath bool) {
var text string
if ui.scanning {
text = "A scan has been running for " +
time.Since(ui.scanStart).Round(time.Second).String() + ".\n\n" +
"Do you really want to quit gdu and abandon it?"
} else {
text = "Do you really want to quit gdu?\n\n" +
"This scan took " + ui.scanDuration.Round(time.Second).String() +
" and the results are not saved.\n" +
"Choose \"no\" and press E to export them first."
}
modal := tview.NewModal().
SetText(text).
AddButtons([]string{"no", "yes", "don't ask me again"}).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
ui.pages.RemovePage("confirm")
ui.app.SetFocus(ui.table)
switch buttonIndex {
case 2:
ui.confirmQuit = false
fallthrough
case 1:
ui.doQuit(printCurrentDirPath)
}
})

if !ui.UseColors {
modal.SetBackgroundColor(tcell.ColorGray)
} else {
modal.SetBackgroundColor(tcell.ColorBlack)
}
modal.SetBorderColor(tcell.ColorDefault)

ui.pages.AddPage("confirm", modal, true, true)
}

func (ui *UI) handleHelp(key *tcell.EventKey) *tcell.EventKey {
if key.Rune() == '?' {
if ui.pages.HasPage("help") {
Expand Down
110 changes: 110 additions & 0 deletions tui/keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,116 @@ func TestStopWithPrintingPath(t *testing.T) {
assert.Equal(t, "test_dir\n", buff.String())
}

// analyzedUIForQuit returns a UI with a finished scan so quit behaviour can be tested.
func analyzedUIForQuit(t *testing.T, buff *bytes.Buffer) *UI {
t.Helper()
simScreen := testapp.CreateSimScreen()
t.Cleanup(simScreen.Fini)

app := testapp.CreateMockedApp(false)
ui := CreateUI(app, simScreen, buff, true, true, false, false)

ui.done = make(chan struct{})
assert.Nil(t, ui.AnalyzePath("test_dir", nil))
<-ui.done // wait for analyzer
for _, f := range ui.app.(*testapp.MockedApp).GetUpdateDraws() {
f()
}
return ui
}

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

ui := analyzedUIForQuit(t, &bytes.Buffer{})
ui.scanDuration = 10 * time.Second // simulate a long scan

key := ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'q', 0))
assert.Nil(t, key)
assert.True(t, ui.pages.HasPage("confirm"))
}

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

ui := analyzedUIForQuit(t, &bytes.Buffer{})
ui.scanDuration = time.Second // below the confirmation threshold

key := ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'q', 0))
assert.Nil(t, key)
assert.False(t, ui.pages.HasPage("confirm"))
}

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

ui := analyzedUIForQuit(t, &bytes.Buffer{})
ui.SetConfirmQuit(false)
ui.scanDuration = 10 * time.Second

key := ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'q', 0))
assert.Nil(t, key)
assert.False(t, ui.pages.HasPage("confirm"))
}

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

ui := analyzedUIForQuit(t, &bytes.Buffer{})
ui.scanDuration = 10 * time.Second

assert.Nil(t, ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'q', 0)))
assert.True(t, ui.pages.HasPage("confirm"))

// pressing q again must not stack dialogs nor quit; the key passes through
key := ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'q', 0))
assert.NotNil(t, key)
assert.True(t, ui.pages.HasPage("confirm"))
}

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

ui := analyzedUIForQuit(t, &bytes.Buffer{})
// simulate a scan that is still running and started long ago
ui.scanning = true
ui.scanStart = time.Now().Add(-10 * time.Second)

key := ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'q', 0))
assert.Nil(t, key)
assert.True(t, ui.pages.HasPage("confirm"))
}

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

ui := analyzedUIForQuit(t, &bytes.Buffer{})
// a scan that just started should not block quitting
ui.scanning = true
ui.scanStart = time.Now()

key := ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'q', 0))
assert.Nil(t, key)
assert.False(t, ui.pages.HasPage("confirm"))
}

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

buff := &bytes.Buffer{}
ui := analyzedUIForQuit(t, buff)

ui.doQuit(true)
assert.Equal(t, "test_dir\n", buff.String())
}

func TestSpawnShell(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
Expand Down
2 changes: 1 addition & 1 deletion tui/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ var (
[::b]c [white:black:-]Show/hide file count
[::b]m [white:black:-]Show/hide latest mtime
[::b]b [white:black:-]Spawn shell in current directory
[::b]q [white:black:-]Quit gdu
[::b]q [white:black:-]Quit gdu (asks to confirm after a long scan)
[::b]Q [white:black:-]Quit gdu and print current directory path

Item under cursor:
Expand Down
11 changes: 11 additions & 0 deletions tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ type UI struct {
browseParentDirs bool
showDiskProgressBar bool
currentDeviceSize int64
confirmQuit bool
scanning bool
scanStart time.Time
scanDuration time.Duration
}

type deleteQueueItem struct {
Expand Down Expand Up @@ -137,6 +141,7 @@ func CreateUI(
screen: screen,
output: output,
askBeforeDelete: true,
confirmQuit: true,
showItemCount: false,
remover: remove.ItemFromDir,
emptier: remove.EmptyFileFromDir,
Expand Down Expand Up @@ -343,6 +348,12 @@ func (ui *UI) StartUILoop() error {
return ui.app.Run()
}

// SetConfirmQuit sets whether gdu asks for confirmation before quitting
// after a scan that took a noticeable amount of time
func (ui *UI) SetConfirmQuit(value bool) {
ui.confirmQuit = value
}

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