From 6109f70a45a3a545777579eebe94dd625c981e8e Mon Sep 17 00:00:00 2001 From: Caleb Denio Date: Wed, 9 Sep 2026 18:00:16 -0500 Subject: [PATCH] ssh --- README.md | 6 ++ game.go => game/game.go | 126 ++++++++++++++++++++++++++++++++++++---- go.mod | 10 +++- go.sum | 6 ++ main.go | 107 +++------------------------------- ssh/main.go | 80 +++++++++++++++++++++++++ 6 files changed, 222 insertions(+), 113 deletions(-) rename game.go => game/game.go (66%) create mode 100644 ssh/main.go diff --git a/README.md b/README.md index 8f0618f..e210bc7 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,9 @@ todo - fix bug with that one 6x6 puzzle - correctly calculate uiHeight with multi-line clues - timer +- reveal grid/word +- .puz checksums +- fix shift+enter in other terminals +- after entering letter, jump to next unfilled cell (even if it's backwards) +- tab/shift+tab +- clear grid diff --git a/game.go b/game/game.go similarity index 66% rename from game.go rename to game/game.go index 19878a4..7bea258 100644 --- a/game.go +++ b/game/game.go @@ -1,11 +1,13 @@ -package main +package game import ( "encoding/json" "errors" "fmt" + "io" "slices" "strings" + "unicode" "github.com/cjdenio/crossword/puz" ) @@ -189,7 +191,7 @@ func (state *State) CheckPuzzle() bool { return true } -func (state *State) RenderUI() int { +func (state *State) RenderUI(w io.Writer) int { selectedClueCells := []int{} if state.SelectedClue != nil { selectedClueCells = state.SelectedClue.Cells @@ -197,39 +199,39 @@ func (state *State) RenderUI() int { uiHeight := 0 - fmt.Printf("\r\nTITLE: %s\r\n", state.Puzzle.Title) + fmt.Fprintf(w, "\r\nTITLE: %s\r\n", state.Puzzle.Title) uiHeight += 2 - fmt.Printf("AUTHOR: %s\r\n", state.Puzzle.Author) + fmt.Fprintf(w, "AUTHOR: %s\r\n", state.Puzzle.Author) uiHeight += 1 - fmt.Print(RenderPuzzle(state.PuzzleState, state.Puzzle.Width, state.Puzzle.Height, selectedClueCells, state.SelectedCell) + "\r\n") + fmt.Fprint(w, RenderPuzzle(state.PuzzleState, state.Puzzle.Width, state.Puzzle.Height, selectedClueCells, state.SelectedCell)+"\r\n") uiHeight += state.Puzzle.Height + 3 if state.SelectedClue != nil { if state.SelectedClue.Direction == puz.DirectionAcross { - fmt.Printf("%d-across: %s\r\n\r\n", state.SelectedClue.Number, state.SelectedClue.Clue) + fmt.Fprintf(w, "%d-across: %s\r\n\r\n", state.SelectedClue.Number, state.SelectedClue.Clue) } else { - fmt.Printf("%d-down: %s\r\n\r\n", state.SelectedClue.Number, state.SelectedClue.Clue) + fmt.Fprintf(w, "%d-down: %s\r\n\r\n", state.SelectedClue.Number, state.SelectedClue.Clue) } uiHeight += 2 } - fmt.Printf("%s\r\n", state.Puzzle.Copyright) + fmt.Fprintf(w, "%s\r\n", state.Puzzle.Copyright) uiHeight += 1 switch state.SolveState { case 1: - fmt.Printf("\r\n%s\r\n", AnsiRed("The puzzle was filled, but at least 1 letter is incorrect...")) + fmt.Fprintf(w, "\r\n%s\r\n", AnsiRed("The puzzle was filled, but at least 1 letter is incorrect...")) uiHeight += 2 case 2: - fmt.Printf("\r\n%s\r\n", AnsiGreen("The puzzle was solved!")) + fmt.Fprintf(w, "\r\n%s\r\n", AnsiGreen("The puzzle was solved!")) uiHeight += 2 } if state.LastKeySequence != "" && state.DebugMode { - fmt.Printf("\r\n%s\r\n", AnsiDimmed(state.LastKeySequence)) + fmt.Fprintf(w, "\r\n%s\r\n", AnsiDimmed(state.LastKeySequence)) uiHeight += 2 } if state.Goodbye { - fmt.Print("\r\nsee ya\r\n") + fmt.Fprint(w, "\r\nsee ya\r\n") uiHeight += 2 } @@ -345,3 +347,103 @@ func (state *State) LoadSaveFile(f []byte) error { return nil } + +func (state *State) HandleInput(buffer []byte) (exited bool) { + state.LastKeySequence = fmt.Sprintf("%v", buffer) + + if buffer[0] == 3 { + return true + } + + if buffer[0] == ' ' { + switch state.SelectedClue.Direction { + case puz.DirectionAcross: + if state.Puzzle.Cells[state.SelectedCell][1] != nil { + state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][1] + } + case puz.DirectionDown: + if state.Puzzle.Cells[state.SelectedCell][0] != nil { + state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0] + } + } + } + + if buffer[0] >= 0x61 && buffer[0] <= 0x7a { + cellWasFilled := state.PuzzleState[state.SelectedCell] != '-' + state.PuzzleState[state.SelectedCell] = unicode.ToUpper(rune(buffer[0])) + + i := slices.Index(state.SelectedClue.Cells, state.SelectedCell) + + if !cellWasFilled { + // jump to next unfilled cell in clue + for x := i + 1; x < len(state.SelectedClue.Cells); x++ { + if state.PuzzleState[state.SelectedClue.Cells[x]] == '-' { + state.SelectedCell = state.SelectedClue.Cells[x] + break + } + } + } else { + // jump to next cell + if i < len(state.SelectedClue.Cells)-1 { + state.SelectedCell = state.SelectedClue.Cells[i+1] + } + } + + if state.GridFilled() { + if state.CheckPuzzle() { + state.SolveState = 2 + } else { + state.SolveState = 1 + } + } else { + state.SolveState = 0 + } + } + + if buffer[0] == 0x7f { + // is there a filled cell underneath the cursor? + if state.PuzzleState[state.SelectedCell] != '-' { + state.PuzzleState[state.SelectedCell] = '-' + } else { + i := slices.Index(state.SelectedClue.Cells, state.SelectedCell) + if i > 0 { + state.PuzzleState[state.SelectedClue.Cells[i-1]] = '-' // clear the previous cell + state.SelectedCell = state.SelectedClue.Cells[i-1] + } + } + } + + if buffer[0] == '\r' { + state.NextWord() + } + if buffer[0] == '~' { + state.PreviousWord() + } + + if string(buffer[0:2]) == "\x1b[" { + if (buffer[2] == 68 || buffer[2] == 67) && state.SelectedClue != nil && state.SelectedClue.Direction == puz.DirectionDown { + state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0] + } else if (buffer[2] == 65 || buffer[2] == 66) && state.SelectedClue != nil && state.SelectedClue.Direction == puz.DirectionAcross { + state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][1] + } else { + switch buffer[2] { + case 68: // left + state.MoveCursor(3) + case 67: // right + state.MoveCursor(1) + case 65: // up + state.MoveCursor(0) + case 66: // down + state.MoveCursor(2) + } + + switch state.SelectedClue.Direction { + case puz.DirectionAcross: + state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0] + case puz.DirectionDown: + state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][1] + } + } + } + return false +} diff --git a/go.mod b/go.mod index 7394634..0475719 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,12 @@ require ( golang.org/x/text v0.41.0 ) -require golang.org/x/sys v0.47.0 // indirect +require ( + github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect + golang.org/x/crypto v0.31.0 // indirect +) + +require ( + github.com/gliderlabs/ssh v0.3.8 + golang.org/x/sys v0.47.0 // indirect +) diff --git a/go.sum b/go.sum index 7283604..f221832 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,9 @@ +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= diff --git a/main.go b/main.go index f48b015..9cd279a 100644 --- a/main.go +++ b/main.go @@ -6,9 +6,8 @@ import ( "fmt" "log" "os" - "slices" - "unicode" + "github.com/cjdenio/crossword/game" "github.com/cjdenio/crossword/puz" "golang.org/x/term" ) @@ -51,7 +50,7 @@ func main() { defer term.Restore(int(os.Stdin.Fd()), termState) - state := State{ + state := game.State{ Puzzle: puzzle, PuzzleState: []rune(puzzle.State), SelectedClue: puzzle.Clues[0], @@ -70,7 +69,7 @@ func main() { } } - uiHeight := state.RenderUI() + uiHeight := state.RenderUI(os.Stdout) scanner := bufio.NewScanner(os.Stdin) scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { @@ -93,9 +92,8 @@ func main() { scanner.Scan() buffer := scanner.Bytes() - state.LastKeySequence = fmt.Sprintf("%v", buffer) - - if buffer[0] == 3 { + exited := state.HandleInput(buffer) + if exited { saveFile, err := state.CreateSaveFile() if err == nil { err = os.WriteFile(filename+".save", saveFile, fileInfo.Mode()) @@ -108,104 +106,13 @@ func main() { state.Goodbye = true fmt.Printf("\r\x1b[%dA", uiHeight) fmt.Print("\x1b[J") - state.RenderUI() + state.RenderUI(os.Stdout) return } - if buffer[0] == ' ' { - switch state.SelectedClue.Direction { - case puz.DirectionAcross: - if state.Puzzle.Cells[state.SelectedCell][1] != nil { - state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][1] - } - case puz.DirectionDown: - if state.Puzzle.Cells[state.SelectedCell][0] != nil { - state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0] - } - } - } - - if buffer[0] >= 0x61 && buffer[0] <= 0x7a { - cellWasFilled := state.PuzzleState[state.SelectedCell] != '-' - state.PuzzleState[state.SelectedCell] = unicode.ToUpper(rune(buffer[0])) - - i := slices.Index(state.SelectedClue.Cells, state.SelectedCell) - - if !cellWasFilled { - // jump to next unfilled cell in clue - for x := i + 1; x < len(state.SelectedClue.Cells); x++ { - if state.PuzzleState[state.SelectedClue.Cells[x]] == '-' { - state.SelectedCell = state.SelectedClue.Cells[x] - break - } - } - } else { - // jump to next cell - if i < len(state.SelectedClue.Cells)-1 { - state.SelectedCell = state.SelectedClue.Cells[i+1] - } - } - - if state.GridFilled() { - if state.CheckPuzzle() { - state.SolveState = 2 - } else { - state.SolveState = 1 - } - } else { - state.SolveState = 0 - } - } - - if buffer[0] == 0x7f { - // is there a filled cell underneath the cursor? - if state.PuzzleState[state.SelectedCell] != '-' { - state.PuzzleState[state.SelectedCell] = '-' - } else { - i := slices.Index(state.SelectedClue.Cells, state.SelectedCell) - if i > 0 { - state.PuzzleState[state.SelectedClue.Cells[i-1]] = '-' // clear the previous cell - state.SelectedCell = state.SelectedClue.Cells[i-1] - } - } - } - - if buffer[0] == '\r' { - state.NextWord() - } - if buffer[0] == '~' { - state.PreviousWord() - } - - if string(buffer[0:2]) == "\x1b[" { - if (buffer[2] == 68 || buffer[2] == 67) && state.SelectedClue != nil && state.SelectedClue.Direction == puz.DirectionDown { - state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0] - } else if (buffer[2] == 65 || buffer[2] == 66) && state.SelectedClue != nil && state.SelectedClue.Direction == puz.DirectionAcross { - state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][1] - } else { - switch buffer[2] { - case 68: // left - state.MoveCursor(3) - case 67: // right - state.MoveCursor(1) - case 65: // up - state.MoveCursor(0) - case 66: // down - state.MoveCursor(2) - } - - switch state.SelectedClue.Direction { - case puz.DirectionAcross: - state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0] - case puz.DirectionDown: - state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][1] - } - } - } - fmt.Printf("\r\x1b[%dA", uiHeight) fmt.Print("\x1b[J") - uiHeight = state.RenderUI() + uiHeight = state.RenderUI(os.Stdout) } } diff --git a/ssh/main.go b/ssh/main.go new file mode 100644 index 0000000..7d114c2 --- /dev/null +++ b/ssh/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "log" + "os" + + "github.com/cjdenio/crossword/game" + "github.com/cjdenio/crossword/puz" + "github.com/gliderlabs/ssh" +) + +func main() { + ssh.Handle(func(s ssh.Session) { + file, err := os.ReadFile(os.Args[1]) + if err != nil { + log.Fatal(err) + } + puzzle, err := puz.ParsePuz(file) + if err != nil { + log.Fatal(err) + } + + state := game.State{ + Puzzle: puzzle, + PuzzleState: []rune(puzzle.State), + SelectedClue: puzzle.Clues[0], + SelectedCell: puzzle.Clues[0].Cells[0], + } + + io.WriteString(s, "\x1b[?25l") + + defer func() { + io.WriteString(s, "\x1b[?25h") + }() + + uiHeight := state.RenderUI(s) + + scanner := bufio.NewScanner(s) + scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { + if len(data) == 0 { + return 0, nil, bufio.ErrFinalToken + } + + if data[0] == 0x1b { + if len(data) < 3 { + return 0, nil, nil + } + + return 3, data[0:3], nil + } + + return 1, data[0:1], nil + }) + + for { + scanner.Scan() + buffer := scanner.Bytes() + + exited := state.HandleInput(buffer) + if exited { + state.SelectedClue = nil + state.SelectedCell = -1 + state.Goodbye = true + fmt.Fprintf(s, "\r\x1b[%dA", uiHeight) + fmt.Fprint(s, "\x1b[J") + state.RenderUI(s) + return + } + + fmt.Fprintf(s, "\r\x1b[%dA", uiHeight) + fmt.Fprint(s, "\x1b[J") + + uiHeight = state.RenderUI(s) + } + }) + log.Fatal(ssh.ListenAndServe(":1234", nil)) +}