From c139490dc2d4103377ee5756d2219a4c72156140 Mon Sep 17 00:00:00 2001 From: Caleb Denio Date: Tue, 29 Sep 2026 13:47:29 -0400 Subject: [PATCH] handle line breaks in clues --- README.md | 1 - cmd/main.go | 11 ++++++++--- game/game.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++----- puz/ipuz.go | 8 ++++++++ ssh/main.go | 7 ++++--- 5 files changed, 70 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d46b27a..87aeb3a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ Very silly terminal-based crossword solver. It can play .puz / .ipuz files from todo -- correctly calculate uiHeight with multi-line clues - timer - reveal grid/word - .puz checksums diff --git a/cmd/main.go b/cmd/main.go index 59aa6b6..19d13e8 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -13,6 +13,11 @@ import ( "golang.org/x/term" ) +func getTermWidth() int { + w, _, _ := term.GetSize(int(os.Stdin.Fd())) + return w +} + func main() { defer func() { if r := recover(); r != nil { @@ -72,7 +77,7 @@ func main() { } } - uiHeight := state.RenderUI(os.Stdout) + uiHeight := state.RenderUI(os.Stdout, getTermWidth()) scanner := bufio.NewScanner(os.Stdin) scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { @@ -108,13 +113,13 @@ func main() { state.SelectedCell = -1 fmt.Printf("\r\x1b[%dA", uiHeight) fmt.Print("\x1b[J") - state.RenderUI(os.Stdout) + state.RenderUI(os.Stdout, getTermWidth()) return } fmt.Printf("\r\x1b[%dA", uiHeight) fmt.Print("\x1b[J") - uiHeight = state.RenderUI(os.Stdout) + uiHeight = state.RenderUI(os.Stdout, getTermWidth()) } } diff --git a/game/game.go b/game/game.go index e0a0bc6..9f3459e 100644 --- a/game/game.go +++ b/game/game.go @@ -32,6 +32,47 @@ type State struct { RebusConfirmation bool } +func RenderWithLineWrap(s string, width int) (out string, lines int) { + builder := strings.Builder{} + runes := []rune(s) + + counter := 0 + lines = 1 + + for i := 0; i < len(runes); i++ { + char := runes[i] + + if char == '\n' { + builder.WriteString("\r\n") + lines++ + counter = 0 + continue + } + + if char == '\r' && i < len(runes)-1 && runes[i+1] == '\n' { + builder.WriteString("\r\n") + lines++ + counter = 0 + i++ + continue + } + + builder.WriteRune(char) + counter++ + + if counter >= width && i < len(runes)-1 { + lines++ + builder.WriteString("\r\n") + counter = 0 + } + } + + builder.WriteString("\r\n") + + out = builder.String() + return +} + func NewState(puzzle *puz.Puzzle) *State { return &State{ Puzzle: puzzle, @@ -243,7 +284,7 @@ func (state *State) CheckWord(clue *puz.Clue) { } } -func (state *State) RenderUI(w io.Writer) int { +func (state *State) RenderUI(w io.Writer, width int) int { if state.RebusConfirmation { fmt.Fprint(w, "This puzzle contains a rebus. This tool does not support rebuses.\r\nPress 'y' to continue anyway, or press any other key to exit.\r\n") return 2 @@ -258,12 +299,16 @@ func (state *State) RenderUI(w io.Writer) int { fmt.Fprint(w, state.RenderPuzzle()+"\r\n") uiHeight += state.Puzzle.Height + 3 if state.SelectedClue != nil { + directionStr := "down" if state.SelectedClue.Direction == puz.DirectionAcross { - fmt.Fprintf(w, "%d-across: %s\r\n\r\n", state.SelectedClue.Number, state.SelectedClue.Clue) - } else { - fmt.Fprintf(w, "%d-down: %s\r\n\r\n", state.SelectedClue.Number, state.SelectedClue.Clue) + directionStr = "across" } - uiHeight += 2 + + clueStr, clueLines := RenderWithLineWrap(fmt.Sprintf("%d-%s: %s", state.SelectedClue.Number, directionStr, state.SelectedClue.Clue), width) + w.Write([]byte(clueStr)) + uiHeight += clueLines + io.WriteString(w, "\r\n") + uiHeight += 1 } fmt.Fprintf(w, "%s\r\n", state.Puzzle.Copyright) uiHeight += 1 diff --git a/puz/ipuz.go b/puz/ipuz.go index ed28f34..dfdcd9f 100644 --- a/puz/ipuz.go +++ b/puz/ipuz.go @@ -3,6 +3,7 @@ package puz import ( "encoding/json" "errors" + "slices" "strconv" "strings" ) @@ -16,6 +17,7 @@ type iPuzClue struct { Clue string `json:"clue"` } type iPuzFile struct { + Kind []string `json:"kind"` Title string `json:"title"` Author string `json:"author"` Copyright string `json:"copyright"` @@ -32,6 +34,12 @@ func ParseIPuz(file []byte) (*Puzzle, error) { return nil, err } + if len(parsed.Kind) == 0 || !slices.ContainsFunc(parsed.Kind, func(k string) bool { + return strings.HasPrefix(k, "http://ipuz.org/crossword") + }) { + return nil, errors.New("ipuz file is not a crossword") + } + puzzle := new(Puzzle) puzzle.Title = parsed.Title diff --git a/ssh/main.go b/ssh/main.go index c800258..47f65c5 100644 --- a/ssh/main.go +++ b/ssh/main.go @@ -39,7 +39,8 @@ func main() { io.WriteString(s, "\x1b[?25h") }() - uiHeight := state.RenderUI(s) + pty, _, _ := s.Pty() // TODO: actually handle terminal size changes + uiHeight := state.RenderUI(s, pty.Window.Width) scanner := bufio.NewScanner(s) scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { @@ -71,14 +72,14 @@ func main() { state.SelectedCell = -1 fmt.Fprintf(s, "\r\x1b[%dA", uiHeight) fmt.Fprint(s, "\x1b[J") - state.RenderUI(s) + state.RenderUI(s, pty.Window.Width) break } fmt.Fprintf(s, "\r\x1b[%dA", uiHeight) fmt.Fprint(s, "\x1b[J") - uiHeight = state.RenderUI(s) + uiHeight = state.RenderUI(s, pty.Window.Width) } log.Printf("%s disconnected\n", s.RemoteAddr().String())