cmd/ncdu: use tcell directly instead of the termbox wrapper

Following up on 36add0af, which switched from termbox
to tcell's termbox wrapper.
This commit is contained in:
eNV25 2022-11-12 18:49:50 +05:30 committed by Nick Craig-Wood
parent 46b080c092
commit a4c65532ea
1 changed files with 128 additions and 104 deletions

View File

@ -13,13 +13,14 @@ import (
"strings" "strings"
"github.com/atotto/clipboard" "github.com/atotto/clipboard"
"github.com/gdamore/tcell/v2/termbox" "github.com/gdamore/tcell/v2"
runewidth "github.com/mattn/go-runewidth" runewidth "github.com/mattn/go-runewidth"
"github.com/rclone/rclone/cmd" "github.com/rclone/rclone/cmd"
"github.com/rclone/rclone/cmd/ncdu/scan" "github.com/rclone/rclone/cmd/ncdu/scan"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/fspath" "github.com/rclone/rclone/fs/fspath"
"github.com/rclone/rclone/fs/operations" "github.com/rclone/rclone/fs/operations"
"github.com/rivo/uniseg"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -114,6 +115,7 @@ func helpText() (tr []string) {
// UI contains the state of the user interface // UI contains the state of the user interface
type UI struct { type UI struct {
s tcell.Screen
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
@ -150,77 +152,91 @@ type dirPos struct {
offset int offset int
} }
// graphemeWidth returns the number of cells in rs.
//
// The original [runewidth.StringWidth] iterates through graphemes
// and uses this same logic. To avoid iterating through graphemes
// repeatedly, we separate that out into its own function.
func graphemeWidth(rs []rune) (wd int) {
// copied/adapted from [runewidth.StringWidth]
for _, r := range rs {
wd = runewidth.RuneWidth(r)
if wd > 0 {
break
}
}
return
}
// Print a string // Print a string
func Print(x, y int, fg, bg termbox.Attribute, msg string) { func (u *UI) Print(x, y int, style tcell.Style, msg string) {
for _, c := range msg { g := uniseg.NewGraphemes(msg)
termbox.SetCell(x, y, c, fg, bg) for g.Next() {
x++ rs := g.Runes()
u.s.SetContent(x, y, rs[0], rs[1:], style)
x += graphemeWidth(rs)
} }
} }
// Printf a string // Printf a string
func Printf(x, y int, fg, bg termbox.Attribute, format string, args ...interface{}) { func (u *UI) Printf(x, y int, style tcell.Style, format string, args ...interface{}) {
s := fmt.Sprintf(format, args...) s := fmt.Sprintf(format, args...)
Print(x, y, fg, bg, s) u.Print(x, y, style, s)
} }
// Line prints a string to given xmax, with given space // Line prints a string to given xmax, with given space
func Line(x, y, xmax int, fg, bg termbox.Attribute, spacer rune, msg string) { func (u *UI) Line(x, y, xmax int, style tcell.Style, spacer rune, msg string) {
for _, c := range msg { g := uniseg.NewGraphemes(msg)
termbox.SetCell(x, y, c, fg, bg) for g.Next() {
x += runewidth.RuneWidth(c) rs := g.Runes()
u.s.SetContent(x, y, rs[0], rs[1:], style)
x += graphemeWidth(rs)
if x >= xmax { if x >= xmax {
return return
} }
} }
for ; x < xmax; x++ { for ; x < xmax; x++ {
termbox.SetCell(x, y, spacer, fg, bg) u.s.SetContent(x, y, spacer, nil, style)
} }
} }
// Linef a string // Linef a string
func Linef(x, y, xmax int, fg, bg termbox.Attribute, spacer rune, format string, args ...interface{}) { func (u *UI) Linef(x, y, xmax int, style tcell.Style, spacer rune, format string, args ...interface{}) {
s := fmt.Sprintf(format, args...) s := fmt.Sprintf(format, args...)
Line(x, y, xmax, fg, bg, spacer, s) u.Line(x, y, xmax, style, spacer, s)
} }
// LineOptions Print line of selectable options // LineOptions Print line of selectable options
func LineOptions(x, y, xmax int, fg, bg termbox.Attribute, options []string, selected int) { func (u *UI) LineOptions(x, y, xmax int, style tcell.Style, options []string, selected int) {
defaultBg := bg for x := x; x < xmax; x++ {
defaultFg := fg u.s.SetContent(x, y, ' ', nil, style) // fill
// 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++ { x += ((xmax - x) - lineOptionLength(options)) / 2 // center
termbox.SetCell(j, y, ' ', fg, bg)
}
x += xoffset
for i, o := range options { for i, o := range options {
termbox.SetCell(x, y, ' ', fg, bg) u.s.SetContent(x, y, ' ', nil, style)
x++
ostyle := style
if i == selected { if i == selected {
bg = termbox.ColorBlack ostyle = tcell.StyleDefault
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) u.s.SetContent(x, y, '<', nil, ostyle)
bg = defaultBg x++
fg = defaultFg
termbox.SetCell(x+1, y, ' ', fg, bg) g := uniseg.NewGraphemes(o)
x += 2 for g.Next() {
rs := g.Runes()
u.s.SetContent(x, y, rs[0], rs[1:], ostyle)
x += graphemeWidth(rs)
}
u.s.SetContent(x, y, '>', nil, ostyle)
x++
u.s.SetContent(x, y, ' ', nil, style)
x++
} }
} }
@ -234,7 +250,7 @@ func lineOptionLength(o []string) int {
// 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 := u.s.Size()
// Find dimensions of text // Find dimensions of text
boxWidth := 10 boxWidth := 10
@ -260,31 +276,31 @@ func (u *UI) Box() {
ymax := y + len(u.boxText) ymax := y + len(u.boxText)
// draw text // draw text
fg, bg := termbox.ColorRed, termbox.ColorWhite style := tcell.StyleDefault.Background(tcell.ColorRed).Reverse(true)
for i, s := range u.boxText { for i, s := range u.boxText {
Line(x, y+i, xmax, fg, bg, ' ', s) u.Line(x, y+i, xmax, style, ' ', s)
fg = termbox.ColorBlack style = tcell.StyleDefault.Reverse(true)
} }
if len(u.boxMenu) != 0 { if len(u.boxMenu) != 0 {
u.LineOptions(x, ymax, xmax, style, u.boxMenu, u.boxMenuButton)
ymax++ ymax++
LineOptions(x, ymax-1, xmax, fg, bg, u.boxMenu, u.boxMenuButton)
} }
// draw top border // draw top border
for i := y; i < ymax; i++ { for i := y; i < ymax; i++ {
termbox.SetCell(x-1, i, '│', fg, bg) u.s.SetContent(x-1, i, tcell.RuneVLine, nil, style)
termbox.SetCell(xmax, i, '│', fg, bg) u.s.SetContent(xmax, i, tcell.RuneVLine, nil, style)
} }
for j := x; j < xmax; j++ { for j := x; j < xmax; j++ {
termbox.SetCell(j, y-1, '─', fg, bg) u.s.SetContent(j, y-1, tcell.RuneHLine, nil, style)
termbox.SetCell(j, ymax, '─', fg, bg) u.s.SetContent(j, ymax, tcell.RuneHLine, nil, style)
} }
termbox.SetCell(x-1, y-1, '┌', fg, bg) u.s.SetContent(x-1, y-1, tcell.RuneULCorner, nil, style)
termbox.SetCell(xmax, y-1, '┐', fg, bg) u.s.SetContent(xmax, y-1, tcell.RuneURCorner, nil, style)
termbox.SetCell(x-1, ymax, '└', fg, bg) u.s.SetContent(x-1, ymax, tcell.RuneLLCorner, nil, style)
termbox.SetCell(xmax, ymax, '┘', fg, bg) u.s.SetContent(xmax, ymax, tcell.RuneLRCorner, nil, style)
} }
func (u *UI) moveBox(to int) { func (u *UI) moveBox(to int) {
@ -336,17 +352,17 @@ func (u *UI) hasEmptyDir() bool {
// Draw the current screen // Draw the current screen
func (u *UI) Draw() error { func (u *UI) Draw() error {
ctx := context.Background() ctx := context.Background()
w, h := termbox.Size() w, h := u.s.Size()
u.dirListHeight = h - 3 u.dirListHeight = h - 3
// Plot // Plot
termbox.Clear(termbox.ColorWhite, termbox.ColorBlack) u.s.Clear()
// Header line // Header line
Linef(0, 0, w, termbox.ColorBlack, termbox.ColorWhite, ' ', "rclone ncdu %s - use the arrow keys to navigate, press ? for help", fs.Version) u.Linef(0, 0, w, tcell.StyleDefault.Reverse(true), ' ', "rclone ncdu %s - use the arrow keys to navigate, press ? for help", fs.Version)
// Directory line // Directory line
Linef(0, 1, w, termbox.ColorWhite, termbox.ColorBlack, '-', "-- %s ", u.path) u.Linef(0, 1, w, tcell.StyleDefault, '-', "-- %s ", u.path)
// graphs // graphs
const ( const (
@ -377,20 +393,18 @@ func (u *UI) Draw() error {
attrs, err = u.d.AttrI(u.sortPerm[n]) attrs, err = u.d.AttrI(u.sortPerm[n])
} }
_, isSelected := u.selectedEntries[entry.String()] _, isSelected := u.selectedEntries[entry.String()]
fg := termbox.ColorWhite style := tcell.StyleDefault
if attrs.EntriesHaveErrors { if attrs.EntriesHaveErrors {
fg = termbox.ColorYellow style = style.Foreground(tcell.ColorYellow)
} }
if err != nil { if err != nil {
fg = termbox.ColorRed style = style.Foreground(tcell.ColorRed)
} }
const colorLightYellow = termbox.ColorYellow + 8
if isSelected { if isSelected {
fg = colorLightYellow style = style.Foreground(tcell.ColorLightYellow)
} }
bg := termbox.ColorBlack
if n == dirPos.entry { if n == dirPos.entry {
fg, bg = bg, fg style = style.Reverse(true)
} }
mark := ' ' mark := ' '
if attrs.IsDir { if attrs.IsDir {
@ -449,31 +463,30 @@ func (u *UI) Draw() error {
} }
extras += "[" + graph[graphBars-bars:2*graphBars-bars] + "] " extras += "[" + graph[graphBars-bars:2*graphBars-bars] + "] "
} }
Linef(0, y, w, fg, bg, ' ', "%c %s %s%c%s%s", fileFlag, operations.SizeStringField(attrs.Size, u.humanReadable, 12), extras, mark, path.Base(entry.Remote()), message) u.Linef(0, y, w, style, ' ', "%c %s %s%c%s%s",
fileFlag, operations.SizeStringField(attrs.Size, u.humanReadable, 12), extras, mark, path.Base(entry.Remote()), message)
y++ y++
} }
} }
// Footer // Footer
if u.d == nil { if u.d == nil {
Line(0, h-1, w, termbox.ColorBlack, termbox.ColorWhite, ' ', "Waiting for root directory...") u.Line(0, h-1, w, tcell.StyleDefault.Reverse(true), ' ', "Waiting for root directory...")
} else { } else {
message := "" message := ""
if u.listing { if u.listing {
message = " [listing in progress]" message = " [listing in progress]"
} }
size, count := u.d.Attr() size, count := u.d.Attr()
Linef(0, h-1, w, termbox.ColorBlack, termbox.ColorWhite, ' ', "Total usage: %s, Objects: %s%s", operations.SizeString(size, u.humanReadable), operations.CountString(count, u.humanReadable), message) u.Linef(0, h-1, w, tcell.StyleDefault.Reverse(true), ' ', "Total usage: %s, Objects: %s%s",
operations.SizeString(size, u.humanReadable), operations.CountString(count, u.humanReadable), message)
} }
// Show the box on top if required // Show the box on top if required
if u.showBox { if u.showBox {
u.Box() u.Box()
} }
err := termbox.Flush() u.s.Show()
if err != nil {
return fmt.Errorf("failed to flush screen: %w", err)
}
return nil return nil
} }
@ -886,37 +899,34 @@ func NewUI(f fs.Fs) *UI {
// Show shows the user interface // Show shows the user interface
func (u *UI) Show() error { func (u *UI) Show() error {
err := termbox.Init() var err error
u.s, err = tcell.NewScreen()
if err != nil { if err != nil {
return fmt.Errorf("termbox init: %w", err) return fmt.Errorf("screen new: %w", err)
} }
defer termbox.Close() err = u.s.Init()
if err != nil {
return fmt.Errorf("screen init: %w", err)
}
defer u.s.Fini()
// scan the disk in the background // scan the disk in the background
u.listing = true u.listing = true
rootChan, errChan, updated := scan.Scan(context.Background(), u.f) rootChan, errChan, updated := scan.Scan(context.Background(), u.f)
// Poll the events into a channel // Poll the events into a channel
events := make(chan termbox.Event) events := make(chan tcell.Event)
doneWithEvent := make(chan bool) go u.s.ChannelEvents(events, nil)
go func() {
for {
events <- termbox.PollEvent()
<-doneWithEvent
}
}()
// Main loop, waiting for events and channels // Main loop, waiting for events and channels
outer: outer:
for { for {
//Reset()
err := u.Draw() err := u.Draw()
if err != nil { if err != nil {
return fmt.Errorf("draw failed: %w", err) return fmt.Errorf("draw failed: %w", err)
} }
var root *scan.Dir
select { select {
case root = <-rootChan: case root := <-rootChan:
u.root = root u.root = root
u.setCurrentDir(root) u.setCurrentDir(root)
case err := <-errChan: case err := <-errChan:
@ -926,39 +936,50 @@ outer:
u.listing = false u.listing = false
case <-updated: case <-updated:
// redraw // redraw
// might want to limit updates per second // TODO: might want to limit updates per second
u.sortCurrentDir() u.sortCurrentDir()
case ev := <-events: case ev := <-events:
doneWithEvent <- true switch ev := ev.(type) {
if ev.Type == termbox.EventKey { case *tcell.EventResize:
switch ev.Key + termbox.Key(ev.Ch) { if u.root != nil {
case termbox.KeyEsc, termbox.KeyCtrlC, 'q': u.sortCurrentDir() // redraw
}
u.s.Sync()
case *tcell.EventKey:
var c rune
if k := ev.Key(); k == tcell.KeyRune {
c = ev.Rune()
} else {
c = key(k)
}
switch c {
case key(tcell.KeyEsc), key(tcell.KeyCtrlC), 'q':
if u.showBox { if u.showBox {
u.showBox = false u.showBox = false
} else { } else {
break outer break outer
} }
case termbox.KeyArrowDown, 'j': case key(tcell.KeyDown), 'j':
u.move(1) u.move(1)
case termbox.KeyArrowUp, 'k': case key(tcell.KeyUp), 'k':
u.move(-1) u.move(-1)
case termbox.KeyPgdn, '-', '_': case key(tcell.KeyPgDn), '-', '_':
u.move(u.dirListHeight) u.move(u.dirListHeight)
case termbox.KeyPgup, '=', '+': case key(tcell.KeyPgUp), '=', '+':
u.move(-u.dirListHeight) u.move(-u.dirListHeight)
case termbox.KeyArrowLeft, 'h': case key(tcell.KeyLeft), 'h':
if u.showBox { if u.showBox {
u.moveBox(-1) u.moveBox(-1)
break break
} }
u.up() u.up()
case termbox.KeyEnter: case key(tcell.KeyEnter):
if len(u.boxMenu) > 0 { if len(u.boxMenu) > 0 {
u.handleBoxOption() u.handleBoxOption()
break break
} }
u.enter() u.enter()
case termbox.KeyArrowRight, 'l': case key(tcell.KeyRight), 'l':
if u.showBox { if u.showBox {
u.moveBox(1) u.moveBox(1)
break break
@ -1001,11 +1022,8 @@ outer:
// Refresh the screen. Not obvious what key to map // Refresh the screen. Not obvious what key to map
// this onto, but ^L is a common choice. // this onto, but ^L is a common choice.
case termbox.KeyCtrlL: case key(tcell.KeyCtrlL):
err := termbox.Sync() u.s.Sync()
if err != nil {
fs.Errorf(nil, "termbox sync returned error: %v", err)
}
} }
} }
} }
@ -1013,3 +1031,9 @@ outer:
} }
return nil return nil
} }
// key returns a rune representing the key k. It is larger than the maximum Unicode code-point.
func key(k tcell.Key) rune {
// This is the maximum possible Unicode code point. Anything greater fails to compile as a Go quoted rune literal.
return '\U0010FFFF' + rune(k)
}