WIP: clean up and slim down.

master v0.0.2
jdl 2021-03-25 11:21:12 +01:00
parent 65f189309e
commit 435ef7aead
10 changed files with 128 additions and 278 deletions

10
escapes.go Normal file
View File

@ -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'}
)

View File

@ -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)
} }

View File

@ -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
View File

@ -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
}
}
}

View File

@ -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...)
} }

View File

@ -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
View File

@ -33,6 +33,7 @@ func Start() {
} }
t = term.NewTerminal(screen, "") t = term.NewTerminal(screen, "")
t.SetSize(80, 24)
} }
func Stop() { func Stop() {

View File

@ -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
} }

60
wordwrap.go Normal file
View File

@ -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
}

View File

@ -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)
} }