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()
|
||||
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...")
|
||||
}
|
||||
|
|
53
input.go
53
input.go
|
@ -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
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
|
||||
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -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
1
tui.go
|
@ -33,6 +33,7 @@ func Start() {
|
|||
}
|
||||
|
||||
t = term.NewTerminal(screen, "")
|
||||
t.SetSize(80, 24)
|
||||
}
|
||||
|
||||
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()))
|
||||
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"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
Reference in New Issue