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:
parent
46b080c092
commit
a4c65532ea
1 changed files with 128 additions and 104 deletions
232
cmd/ncdu/ncdu.go
232
cmd/ncdu/ncdu.go
|
@ -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)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue