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
@@ -107,7 +107,7 @@ func (c *Client) doRequest(method, path string, body any, result any) error {
|
||||
if resp.StatusCode >= 400 {
|
||||
statusColor = display.Red
|
||||
}
|
||||
fmt.Printf("%s[%d %s]%s\n", statusColor, resp.StatusCode, http.StatusText(resp.StatusCode), display.Reset)
|
||||
display.Printf("%s[%d %s]%s\n", statusColor, resp.StatusCode, http.StatusText(resp.StatusCode), display.Reset)
|
||||
|
||||
// Display response body if verbose
|
||||
if c.Verbose && len(respBody) > 0 {
|
||||
|
||||
@@ -81,8 +81,8 @@ func registerHandler(s *session.Session, args []string) error {
|
||||
c.SetToken(resp.Token)
|
||||
|
||||
display.Println(display.Green, "Registered successfully")
|
||||
fmt.Printf("User ID: %s\n", resp.UserID)
|
||||
fmt.Printf("Username: %s\n", resp.Username)
|
||||
display.Printf("User ID: %s\n", resp.UserID)
|
||||
display.Printf("Username: %s\n", resp.Username)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -111,8 +111,8 @@ func loginHandler(s *session.Session, args []string) error {
|
||||
c.SetToken(resp.Token)
|
||||
|
||||
display.Println(display.Green, "Logged in successfully")
|
||||
fmt.Printf("User ID: %s\n", resp.UserID)
|
||||
fmt.Printf("Username: %s\n", resp.Username)
|
||||
display.Printf("User ID: %s\n", resp.UserID)
|
||||
display.Printf("Username: %s\n", resp.Username)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -151,14 +151,14 @@ func whoamiHandler(s *session.Session, args []string) error {
|
||||
}
|
||||
|
||||
display.Println(display.Cyan, "Current User:")
|
||||
fmt.Printf(" User ID: %s\n", user.UserID)
|
||||
fmt.Printf(" Username: %s\n", user.Username)
|
||||
display.Printf(" User ID: %s\n", user.UserID)
|
||||
display.Printf(" Username: %s\n", user.Username)
|
||||
if user.Email != "" {
|
||||
fmt.Printf(" Email: %s\n", user.Email)
|
||||
display.Printf(" Email: %s\n", user.Email)
|
||||
}
|
||||
fmt.Printf(" Created: %s\n", user.CreatedAt.Format("2006-01-02 15:04:05"))
|
||||
display.Printf(" Created: %s\n", user.CreatedAt.Format("2006-01-02 15:04:05"))
|
||||
if user.LastLogin != nil {
|
||||
fmt.Printf(" Last Login: %s\n", user.LastLogin.Format("2006-01-02 15:04:05"))
|
||||
display.Printf(" Last Login: %s\n", user.LastLogin.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -172,7 +172,7 @@ func setUserHandler(s *session.Session, args []string) error {
|
||||
userID := args[0]
|
||||
s.SetCurrentUser(userID)
|
||||
display.Println(display.Cyan, "User ID set to: %s", userID)
|
||||
fmt.Println("Note: This doesn't authenticate, just sets the ID for display")
|
||||
display.Line("Note: This doesn't authenticate, just sets the ID for display")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -44,12 +44,12 @@ func healthHandler(s *session.Session, args []string) error {
|
||||
}
|
||||
|
||||
display.Println(display.Cyan, "Server Health:")
|
||||
fmt.Printf(" Status: %s\n", resp.Status)
|
||||
display.Printf(" Status: %s\n", resp.Status)
|
||||
// Convert Unix timestamp to readable time
|
||||
t := time.Unix(resp.Time, 0)
|
||||
fmt.Printf(" Time: %s\n", t.Format("2006-01-02 15:04:05"))
|
||||
display.Printf(" Time: %s\n", t.Format("2006-01-02 15:04:05"))
|
||||
if resp.Storage != "" {
|
||||
fmt.Printf(" Storage: %s\n", resp.Storage)
|
||||
display.Printf(" Storage: %s\n", resp.Storage)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -57,7 +57,7 @@ func healthHandler(s *session.Session, args []string) error {
|
||||
|
||||
func urlHandler(s *session.Session, args []string) error {
|
||||
if len(args) == 0 {
|
||||
fmt.Printf("Current API URL: %s\n", s.GetAPIBaseURL())
|
||||
display.Printf("Current API URL: %s\n", s.GetAPIBaseURL())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ func (r *Registry) registerGameCommands() {
|
||||
Description: "Show board and game state",
|
||||
Usage: "show",
|
||||
Handler: showBoardHandler,
|
||||
Paged: true,
|
||||
})
|
||||
|
||||
r.Register(&Command{
|
||||
@@ -67,6 +68,7 @@ func (r *Registry) registerGameCommands() {
|
||||
Description: "Show raw game JSON",
|
||||
Usage: "state",
|
||||
Handler: gameStateHandler,
|
||||
Paged: true,
|
||||
})
|
||||
|
||||
r.Register(&Command{
|
||||
@@ -99,6 +101,7 @@ func (r *Registry) registerGameCommands() {
|
||||
Description: "List your stored games, newest first (login required)",
|
||||
Usage: "games [more]",
|
||||
Handler: gamesHandler,
|
||||
Paged: true,
|
||||
})
|
||||
|
||||
r.Register(&Command{
|
||||
@@ -107,6 +110,7 @@ func (r *Registry) registerGameCommands() {
|
||||
Description: "Print or save a stored game as PGN",
|
||||
Usage: "pgn [gameId] [ply] [save <path>] (default: current or replayed game)",
|
||||
Handler: pgnHandler,
|
||||
Paged: true,
|
||||
})
|
||||
|
||||
r.Register(&Command{
|
||||
@@ -259,8 +263,8 @@ func joinGameHandler(s *session.Session, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("%sJoined game: %s%s\n", display.Green, gameID, display.Reset)
|
||||
fmt.Printf("Turn: %s | State: %s | Moves: %d\n", resp.Turn, resp.State, len(resp.Moves))
|
||||
display.Printf("%sJoined game: %s%s\n", display.Green, gameID, display.Reset)
|
||||
display.Printf("Turn: %s | State: %s | Moves: %d\n", resp.Turn, resp.State, len(resp.Moves))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -368,9 +372,9 @@ func computerMoveHandler(s *session.Session, args []string) error {
|
||||
if final.LastMove != nil && len(final.Moves) > baselineMoves {
|
||||
display.Print(display.Magenta, "Computer played: %s", final.LastMove.Move)
|
||||
if final.LastMove.Depth > 0 {
|
||||
fmt.Printf(" (depth %d, score %d)", final.LastMove.Depth, final.LastMove.Score)
|
||||
display.Printf(" (depth %d, score %d)", final.LastMove.Depth, final.LastMove.Score)
|
||||
}
|
||||
fmt.Println()
|
||||
display.Line()
|
||||
}
|
||||
|
||||
printOutcome(final)
|
||||
@@ -546,28 +550,28 @@ func showBoardHandler(s *session.Session, args []string) error {
|
||||
s.SetGameState(game)
|
||||
|
||||
// Display board with colors
|
||||
fmt.Println()
|
||||
display.Line()
|
||||
display.RenderBoard(board.Board)
|
||||
|
||||
// Display game info
|
||||
fmt.Printf("\nFEN: %s\n", game.FEN)
|
||||
fmt.Printf("Turn: %s | State: %s | Moves: %d\n",
|
||||
display.Printf("\nFEN: %s\n", game.FEN)
|
||||
display.Printf("Turn: %s | State: %s | Moves: %d\n",
|
||||
display.ColorForTurn(game.Turn), game.State, len(game.Moves))
|
||||
|
||||
// Display move history
|
||||
if len(game.Moves) > 0 {
|
||||
fmt.Printf("\nHistory: ")
|
||||
display.Printf("\nHistory: ")
|
||||
for i, move := range game.Moves {
|
||||
if i > 0 {
|
||||
fmt.Print(" ")
|
||||
display.Write(" ")
|
||||
}
|
||||
if i%2 == 0 {
|
||||
fmt.Printf("%d.%s", (i/2)+1, move)
|
||||
display.Printf("%d.%s", (i/2)+1, move)
|
||||
} else {
|
||||
fmt.Printf(" %s", move)
|
||||
display.Printf(" %s", move)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
display.Line()
|
||||
}
|
||||
|
||||
// Display last move info
|
||||
@@ -576,11 +580,11 @@ func showBoardHandler(s *session.Session, args []string) error {
|
||||
if game.LastMove.PlayerColor == "b" {
|
||||
color = "Black"
|
||||
}
|
||||
fmt.Printf("Last move: %s by %s", game.LastMove.Move, color)
|
||||
display.Printf("Last move: %s by %s", game.LastMove.Move, color)
|
||||
if game.LastMove.Depth > 0 {
|
||||
fmt.Printf(" (depth %d, score %d)", game.LastMove.Depth, game.LastMove.Score)
|
||||
display.Printf(" (depth %d, score %d)", game.LastMove.Depth, game.LastMove.Score)
|
||||
}
|
||||
fmt.Println()
|
||||
display.Line()
|
||||
}
|
||||
printOutcome(game)
|
||||
printConcession(game)
|
||||
@@ -630,7 +634,7 @@ func deleteGameHandler(s *session.Session, args []string) error {
|
||||
s.SetLastMoveCount(0)
|
||||
}
|
||||
|
||||
fmt.Printf("%sLive game unloaded (history retained): %s%s\n", display.Green, gameID, display.Reset)
|
||||
display.Printf("%sLive game unloaded (history retained): %s%s\n", display.Green, gameID, display.Reset)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -661,7 +665,7 @@ func pollHandler(s *session.Session, args []string) error {
|
||||
case len(resp.Moves) > moveCount:
|
||||
display.Println(display.Green, "Game updated! New moves detected")
|
||||
if resp.LastMove != nil {
|
||||
fmt.Printf("Last move: %s\n", resp.LastMove.Move)
|
||||
display.Printf("Last move: %s\n", resp.LastMove.Move)
|
||||
}
|
||||
case len(resp.Moves) < moveCount:
|
||||
display.Println(display.Green, "Game updated: %d move(s) taken back", moveCount-len(resp.Moves))
|
||||
@@ -701,9 +705,9 @@ func gamesHandler(s *session.Session, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("\n%3s %-36s %-20s %-20s %-7s %5s %s\n", "#", "Game ID", "White", "Black", "Result", "Plies", "Started (UTC)")
|
||||
display.Printf("\n%3s %-36s %-20s %-20s %-7s %5s %s\n", "#", "Game ID", "White", "Black", "Result", "Plies", "Started (UTC)")
|
||||
for i, g := range resp.Games {
|
||||
fmt.Printf("%3d %-36s %-20s %-20s %-7s %5d %s\n", i+1, g.GameID,
|
||||
display.Printf("%3d %-36s %-20s %-20s %-7s %5d %s\n", i+1, g.GameID,
|
||||
playerLabel(g.Players.White), playerLabel(g.Players.Black), g.PGNResult, g.MoveCount,
|
||||
g.StartTimeUTC.UTC().Format("2006-01-02 15:04"))
|
||||
}
|
||||
@@ -761,7 +765,7 @@ func pgnHandler(s *session.Session, args []string) error {
|
||||
display.Println(display.Green, "Saved PGN of %s to %s", describePGNTarget(gameID, ply), path)
|
||||
return nil
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Print(text)
|
||||
display.Line()
|
||||
display.Write(text)
|
||||
return nil
|
||||
}
|
||||
@@ -3,15 +3,15 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/lixenwraith/chess/internal/client/display"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
func readPassword(prompt string) (string, error) {
|
||||
fmt.Print(prompt)
|
||||
display.Write(prompt)
|
||||
bytePassword, err := term.ReadPassword(0) // 0 is stdin
|
||||
fmt.Println()
|
||||
display.Line()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -16,6 +17,9 @@ type Command struct {
|
||||
Description string
|
||||
Usage string
|
||||
Handler func(*session.Session, []string) error
|
||||
// Paged commands print at length and read nothing: their output is
|
||||
// shown one screen at a time (display.Page).
|
||||
Paged bool
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
@@ -43,6 +47,7 @@ func NewRegistry(s *session.Session) *Registry {
|
||||
Description: "Show available commands",
|
||||
Usage: "help [command]",
|
||||
Handler: r.helpHandler,
|
||||
Paged: true,
|
||||
})
|
||||
|
||||
// Exit command (handled in main loop, but registered for help display)
|
||||
@@ -89,6 +94,18 @@ func (r *Registry) Execute(input string) {
|
||||
cl.SetVerbose(r.session.IsVerbose())
|
||||
}
|
||||
|
||||
if cmd.Paged && r.session.ReadLine != nil {
|
||||
var buf bytes.Buffer
|
||||
previous := display.Out
|
||||
display.Out = &buf
|
||||
err := cmd.Handler(r.session, args)
|
||||
display.Out = previous
|
||||
display.Page(buf.String(), r.session.ReadLine)
|
||||
if err != nil {
|
||||
display.Println(display.Red, "Error: %s", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := cmd.Handler(r.session, args); err != nil {
|
||||
display.Println(display.Red, "Error: %s", err.Error())
|
||||
}
|
||||
@@ -101,13 +118,13 @@ func (r *Registry) helpHandler(s *session.Session, args []string) error {
|
||||
if !exists {
|
||||
return fmt.Errorf("unknown command: %s", args[0])
|
||||
}
|
||||
fmt.Println()
|
||||
display.Line()
|
||||
display.Print(display.Cyan, cmd.Name)
|
||||
display.Println(display.Reset, " - %s", cmd.Description)
|
||||
if cmd.ShortName != "" {
|
||||
display.Println(display.Cyan, "Short form: %s", cmd.ShortName)
|
||||
}
|
||||
fmt.Printf("Usage: %s\n", cmd.Usage)
|
||||
display.Printf("Usage: %s\n", cmd.Usage)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -173,10 +190,10 @@ func (r *Registry) helpHandler(s *session.Session, args []string) error {
|
||||
if info.shortName != "" {
|
||||
shortPart = fmt.Sprintf("[%s%s%s] ", display.Cyan, info.shortName, display.Reset)
|
||||
}
|
||||
fmt.Printf(" %s%-10s %s\n", shortPart, cmd.Name, cmd.Description)
|
||||
display.Printf(" %s%-10s %s\n", shortPart, cmd.Name, cmd.Description)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
display.Line()
|
||||
}
|
||||
|
||||
printCommandGroup("Game Commands", gameCommands)
|
||||
@@ -185,7 +202,8 @@ func (r *Registry) helpHandler(s *session.Session, args []string) error {
|
||||
printCommandGroup("Utility Commands", utilCommands)
|
||||
|
||||
display.Println(display.Reset, "Type 'help <command>' for detailed usage")
|
||||
display.Println(display.Reset, "Add '-v' to any command for verbose output\n")
|
||||
display.Println(display.Reset, "Add '-v' to any command for verbose output")
|
||||
display.Println(display.Reset, "Long output pauses after each screen: Enter continues, q stops\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ func (r *Registry) registerReplayCommands() {
|
||||
Description: "Step through a stored game",
|
||||
Usage: "replay <gameId|index> (index from the last 'games' listing)",
|
||||
Handler: replayHandler,
|
||||
Paged: true,
|
||||
})
|
||||
r.Register(&Command{
|
||||
Name: "next",
|
||||
@@ -122,7 +123,7 @@ func replayHandler(s *session.Session, args []string) error {
|
||||
|
||||
white, black := s.Replay.Players()
|
||||
display.Println(display.Cyan, "\nReplaying %s", history.GameID)
|
||||
fmt.Printf("%s vs %s · %s · %d plies · started %s\n",
|
||||
display.Printf("%s vs %s · %s · %d plies · started %s\n",
|
||||
display.C(display.Blue, white), display.C(display.Red, black), s.Replay.Result(), s.Replay.Len(),
|
||||
history.StartTimeUTC.UTC().Format("2006-01-02 15:04"))
|
||||
if c := history.Concession; c != nil {
|
||||
@@ -135,8 +136,8 @@ func replayHandler(s *session.Session, args []string) error {
|
||||
}
|
||||
display.Println(display.Yellow, "%s at ply %d; play continued.", what, c.Ply)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println(s.Replay.Movetext(80))
|
||||
display.Line()
|
||||
display.Line(s.Replay.Movetext(80))
|
||||
display.Println(display.Reset, "\nnext, prev, first, last, goto <ply>, auto [ms], fen, pgn [save <path>], live")
|
||||
return showReplayPosition(s)
|
||||
}
|
||||
@@ -217,7 +218,7 @@ func fenHandler(s *session.Session, args []string) error {
|
||||
if s.Replay == nil {
|
||||
return errNotReplaying
|
||||
}
|
||||
fmt.Println(s.Replay.FEN())
|
||||
display.Line(s.Replay.FEN())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -241,13 +242,13 @@ func showReplayPosition(s *session.Session) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("stored position cannot be shown: %w", err)
|
||||
}
|
||||
fmt.Println()
|
||||
display.Line()
|
||||
display.RenderBoard(board)
|
||||
label := r.Label(r.Ply)
|
||||
if r.Ply == 0 {
|
||||
label = "initial position"
|
||||
}
|
||||
fmt.Printf("\nPly %d/%d %s\n", r.Ply, r.Len(), display.C(display.Cyan, label))
|
||||
display.Printf("\nPly %d/%d %s\n", r.Ply, r.Len(), display.C(display.Cyan, label))
|
||||
if r.AtEnd() && r.History.Result != "" {
|
||||
display.Println(display.Green, "Result: %s", r.Result())
|
||||
}
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
package command
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/lixenwraith/chess/internal/client/display"
|
||||
)
|
||||
|
||||
// savePGN has no file system in the browser: the PGN is printed instead,
|
||||
// to be copied from the terminal.
|
||||
func savePGN(path, text string) error {
|
||||
fmt.Print(text)
|
||||
display.Write(text)
|
||||
return fmt.Errorf("files cannot be saved from the browser; the PGN is printed above")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,11 @@ type Session struct {
|
||||
Client *api.Client
|
||||
// Replay is the stored game being stepped through, nil while live. The
|
||||
// live game, user, and poll state above are untouched by a replay.
|
||||
Replay *replay.State
|
||||
Verbose bool
|
||||
Replay *replay.State
|
||||
// ReadLine reads the next input line (false at end of input); the pager
|
||||
// uses it to wait between screens.
|
||||
ReadLine func() (string, bool)
|
||||
Verbose bool
|
||||
// Game state for prompt
|
||||
CurrentGameState *api.GameResponse
|
||||
PlayerColor string // "w", "b", or ""
|
||||
|
||||
Reference in new issue
Block a user