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()
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.GetString("...")
tui.Clear()
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)
tui.Printf("%sHello%s", tui.EscapeInvert, tui.EscapeNormal)
tui.Printf(" world\n")
tui.Readline("Press enter to continue...")
}

View File

@ -1,31 +1,54 @@
package tui
import "fmt"
import (
"crypto/rand"
"fmt"
"os"
"os/exec"
"path/filepath"
)
// String prompts a user for a string and returns the result.
func GetString(prompt string) string {
// Readline reads a line of input from the user.
func Readline(prompt string) string {
t.SetPrompt(prompt)
s, err := t.ReadLine()
must(err)
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)
must(err)
return s
}
func GetInt(prompt string) int {
for {
s := GetString(prompt)
value := int(0)
n, err := fmt.Sscanf(s, "%d", &value)
if n != 1 || err != nil {
continue
}
return value
// Edit the string s using the editor defined by the environment variable
// EDITOR.
func EditInEditor(s string) (string, error) {
buf := make([]byte, 16)
rand.Read(buf)
path := filepath.Join(os.TempDir(), fmt.Sprintf("tui.%x", buf))
defer os.RemoveAll(path)
if err := os.WriteFile(path, []byte(s), 0600); err != nil {
return s, err
}
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
import (
"bytes"
"fmt"
)
// Clear clears the screen.
// Clear clears the terminal.
func Clear() {
_, h := WindowSize()
_, h := TermSize()
buf := make([]byte, 2*h)
for i := range buf {
buf[i] = '\n'
@ -15,89 +14,22 @@ func Clear() {
mustWrite(buf)
}
// PrintHLine prints a line across the screen followed by a newline.
func PrintHLine() {
w, _ := WindowSize()
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)
// Write sends raw bytes to the terminal.
func Write(b []byte) {
mustWrite(b)
}
// Prints a box around text followed by a newline.
func PrintBoxed(s string, args ...interface{}) {
if len(args) > 0 {
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())
func Print(a ...interface{}) {
s := fmt.Sprint(a...)
mustWrite([]byte(s))
}
// Print a string to the terminal followed by a newline. No wrapping.
func PrintString(s string, args ...interface{}) {
if len(args) > 0 {
s = fmt.Sprintf(s, args...)
}
mustWrite([]byte(s + "\n"))
func Println(a ...interface{}) {
s := fmt.Sprintln(a...)
mustWrite([]byte(s))
}
// Format and wrap text, print to screen with given prefix on each wrapped
// line.
func PrintTextWithPrefix(prefix, s string, args ...interface{}) {
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...)
func Printf(format string, a ...interface{}) {
s := fmt.Sprintf(format, a...)
mustWrite([]byte(s))
}

View File

@ -66,69 +66,3 @@ func splitText(s string) [][]rune {
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.SetSize(80, 24)
}
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()))
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"
)
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) {
type TestCase struct {
In string
@ -65,7 +44,7 @@ func TestWrapText(t *testing.T) {
}
for _, tc := range cases {
lines, width := wrapText(tc.In, tc.W)
lines, width := WrapText(tc.In, tc.W)
if !reflect.DeepEqual(lines, tc.Lines) {
t.Fatalf("%#v %#v", lines, tc.Lines)
}