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"
"github.com/atotto/clipboard"
"github.com/gdamore/tcell/v2/termbox"
"github.com/gdamore/tcell/v2"
runewidth "github.com/mattn/go-runewidth"
"github.com/rclone/rclone/cmd"
"github.com/rclone/rclone/cmd/ncdu/scan"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/fspath"
"github.com/rclone/rclone/fs/operations"
"github.com/rivo/uniseg"
"github.com/spf13/cobra"
)
@ -114,6 +115,7 @@ func helpText() (tr []string) {
// UI contains the state of the user interface
type UI struct {
s tcell.Screen
f fs.Fs // fs being displayed
fsName string // human name of Fs
root *scan.Dir // root directory
@ -150,77 +152,91 @@ type dirPos struct {
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
func Print(x, y int, fg, bg termbox.Attribute, msg string) {
for _, c := range msg {
termbox.SetCell(x, y, c, fg, bg)
x++
func (u *UI) Print(x, y int, style tcell.Style, msg string) {
g := uniseg.NewGraphemes(msg)
for g.Next() {
rs := g.Runes()
u.s.SetContent(x, y, rs[0], rs[1:], style)
x += graphemeWidth(rs)
}
}
// 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...)
Print(x, y, fg, bg, s)
u.Print(x, y, style, s)
}
// Line prints a string to given xmax, with given space
func Line(x, y, xmax int, fg, bg termbox.Attribute, spacer rune, msg string) {
for _, c := range msg {
termbox.SetCell(x, y, c, fg, bg)
x += runewidth.RuneWidth(c)
func (u *UI) Line(x, y, xmax int, style tcell.Style, spacer rune, msg string) {
g := uniseg.NewGraphemes(msg)
for g.Next() {
rs := g.Runes()
u.s.SetContent(x, y, rs[0], rs[1:], style)
x += graphemeWidth(rs)
if x >= xmax {
return
}
}
for ; x < xmax; x++ {
termbox.SetCell(x, y, spacer, fg, bg)
u.s.SetContent(x, y, spacer, nil, style)
}
}
// 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...)
Line(x, y, xmax, fg, bg, spacer, s)
u.Line(x, y, xmax, style, 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)
func (u *UI) LineOptions(x, y, xmax int, style tcell.Style, options []string, selected int) {
for x := x; x < xmax; x++ {
u.s.SetContent(x, y, ' ', nil, style) // fill
}
for j := xmax - xoffset; j < xmax; j++ {
termbox.SetCell(j, y, ' ', fg, bg)
}
x += xoffset
x += ((xmax - x) - lineOptionLength(options)) / 2 // center
for i, o := range options {
termbox.SetCell(x, y, ' ', fg, bg)
u.s.SetContent(x, y, ' ', nil, style)
x++
ostyle := style
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++
ostyle = tcell.StyleDefault
}
termbox.SetCell(x, y, '>', fg, bg)
bg = defaultBg
fg = defaultFg
u.s.SetContent(x, y, '<', nil, ostyle)
x++
termbox.SetCell(x+1, y, ' ', fg, bg)
x += 2
g := uniseg.NewGraphemes(o)
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
func (u *UI) Box() {
w, h := termbox.Size()
w, h := u.s.Size()
// Find dimensions of text
boxWidth := 10
@ -260,31 +276,31 @@ func (u *UI) Box() {
ymax := y + len(u.boxText)
// draw text
fg, bg := termbox.ColorRed, termbox.ColorWhite
style := tcell.StyleDefault.Background(tcell.ColorRed).Reverse(true)
for i, s := range u.boxText {
Line(x, y+i, xmax, fg, bg, ' ', s)
fg = termbox.ColorBlack
u.Line(x, y+i, xmax, style, ' ', s)
style = tcell.StyleDefault.Reverse(true)
}
if len(u.boxMenu) != 0 {
u.LineOptions(x, ymax, xmax, style, u.boxMenu, u.boxMenuButton)
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)
u.s.SetContent(x-1, i, tcell.RuneVLine, nil, style)
u.s.SetContent(xmax, i, tcell.RuneVLine, nil, style)
}
for j := x; j < xmax; j++ {
termbox.SetCell(j, y-1, '─', fg, bg)
termbox.SetCell(j, ymax, '─', fg, bg)
u.s.SetContent(j, y-1, tcell.RuneHLine, nil, style)
u.s.SetContent(j, ymax, tcell.RuneHLine, nil, style)
}
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)
u.s.SetContent(x-1, y-1, tcell.RuneULCorner, nil, style)
u.s.SetContent(xmax, y-1, tcell.RuneURCorner, nil, style)
u.s.SetContent(x-1, ymax, tcell.RuneLLCorner, nil, style)
u.s.SetContent(xmax, ymax, tcell.RuneLRCorner, nil, style)
}
func (u *UI) moveBox(to int) {
@ -336,17 +352,17 @@ func (u *UI) hasEmptyDir() bool {
// Draw the current screen
func (u *UI) Draw() error {
ctx := context.Background()
w, h := termbox.Size()
w, h := u.s.Size()
u.dirListHeight = h - 3
// Plot
termbox.Clear(termbox.ColorWhite, termbox.ColorBlack)
u.s.Clear()
// 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
Linef(0, 1, w, termbox.ColorWhite, termbox.ColorBlack, '-', "-- %s ", u.path)
u.Linef(0, 1, w, tcell.StyleDefault, '-', "-- %s ", u.path)
// graphs
const (
@ -377,20 +393,18 @@ func (u *UI) Draw() error {
attrs, err = u.d.AttrI(u.sortPerm[n])
}
_, isSelected := u.selectedEntries[entry.String()]
fg := termbox.ColorWhite
style := tcell.StyleDefault
if attrs.EntriesHaveErrors {
fg = termbox.ColorYellow
style = style.Foreground(tcell.ColorYellow)
}
if err != nil {
fg = termbox.ColorRed
style = style.Foreground(tcell.ColorRed)
}
const colorLightYellow = termbox.ColorYellow + 8
if isSelected {
fg = colorLightYellow
style = style.Foreground(tcell.ColorLightYellow)
}
bg := termbox.ColorBlack
if n == dirPos.entry {
fg, bg = bg, fg
style = style.Reverse(true)
}
mark := ' '
if attrs.IsDir {
@ -449,31 +463,30 @@ func (u *UI) Draw() error {
}
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++
}
}
// Footer
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 {
message := ""
if u.listing {
message = " [listing in progress]"
}
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
if u.showBox {
u.Box()
}
err := termbox.Flush()
if err != nil {
return fmt.Errorf("failed to flush screen: %w", err)
}
u.s.Show()
return nil
}
@ -886,37 +899,34 @@ func NewUI(f fs.Fs) *UI {
// Show shows the user interface
func (u *UI) Show() error {
err := termbox.Init()
var err error
u.s, err = tcell.NewScreen()
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
u.listing = true
rootChan, errChan, updated := scan.Scan(context.Background(), u.f)
// Poll the events into a channel
events := make(chan termbox.Event)
doneWithEvent := make(chan bool)
go func() {
for {
events <- termbox.PollEvent()
<-doneWithEvent
}
}()
events := make(chan tcell.Event)
go u.s.ChannelEvents(events, nil)
// Main loop, waiting for events and channels
outer:
for {
//Reset()
err := u.Draw()
if err != nil {
return fmt.Errorf("draw failed: %w", err)
}
var root *scan.Dir
select {
case root = <-rootChan:
case root := <-rootChan:
u.root = root
u.setCurrentDir(root)
case err := <-errChan:
@ -926,39 +936,50 @@ outer:
u.listing = false
case <-updated:
// redraw
// might want to limit updates per second
// TODO: might want to limit updates per second
u.sortCurrentDir()
case ev := <-events:
doneWithEvent <- true
if ev.Type == termbox.EventKey {
switch ev.Key + termbox.Key(ev.Ch) {
case termbox.KeyEsc, termbox.KeyCtrlC, 'q':
switch ev := ev.(type) {
case *tcell.EventResize:
if u.root != nil {
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 {
u.showBox = false
} else {
break outer
}
case termbox.KeyArrowDown, 'j':
case key(tcell.KeyDown), 'j':
u.move(1)
case termbox.KeyArrowUp, 'k':
case key(tcell.KeyUp), 'k':
u.move(-1)
case termbox.KeyPgdn, '-', '_':
case key(tcell.KeyPgDn), '-', '_':
u.move(u.dirListHeight)
case termbox.KeyPgup, '=', '+':
case key(tcell.KeyPgUp), '=', '+':
u.move(-u.dirListHeight)
case termbox.KeyArrowLeft, 'h':
case key(tcell.KeyLeft), 'h':
if u.showBox {
u.moveBox(-1)
break
}
u.up()
case termbox.KeyEnter:
case key(tcell.KeyEnter):
if len(u.boxMenu) > 0 {
u.handleBoxOption()
break
}
u.enter()
case termbox.KeyArrowRight, 'l':
case key(tcell.KeyRight), 'l':
if u.showBox {
u.moveBox(1)
break
@ -1001,11 +1022,8 @@ outer:
// Refresh the screen. Not obvious what key to map
// this onto, but ^L is a common choice.
case termbox.KeyCtrlL:
err := termbox.Sync()
if err != nil {
fs.Errorf(nil, "termbox sync returned error: %v", err)
}
case key(tcell.KeyCtrlL):
u.s.Sync()
}
}
}
@ -1013,3 +1031,9 @@ outer:
}
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)
}