Exit the process on game-over instead of unwinding a panic
One game run is one process, so game-over ends the process directly, as the C game did with exit(). myExit now restores the terminal (via the new Terminal.Fini) and calls os.Exit(0); the gameEnd sentinel, the recover in Run, and the recover in DeathDemo are gone. Run() no longer returns an error (it does not return — the game exits from within), and playit's pre-loop setup is split into startLevel/prePlay so tests can drive a bounded number of turns. Because death (combat, and starvation over a long session) now exits the process, the four Run()-to-completion tests can no longer run through the exit path: TestDeathUnwindsWithGameEnd is removed (it tested the deleted unwind), the crash-sweep and quit/save session tests are dropped, and TestRunDownStairs is reworked to drive the turn loop for a single descend. Score rendering, previously checked after a scripted quit, is now covered directly by TestScoreRendersList. Save/restore integrity remains covered by TestSaveRestoreRoundTrip.
This commit is contained in:
@@ -1194,7 +1194,7 @@ The command.c/io.c/things.c file-statics become lowercase fields of `RogueGame`,
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
func New(params Params) *RogueGame // Params: seed, name, ROGUEOPTS string, score path, wizard
|
func New(params Params) *RogueGame // Params: seed, name, ROGUEOPTS string, score path, wizard
|
||||||
func (g *RogueGame) Run() error // main.c main()+playit(): init tables, first level, daemons, loop
|
func (g *RogueGame) Run() // main.c main()+playit(): init tables, first level, daemons, loop
|
||||||
func (g *RogueGame) command() // one turn (command.c)
|
func (g *RogueGame) command() // one turn (command.c)
|
||||||
func Restore(path string, params Params) (*RogueGame, error) // save.c restore()
|
func Restore(path string, params Params) (*RogueGame, error) // save.c restore()
|
||||||
```
|
```
|
||||||
@@ -1553,7 +1553,7 @@ Tombstone/victory screens port verbatim from rip.c.
|
|||||||
## 6. C construct → Go construct map
|
## 6. C construct → Go construct map
|
||||||
|
|
||||||
| C construct | Go translation |
|
| C construct | Go translation |
|
||||||
| ---------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
| ---------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
||||||
| global variable | `RogueGame` field (or Player/Level/subsystem field) |
|
| global variable | `RogueGame` field (or Player/Level/subsystem field) |
|
||||||
| file-scope static | unexported field on `RogueGame` or subsystem struct |
|
| file-scope static | unexported field on `RogueGame` or subsystem struct |
|
||||||
| `THING` union | `Creature` / `Object` structs |
|
| `THING` union | `Creature` / `Object` structs |
|
||||||
@@ -1567,7 +1567,7 @@ Tombstone/victory screens port verbatim from rip.c.
|
|||||||
| curses stdscr/hw windows | `Window` cell buffers over tcell |
|
| curses stdscr/hw windows | `Window` cell buffers over tcell |
|
||||||
| `mvinch` screen reads | `Window.Inch` from the buffer |
|
| `mvinch` screen reads | `Window.Inch` from the buffer |
|
||||||
| signal handlers (SIGHUP autosave, SIGTSTP, SIGINT) | `os/signal` goroutine → channel checked in ReadChar; tcell handles TSTP/resize |
|
| signal handlers (SIGHUP autosave, SIGTSTP, SIGINT) | `os/signal` goroutine → channel checked in ReadChar; tcell handles TSTP/resize |
|
||||||
| `setjmp`-free exits (`my_exit`, `exit()` everywhere) | error returns from `Run` + a small `gameOver` sentinel; no `os.Exit` inside game logic |
|
| `setjmp`-free exits (`my_exit`, `exit()` everywhere) | `myExit` restores the terminal and calls `os.Exit(0)`; one game run is one process |
|
||||||
| `vsprintf` message building | `fmt.Sprintf` |
|
| `vsprintf` message building | `fmt.Sprintf` |
|
||||||
| XOR-encrypted binary saves (state.c) | gob snapshot (§5.6) |
|
| XOR-encrypted binary saves (state.c) | gob snapshot (§5.6) |
|
||||||
| DES wizard password | `ROGUE_WIZARD=1` env check |
|
| DES wizard password | `ROGUE_WIZARD=1` env check |
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ func main() {
|
|||||||
os.Exit(run())
|
os.Exit(run())
|
||||||
}
|
}
|
||||||
|
|
||||||
// run carries the real main so that deferred terminal restoration runs
|
// run does the real work and returns an exit code. It only returns on a
|
||||||
// before the process exits (os.Exit skips defers).
|
// startup error; once the game starts, it ends by exiting the process
|
||||||
|
// from within (game.myExit restores the terminal first). The deferred
|
||||||
|
// Fini covers the early-return paths.
|
||||||
func run() int {
|
func run() int {
|
||||||
scores := flag.Bool("s", false, "print the scoreboard and exit")
|
scores := flag.Bool("s", false, "print the scoreboard and exit")
|
||||||
deathDemo := flag.Bool("d", false, "die a random death (demo)")
|
deathDemo := flag.Bool("d", false, "die a random death (demo)")
|
||||||
@@ -53,8 +55,7 @@ func run() int {
|
|||||||
// restore a saved game
|
// restore a saved game
|
||||||
g, err = game.Restore(args[0], params)
|
g, err = game.Restore(args[0], params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fini()
|
fmt.Fprintln(os.Stderr, err) // deferred Fini restores the terminal
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
@@ -63,20 +64,14 @@ func run() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if *deathDemo {
|
if *deathDemo {
|
||||||
g.DeathDemo()
|
g.DeathDemo() // does not return: death exits the process
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
installAutosave(g, t)
|
installAutosave(g, t)
|
||||||
|
|
||||||
runErr := g.Run()
|
g.Run() // does not return: the game ends by exiting the process
|
||||||
if runErr != nil {
|
|
||||||
t.Fini()
|
|
||||||
fmt.Fprintln(os.Stderr, runErr)
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,24 +79,6 @@ func TestAttackHurtsPlayer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeathUnwindsWithGameEnd(t *testing.T) {
|
|
||||||
g := mkGame(t, 11)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
r := recover()
|
|
||||||
if _, ok := r.(gameEnd); !ok {
|
|
||||||
t.Fatalf("death did not unwind with gameEnd, got %v", r)
|
|
||||||
}
|
|
||||||
|
|
||||||
if g.Playing {
|
|
||||||
t.Error("still playing after death")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
g.Options.Tombstone = false
|
|
||||||
g.death('K')
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunnersChaseHero(t *testing.T) {
|
func TestRunnersChaseHero(t *testing.T) {
|
||||||
g := mkGame(t, 3)
|
g := mkGame(t, 3)
|
||||||
// Place a hobgoblin a few squares away in the hero's room and set it
|
// Place a hobgoblin a few squares away in the hero's room and set it
|
||||||
|
|||||||
52
game/game.go
52
game/game.go
@@ -199,37 +199,45 @@ func New(params Params) *RogueGame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run plays the game to its end: the back half of main.c main() plus
|
// Run plays the game to its end: the back half of main.c main() plus
|
||||||
// playit(). It returns after death, victory, quitting, or saving.
|
// playit(). It does not return — the game ends by exiting the process
|
||||||
func (g *RogueGame) Run() error {
|
// (see myExit); one game run is one process.
|
||||||
// A gameEnd panic is the port's my_exit(): recovering it here makes
|
func (g *RogueGame) Run() {
|
||||||
// Run return normally (zero values), restoring the terminal via the
|
g.startLevel()
|
||||||
// caller's defers.
|
g.playit()
|
||||||
defer func() {
|
}
|
||||||
if r := recover(); r != nil {
|
|
||||||
if _, ok := r.(gameEnd); ok {
|
// startLevel draws the first level and starts the standing daemons and
|
||||||
return // normal game over / save exit
|
// fuses for a fresh game; a restored game brings its own (the back half
|
||||||
|
// of main.c main()).
|
||||||
|
func (g *RogueGame) startLevel() {
|
||||||
|
if g.restored {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
panic(r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if !g.restored {
|
|
||||||
g.NewLevel() // draw current level
|
g.NewLevel() // draw current level
|
||||||
// Start up daemons and fuses
|
// Start up daemons and fuses
|
||||||
g.StartDaemon(DRunners, 0, After)
|
g.StartDaemon(DRunners, 0, After)
|
||||||
g.StartDaemon(DDoctor, 0, After)
|
g.StartDaemon(DDoctor, 0, After)
|
||||||
g.Fuse(DSwander, 0, wanderTime(g), After)
|
g.Fuse(DSwander, 0, wanderTime(g), After)
|
||||||
g.StartDaemon(DStomach, 0, After)
|
g.StartDaemon(DStomach, 0, After)
|
||||||
}
|
|
||||||
|
|
||||||
g.playit()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// playit is the main loop of the program (main.c playit).
|
// playit is the main loop of the program (main.c playit).
|
||||||
func (g *RogueGame) playit() {
|
func (g *RogueGame) playit() {
|
||||||
|
g.prePlay()
|
||||||
|
|
||||||
|
for g.Playing {
|
||||||
|
g.command() // command execution
|
||||||
|
}
|
||||||
|
|
||||||
|
g.endit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// prePlay does the option and position setup at the top of playit,
|
||||||
|
// before the command loop (main.c playit). It is split out so tests can
|
||||||
|
// drive a bounded number of turns; the loop itself never returns,
|
||||||
|
// because game-over exits the process.
|
||||||
|
func (g *RogueGame) prePlay() {
|
||||||
// set up defaults for modern terminals: curses' md_hasclreol() is
|
// set up defaults for modern terminals: curses' md_hasclreol() is
|
||||||
// always true, so the C default inventory style applies
|
// always true, so the C default inventory style applies
|
||||||
if !g.restored {
|
if !g.restored {
|
||||||
@@ -242,13 +250,7 @@ func (g *RogueGame) playit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
g.Oldpos = g.Player.Pos
|
g.Oldpos = g.Player.Pos
|
||||||
|
|
||||||
g.Oldrp = g.roomIn(g.Player.Pos)
|
g.Oldrp = g.roomIn(g.Player.Pos)
|
||||||
for g.Playing {
|
|
||||||
g.command() // command execution
|
|
||||||
}
|
|
||||||
|
|
||||||
g.endit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// endit exits the game (main.c endit).
|
// endit exits the game (main.c endit).
|
||||||
|
|||||||
30
game/rip.go
30
game/rip.go
@@ -2,23 +2,20 @@ package game
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// rip.c — the fun ends: death or a total win.
|
// rip.c — the fun ends: death or a total win.
|
||||||
//
|
//
|
||||||
// The C functions here call exit(); the port panics with a gameEnd sentinel
|
// The C functions here call exit(). One game run is one process, so the
|
||||||
// that Run recovers, so the terminal is restored by normal unwinding.
|
// port does the same: myExit restores the terminal and exits directly.
|
||||||
|
|
||||||
// gameEnd is the sentinel carried by the panic that replaces my_exit().
|
// myExit leaves the process properly (main.c my_exit): it restores the
|
||||||
type gameEnd struct{}
|
// terminal and ends the process. Every C caller exited with status 0.
|
||||||
|
|
||||||
// myExit leaves the process properly (main.c my_exit): it unwinds to Run.
|
|
||||||
// Every C caller exited with status 0; abnormal exits panic for real.
|
|
||||||
func (g *RogueGame) myExit() {
|
func (g *RogueGame) myExit() {
|
||||||
g.Playing = false
|
g.scr.Fini()
|
||||||
|
os.Exit(0)
|
||||||
panic(gameEnd{})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// death does something really fun when he dies (rip.c death).
|
// death does something really fun when he dies (rip.c death).
|
||||||
@@ -255,18 +252,9 @@ func (g *RogueGame) killname(monst byte, doart bool) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeathDemo implements the -d command line option (main.c): burn some
|
// DeathDemo implements the -d command line option (main.c): burn some
|
||||||
// random numbers to break patterns, then die a random death.
|
// random numbers to break patterns, then die a random death. It does not
|
||||||
|
// return — death exits the process.
|
||||||
func (g *RogueGame) DeathDemo() {
|
func (g *RogueGame) DeathDemo() {
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
if _, ok := r.(gameEnd); ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
dnum := g.rnd(100)
|
dnum := g.rnd(100)
|
||||||
for dnum--; dnum > 0; dnum-- {
|
for dnum--; dnum > 0; dnum-- {
|
||||||
g.rnd(100)
|
g.rnd(100)
|
||||||
|
|||||||
149
game/run_test.go
149
game/run_test.go
@@ -5,23 +5,56 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestRunScriptedSession drives a complete game through Run(): a few
|
// driveTurns runs the game's per-turn loop up to n times, doing the same
|
||||||
// moves, a rest, an inventory, then Q-quit answered yes.
|
// first-level and pre-play setup Run() does. Run() itself no longer
|
||||||
func TestRunScriptedSession(t *testing.T) {
|
// returns — game-over exits the process — so tests drive command()
|
||||||
tt := &testTerm{input: []byte("hjkl.i Qy")}
|
// directly, with short scripts that avoid quitting, saving, or playing
|
||||||
|
// long enough to starve, any of which would exit the test binary.
|
||||||
|
func driveTurns(t *testing.T, g *RogueGame, n int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
g := New(Params{Seed: 99, Term: tt})
|
g.startLevel()
|
||||||
|
g.prePlay()
|
||||||
|
|
||||||
err := g.Run()
|
for range n {
|
||||||
if err != nil {
|
g.command()
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if g.Playing {
|
// TestRunDownStairs stands the hero on the staircase and descends via the
|
||||||
t.Error("still playing after quit")
|
// '>' command through the real turn loop, then checks the level changed.
|
||||||
|
func TestRunDownStairs(t *testing.T) {
|
||||||
|
// '>' is a free action (After=false), so it is followed by a paying
|
||||||
|
// rest ('.') to end the command() call; without a paying action the
|
||||||
|
// turn loop would spin forever on the auto-fed prompt input.
|
||||||
|
tt := &testTerm{input: []byte(">.")}
|
||||||
|
g := New(Params{Seed: 7, Term: tt})
|
||||||
|
g.NewLevel()
|
||||||
|
g.Player.Pos = g.Level.Stairs // stand on the stairs
|
||||||
|
g.restored = true // keep startLevel from regenerating
|
||||||
|
g.Daemons = DaemonList{} // and give it a fresh daemon table
|
||||||
|
g.StartDaemon(DRunners, 0, After)
|
||||||
|
g.StartDaemon(DDoctor, 0, After)
|
||||||
|
g.Fuse(DSwander, 0, wanderTime(g), After)
|
||||||
|
g.StartDaemon(DStomach, 0, After)
|
||||||
|
|
||||||
|
driveTurns(t, g, 1)
|
||||||
|
|
||||||
|
if g.Depth != 2 {
|
||||||
|
t.Errorf("depth = %d after descending, want 2", g.Depth)
|
||||||
}
|
}
|
||||||
// After quitting, the scoreboard is the last thing shown (in C it went
|
}
|
||||||
// to stdout after endwin; here it is drawn on the screen).
|
|
||||||
|
// TestScoreRendersList checks that the scoreboard is drawn on the screen
|
||||||
|
// (in C it went to stdout after endwin; here it stays on the screen). The
|
||||||
|
// quit and death paths that normally show it now exit the process, so the
|
||||||
|
// display is exercised through score() directly.
|
||||||
|
func TestScoreRendersList(t *testing.T) {
|
||||||
|
g := New(Params{Seed: 1, Term: &testTerm{}})
|
||||||
|
g.Player.Purse = 100
|
||||||
|
|
||||||
|
g.score(g.Player.Purse, 1, 0) // flags 1 = quit; posts the top-ten list
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
|
|
||||||
for y := range NumLines {
|
for y := range NumLines {
|
||||||
@@ -31,96 +64,6 @@ func TestRunScriptedSession(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
t.Error("score list not on screen after quit")
|
t.Error("score list not on screen")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRunManyTurns mashes movement keys for a while as a crash sweep of
|
|
||||||
// the whole turn loop (daemons, hunger, monsters, combat), ending with a
|
|
||||||
// quit. The input alternates directions so the hero bumps around rooms.
|
|
||||||
func TestRunManyTurns(t *testing.T) {
|
|
||||||
// Spaces between commands double as answers to any --More-- prompts;
|
|
||||||
// without them a single prompt would swallow the rest of the script
|
|
||||||
// (wait_for eats everything that isn't a space).
|
|
||||||
moves := []byte("h j k l y u b n s .")
|
|
||||||
script := make([]byte, 0, len(moves)*200+7)
|
|
||||||
|
|
||||||
for range 200 {
|
|
||||||
script = append(script, moves...)
|
|
||||||
}
|
|
||||||
|
|
||||||
script = append(script, " Q y Qy"...)
|
|
||||||
tt := &testTerm{input: script}
|
|
||||||
|
|
||||||
g := New(Params{Seed: 31337, Term: tt})
|
|
||||||
|
|
||||||
err := g.Run()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if g.Playing {
|
|
||||||
t.Error("session did not end")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRunDownStairs walks the hero onto the stairs by teleporting there in
|
|
||||||
// wizard style, then descends and keeps playing.
|
|
||||||
func TestRunDownStairs(t *testing.T) {
|
|
||||||
tt := &testTerm{input: []byte(">..Qy")}
|
|
||||||
g := New(Params{Seed: 7, Term: tt})
|
|
||||||
g.NewLevel()
|
|
||||||
g.Player.Pos = g.Level.Stairs // stand on the stairs
|
|
||||||
g.restored = true // keep Run from regenerating the level
|
|
||||||
g.Daemons = DaemonList{} // and give it a fresh daemon table
|
|
||||||
g.StartDaemon(DRunners, 0, After)
|
|
||||||
g.StartDaemon(DDoctor, 0, After)
|
|
||||||
g.Fuse(DSwander, 0, wanderTime(g), After)
|
|
||||||
g.StartDaemon(DStomach, 0, After)
|
|
||||||
|
|
||||||
err := g.Run()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if g.Depth != 2 {
|
|
||||||
t.Errorf("depth = %d after descending, want 2", g.Depth)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSaveCommandRoundTrip saves via the 'S' command (as a player would)
|
|
||||||
// and restores the game.
|
|
||||||
func TestSaveCommandRoundTrip(t *testing.T) {
|
|
||||||
// The C get_str caps input at MAXINP=50 characters, so the save path
|
|
||||||
// must be short: work from the temp directory.
|
|
||||||
t.Chdir(t.TempDir())
|
|
||||||
|
|
||||||
path := "cmd.save"
|
|
||||||
// 'S' with no default file name goes straight to the name prompt.
|
|
||||||
script := "S" + path + "\n"
|
|
||||||
tt := &testTerm{input: []byte(script)}
|
|
||||||
g := New(Params{Seed: 55, Term: tt})
|
|
||||||
|
|
||||||
g.FileName = "" // force the name prompt
|
|
||||||
|
|
||||||
runErr := g.Run()
|
|
||||||
if runErr != nil {
|
|
||||||
t.Fatalf("Run: %v", runErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
h, err := Restore(path, Params{Term: &testTerm{}})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Restore: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.Depth != g.Depth || h.Player.Purse != g.Player.Purse {
|
|
||||||
t.Error("restored game does not match saved game")
|
|
||||||
}
|
|
||||||
// The restored game must be playable.
|
|
||||||
setInput(t, h, []byte("..Qy")...)
|
|
||||||
|
|
||||||
restoredErr := h.Run()
|
|
||||||
if restoredErr != nil {
|
|
||||||
t.Fatalf("restored Run: %v", restoredErr)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ type Terminal interface {
|
|||||||
// ReadChar blocks for the next key, translated to Rogue's input bytes
|
// ReadChar blocks for the next key, translated to Rogue's input bytes
|
||||||
// (arrows become hjkl, control keys their C0 codes).
|
// (arrows become hjkl, control keys their C0 codes).
|
||||||
ReadChar() byte
|
ReadChar() byte
|
||||||
|
// Fini restores the device to its pre-game state (curses endwin). The
|
||||||
|
// game calls it on its way out, since one game run is one process.
|
||||||
|
Fini()
|
||||||
}
|
}
|
||||||
|
|
||||||
// cell is one screen position.
|
// cell is one screen position.
|
||||||
@@ -213,6 +216,13 @@ func (s *Screen) Refresh() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fini restores the terminal device, if there is one (curses endwin).
|
||||||
|
func (s *Screen) Fini() {
|
||||||
|
if s.term != nil {
|
||||||
|
s.term.Fini()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RefreshWin pushes an arbitrary window to the device (curses wrefresh).
|
// RefreshWin pushes an arbitrary window to the device (curses wrefresh).
|
||||||
func (s *Screen) RefreshWin(w *Window) {
|
func (s *Screen) RefreshWin(w *Window) {
|
||||||
if s.term != nil {
|
if s.term != nil {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ type testTerm struct {
|
|||||||
|
|
||||||
func (t *testTerm) Render(*Window) {}
|
func (t *testTerm) Render(*Window) {}
|
||||||
|
|
||||||
|
func (t *testTerm) Fini() {}
|
||||||
|
|
||||||
func (t *testTerm) ReadChar() byte {
|
func (t *testTerm) ReadChar() byte {
|
||||||
if t.pos < len(t.input) {
|
if t.pos < len(t.input) {
|
||||||
c := t.input[t.pos]
|
c := t.input[t.pos]
|
||||||
|
|||||||
Reference in New Issue
Block a user