Files
crossword/main.go
T
2026-09-01 21:56:48 -06:00

186 lines
3.5 KiB
Go

package main
import (
"fmt"
"log"
"os"
"slices"
"strings"
"github.com/cjdenio/crossword/puz"
"golang.org/x/term"
)
const (
AnsiInvert string = "\x1b[7m"
AnsiReset string = "\x1b[m"
AnsiWhiteBackground string = "\x1b[100m"
)
func AnsiInverted(s string) string {
return AnsiInvert + s + AnsiReset
}
func AnsiWhiteBackgrounded(s string) string {
return AnsiWhiteBackground + s + AnsiReset
}
func RenderPuzzle(puzzle string, width, height int, selectedClueCells []int, selectedCell int) string {
b := strings.Builder{}
b.WriteRune('┌')
for range (width * 2) + 1 {
b.WriteRune('─')
}
b.WriteString("┐\r\n")
for index, char := range puzzle {
if index%width == 0 {
b.WriteString("│ ")
}
cell := ""
switch char {
case '.':
cell = string(rune(0x2588))
case '-':
cell = "_"
default:
cell = string(char)
}
if selectedCell == index {
cell = AnsiInverted(cell)
} else if slices.Contains(selectedClueCells, index) {
cell = AnsiWhiteBackgrounded(cell)
}
b.WriteString(cell)
if (index+1)%width == 0 {
b.WriteString(" │\r\n")
} else if char == '.' && puzzle[index+1] == '.' {
b.WriteRune(0x2588)
} else {
// inefficient
if slices.Contains(selectedClueCells, index) && slices.Contains(selectedClueCells, index+1) {
b.WriteString(AnsiWhiteBackgrounded(" "))
} else {
b.WriteRune(' ')
}
}
}
b.WriteRune('└')
for range (width * 2) + 1 {
b.WriteRune('─')
}
b.WriteString("┘\r\n")
return b.String()
}
type State struct {
Puzzle *puz.Puzzle
SelectedCell int
SelectedClue *puz.Clue
Goodbye bool
}
func RenderUI(state *State) int {
selectedClueCells := []int{}
if state.SelectedClue != nil {
selectedClueCells = state.SelectedClue.Cells
}
uiHeight := 0
fmt.Printf("\r\nTITLE: %s\r\n", state.Puzzle.Title)
uiHeight += 2
fmt.Printf("AUTHOR: %s\r\n", state.Puzzle.Author)
uiHeight += 1
fmt.Print(RenderPuzzle(state.Puzzle.State, state.Puzzle.Width, state.Puzzle.Height, selectedClueCells, state.SelectedCell) + "\r\n")
uiHeight += state.Puzzle.Height + 3
if state.SelectedClue != nil {
fmt.Printf("%d: %s\r\n\r\n", state.SelectedClue.Number, state.SelectedClue.Clue)
uiHeight += 2
}
fmt.Printf("%s\r\n", state.Puzzle.Copyright)
uiHeight += 1
if state.Goodbye {
fmt.Print("\r\nsee ya\r\n")
uiHeight += 2
}
return uiHeight
}
func main() {
fmt.Print("\x1b[?25l")
defer func() {
fmt.Print("\x1b[?25h")
}()
termState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
log.Fatal(err)
}
defer term.Restore(int(os.Stdin.Fd()), termState)
filename := os.Args[1]
file, err := os.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
puzzle, err := puz.ParsePuz(file)
if err != nil {
log.Fatal(err)
}
state := State{
Puzzle: puzzle,
SelectedClue: puzzle.Clues[0],
SelectedCell: puzzle.Clues[0].Cells[0],
}
uiHeight := RenderUI(&state)
for {
buffer := make([]byte, 3)
_, err = os.Stdin.Read(buffer)
if err != nil {
log.Fatal(err)
}
if buffer[0] == 3 {
state.SelectedClue = nil
state.SelectedCell = -1
state.Goodbye = true
fmt.Printf("\r\x1b[%dA", uiHeight)
fmt.Print("\x1b[J")
RenderUI(&state)
return
}
switch buffer[2] {
case 68:
state.SelectedCell--
case 67:
state.SelectedCell++
case 65:
state.SelectedCell -= puzzle.Width
case 66:
state.SelectedCell += puzzle.Width
}
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0]
fmt.Printf("\r\x1b[%dA", uiHeight)
fmt.Print("\x1b[J")
uiHeight = RenderUI(&state)
}
}