diff --git a/escapes.go b/escapes.go new file mode 100644 index 0000000..a6d0536 --- /dev/null +++ b/escapes.go @@ -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'} +) diff --git a/example/simple/main.go b/example/simple/main.go index f212065..5cf6090 100644 --- a/example/simple/main.go +++ b/example/simple/main.go @@ -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...") } diff --git a/input.go b/input.go index fb8dbeb..e15ab65 100644 --- a/input.go +++ b/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 } diff --git a/menu.go b/menu.go deleted file mode 100644 index ba4c971..0000000 --- a/menu.go +++ /dev/null @@ -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 - } - } -} diff --git a/output.go b/output.go index 9729d92..83c37c6 100644 --- a/output.go +++ b/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)) } diff --git a/stringutil.go b/stringutil.go index df9f5b4..c76a750 100644 --- a/stringutil.go +++ b/stringutil.go @@ -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 -} diff --git a/tui.go b/tui.go index b1c655f..935908d 100644 --- a/tui.go +++ b/tui.go @@ -33,6 +33,7 @@ func Start() { } t = term.NewTerminal(screen, "") + t.SetSize(80, 24) } func Stop() { diff --git a/util.go b/util.go index 0b7d515..a50d050 100644 --- a/util.go +++ b/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 } diff --git a/wordwrap.go b/wordwrap.go new file mode 100644 index 0000000..766d335 --- /dev/null +++ b/wordwrap.go @@ -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 +} diff --git a/stringutil_test.go b/wordwrap_test.go similarity index 67% rename from stringutil_test.go rename to wordwrap_test.go index 4bcc40e..d5f2f4b 100644 --- a/stringutil_test.go +++ b/wordwrap_test.go @@ -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) }