Replace .golangci.yml with the shared canonical config. The old config's top-level linters-settings block was silently ignored under the v2 schema, so the lll/funlen/cyclop/dupl thresholds now actually apply. The four repo-specific disables (mnd, exhaustive, paralleltest, testpackage) move out of the config into targeted in-code nolint directives carrying their original approval dates, keeping the config byte-identical to the canonical one. Fixes surfaced by the stricter settings: t.Parallel() added to all 32 tests, 24 overlong lines wrapped or their comments tightened, tcell control-code returns rewritten as character literals, dupl markers on the identically-shaped item data tables, and two wsl_v5 defer cuddles. No behavior changes. The repo has no golangci-lint version pin (no Dockerfile or CI; make lint runs the host binary, currently v2.12.2), so there was nothing to bump.
205 lines
4.6 KiB
Go
205 lines
4.6 KiB
Go
// Package term provides the tcell-backed Terminal for the Rogue port. It
|
|
// replaces curses and the 900-line escape-sequence decoder in mdport.c:
|
|
// tcell delivers decoded key events, which are translated here to the
|
|
// single-byte command codes the game understands.
|
|
package term
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
|
|
"git.eeqj.de/sneak/rgoue/game"
|
|
)
|
|
|
|
// Tcell renders game Windows on a tcell screen and turns key events into
|
|
// Rogue input bytes.
|
|
type Tcell struct {
|
|
screen tcell.Screen
|
|
last *game.Window // last rendered window, for resize redraws
|
|
}
|
|
|
|
// ErrScreenTooSmall reports a terminal below the required 80x24.
|
|
var ErrScreenTooSmall = errors.New("screen too small")
|
|
|
|
// New initializes the terminal. The screen must be at least 80x24, as the
|
|
// C game required.
|
|
func New() (*Tcell, error) {
|
|
s, err := tcell.NewScreen()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
initErr := s.Init()
|
|
if initErr != nil {
|
|
return nil, initErr
|
|
}
|
|
|
|
w, h := s.Size()
|
|
if h < game.NumLines || w < game.NumCols {
|
|
s.Fini()
|
|
|
|
return nil, fmt.Errorf("sorry, %w: %dx%d required",
|
|
ErrScreenTooSmall, game.NumCols, game.NumLines)
|
|
}
|
|
|
|
s.HideCursor()
|
|
|
|
return &Tcell{screen: s}, nil
|
|
}
|
|
|
|
// Fini restores the terminal.
|
|
func (t *Tcell) Fini() {
|
|
t.screen.Fini()
|
|
}
|
|
|
|
// Render blits a game window to the terminal (curses refresh).
|
|
func (t *Tcell) Render(w *game.Window) {
|
|
t.last = w
|
|
|
|
rows, cols := w.Size()
|
|
for y := range rows {
|
|
for x := range cols {
|
|
ch, standout := w.CellAt(y, x)
|
|
|
|
style := tcell.StyleDefault
|
|
if standout {
|
|
style = style.Reverse(true)
|
|
}
|
|
|
|
t.screen.SetContent(x, y, rune(ch), nil, style)
|
|
}
|
|
}
|
|
|
|
t.screen.Show()
|
|
}
|
|
|
|
// ReadChar blocks for the next key, translated to the byte codes the C
|
|
// game reads: arrows become hjkl, control keys their C0 codes.
|
|
func (t *Tcell) ReadChar() byte {
|
|
for {
|
|
ev := t.screen.PollEvent()
|
|
switch ev := ev.(type) {
|
|
case *tcell.EventResize:
|
|
if t.last != nil {
|
|
t.Render(t.last)
|
|
}
|
|
case *tcell.EventKey:
|
|
if b, ok := translateKey(ev); ok {
|
|
return b
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// translateKey converts a key event to a game input byte; ok is false
|
|
// for keys the C game does not understand.
|
|
func translateKey(ev *tcell.EventKey) (byte, bool) {
|
|
if b, ok := namedKey(ev.Key()); ok {
|
|
return b, true
|
|
}
|
|
|
|
if ev.Key() >= tcell.KeyCtrlA && ev.Key() <= tcell.KeyCtrlZ {
|
|
return byte(ev.Key()), true //nolint:gosec // G115: 1..26 fits
|
|
}
|
|
|
|
if r := ev.Rune(); r > 0 && r < 0x80 {
|
|
return byte(r), true
|
|
}
|
|
|
|
return 0, false
|
|
}
|
|
|
|
// namedKey translates tcell's navigation and editing keys to the single
|
|
// bytes the C game reads (arrows become hjkl, etc.); ok is false for
|
|
// keys handled elsewhere.
|
|
func namedKey(k tcell.Key) (byte, bool) {
|
|
if b, ok := motionKey(k); ok {
|
|
return b, true
|
|
}
|
|
|
|
return editingKey(k)
|
|
}
|
|
|
|
// motionKey translates the arrow and paging keys to Rogue's movement
|
|
// letters (tcell.go ReadChar).
|
|
func motionKey(k tcell.Key) (byte, bool) {
|
|
//nolint:exhaustive // translation table: all other keys fall through
|
|
switch k {
|
|
case tcell.KeyUp:
|
|
return 'k', true
|
|
case tcell.KeyDown:
|
|
return 'j', true
|
|
case tcell.KeyLeft:
|
|
return 'h', true
|
|
case tcell.KeyRight:
|
|
return 'l', true
|
|
case tcell.KeyHome:
|
|
return 'y', true
|
|
case tcell.KeyPgUp:
|
|
return 'u', true
|
|
case tcell.KeyEnd:
|
|
return 'b', true
|
|
case tcell.KeyPgDn:
|
|
return 'n', true
|
|
}
|
|
|
|
return 0, false
|
|
}
|
|
|
|
// editingKey translates the editing and control keys to their C0 codes
|
|
// (tcell.go ReadChar).
|
|
func editingKey(k tcell.Key) (byte, bool) {
|
|
//nolint:exhaustive // translation table: all other keys fall through
|
|
switch k {
|
|
case tcell.KeyEnter:
|
|
return '\n', true
|
|
case tcell.KeyEscape:
|
|
return game.Escape, true
|
|
case tcell.KeyBackspace, tcell.KeyBackspace2:
|
|
return '\b', true
|
|
case tcell.KeyDelete:
|
|
return '\x7f', true
|
|
case tcell.KeyTab:
|
|
return '\t', true
|
|
case tcell.KeyCtrlC:
|
|
return '\x03', true
|
|
}
|
|
|
|
return 0, false
|
|
}
|
|
|
|
// ShellEscape suspends the screen and runs the user's shell (main.c
|
|
// shell + md_shellescape).
|
|
func (t *Tcell) ShellEscape() {
|
|
err := t.screen.Suspend()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
shell := os.Getenv("SHELL")
|
|
if shell == "" {
|
|
shell = "/bin/sh"
|
|
}
|
|
|
|
_, _ = fmt.Fprintln(os.Stdout,
|
|
"[Entering shell; exit to return to the game]")
|
|
|
|
// The shell session has no deadline by design; Background context.
|
|
//nolint:gosec // G204: the user's own $SHELL
|
|
cmd := exec.CommandContext(context.Background(), shell)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
_ = cmd.Run() // best effort: the shell is the user's business
|
|
|
|
resumeErr := t.screen.Resume()
|
|
if resumeErr != nil {
|
|
panic(resumeErr) // terminal resume failure is unrecoverable
|
|
}
|
|
}
|