Some basic TUI widgets ported to Go

This commit is contained in:
Kovid Goyal 2022-11-16 12:48:13 +05:30
parent f3b3d6c0ef
commit e70c021371
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 294 additions and 2 deletions

View File

@ -12,6 +12,7 @@ from typing import Dict, Iterator, List, Set, Tuple, Union
import kitty.constants as kc import kitty.constants as kc
from kittens.tui.operations import Mode from kittens.tui.operations import Mode
from kittens.tui.spinners import spinners
from kitty.cli import ( from kitty.cli import (
CompletionSpec, GoOption, go_options_for_seq, parse_option_spec, CompletionSpec, GoOption, go_options_for_seq, parse_option_spec,
serialize_as_go_string, serialize_as_go_string,
@ -252,6 +253,28 @@ def go_code_for_remote_command(name: str, cmd: RemoteCommand, template: str) ->
# Constants {{{ # Constants {{{
def generate_spinners() -> str:
ans = ['package tui', 'import "time"', 'func NewSpinner(name string) *Spinner {', 'var ans *Spinner', 'switch name {']
a = ans.append
for name, spinner in spinners.items():
a(f'case "{serialize_as_go_string(name)}":')
a('ans = &Spinner{')
a(f'Name: "{serialize_as_go_string(name)}",')
a(f'interval: {spinner["interval"]},')
frames = ', '.join(f'"{serialize_as_go_string(x)}"' for x in spinner['frames'])
a(f'frames: []string{{{frames}}},')
a('}')
a('}')
a('if ans != nil {')
a('ans.interval *= time.Millisecond')
a('ans.current_frame = -1')
a('ans.last_change_at = time.Now().Add(-ans.interval)')
a('}')
a('return ans}')
return '\n'.join(ans)
def generate_color_names() -> str: def generate_color_names() -> str:
return 'package style\n\nvar ColorNames = map[string]RGBA{' + '\n'.join( return 'package style\n\nvar ColorNames = map[string]RGBA{' + '\n'.join(
f'\t"{name}": RGBA{{ Red:{val.red}, Green:{val.green}, Blue:{val.blue} }},' f'\t"{name}": RGBA{{ Red:{val.red}, Green:{val.green}, Blue:{val.blue} }},'
@ -448,6 +471,8 @@ def main() -> None:
f.write(generate_color_names()) f.write(generate_color_names())
with replace_if_needed('tools/tui/readline/actions_generated.go') as f: with replace_if_needed('tools/tui/readline/actions_generated.go') as f:
f.write(generate_readline_actions()) f.write(generate_readline_actions())
with replace_if_needed('tools/tui/spinners_generated.go') as f:
f.write(generate_spinners())
update_completion() update_completion()
update_at_commands() update_at_commands()
print(json.dumps(changed, indent=2)) print(json.dumps(changed, indent=2))

View File

@ -18,8 +18,8 @@ var _ = fmt.Print
type Context struct { type Context struct {
fmt_ctx style.Context fmt_ctx style.Context
Cyan, Green, Blue, BrightRed, Yellow, Italic, Bold, Title, Exe, Opt, Emph, Err, Code func(args ...interface{}) string Cyan, Green, Blue, BrightRed, Yellow, Italic, Bold, Dim, Title, Exe, Opt, Emph, Err, Code func(args ...interface{}) string
Url func(string, string) string Url func(string, string) string
} }
var ( var (
@ -38,6 +38,7 @@ func New(allow_escape_codes bool) *Context {
ans.Yellow = fmt_ctx.SprintFunc("fg=bright-yellow") ans.Yellow = fmt_ctx.SprintFunc("fg=bright-yellow")
ans.Italic = fmt_ctx.SprintFunc("italic") ans.Italic = fmt_ctx.SprintFunc("italic")
ans.Bold = fmt_ctx.SprintFunc("bold") ans.Bold = fmt_ctx.SprintFunc("bold")
ans.Dim = fmt_ctx.SprintFunc("dim")
ans.Title = fmt_ctx.SprintFunc("bold fg=blue") ans.Title = fmt_ctx.SprintFunc("bold fg=blue")
ans.Exe = fmt_ctx.SprintFunc("bold fg=bright-yellow") ans.Exe = fmt_ctx.SprintFunc("bold fg=bright-yellow")
ans.Opt = ans.Green ans.Opt = ans.Green

61
tools/tui/progress-bar.go Normal file
View File

@ -0,0 +1,61 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package tui
import (
"fmt"
"kitty/tools/cli/markup"
"strings"
)
var _ = fmt.Print
func RepeatChar(char string, count int) string {
if count <= 5 {
return strings.Repeat(char, count)
}
return fmt.Sprintf("%s\x1b[%db", char, count-1)
}
func RenderProgressBar(frac float64, width int) string {
fc := markup.New(true)
if frac >= 1 {
return fc.Green(RepeatChar("🬋", width))
}
if frac <= 0 {
return fc.Dim(RepeatChar("🬋", width))
}
w := frac * float64(width)
fl := int(w)
overhang := w - float64(fl)
filled := RepeatChar("🬋", fl)
needs_break := false
if overhang < 0.2 {
needs_break = true
} else if overhang < 0.8 {
filled += "🬃"
fl += 1
} else {
if fl < width-1 {
filled += "🬋"
fl += 1
needs_break = true
} else {
filled += "🬃"
fl += 1
}
}
ans := fc.Blue(filled)
unfilled := ""
if width > fl && needs_break {
unfilled = "🬇"
}
filler := width - fl - len(unfilled)
if filler > 0 {
unfilled += RepeatChar("🬋", filler)
}
if unfilled != "" {
ans += fc.Dim(unfilled)
}
return ans
}

27
tools/tui/spinners.go Normal file
View File

@ -0,0 +1,27 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package tui
import (
"fmt"
"time"
)
var _ = fmt.Print
type Spinner struct {
Name string
interval time.Duration
frames []string
current_frame int
last_change_at time.Time
}
func (self *Spinner) Tick() string {
now := time.Now()
if now.Sub(self.last_change_at) >= self.interval {
self.last_change_at = now
self.current_frame = (self.current_frame + 1) % len(self.frames)
}
return self.frames[self.current_frame]
}

View File

@ -0,0 +1,61 @@
package humanize
import (
"fmt"
"math"
)
// IEC Sizes.
// kibis of bits
const (
Byte = 1 << (iota * 10)
KiByte
MiByte
GiByte
TiByte
PiByte
EiByte
)
// SI Sizes.
const (
IByte = 1
KByte = IByte * 1000
MByte = KByte * 1000
GByte = MByte * 1000
TByte = GByte * 1000
PByte = TByte * 1000
EByte = PByte * 1000
)
func logn(n, b float64) float64 {
return math.Log(n) / math.Log(b)
}
func humanize_bytes(s uint64, base float64, sizes []string, sep string) string {
if s < 10 {
return fmt.Sprintf("%d%sB", s, sep)
}
e := math.Floor(logn(float64(s), base))
suffix := sizes[int(e)]
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
f := "%.0f%s%s"
if val < 10 {
f = "%.1f%s%s"
}
return fmt.Sprintf(f, val, sep, suffix)
}
// Bytes produces a human readable representation of an SI size.
// Bytes(82854982) -> 83 MB
func Bytes(s uint64) string {
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
return humanize_bytes(s, 1000, sizes, " ")
}
// IBytes produces a human readable representation of an IEC size.
// IBytes(82854982) -> 79 MiB
func IBytes(s uint64) string {
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
return humanize_bytes(s, 1024, sizes, " ")
}

View File

@ -0,0 +1,117 @@
package humanize
import (
"fmt"
"math"
"sort"
"time"
)
// Seconds-based time units
const (
Day = 24 * time.Hour
Week = 7 * Day
Month = 30 * Day
Year = 12 * Month
LongTime = 37 * Year
)
// Time formats a time into a relative string.
//
// Time(someT) -> "3 weeks ago"
func Time(then time.Time) string {
return RelTime(then, time.Now(), "ago", "from now")
}
// A RelTimeMagnitude struct contains a relative time point at which
// the relative format of time will switch to a new format string. A
// slice of these in ascending order by their "D" field is passed to
// CustomRelTime to format durations.
//
// The Format field is a string that may contain a "%s" which will be
// replaced with the appropriate signed label (e.g. "ago" or "from
// now") and a "%d" that will be replaced by the quantity.
//
// The DivBy field is the amount of time the time difference must be
// divided by in order to display correctly.
//
// e.g. if D is 2*time.Minute and you want to display "%d minutes %s"
// DivBy should be time.Minute so whatever the duration is will be
// expressed in minutes.
type RelTimeMagnitude struct {
D time.Duration
Format string
DivBy time.Duration
}
var defaultMagnitudes = []RelTimeMagnitude{
{time.Second, "now", time.Second},
{2 * time.Second, "1 second %s", 1},
{time.Minute, "%d seconds %s", time.Second},
{2 * time.Minute, "1 minute %s", 1},
{time.Hour, "%d minutes %s", time.Minute},
{2 * time.Hour, "1 hour %s", 1},
{Day, "%d hours %s", time.Hour},
{2 * Day, "1 day %s", 1},
{Week, "%d days %s", Day},
{2 * Week, "1 week %s", 1},
{Month, "%d weeks %s", Week},
{2 * Month, "1 month %s", 1},
{Year, "%d months %s", Month},
{18 * Month, "1 year %s", 1},
{2 * Year, "2 years %s", 1},
{LongTime, "%d years %s", Year},
{math.MaxInt64, "a long while %s", 1},
}
// RelTime formats a time into a relative string.
//
// It takes two times and two labels. In addition to the generic time
// delta string (e.g. 5 minutes), the labels are used applied so that
// the label corresponding to the smaller time is applied.
//
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
func RelTime(a, b time.Time, albl, blbl string) string {
return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
}
// CustomRelTime formats a time into a relative string.
//
// It takes two times two labels and a table of relative time formats.
// In addition to the generic time delta string (e.g. 5 minutes), the
// labels are used applied so that the label corresponding to the
// smaller time is applied.
func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string {
lbl := albl
diff := b.Sub(a)
if a.After(b) {
lbl = blbl
diff = a.Sub(b)
}
n := sort.Search(len(magnitudes), func(i int) bool {
return magnitudes[i].D > diff
})
if n >= len(magnitudes) {
n = len(magnitudes) - 1
}
mag := magnitudes[n]
args := []interface{}{}
escaped := false
for _, ch := range mag.Format {
if escaped {
switch ch {
case 's':
args = append(args, lbl)
case 'd':
args = append(args, diff/mag.DivBy)
}
escaped = false
} else {
escaped = ch == '%'
}
}
return fmt.Sprintf(mag.Format, args...)
}