// 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: switch ev.Key() { case tcell.KeyUp: return 'k' case tcell.KeyDown: return 'j' case tcell.KeyLeft: return 'h' case tcell.KeyRight: return 'l' case tcell.KeyHome: return 'y' case tcell.KeyPgUp: return 'u' case tcell.KeyEnd: return 'b' case tcell.KeyPgDn: return 'n' case tcell.KeyEnter: return '\n' case tcell.KeyEscape: return game.Escape case tcell.KeyBackspace, tcell.KeyBackspace2: return 8 case tcell.KeyDelete: return 0x7f case tcell.KeyTab: return '\t' case tcell.KeyCtrlC: return 3 default: if ev.Key() >= tcell.KeyCtrlA && ev.Key() <= tcell.KeyCtrlZ { return byte(ev.Key()) //nolint:gosec // G115: 1..26 fits } if r := ev.Rune(); r > 0 && r < 0x80 { return byte(r) } } } } } // 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. cmd := exec.CommandContext(context.Background(), //nolint:gosec // G204: the user's own $SHELL 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 } }