ncdu: remove option ('d' key)

delete files by pressing 'd' in the ncdu listing

GUI Improvements:
Boxes now have a border around them
Boxes can ask questions and allow the selection of options. The
selected option will be given to the UI.boxMenuHandler function.

Fixes #2571
This commit is contained in:
Henning Surmeier 2018-10-14 16:59:27 +02:00 committed by Nick Craig-Wood
parent 9486df0226
commit 04a0da1f92
1 changed files with 209 additions and 22 deletions

View File

@ -13,6 +13,7 @@ import (
"github.com/ncw/rclone/cmd" "github.com/ncw/rclone/cmd"
"github.com/ncw/rclone/cmd/ncdu/scan" "github.com/ncw/rclone/cmd/ncdu/scan"
"github.com/ncw/rclone/fs" "github.com/ncw/rclone/fs"
"github.com/ncw/rclone/fs/operations"
termbox "github.com/nsf/termbox-go" termbox "github.com/nsf/termbox-go"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -42,8 +43,11 @@ Here are the keys - press '?' to toggle the help on and off
` + strings.Join(helpText[1:], "\n ") + ` ` + strings.Join(helpText[1:], "\n ") + `
This an homage to the [ncdu tool](https://dev.yorhel.nl/ncdu) but for This an homage to the [ncdu tool](https://dev.yorhel.nl/ncdu) but for
rclone remotes. It is missing lots of features at the moment, most rclone remotes. It is missing lots of features at the moment
importantly deleting files, but is useful as it stands. but is useful as it stands.
Note that it might take some time to delete big files/folders. The
UI won't respond in the meantime since the deletion is done synchronously.
`, `,
Run: func(command *cobra.Command, args []string) { Run: func(command *cobra.Command, args []string) {
cmd.CheckArgs(1, 1, command, args) cmd.CheckArgs(1, 1, command, args)
@ -63,6 +67,7 @@ var helpText = []string{
" c toggle counts", " c toggle counts",
" g toggle graph", " g toggle graph",
" n,s,C sort by name,size,count", " n,s,C sort by name,size,count",
" d delete file/directory",
" ^L refresh screen", " ^L refresh screen",
" ? to toggle help on and off", " ? to toggle help on and off",
" q/ESC/c-C to quit", " q/ESC/c-C to quit",
@ -70,24 +75,27 @@ var helpText = []string{
// UI contains the state of the user interface // UI contains the state of the user interface
type UI struct { type UI struct {
f fs.Fs // fs being displayed f fs.Fs // fs being displayed
fsName string // human name of Fs fsName string // human name of Fs
root *scan.Dir // root directory root *scan.Dir // root directory
d *scan.Dir // current directory being displayed d *scan.Dir // current directory being displayed
path string // path of current directory path string // path of current directory
showBox bool // whether to show a box showBox bool // whether to show a box
boxText []string // text to show in box boxText []string // text to show in box
entries fs.DirEntries // entries of current directory boxMenu []string // box menu options
sortPerm []int // order to display entries in after sorting boxMenuButton int
invSortPerm []int // inverse order boxMenuHandler func(fs fs.Fs, path string, option int) (string, error)
dirListHeight int // height of listing entries fs.DirEntries // entries of current directory
listing bool // whether listing is in progress sortPerm []int // order to display entries in after sorting
showGraph bool // toggle showing graph invSortPerm []int // inverse order
showCounts bool // toggle showing counts dirListHeight int // height of listing
sortByName int8 // +1 for normal, 0 for off, -1 for reverse listing bool // whether listing is in progress
sortBySize int8 showGraph bool // toggle showing graph
sortByCount int8 showCounts bool // toggle showing counts
dirPosMap map[string]dirPos // store for directory positions sortByName int8 // +1 for normal, 0 for off, -1 for reverse
sortBySize int8
sortByCount int8
dirPosMap map[string]dirPos // store for directory positions
} }
// Where we have got to in the directory listing // Where we have got to in the directory listing
@ -130,6 +138,54 @@ func Linef(x, y, xmax int, fg, bg termbox.Attribute, spacer rune, format string,
Line(x, y, xmax, fg, bg, spacer, s) Line(x, y, xmax, fg, bg, spacer, s)
} }
// LineOptions Print line of selectable options
func LineOptions(x, y, xmax int, fg, bg termbox.Attribute, options []string, selected int) {
defaultBg := bg
defaultFg := fg
// Print left+right whitespace to center the options
xoffset := ((xmax - x) - lineOptionLength(options)) / 2
for j := x; j < x+xoffset; j++ {
termbox.SetCell(j, y, ' ', fg, bg)
}
for j := xmax - xoffset; j < xmax; j++ {
termbox.SetCell(j, y, ' ', fg, bg)
}
x += xoffset
for i, o := range options {
termbox.SetCell(x, y, ' ', fg, bg)
if i == selected {
bg = termbox.ColorBlack
fg = termbox.ColorWhite
}
termbox.SetCell(x+1, y, '<', fg, bg)
x += 2
// print option text
for _, c := range o {
termbox.SetCell(x, y, c, fg, bg)
x++
}
termbox.SetCell(x, y, '>', fg, bg)
bg = defaultBg
fg = defaultFg
termbox.SetCell(x+1, y, ' ', fg, bg)
x += 2
}
}
func lineOptionLength(o []string) int {
count := 0
for _, i := range o {
count += len(i)
}
return count + 4*len(o) // spacer and arrows <entry>
}
// Box the u.boxText onto the screen // Box the u.boxText onto the screen
func (u *UI) Box() { func (u *UI) Box() {
w, h := termbox.Size() w, h := termbox.Size()
@ -147,6 +203,15 @@ func (u *UI) Box() {
x := (w - boxWidth) / 2 x := (w - boxWidth) / 2
y := (h - boxHeight) / 2 y := (h - boxHeight) / 2
xmax := x + boxWidth xmax := x + boxWidth
if len(u.boxMenu) != 0 {
count := lineOptionLength(u.boxMenu)
if x+boxWidth > x+count {
xmax = x + boxWidth
} else {
xmax = x + count
}
}
ymax := y + len(u.boxText)
// draw text // draw text
fg, bg := termbox.ColorRed, termbox.ColorWhite fg, bg := termbox.ColorRed, termbox.ColorWhite
@ -155,7 +220,43 @@ func (u *UI) Box() {
fg = termbox.ColorBlack fg = termbox.ColorBlack
} }
// FIXME draw a box around if len(u.boxMenu) != 0 {
ymax++
LineOptions(x, ymax-1, xmax, fg, bg, u.boxMenu, u.boxMenuButton)
}
// draw top border
for i := y; i < ymax; i++ {
termbox.SetCell(x-1, i, '│', fg, bg)
termbox.SetCell(xmax, i, '│', fg, bg)
}
for j := x; j < xmax; j++ {
termbox.SetCell(j, y-1, '─', fg, bg)
termbox.SetCell(j, ymax, '─', fg, bg)
}
termbox.SetCell(x-1, y-1, '┌', fg, bg)
termbox.SetCell(xmax, y-1, '┐', fg, bg)
termbox.SetCell(x-1, ymax, '└', fg, bg)
termbox.SetCell(xmax, ymax, '┘', fg, bg)
}
func (u *UI) moveBox(to int) {
if len(u.boxMenu) == 0 {
return
}
if to > 0 { // move right
u.boxMenuButton++
} else { // move left
u.boxMenuButton--
}
if u.boxMenuButton >= len(u.boxMenu) {
u.boxMenuButton = len(u.boxMenu) - 1
} else if u.boxMenuButton < 0 {
u.boxMenuButton = 0
}
} }
// find the biggest entry in the current listing // find the biggest entry in the current listing
@ -314,6 +415,57 @@ func (u *UI) move(d int) {
u.dirPosMap[u.path] = dirPos u.dirPosMap[u.path] = dirPos
} }
func (u *UI) removeEntry(pos int) {
u.d.Remove(pos)
u.setCurrentDir(u.d)
}
// delete the entry at the current position
func (u *UI) delete() {
dirPos := u.sortPerm[u.dirPosMap[u.path].entry]
entry := u.entries[dirPos]
file := false
d, _ := u.d.GetDir(dirPos)
if d == nil {
file = true
}
u.boxMenu = []string{"cancel", "confirm"}
if file {
u.boxMenuHandler = func(f fs.Fs, p string, o int) (string, error) {
if o != 1 {
return "Aborted!", nil
}
err := f.Rmdir(entry.String())
if err != nil {
return "", err
}
u.removeEntry(dirPos)
return "Successfully deleted file!", nil
}
u.popupBox([]string{
"Delete this file?",
u.fsName + entry.String()})
} else {
u.boxMenuHandler = func(f fs.Fs, p string, o int) (string, error) {
if o != 1 {
return "Aborted!", nil
}
err := operations.Purge(f, entry.String())
if err != nil {
return "", err
}
u.removeEntry(dirPos)
return "Successfully purged folder!", nil
}
u.popupBox([]string{
"Purge this directory?",
"ALL files in it will be deleted",
u.fsName + entry.String()})
}
}
// Sort by the configured sort method // Sort by the configured sort method
type ncduSort struct { type ncduSort struct {
sortPerm []int sortPerm []int
@ -405,6 +557,25 @@ func (u *UI) enter() {
u.setCurrentDir(d) u.setCurrentDir(d)
} }
// handles a box option that was selected
func (u *UI) handleBoxOption() {
msg, err := u.boxMenuHandler(u.f, u.path, u.boxMenuButton)
// reset
u.boxMenuButton = 0
u.boxMenu = []string{}
u.boxMenuHandler = nil
if err != nil {
u.popupBox([]string{
"error:",
err.Error(),
})
return
}
u.popupBox([]string{"Finished:", msg})
}
// up goes up to the parent directory // up goes up to the parent directory
func (u *UI) up() { func (u *UI) up() {
if u.d == nil { if u.d == nil {
@ -524,8 +695,22 @@ outer:
case termbox.KeyPgup, '=', '+': case termbox.KeyPgup, '=', '+':
u.move(-u.dirListHeight) u.move(-u.dirListHeight)
case termbox.KeyArrowLeft, 'h': case termbox.KeyArrowLeft, 'h':
if u.showBox {
u.moveBox(-1)
break
}
u.up() u.up()
case termbox.KeyArrowRight, 'l', termbox.KeyEnter: case termbox.KeyEnter:
if len(u.boxMenu) > 0 {
u.handleBoxOption()
break
}
u.enter()
case termbox.KeyArrowRight, 'l':
if u.showBox {
u.moveBox(1)
break
}
u.enter() u.enter()
case 'c': case 'c':
u.showCounts = !u.showCounts u.showCounts = !u.showCounts
@ -537,6 +722,8 @@ outer:
u.toggleSort(&u.sortBySize) u.toggleSort(&u.sortBySize)
case 'C': case 'C':
u.toggleSort(&u.sortByCount) u.toggleSort(&u.sortByCount)
case 'd':
u.delete()
case '?': case '?':
u.togglePopupBox(helpText) u.togglePopupBox(helpText)