parent
65f189309e
commit
435ef7aead
|
@ -0,0 +1,10 @@
|
||||||
|
package tui
|
||||||
|
|
||||||
|
var (
|
||||||
|
EscapeNormal = []byte{27, '[', '0', 'm'}
|
||||||
|
EscapeBold = []byte{27, '[', '1', 'm'}
|
||||||
|
EscapeFaint = []byte{27, '[', '2', 'm'}
|
||||||
|
EscapeItalic = []byte{27, '[', '3', 'm'}
|
||||||
|
EscapeUnderline = []byte{27, '[', '4', 'm'}
|
||||||
|
EscapeInvert = []byte{27, '[', '7', 'm'}
|
||||||
|
)
|
|
@ -8,36 +8,8 @@ func main() {
|
||||||
tui.Start()
|
tui.Start()
|
||||||
defer tui.Stop()
|
defer tui.Stop()
|
||||||
|
|
||||||
pwd := tui.GetPassword("Password: ")
|
|
||||||
tui.PrintString("Got %s", pwd)
|
|
||||||
tui.GetString("...")
|
|
||||||
|
|
||||||
c := tui.GetMenu(
|
|
||||||
"Menu",
|
|
||||||
"Some long or short text.",
|
|
||||||
"aa", "Accessory long text description with some other stuff. Whatever.",
|
|
||||||
"b", "Bass",
|
|
||||||
"c", "Case",
|
|
||||||
"e", "Email address\n\naddr")
|
|
||||||
tui.PrintString("Got %s", c)
|
|
||||||
tui.GetString("...")
|
|
||||||
|
|
||||||
tui.PrintHLine()
|
|
||||||
tui.GetString("What would you like? ")
|
|
||||||
|
|
||||||
tui.Clear()
|
tui.Clear()
|
||||||
|
tui.Printf("%sHello%s", tui.EscapeInvert, tui.EscapeNormal)
|
||||||
tui.GetString("...")
|
tui.Printf(" world\n")
|
||||||
tui.Clear()
|
tui.Readline("Press enter to continue...")
|
||||||
|
|
||||||
tui.PrintBoxed("the rain in spain falls mainly in the plane. The rain in spain falls mainlyx in the plane.")
|
|
||||||
tui.GetString("...")
|
|
||||||
tui.Clear()
|
|
||||||
|
|
||||||
tui.PrintTextWithPrefix(" ", "the rain in spain falls mainly in the plane. The rain in spain fallsxxx mainly in the plane.")
|
|
||||||
tui.GetString("...")
|
|
||||||
tui.Clear()
|
|
||||||
|
|
||||||
i := tui.GetInt("an integer: ")
|
|
||||||
tui.PrintTextWithPrefix("", "Got %d", i)
|
|
||||||
}
|
}
|
||||||
|
|
53
input.go
53
input.go
|
@ -1,31 +1,54 @@
|
||||||
package tui
|
package tui
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
// String prompts a user for a string and returns the result.
|
// Readline reads a line of input from the user.
|
||||||
func GetString(prompt string) string {
|
func Readline(prompt string) string {
|
||||||
t.SetPrompt(prompt)
|
t.SetPrompt(prompt)
|
||||||
s, err := t.ReadLine()
|
s, err := t.ReadLine()
|
||||||
must(err)
|
must(err)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPassword(prompt string) string {
|
// ReadPassword reads a line of input from the user, but doesn't echo to the
|
||||||
|
// screen.
|
||||||
|
func ReadPassword(prompt string) string {
|
||||||
s, err := t.ReadPassword(prompt)
|
s, err := t.ReadPassword(prompt)
|
||||||
must(err)
|
must(err)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetInt(prompt string) int {
|
// Edit the string s using the editor defined by the environment variable
|
||||||
for {
|
// EDITOR.
|
||||||
s := GetString(prompt)
|
func EditInEditor(s string) (string, error) {
|
||||||
|
buf := make([]byte, 16)
|
||||||
value := int(0)
|
rand.Read(buf)
|
||||||
|
path := filepath.Join(os.TempDir(), fmt.Sprintf("tui.%x", buf))
|
||||||
n, err := fmt.Sscanf(s, "%d", &value)
|
defer os.RemoveAll(path)
|
||||||
if n != 1 || err != nil {
|
if err := os.WriteFile(path, []byte(s), 0600); err != nil {
|
||||||
continue
|
return s, err
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editor := os.Getenv("EDITOR")
|
||||||
|
if editor == "" {
|
||||||
|
editor = "emacs"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(editor, path)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(buf), nil
|
||||||
}
|
}
|
||||||
|
|
61
menu.go
61
menu.go
|
@ -1,61 +0,0 @@
|
||||||
package tui
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
func GetMenu(
|
|
||||||
title string,
|
|
||||||
text string,
|
|
||||||
items ...string, // key -> value pairs
|
|
||||||
) string {
|
|
||||||
keys := map[string]bool{}
|
|
||||||
keyWidth := 0
|
|
||||||
for i := 0; i < len(items); i += 2 {
|
|
||||||
key := items[i]
|
|
||||||
keyLen := len([]rune(key))
|
|
||||||
keys[items[i]] = true
|
|
||||||
if keyLen > keyWidth {
|
|
||||||
keyWidth = keyLen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
drawMenu := func() {
|
|
||||||
Clear()
|
|
||||||
PrintBoxed(title)
|
|
||||||
PrintString("┆")
|
|
||||||
if text != "" {
|
|
||||||
PrintTextWithPrefix("│ ", text)
|
|
||||||
PrintString("│")
|
|
||||||
}
|
|
||||||
|
|
||||||
w, _ := WindowSize()
|
|
||||||
valWidth := w - keyWidth - 5
|
|
||||||
|
|
||||||
for i := 0; i < len(items); i += 2 {
|
|
||||||
key := items[i]
|
|
||||||
keyLen := len([]rune(key))
|
|
||||||
text := items[i+1]
|
|
||||||
|
|
||||||
lines, _ := wrapText(text, valWidth)
|
|
||||||
indent := keyWidth - keyLen
|
|
||||||
|
|
||||||
// First line.
|
|
||||||
sIndent := strings.Repeat(" ", indent)
|
|
||||||
PrintString("│ %s%s %s", key, sIndent, lines[0])
|
|
||||||
|
|
||||||
// Additional lines.
|
|
||||||
for _, line := range lines[1:] {
|
|
||||||
sIndent := strings.Repeat(" ", keyWidth)
|
|
||||||
PrintString("│ %s %s", sIndent, line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PrintString("│")
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
drawMenu()
|
|
||||||
in := GetString("╰─┄ ")
|
|
||||||
if _, ok := keys[in]; ok {
|
|
||||||
return in
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
96
output.go
96
output.go
|
@ -1,13 +1,12 @@
|
||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Clear clears the screen.
|
// Clear clears the terminal.
|
||||||
func Clear() {
|
func Clear() {
|
||||||
_, h := WindowSize()
|
_, h := TermSize()
|
||||||
buf := make([]byte, 2*h)
|
buf := make([]byte, 2*h)
|
||||||
for i := range buf {
|
for i := range buf {
|
||||||
buf[i] = '\n'
|
buf[i] = '\n'
|
||||||
|
@ -15,89 +14,22 @@ func Clear() {
|
||||||
mustWrite(buf)
|
mustWrite(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrintHLine prints a line across the screen followed by a newline.
|
// Write sends raw bytes to the terminal.
|
||||||
func PrintHLine() {
|
func Write(b []byte) {
|
||||||
w, _ := WindowSize()
|
mustWrite(b)
|
||||||
|
|
||||||
runes := make([]rune, w)
|
|
||||||
for i := range runes {
|
|
||||||
runes[i] = '━'
|
|
||||||
}
|
|
||||||
|
|
||||||
runes[0] = '┅'
|
|
||||||
runes[len(runes)-2] = '┅'
|
|
||||||
runes[len(runes)-1] = '\n'
|
|
||||||
|
|
||||||
buf := []byte(string(runes))
|
|
||||||
mustWrite(buf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prints a box around text followed by a newline.
|
func Print(a ...interface{}) {
|
||||||
func PrintBoxed(s string, args ...interface{}) {
|
s := fmt.Sprint(a...)
|
||||||
if len(args) > 0 {
|
mustWrite([]byte(s))
|
||||||
s = fmt.Sprintf(s, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
w, _ := WindowSize()
|
|
||||||
|
|
||||||
lines, textWidth := wrapText(s, w-4)
|
|
||||||
|
|
||||||
if w < textWidth+4 {
|
|
||||||
w = textWidth + 4
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
|
|
||||||
// Top of box.
|
|
||||||
buf.Write([]byte("╭"))
|
|
||||||
for i := 0; i < w-2; i++ {
|
|
||||||
buf.Write([]byte("─"))
|
|
||||||
}
|
|
||||||
buf.Write([]byte("╮\n"))
|
|
||||||
|
|
||||||
// Lines.
|
|
||||||
for _, line := range lines {
|
|
||||||
buf.Write([]byte("│ "))
|
|
||||||
buf.Write([]byte(line))
|
|
||||||
for i := len([]rune(line)); i < w-4; i++ {
|
|
||||||
buf.Write([]byte(" "))
|
|
||||||
}
|
|
||||||
buf.Write([]byte(" │\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bottom of box.
|
|
||||||
buf.Write([]byte("╰"))
|
|
||||||
for i := 0; i < w-2; i++ {
|
|
||||||
buf.Write([]byte("─"))
|
|
||||||
}
|
|
||||||
buf.Write([]byte("╯\n"))
|
|
||||||
|
|
||||||
mustWrite(buf.Bytes())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print a string to the terminal followed by a newline. No wrapping.
|
func Println(a ...interface{}) {
|
||||||
func PrintString(s string, args ...interface{}) {
|
s := fmt.Sprintln(a...)
|
||||||
if len(args) > 0 {
|
mustWrite([]byte(s))
|
||||||
s = fmt.Sprintf(s, args...)
|
|
||||||
}
|
|
||||||
mustWrite([]byte(s + "\n"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format and wrap text, print to screen with given prefix on each wrapped
|
func Printf(format string, a ...interface{}) {
|
||||||
// line.
|
s := fmt.Sprintf(format, a...)
|
||||||
func PrintTextWithPrefix(prefix, s string, args ...interface{}) {
|
mustWrite([]byte(s))
|
||||||
if len(args) > 0 {
|
|
||||||
s = fmt.Sprintf(s, args...)
|
|
||||||
}
|
|
||||||
indent := len([]rune(prefix))
|
|
||||||
//sIndent := strings.Repeat(" ", indent)
|
|
||||||
w, _ := WindowSize()
|
|
||||||
lines, _ := wrapText(s, w-1-indent)
|
|
||||||
for _, line := range lines {
|
|
||||||
PrintString(prefix + line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrintText(s string, args ...interface{}) {
|
|
||||||
PrintTextWithPrefix("", s, args...)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,69 +66,3 @@ func splitText(s string) [][]rune {
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeText(s string) string {
|
|
||||||
out := make([]rune, 0, len(s))
|
|
||||||
parts := splitText(s)
|
|
||||||
|
|
||||||
prevWasWord := false
|
|
||||||
|
|
||||||
for _, p := range parts {
|
|
||||||
if p[0] == '\n' {
|
|
||||||
out = append(out, '\n', '\n')
|
|
||||||
prevWasWord = false
|
|
||||||
} else {
|
|
||||||
if prevWasWord {
|
|
||||||
out = append(out, ' ')
|
|
||||||
}
|
|
||||||
out = append(out, p...)
|
|
||||||
prevWasWord = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return string(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrapText(s string, w int) ([]string, int) {
|
|
||||||
maxLine := 0
|
|
||||||
parts := splitText(s)
|
|
||||||
|
|
||||||
nextLine := func() (string, int) {
|
|
||||||
if parts[0][0] == '\n' {
|
|
||||||
parts = parts[1:]
|
|
||||||
return "", 0
|
|
||||||
}
|
|
||||||
|
|
||||||
words := append([]string{}, string(parts[0]))
|
|
||||||
length := len(parts[0])
|
|
||||||
parts = parts[1:]
|
|
||||||
|
|
||||||
for len(parts) > 0 {
|
|
||||||
p := parts[0]
|
|
||||||
|
|
||||||
if p[0] == '\n' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if length+len(p)+1 > w {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
length += len(p) + 1
|
|
||||||
words = append(words, string(p))
|
|
||||||
parts = parts[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(words, " "), length
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := make([]string, 0, 2)
|
|
||||||
for len(parts) > 0 {
|
|
||||||
line, lineLen := nextLine()
|
|
||||||
if lineLen > maxLine {
|
|
||||||
maxLine = lineLen
|
|
||||||
}
|
|
||||||
lines = append(lines, line)
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines, maxLine
|
|
||||||
}
|
|
||||||
|
|
1
tui.go
1
tui.go
|
@ -33,6 +33,7 @@ func Start() {
|
||||||
}
|
}
|
||||||
|
|
||||||
t = term.NewTerminal(screen, "")
|
t = term.NewTerminal(screen, "")
|
||||||
|
t.SetSize(80, 24)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Stop() {
|
func Stop() {
|
||||||
|
|
2
util.go
2
util.go
|
@ -12,7 +12,7 @@ func must(err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WindowSize() (w, h int) {
|
func TermSize() (w, h int) {
|
||||||
w, h, _ = term.GetSize(int(os.Stdin.Fd()))
|
w, h, _ = term.GetSize(int(os.Stdin.Fd()))
|
||||||
return w, h
|
return w, h
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
package tui
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// WrapText takes the text in s and wraps it to the width. It returns the
|
||||||
|
// individual wrapped lines, and the length of the longest wrapped line.
|
||||||
|
//
|
||||||
|
// If the width is less than or equal to zero, it is set to the terminal width.
|
||||||
|
//
|
||||||
|
// In order to wrap the text in a reasonable way, white space is trimmed and
|
||||||
|
// collapsed througout the text.
|
||||||
|
func WrapText(s string, width int) ([]string, int) {
|
||||||
|
w := width
|
||||||
|
if w <= 0 {
|
||||||
|
w, _ = TermSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
maxLine := 0
|
||||||
|
parts := splitText(s)
|
||||||
|
|
||||||
|
nextLine := func() (string, int) {
|
||||||
|
if parts[0][0] == '\n' {
|
||||||
|
parts = parts[1:]
|
||||||
|
return "", 0
|
||||||
|
}
|
||||||
|
|
||||||
|
words := append([]string{}, string(parts[0]))
|
||||||
|
length := len(parts[0])
|
||||||
|
parts = parts[1:]
|
||||||
|
|
||||||
|
for len(parts) > 0 {
|
||||||
|
p := parts[0]
|
||||||
|
|
||||||
|
if p[0] == '\n' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if length+len(p)+1 > w {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
length += len(p) + 1
|
||||||
|
words = append(words, string(p))
|
||||||
|
parts = parts[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(words, " "), length
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := make([]string, 0, 2)
|
||||||
|
for len(parts) > 0 {
|
||||||
|
line, lineLen := nextLine()
|
||||||
|
if lineLen > maxLine {
|
||||||
|
maxLine = lineLen
|
||||||
|
}
|
||||||
|
lines = append(lines, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines, maxLine
|
||||||
|
}
|
|
@ -5,27 +5,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNormalizeText(t *testing.T) {
|
|
||||||
type TestCase struct {
|
|
||||||
In string
|
|
||||||
Out string
|
|
||||||
}
|
|
||||||
|
|
||||||
cases := []TestCase{
|
|
||||||
{"a", "a"},
|
|
||||||
{" a\n", "a"},
|
|
||||||
{" a\nbc\n\nd ", "a bc\n\nd"},
|
|
||||||
{" a\n b\n \n\n\tc d ", "a b\n\nc d"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
out := normalizeText(tc.In)
|
|
||||||
if out != tc.Out {
|
|
||||||
t.Fatal(out, tc.Out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWrapText(t *testing.T) {
|
func TestWrapText(t *testing.T) {
|
||||||
type TestCase struct {
|
type TestCase struct {
|
||||||
In string
|
In string
|
||||||
|
@ -65,7 +44,7 @@ func TestWrapText(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
lines, width := wrapText(tc.In, tc.W)
|
lines, width := WrapText(tc.In, tc.W)
|
||||||
if !reflect.DeepEqual(lines, tc.Lines) {
|
if !reflect.DeepEqual(lines, tc.Lines) {
|
||||||
t.Fatalf("%#v %#v", lines, tc.Lines)
|
t.Fatalf("%#v %#v", lines, tc.Lines)
|
||||||
}
|
}
|
Reference in New Issue