handle line breaks in clues

This commit is contained in:
cjdenio committed 2026-09-29 13:47:29 -04:00
1 parent 21f9caec34
commit c139490dc2
5 files changed
+70 -12

No files matched your search

-1
View File
@@ -10,7 +10,6 @@ Very silly terminal-based crossword solver. It can play .puz / .ipuz files from
todo todo
- correctly calculate uiHeight with multi-line clues
- timer - timer
- reveal grid/word - reveal grid/word
- .puz checksums - .puz checksums
+8 -3
View File
@@ -13,6 +13,11 @@ import (
"golang.org/x/term" "golang.org/x/term"
) )
func getTermWidth() int {
w, _, _ := term.GetSize(int(os.Stdin.Fd()))
return w
}
func main() { func main() {
defer func() { defer func() {
if r := recover(); r != nil { 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 := bufio.NewScanner(os.Stdin)
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
@@ -108,13 +113,13 @@ func main() {
state.SelectedCell = -1 state.SelectedCell = -1
fmt.Printf("\r\x1b[%dA", uiHeight) fmt.Printf("\r\x1b[%dA", uiHeight)
fmt.Print("\x1b[J") fmt.Print("\x1b[J")
state.RenderUI(os.Stdout) state.RenderUI(os.Stdout, getTermWidth())
return return
} }
fmt.Printf("\r\x1b[%dA", uiHeight) fmt.Printf("\r\x1b[%dA", uiHeight)
fmt.Print("\x1b[J") fmt.Print("\x1b[J")
uiHeight = state.RenderUI(os.Stdout) uiHeight = state.RenderUI(os.Stdout, getTermWidth())
} }
} }
+50 -5
View File
@@ -32,6 +32,47 @@ type State struct {
RebusConfirmation bool 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 { func NewState(puzzle *puz.Puzzle) *State {
return &State{ return &State{
Puzzle: puzzle, 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 { 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") 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 return 2
@@ -258,12 +299,16 @@ func (state *State) RenderUI(w io.Writer) int {
fmt.Fprint(w, state.RenderPuzzle()+"\r\n") fmt.Fprint(w, state.RenderPuzzle()+"\r\n")
uiHeight += state.Puzzle.Height + 3 uiHeight += state.Puzzle.Height + 3
if state.SelectedClue != nil { if state.SelectedClue != nil {
directionStr := "down"
if state.SelectedClue.Direction == puz.DirectionAcross { if state.SelectedClue.Direction == puz.DirectionAcross {
fmt.Fprintf(w, "%d-across: %s\r\n\r\n", state.SelectedClue.Number, state.SelectedClue.Clue) directionStr = "across"
} else {
fmt.Fprintf(w, "%d-down: %s\r\n\r\n", state.SelectedClue.Number, state.SelectedClue.Clue)
} }
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) fmt.Fprintf(w, "%s\r\n", state.Puzzle.Copyright)
uiHeight += 1 uiHeight += 1
+8
View File
@@ -3,6 +3,7 @@ package puz
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"slices"
"strconv" "strconv"
"strings" "strings"
) )
@@ -16,6 +17,7 @@ type iPuzClue struct {
Clue string `json:"clue"` Clue string `json:"clue"`
} }
type iPuzFile struct { type iPuzFile struct {
Kind []string `json:"kind"`
Title string `json:"title"` Title string `json:"title"`
Author string `json:"author"` Author string `json:"author"`
Copyright string `json:"copyright"` Copyright string `json:"copyright"`
@@ -32,6 +34,12 @@ func ParseIPuz(file []byte) (*Puzzle, error) {
return nil, err 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 := new(Puzzle)
puzzle.Title = parsed.Title puzzle.Title = parsed.Title
+4 -3
View File
@@ -39,7 +39,8 @@ func main() {
io.WriteString(s, "\x1b[?25h") 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 := bufio.NewScanner(s)
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
@@ -71,14 +72,14 @@ func main() {
state.SelectedCell = -1 state.SelectedCell = -1
fmt.Fprintf(s, "\r\x1b[%dA", uiHeight) fmt.Fprintf(s, "\r\x1b[%dA", uiHeight)
fmt.Fprint(s, "\x1b[J") fmt.Fprint(s, "\x1b[J")
state.RenderUI(s) state.RenderUI(s, pty.Window.Width)
break break
} }
fmt.Fprintf(s, "\r\x1b[%dA", uiHeight) fmt.Fprintf(s, "\r\x1b[%dA", uiHeight)
fmt.Fprint(s, "\x1b[J") fmt.Fprint(s, "\x1b[J")
uiHeight = state.RenderUI(s) uiHeight = state.RenderUI(s, pty.Window.Width)
} }
log.Printf("%s disconnected\n", s.RemoteAddr().String()) log.Printf("%s disconnected\n", s.RemoteAddr().String())