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:
parent
9486df0226
commit
04a0da1f92
1 changed files with 209 additions and 22 deletions
231
cmd/ncdu/ncdu.go
231
cmd/ncdu/ncdu.go
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue