v0.15.0 password recovery, phone layout, CLI pager, embed policy fixes
This commit is contained in:
1 parent
a999177f0c
commit
e1fa5242e3
48 files changed
+2101
-135
No files matched your search
@@ -40,7 +40,7 @@ func RenderBoard(asciiBoard string) {
|
||||
Print(Reset, "%c", char)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Fprintln(Out)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,12 +24,12 @@ func C(color, text string) string {
|
||||
|
||||
// Print outputs colored text immediately
|
||||
func Print(color, format string, args ...any) {
|
||||
fmt.Printf(C(color, format), args...)
|
||||
fmt.Fprintf(Out, C(color, format), args...)
|
||||
}
|
||||
|
||||
// Println outputs colored text with newline
|
||||
func Println(color, format string, args ...any) {
|
||||
fmt.Println(C(color, fmt.Sprintf(format, args...)))
|
||||
fmt.Fprintln(Out, C(color, fmt.Sprintf(format, args...)))
|
||||
}
|
||||
|
||||
// Build creates a multi-colored string
|
||||
|
||||
@@ -12,5 +12,5 @@ func PrettyPrintJSON(v any) {
|
||||
Print(Red, "Error formatting JSON: %s\n", err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
fmt.Fprintln(Out, string(data))
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Out receives everything the client prints. A paged command runs with Out
|
||||
// swapped for a buffer, which Page then shows one screen at a time.
|
||||
var Out io.Writer = os.Stdout
|
||||
|
||||
// Printf, Line and Write are fmt.Printf, fmt.Println and fmt.Print to Out.
|
||||
func Printf(format string, args ...any) { fmt.Fprintf(Out, format, args...) }
|
||||
func Line(args ...any) { fmt.Fprintln(Out, args...) }
|
||||
func Write(args ...any) { fmt.Fprint(Out, args...) }
|
||||
|
||||
var ansiPattern = regexp.MustCompile("\x1b\\[[0-9;?]*[ -/]*[@-~]")
|
||||
|
||||
// TermSize returns the terminal's rows and columns, or zeros when the output
|
||||
// is not a terminal (then nothing is paged). Platform files set it.
|
||||
var TermSize = func() (rows, cols int) { return 0, 0 }
|
||||
|
||||
// Page prints text to Out one screen at a time. After each full screen it
|
||||
// asks to continue; readLine supplies the answer (an empty line continues,
|
||||
// "q" stops). Without a terminal, or when the text fits, it prints at once.
|
||||
func Page(text string, readLine func() (string, bool)) {
|
||||
rows, cols := TermSize()
|
||||
lines := strings.Split(strings.TrimRight(text, "\n"), "\n")
|
||||
if rows < 4 || readLine == nil || visualRows(lines, cols) <= rows-1 {
|
||||
if text != "" {
|
||||
fmt.Fprint(Out, strings.TrimRight(text, "\n")+"\n")
|
||||
}
|
||||
return
|
||||
}
|
||||
pageRows := rows - 1 // one line for the prompt
|
||||
used, i := 0, 0
|
||||
for i < len(lines) {
|
||||
need := wrappedRows(lines[i], cols)
|
||||
if used > 0 && used+need > pageRows {
|
||||
remaining := len(lines) - i
|
||||
fmt.Fprintf(Out, "%s-- More (%d lines left; Enter continues, q quits) --%s", Yellow, remaining, Reset)
|
||||
answer, ok := readLine()
|
||||
// Clear the prompt and the echoed answer line.
|
||||
fmt.Fprint(Out, "\r\x1b[2K\x1b[1A\r\x1b[2K")
|
||||
if !ok || strings.HasPrefix(strings.ToLower(strings.TrimSpace(answer)), "q") {
|
||||
return
|
||||
}
|
||||
used = 0
|
||||
continue
|
||||
}
|
||||
fmt.Fprintln(Out, lines[i])
|
||||
used += need
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func visualRows(lines []string, cols int) int {
|
||||
total := 0
|
||||
for _, line := range lines {
|
||||
total += wrappedRows(line, cols)
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// wrappedRows is how many terminal rows a line takes, ignoring colour codes.
|
||||
func wrappedRows(line string, cols int) int {
|
||||
if cols <= 0 {
|
||||
return 1
|
||||
}
|
||||
width := utf8.RuneCountInString(ansiPattern.ReplaceAllString(line, ""))
|
||||
if width == 0 {
|
||||
return 1
|
||||
}
|
||||
return (width + cols - 1) / cols
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPagePrintsScreensAndStopsOnQ(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
prevOut, prevSize := Out, TermSize
|
||||
Out = &buf
|
||||
TermSize = func() (int, int) { return 5, 80 } // 4 lines a page
|
||||
defer func() { Out, TermSize = prevOut, prevSize }()
|
||||
|
||||
var lines []string
|
||||
for i := 1; i <= 10; i++ {
|
||||
lines = append(lines, "line "+string(rune('0'+i%10)))
|
||||
}
|
||||
answers := []string{"", "q"}
|
||||
reads := 0
|
||||
readLine := func() (string, bool) {
|
||||
if reads >= len(answers) {
|
||||
return "", false
|
||||
}
|
||||
reads++
|
||||
return answers[reads-1], true
|
||||
}
|
||||
Page(strings.Join(lines, "\n")+"\n", readLine)
|
||||
out := buf.String()
|
||||
if reads != 2 {
|
||||
t.Errorf("prompted %d times, want 2", reads)
|
||||
}
|
||||
if !strings.Contains(out, "line 1\nline 2\nline 3\nline 4\n") || !strings.Contains(out, "More (6 lines left") {
|
||||
t.Errorf("first page = %q", out)
|
||||
}
|
||||
if !strings.Contains(out, "line 5\nline 6\nline 7\nline 8\n") || strings.Contains(out, "line 9") {
|
||||
t.Errorf("second page / stop = %q", out)
|
||||
}
|
||||
|
||||
// Fits on one screen: no prompt. A wrapped long line counts as two rows.
|
||||
buf.Reset()
|
||||
reads = 0
|
||||
Page("a\nb\n", readLine)
|
||||
if buf.String() != "a\nb\n" || reads != 0 {
|
||||
t.Errorf("short text = %q, prompts %d", buf.String(), reads)
|
||||
}
|
||||
if wrappedRows(Red+strings.Repeat("x", 100)+Reset, 80) != 2 || wrappedRows("", 80) != 1 {
|
||||
t.Error("wrapped rows")
|
||||
}
|
||||
// No terminal: everything at once.
|
||||
TermSize = func() (int, int) { return 0, 0 }
|
||||
buf.Reset()
|
||||
Page(strings.Join(lines, "\n"), readLine)
|
||||
if strings.Count(buf.String(), "\n") != 10 || reads != 0 {
|
||||
t.Errorf("unpaged = %q", buf.String())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//go:build !js && !wasm
|
||||
|
||||
package display
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
func init() {
|
||||
TermSize = func() (rows, cols int) {
|
||||
fd := int(os.Stdout.Fd())
|
||||
if !term.IsTerminal(fd) {
|
||||
return 0, 0
|
||||
}
|
||||
cols, rows, err := term.GetSize(fd)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
return rows, cols
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//go:build js && wasm
|
||||
|
||||
package display
|
||||
|
||||
import "syscall/js"
|
||||
|
||||
// The page's terminal wrapper (terminal.js) publishes xterm's size as
|
||||
// globalThis.chessTerminal.rows and .cols.
|
||||
func init() {
|
||||
TermSize = func() (rows, cols int) {
|
||||
t := js.Global().Get("chessTerminal")
|
||||
if t.IsUndefined() || t.IsNull() {
|
||||
return 24, 80
|
||||
}
|
||||
rows, cols = t.Get("rows").Int(), t.Get("cols").Int()
|
||||
if rows <= 0 || cols <= 0 {
|
||||
return 24, 80
|
||||
}
|
||||
return rows, cols
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user