Compare commits

..
7 Commits
Author SHA1 Message Date
cjdenio 1f208f80b7 update readme 2026-10-03 10:52:41 -06:00
cjdenio 19e295493e oops 2026-10-03 10:50:02 -06:00
cjdenio 6242535184 note + fix bug 2026-10-03 10:46:20 -06:00
cjdenio d50b572fb5 license 2026-10-02 09:11:04 -06:00
cjdenio 7b35f86e32 fix issues 2026-10-01 16:07:45 -06:00
cjdenio 419f0ae3cf handle empty copyright 2026-10-01 10:40:54 -06:00
cjdenio b918c5be23 parse rebus cells 2026-09-29 22:50:24 -04:00
7 changed files with 113 additions and 20 deletions

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Caleb Denio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1 -1
View File
@@ -2,7 +2,7 @@ Very silly terminal-based crossword solver. It can play .puz / .ipuz files from
- Requirements: [Go](https://go.dev)
- Build: `make`
- Usage: `./bin/crossword <file>.puz`
- Usage: `./bin/crossword <file>.{ipuz,puz}`
![demo](demo.gif)
+34 -10
View File
@@ -40,6 +40,7 @@ type State struct {
SolveState SolveState
CheckState []rune
UIState UIState
NoteVisible bool
}
func RenderWithLineWrap(s string, width int) (out string, lines int) {
@@ -327,8 +328,17 @@ func (state *State) RenderUI(w io.Writer, width int) int {
uiHeight += 2
fmt.Fprintf(w, "AUTHOR: %s\r\n", state.Puzzle.Author)
uiHeight += 1
if state.Puzzle.Note != "" && state.NoteVisible {
note, noteLines := RenderWithLineWrap(state.Puzzle.Note, min(width, 60))
fmt.Fprintf(w, "\r\n%s\r\n", AnsiDimmed(note))
uiHeight += noteLines + 2
}
fmt.Fprint(w, state.RenderPuzzle()+"\r\n")
uiHeight += state.Puzzle.Height + 3
if state.SelectedClue != nil {
directionStr := "down"
if state.SelectedClue.Direction == puz.DirectionAcross {
@@ -341,22 +351,29 @@ func (state *State) RenderUI(w io.Writer, width int) int {
io.WriteString(w, "\r\n")
uiHeight += 1
}
fmt.Fprintf(w, "%s\r\n", state.Puzzle.Copyright)
uiHeight += 1
if state.Puzzle.Copyright != "" {
fmt.Fprintf(w, "%s\r\n\r\n", state.Puzzle.Copyright)
uiHeight += 2
}
switch state.UIState {
case StateGame:
fmt.Fprintln(w, AnsiDimmed("\r\n[enter]/[tab]: next word | [ctrl+a]: menu | [ctrl+c]: exit"))
uiHeight += 2
line := "[enter]/[tab]: next word | [ctrl+a]: menu"
if state.Puzzle.Note != "" {
line += " | [ctrl+n]: toggle note"
}
line += " | [ctrl+c]: exit\r\n"
fmt.Fprint(w, AnsiDimmed(line))
uiHeight += 1
case StateMenu:
fmt.Fprintln(w, AnsiDimmed("\r\n[c]: check puzzle/word | [r]: reveal puzzle/word | [x]: clear grid | [q]: exit menu"))
uiHeight += 2
fmt.Fprint(w, AnsiDimmed("[c]: check puzzle/word | [r]: reveal puzzle/word | [x]: clear grid | [q]: exit menu\r\n"))
uiHeight += 1
case StateCheckMenu:
fmt.Fprintln(w, AnsiDimmed("\r\n[w]: check word | [p]: check puzzle | [q]: exit menu"))
uiHeight += 2
fmt.Fprint(w, AnsiDimmed("[w]: check word | [p]: check puzzle | [q]: exit menu\r\n"))
uiHeight += 1
case StateRevealMenu:
fmt.Fprintln(w, AnsiDimmed("\r\n[w]: reveal word | [p]: reveal puzzle | [q]: exit menu"))
uiHeight += 2
fmt.Fprint(w, AnsiDimmed("[w]: reveal word | [p]: reveal puzzle | [q]: exit menu\r\n"))
uiHeight += 1
}
switch state.SolveState {
@@ -584,6 +601,11 @@ func (state *State) HandleInput(buffer []byte) (exited bool) {
return false
}
if buffer[0] == '\x0E' {
state.NoteVisible = !state.NoteVisible
return false
}
if buffer[0] == ' ' {
state.SwitchDirections()
}
@@ -626,10 +648,12 @@ func (state *State) HandleInput(buffer []byte) (exited bool) {
// is there a filled cell underneath the cursor?
if state.PuzzleState[state.SelectedCell] != '-' {
state.PuzzleState[state.SelectedCell] = '-'
state.CheckState[state.SelectedCell] = 0x00
} else {
i := slices.Index(state.SelectedClue.Cells, state.SelectedCell)
if i > 0 {
state.PuzzleState[state.SelectedClue.Cells[i-1]] = '-' // clear the previous cell
state.CheckState[state.SelectedClue.Cells[i-1]] = 0x00
state.SelectedCell = state.SelectedClue.Cells[i-1]
}
}
+7 -2
View File
@@ -21,6 +21,7 @@ type iPuzFile struct {
Title string `json:"title"`
Author string `json:"author"`
Copyright string `json:"copyright"`
Notes string `json:"notes"`
Puzzle [][]any `json:"puzzle"`
Solution [][]string `json:"solution"`
Dimensions iPuzDimensions `json:"dimensions"`
@@ -63,9 +64,12 @@ func ParseIPuz(file []byte) (*Puzzle, error) {
puzzle := new(Puzzle)
puzzle.RebusCells = make(map[int]string)
puzzle.Title = parsed.Title
puzzle.Author = parsed.Author
puzzle.Copyright = parsed.Copyright
puzzle.Note = parsed.Notes
puzzle.Width = parsed.Dimensions.Width
puzzle.Height = parsed.Dimensions.Height
@@ -95,12 +99,13 @@ func ParseIPuz(file []byte) (*Puzzle, error) {
puzzle.State = state.String()
solution := strings.Builder{}
for _, row := range parsed.Solution {
for _, cell := range row {
for y, row := range parsed.Solution {
for x, cell := range row {
if cell == "#" {
solution.WriteRune('.')
} else if len(cell) > 1 {
solution.WriteByte(cell[0])
puzzle.RebusCells[y*puzzle.Width+x] = cell
puzzle.HasRebus = true
} else if len(cell) == 1 {
solution.WriteString(cell)
+31 -3
View File
@@ -7,6 +7,8 @@ import (
"errors"
"io"
"math"
"regexp"
"strconv"
"strings"
"golang.org/x/text/encoding/charmap"
@@ -123,6 +125,8 @@ func ParsePuz(file []byte) (*Puzzle, error) {
puzzle := new(Puzzle)
puzzle.RebusCells = make(map[int]string)
_, err = r.Seek(10, io.SeekCurrent)
if err != nil {
return nil, err
@@ -211,11 +215,13 @@ func ParsePuz(file []byte) (*Puzzle, error) {
clues = append(clues, clue)
}
// read the "note" field, currently unused
_, err = readText(buf, string(version[0]))
note, err := readText(buf, string(version[0]))
if err != nil {
return nil, err
}
puzzle.Note = note
extraBlocks := make(map[string]*ExtraBlock)
for {
extra, err := readExtraBlock(buf)
@@ -224,8 +230,30 @@ func ParsePuz(file []byte) (*Puzzle, error) {
} else if err != nil {
return nil, err
}
if extra.Name == "RTBL" {
extraBlocks[extra.Name] = extra
}
grbs, grbsOk := extraBlocks["GRBS"]
trbl, trblOk := extraBlocks["RTBL"]
if grbsOk && trblOk {
puzzle.HasRebus = true
rebusCellMap := make(map[int][]int)
for i, b := range grbs.Data {
if b > 0 {
rebusCellMap[int(b)-1] = append(rebusCellMap[int(b)-1], i)
}
}
rebusData := regexp.MustCompile(`\s*(\d+):(\w+);\s*`).FindAllStringSubmatch(string(trbl.Data), -1)
for _, rebus := range rebusData {
cell, err := strconv.Atoi(rebus[1])
if err != nil {
continue
}
for _, c := range rebusCellMap[cell] {
puzzle.RebusCells[c] = rebus[2]
}
}
}
+3
View File
@@ -29,6 +29,7 @@ type Puzzle struct {
Title string
Author string
Copyright string
Note string
Solution string
State string
@@ -38,6 +39,8 @@ type Puzzle struct {
Cells [][2]*Clue
HasRebus bool
RebusCells map[int]string
}
func LoadPuzzle(r io.Reader) (*Puzzle, error) {
+16 -4
View File
@@ -39,8 +39,20 @@ func main() {
io.WriteString(s, "\x1b[?25h")
}()
pty, _, _ := s.Pty() // TODO: actually handle terminal size changes
uiHeight := state.RenderUI(s, pty.Window.Width)
pty, winCh, isPty := s.Pty()
if !isPty {
return
}
windowWidth := pty.Window.Width
uiHeight := state.RenderUI(s, windowWidth)
go func() {
for window := range winCh {
windowWidth = window.Width
}
}()
scanner := bufio.NewScanner(s)
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
@@ -72,14 +84,14 @@ func main() {
state.SelectedCell = -1
fmt.Fprintf(s, "\r\x1b[%dA", uiHeight)
fmt.Fprint(s, "\x1b[J")
state.RenderUI(s, pty.Window.Width)
state.RenderUI(s, windowWidth)
break
}
fmt.Fprintf(s, "\r\x1b[%dA", uiHeight)
fmt.Fprint(s, "\x1b[J")
uiHeight = state.RenderUI(s, pty.Window.Width)
uiHeight = state.RenderUI(s, windowWidth)
}
log.Printf("%s disconnected\n", s.RemoteAddr().String())