Skip to content

Commit

Permalink
Tty (#80)
Browse files Browse the repository at this point in the history
* Use a stateful console-reading package

This pull-request changes the IO interface we're using to maintaining
state - although this doesn't (yet) remove the use of `stty` which
is tracked in #65, it does close #78 by avoiding the need to re-exec
the program after every character.

Keeping track of the current "echo" vs "noecho" state means that
we need to make this a global object, which we can reuse so we
do that here.
  • Loading branch information
skx authored May 18, 2024
1 parent b108e08 commit 6b0371b
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 80 deletions.
110 changes: 63 additions & 47 deletions io/io.go → consolein/consolein.go
Original file line number Diff line number Diff line change
@@ -1,69 +1,62 @@
// Package io is designed to collect the code that reads from STDIN.
// Package consolein handles the reading of console input
// for our emulator.
//
// There are three functions we need to care about:
// The package supports the minimum required functionality
// we need - which boils down to reading a single character
// of input, with and without echo, and reading a line of text.
//
// * Block for a character, and return it.
//
// * Block for a character, and return it, but disable echo first.
//
// * Read a single line of input.
//
// There are functions for polling console status in CP/M, however it
// seems to work just fine if we fake their results - which means this
// package is simpler than it would otherwise need to be.
package io
// Note that no output functions are handled by this package,
// it is exclusively used for input.
package consolein

import (
"bufio"
"fmt"
"log/slog"
"os"
"os/exec"
"strings"

"golang.org/x/term"
)

// IO is used to hold our package state
type IO struct {
Logger *slog.Logger
}
// Status is used to record our current state
type Status int

// New is our package constructor.
func New(log *slog.Logger) *IO {
return &IO{Logger: log}
}
var (
// Unknown means we don't know the status of echo/noecho
Unknown Status = 0

// disableEcho is the single place where we disable echoing.
func (io *IO) disableEcho() {
err := exec.Command("stty", "-F", "/dev/tty", "-echo").Run()
if err != nil {
io.Logger.Debug("disableEcho",
slog.String("error", err.Error()))
}
}
// Echo means that input will echo characters.
Echo Status = 1

// enableEcho is the single place where we enable echoing.
func (io *IO) enableEcho() {
err := exec.Command("stty", "-F", "/dev/tty", "echo").Run()
if err != nil {
io.Logger.Debug("enableEcho",
slog.String("error", err.Error()))
}
// NoEcho means that input will not echo characters.
NoEcho Status = 2
)

// ConsoleIn holds our state
type ConsoleIn struct {
// State holds our current state
State Status
}

// Restore enables echoing.
func (io *IO) Restore() {
io.enableEcho()
// New is our constructor
func New() *ConsoleIn {
t := &ConsoleIn{
State: Unknown,
}
return t
}

// BlockForCharacter returns the next character from the console, blocking until
// BlockForCharacterNoEcho returns the next character from the console, blocking until
// one is available.
//
// NOTE: This function should not echo keystrokes which are entered.
func (io *IO) BlockForCharacter() (byte, error) {
func (io *ConsoleIn) BlockForCharacterNoEcho() (byte, error) {

io.disableEcho()
// Do we need to change state? If so then do it.
if io.State != NoEcho {
io.disableEcho()
}

// switch stdin into 'raw' mode
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
Expand Down Expand Up @@ -92,9 +85,12 @@ func (io *IO) BlockForCharacter() (byte, error) {
// blocking until one is available.
//
// NOTE: Characters should be echo'd as they are input.
func (io *IO) BlockForCharacterWithEcho() (byte, error) {
func (io *ConsoleIn) BlockForCharacterWithEcho() (byte, error) {

io.enableEcho()
// Do we need to change state? If so then do it.
if io.State != Echo {
io.enableEcho()
}

// switch stdin into 'raw' mode
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
Expand All @@ -115,7 +111,6 @@ func (io *IO) BlockForCharacterWithEcho() (byte, error) {
return 0x00, fmt.Errorf("error restoring terminal state %s", err)
}

fmt.Printf("%c", b[0])
return b[0], nil
}

Expand All @@ -124,9 +119,12 @@ func (io *IO) BlockForCharacterWithEcho() (byte, error) {
// buffer-overruns will occur!)
//
// Note: We should enable echo in this function.
func (io *IO) ReadLine(max uint8) (string, error) {
func (io *ConsoleIn) ReadLine(max uint8) (string, error) {

io.enableEcho()
// Do we need to change state? If so then do it.
if io.State != Echo {
io.enableEcho()
}

// Create a reader
reader := bufio.NewReader(os.Stdin)
Expand All @@ -148,3 +146,21 @@ func (io *IO) ReadLine(max uint8) (string, error) {
// Return the text
return text, err
}

// Reset restores echo.
func (io *ConsoleIn) Reset() {

if io.State == NoEcho {
io.enableEcho()
}
}

// disableEcho is the single place where we disable echoing.
func (io *ConsoleIn) disableEcho() {
_ = exec.Command("stty", "-F", "/dev/tty", "-echo").Run()
}

// enableEcho is the single place where we enable echoing.
func (io *ConsoleIn) enableEcho() {
_ = exec.Command("stty", "-F", "/dev/tty", "echo").Run()
}
12 changes: 12 additions & 0 deletions cpm/cpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"github.com/koron-go/z80"
"github.com/skx/cpmulator/ccp"
"github.com/skx/cpmulator/consolein"
"github.com/skx/cpmulator/fcb"
"github.com/skx/cpmulator/memory"
)
Expand Down Expand Up @@ -98,6 +99,11 @@ type CPM struct {
// files is the cache we use for File handles.
files map[uint16]FileCache

// input is our interface for reading from the console.
//
// This needs to take account of echo/no-echo status.
input *consolein.ConsoleIn

// dma contains the address of the DMA area in RAM.
//
// The DMA area is used for all file I/O, and is 128 bytes in length.
Expand Down Expand Up @@ -394,6 +400,7 @@ func New(logger *slog.Logger, prn string) *CPM {
// Create the emulator object and return it
tmp := &CPM{
Logger: logger,
input: consolein.New(),
BDOSSyscalls: sys,
BIOSSyscalls: b,
dma: 0x0080,
Expand All @@ -404,6 +411,11 @@ func New(logger *slog.Logger, prn string) *CPM {
return tmp
}

// Cleanup cleans up the state of the terminal, if necessary.
func (cpm *CPM) Cleanup() {
cpm.input.Reset()
}

// LoadBinary loads the given CP/M binary at the default address of 0x0100,
// where it can then be launched by Execute.
func (cpm *CPM) LoadBinary(filename string) error {
Expand Down
22 changes: 6 additions & 16 deletions cpm/cpm_bdos.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"strings"

"github.com/skx/cpmulator/fcb"
cpmio "github.com/skx/cpmulator/io"
)

// blkSize is the size of block-based I/O operations
Expand All @@ -32,11 +31,8 @@ func SysCallExit(cpm *CPM) error {
// SysCallReadChar reads a single character from the console.
func SysCallReadChar(cpm *CPM) error {

// Use our I/O package
obj := cpmio.New(cpm.Logger)

// Block for input
c, err := obj.BlockForCharacterWithEcho()
c, err := cpm.input.BlockForCharacterWithEcho()
if err != nil {
return fmt.Errorf("error in call to BlockForCharacter: %s", err)
}
Expand Down Expand Up @@ -64,13 +60,10 @@ func SysCallWriteChar(cpm *CPM) error {
// Note: Echo is not enabled in this function.
func SysCallAuxRead(cpm *CPM) error {

// Use our I/O package
obj := cpmio.New(cpm.Logger)

// Block for input
c, err := obj.BlockForCharacter()
c, err := cpm.input.BlockForCharacterNoEcho()
if err != nil {
return fmt.Errorf("error in call to BlockForCharacter: %s", err)
return fmt.Errorf("error in call to BlockForCharacterNoEcho: %s", err)
}

// Return values:
Expand Down Expand Up @@ -105,13 +98,10 @@ func SysCallAuxWrite(cpm *CPM) error {
// SysCallRawIO handles both simple character output, and input.
func SysCallRawIO(cpm *CPM) error {

// Use our I/O package
obj := cpmio.New(cpm.Logger)

switch cpm.CPU.States.DE.Lo {
case 0xFF, 0xFD:

out, err := obj.BlockForCharacter()
out, err := cpm.input.BlockForCharacterNoEcho()
if err != nil {
return err
}
Expand Down Expand Up @@ -180,8 +170,8 @@ func SysCallReadString(cpm *CPM) error {
// First byte is the max len
max := cpm.CPU.Memory.Get(addr)

obj := cpmio.New(cpm.Logger)
text, err := obj.ReadLine(max)
// read the input
text, err := cpm.input.ReadLine(max)

if err != nil {
return err
Expand Down
8 changes: 1 addition & 7 deletions cpm/cpm_bios.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ package cpm
import (
"fmt"
"log/slog"

cpmio "github.com/skx/cpmulator/io"
)

// BiosSysCallBoot handles a warm/cold boot.
Expand All @@ -34,11 +32,7 @@ func BiosSysCallConsoleStatus(cpm *CPM) error {
// and return the character pressed in the A-register.
func BiosSysCallConsoleInput(cpm *CPM) error {

// Wait until the keyboard is ready to provide a character, and return it in A.
// Use our I/O package
obj := cpmio.New(cpm.Logger)

out, err := obj.BlockForCharacter()
out, err := cpm.input.BlockForCharacterWithEcho()
if err != nil {
return err
}
Expand Down
12 changes: 2 additions & 10 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,13 @@ import (

cpmccp "github.com/skx/cpmulator/ccp"
"github.com/skx/cpmulator/cpm"
cpmio "github.com/skx/cpmulator/io"
)

// log holds our logging handle
var log *slog.Logger

// restoreEcho is designed to ensure we leave our terminal in a good state,
// when we terminate, by enabling console-echoing if it had been disabled.
func restoreEcho() {
// Use our I/O package
obj := cpmio.New(log)
obj.Restore()
}

func main() {

defer restoreEcho()
//
// Parse the command-line flags for this driver-application
//
Expand Down Expand Up @@ -141,6 +131,8 @@ func main() {
// Create a new emulator.
obj := cpm.New(log, *prnPath)

defer obj.Cleanup()

// change directory?
if *cd != "" {
err := os.Chdir(*cd)
Expand Down

0 comments on commit 6b0371b

Please sign in to comment.