v0.15.0 password recovery, phone layout, CLI pager, embed policy fixes
test / test (push) Canceled after 0s
test / e2e (push) Canceled after 0s

This commit is contained in:
lixen committed 2026-10-19 18:15:31 -04:00
1 parent a999177f0c
commit e1fa5242e3
48 files changed
+2101 -135

No files matched your search

+10 -10
View File
@@ -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
}
+4 -4
View File
@@ -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
}
+25 -21
View File
@@ -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 -3
View File
@@ -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
}
+23 -5
View File
@@ -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
}
+7 -6
View File
@@ -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())
}
+6 -2
View File
@@ -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")
}