From d8847348722df8511ffb590dfd445864b5f265af Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Sun, 29 Dec 2024 15:50:06 +0200 Subject: [PATCH 01/21] Avoid the unnecessary invokation of STTY CP/M has a couple of BIOS functions for reading input from the user, there is a function for reading a string and a number of functions relating to reading a single character. The character-reading functions are in two types: * Those that should _echo_ the input, as received. * Those that should _not_ echo the input. In the past I didn't want to deal with the per-system handling to get this echo on/echo off support working in a good way. Instead I just executed the system `stty` binary to disable/enable echoing, and then read STDIN in the naive way. Unfortunately using `stty` this way was both slow, and non-portable. Slow because I exected `stty` at every time a keystroke was needing to be read, and non-portable because Microsoft Windows wouldn't work. I solved the slowness problem by keeping track of the echo/no-echo state to cut down on executing the binary more than the minimum number of times required (i.e. only when the state needed to change). But the portability was still a concern. This pull-request takes inspiration from the way that we support multiple methods for console _output_ by introducing that same flexibility for input. We now have a generalized layer to select between different input-handlers, and to make that worthwhile we've got two of them: * The old `stty`-based approach. * A new approach based on the termbox-go library. The intention is to test the termbox-based solution, and if it works as expected it can become the default, and if not we can revert back to the `stty`-based solution, which while inelegant has the advantage that it does actually work. This will close #65, once complete, but it needs testing on a MacOS host and ideally Windows too. --- README.md | 19 ++- consolein/consolein.go | 316 +++++++++++++++--------------------- consolein/consolein_test.go | 120 ++++++++------ consolein/drv_stty.go | 157 ++++++++++++++++++ consolein/drv_term.go | 159 ++++++++++++++++++ cpm/cpm.go | 44 ++++- cpm/cpm_bdos_test.go | 7 +- go.mod | 5 + go.sum | 4 + main.go | 38 ++++- 10 files changed, 606 insertions(+), 263 deletions(-) create mode 100644 consolein/drv_stty.go create mode 100644 consolein/drv_term.go diff --git a/README.md b/README.md index dade379..0790935 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Over time this project has become more complete, and I've now implemented enough * BBC and Microsoft BASIC. * Wordstar. -As things stand this project is "complete". I'd like to increase the test-coverage for my own reassurance, but I've now reached a point where all the binaries I've tried to execute work as expected. If you find a program that _doesn't_ work please [open an issue](https://github.com/skx/cpmulator/issues), beyond that I think this project is "complete" and future development will be minimal, and sporadic. +As things stand this project is "complete". I'd like to increase the test-coverage for my own reassurance, but I've now reached a point where all the binaries I've tried to execute work as expected. If you find a program that _doesn't_ work please [open an issue](https://github.com/skx/cpmulator/issues), otherwise I suspect ongoing development will be minimal, and sporadic. > **NOTE** I've not implemented any notion of disk-support. This means that opening, reading/writing, and closing files is absolutely fine, but any API call that refers to tracks, sectors, or disks will fail (with an "unimplemented syscall" error). @@ -64,16 +64,16 @@ Releases will be made as/when features seem to justify it, but it should be note # Portability -The CP/M input handlers need to disable echoing when reading (single) characters from STDIN. There isn't a simple and portable solution for this in golang, although the appropriate primitives exist so building such support isn't impossible. +The CP/M input handlers need to disable echoing when reading (single) characters from STDIN. There isn't a simple and portable solution for this in golang, although the appropriate primitives exist so building such support isn't impossible, it just relies upon writing per-environment support, using something like the [ReadPassword](https://pkg.go.dev/golang.org/x/term#ReadPassword) function from the standard-library. -Usage of is demonstrated in the standard library: +I sidestepped this whole problem initially, just invoking the `stty` binary to enable/disable the echoing of characters on-demand, but that only works on Linux, BSD, and Mac hosts. To be properly portable I had to use the [termbox](https://github.com/nsf/termbox-go) library for all input, but that means we get no scrollback/history so there's a tradeoff to be made. -* [x/term package](https://pkg.go.dev/golang.org/x/term) - * [ReadPassword](https://pkg.go.dev/golang.org/x/term#ReadPassword) - ReadPassword reads a line of input from a terminal without local echo. +By default input will be read via `termbox` but you may you specify a different driver via the CLI arguments: -Unfortunately there is no code there for reading only a _single character_, rather than a complete line. In the interest of expediency I resort to executing the `stty` binary, rather than attempting to use the `x/term` package to manage echo/noecho state myself, and this means the code in this repository isn't 100% portable; it will work on Linux and MacOS hosts, but not Windows. - -I've got an open bug about fixing the console (input), [#65](https://github.com/skx/cpmulator/issues/65). +* `cpmulator -input xxx` + * Use the input-driver named `xxx`. +* `cpmulator -list-input-drivers` + * List all available input-drivers. @@ -139,7 +139,7 @@ $ cpmulator /path/to/binary [optional-args] ## Command Line Flags -There are several command-line options which are shown in the output of `cpmulator -help`, but the following summary shows the most important/useful options: +There are many available command-line options, which are shown in the output of `cpmulator -help`, but the following summary shows the most important/useful options: * `-cd /path/to/directory` * Change to the given directory before running. @@ -152,6 +152,7 @@ There are several command-line options which are shown in the output of `cpmulat * All output which CP/M sends to the "printer" will be written to the given file. * `-list-syscalls` * Dump the list of implemented BDOS and BIOS syscalls. +* `-list-input-drivers` and `-list-output-drivers` to see the available I/O driver-names, which may be enabled via `-input` and `-output`. * `-version` * Show the version number of the emulator, and exit. diff --git a/consolein/consolein.go b/consolein/consolein.go index 9d54a54..936366e 100644 --- a/consolein/consolein.go +++ b/consolein/consolein.go @@ -1,210 +1,169 @@ -// Package consolein handles the reading of console input -// for our emulator. +// Package consolein is an abstraction over console input. // -// 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. -// -// Note that no output functions are handled by this package, -// it is exclusively used for input. +// We support two methods of getting input, whilst selectively +// disabling/enabling echo - the use of `termbox' and the use of +// the `stty` binary. package consolein import ( "fmt" - "os" - "os/exec" "strings" "unicode" - - "golang.org/x/term" ) -// Status is used to record our current state -type Status int - -var ( - // Unknown means we don't know the status of echo/noecho - Unknown Status = 0 +// ErrInterrupted is returned if the user presses Ctrl-C when in our ReadLine function. +var ErrInterrupted error = fmt.Errorf("INTERRUPTED") - // Echo means that input will echo characters. - Echo Status = 1 +// ConsoleInput is the interface that must be implemented by anything +// that wishes to be used as an input driver. +// +// Providing this interface is implemented an object may register itself, +// by name, via the Register method. +type ConsoleInput interface { - // NoEcho means that input will not echo characters. - NoEcho Status = 2 + // Setup performs any specific setup which is required. + Setup() - // ErrInterrupted is returned if the user presses Ctrl-C when in our ReadLine function. - ErrInterrupted = fmt.Errorf("INTERRUPTED") -) + // TearDown performs any specific cleanup which is required. + TearDown() -// ConsoleIn holds our state -type ConsoleIn struct { - // State holds our current echo state; either Echo, NoEcho, or Unknown. - State Status + // StuffInput saves fake-input into the drivers' buffer, to be returned later. + StuffInput(input string) - // InterruptCount holds the number of consecutive Ctrl-Cs which are necessary - // to trigger an interrupt response from ReadLine - InterruptCount int + // PendingInput returns true if there is pending input available to be read. + PendingInput() bool - // stuffed holds fake input which has been forced into the buffer used - // by ReadLine - stuffed string + // BlockForCharacterNoEcho reads a single character from the console, without + // echoing it. + BlockForCharacterNoEcho() (byte, error) - // history holds previous (line) input. - history []string + // GetName will return the name of the driver. + GetName() string } -// New is our constructor. -func New() *ConsoleIn { - t := &ConsoleIn{ - State: Unknown, - InterruptCount: 2, - } - return t -} +// This is a map of known-drivers +var handlers = struct { + m map[string]Constructor +}{m: make(map[string]Constructor)} -// StuffInput forces input into the buffer which our ReadLine function will -// return. It is used solely for the AUTOEXEC.SUB behaviour by our driver. -func (ci *ConsoleIn) StuffInput(text string) { - ci.stuffed = text -} +// This is the count of Ctrl-C which we keep track of to allow "reboots" +var interruptCount int = 1 -// SetInterruptCount updates the number of consecutive Ctrl-Cs which are necessary -// to trigger an interrupt in ReadLine. -func (ci *ConsoleIn) SetInterruptCount(val int) { - ci.InterruptCount = val -} +// history holds previous (line) input. +var history []string -// GetInterruptCount returns the number of consecutive Ctrl-Cs which are necessary -// to trigger an interrupt in ReadLine. -func (ci *ConsoleIn) GetInterruptCount() int { - return ci.InterruptCount -} +// Constructor is the signature of a constructor-function +// which is used to instantiate an instance of a driver. +type Constructor func() ConsoleInput -// PendingInput returns true if there is pending input from STDIN.. +// Register makes a console driver available, by name. // -// Note that we have to set RAW mode, without this input is laggy -// and zork doesn't run. -func (ci *ConsoleIn) PendingInput() bool { +// When one needs to be created the constructor can be called +// to create an instance of it. +func Register(name string, obj Constructor) { + handlers.m[name] = obj +} - // Do we have faked/stuffed input to process? - if len(ci.stuffed) > 0 { - return true - } +// ConsoleIn holds our state, which is basically just a +// pointer to the object handling our input +type ConsoleIn struct { - // switch stdin into 'raw' mode - oldState, err := term.MakeRaw(int(os.Stdin.Fd())) - if err != nil { - return false - } + // driver is the thing that actually reads our output. + driver ConsoleInput +} - // Platform-specific code in select_XXXX.go - res := canSelect() +// New is our constructore, it creates an input device which uses +// the specified driver. +func New(name string) (*ConsoleIn, error) { - // restore the state of the terminal to avoid mixing RAW/Cooked - err = term.Restore(int(os.Stdin.Fd()), oldState) - if err != nil { - return false + // Do we have a constructor with the given name? + ctor, ok := handlers.m[name] + if !ok { + return nil, fmt.Errorf("failed to lookup driver by name '%s'", name) } - // Return true if we have something ready to read. - return res + // OK we do, return ourselves with that driver. + return &ConsoleIn{ + driver: ctor(), + }, nil } -// BlockForCharacterNoEcho returns the next character from the console, blocking until -// one is available. -// -// NOTE: This function should not echo keystrokes which are entered. -func (ci *ConsoleIn) BlockForCharacterNoEcho() (byte, error) { - - // Do we have faked/stuffed input to process? - if len(ci.stuffed) > 0 { - c := ci.stuffed[0] - ci.stuffed = ci.stuffed[1:] - return c, nil - } +// GetDriver allows getting our driver at runtime. +func (co *ConsoleIn) GetDriver() ConsoleInput { + return co.driver +} - // Do we need to change state? If so then do it. - if ci.State != NoEcho { - ci.disableEcho() - } +// GetName returns the name of our selected driver. +func (co *ConsoleIn) GetName() string { + return co.driver.GetName() +} - // switch stdin into 'raw' mode - oldState, err := term.MakeRaw(int(os.Stdin.Fd())) - if err != nil { - return 0x00, fmt.Errorf("error making raw terminal %s", err) - } +// GetDrivers returns all available driver-names. +func (co *ConsoleIn) GetDrivers() []string { + valid := []string{} - // read only a single byte - b := make([]byte, 1) - _, err = os.Stdin.Read(b) - if err != nil { - return 0x00, fmt.Errorf("error reading a byte from stdin %s", err) + for x := range handlers.m { + valid = append(valid, x) } + return valid +} - // restore the state of the terminal to avoid mixing RAW/Cooked - err = term.Restore(int(os.Stdin.Fd()), oldState) - if err != nil { - return 0x00, fmt.Errorf("error restoring terminal state %s", err) - } +// Setup proxies into our registered console-input driver. +func (co *ConsoleIn) Setup() { + co.driver.Setup() +} + +// TearDown proxies into our registered console-input driver. +func (co *ConsoleIn) TearDown() { + co.driver.TearDown() +} - // Return the character we read - return b[0], nil +// StuffInput proxies into our registered console-input driver. +func (co *ConsoleIn) StuffInput(input string) { + co.driver.StuffInput(input) } -// BlockForCharacterWithEcho returns the next character from the console, -// blocking until one is available. +// SetInterruptCount sets the number of consecutive Ctrl-C characters +// are required to trigger a reboot. // -// NOTE: Characters should be echo'd as they are input. -func (ci *ConsoleIn) BlockForCharacterWithEcho() (byte, error) { - - // Do we have faked/stuffed input to process? - if len(ci.stuffed) > 0 { - c := ci.stuffed[0] - ci.stuffed = ci.stuffed[1:] - return c, nil - } +// This function DOES NOT proxy to our registered console-input driver. +func (co *ConsoleIn) SetInterruptCount(val int) { + interruptCount = val +} - // Do we need to change state? If so then do it. - if ci.State != Echo { - ci.enableEcho() - } +// GetInterruptCount retrieves the number of consecutive Ctrl-C characters are required to trigger a reboot. +// +// This function DOES NOT proxy to our registered console-input driver. +func (co *ConsoleIn) GetInterruptCount() int { + return interruptCount +} - // switch stdin into 'raw' mode - oldState, err := term.MakeRaw(int(os.Stdin.Fd())) - if err != nil { - return 0x00, fmt.Errorf("error making raw terminal %s", err) - } +// PendingInput proxies into our registered console-input driver. +func (co *ConsoleIn) PendingInput() bool { + return co.driver.PendingInput() +} - // read only a single byte - b := make([]byte, 1) - _, err = os.Stdin.Read(b) - if err != nil { - return 0x00, fmt.Errorf("error reading a byte from stdin %s", err) - } +// BlockForCharacterNoEcho proxies into our registered console-input driver. +func (co *ConsoleIn) BlockForCharacterNoEcho() (byte, error) { + return co.driver.BlockForCharacterNoEcho() +} - // restore the state of the terminal to avoid mixing RAW/Cooked - err = term.Restore(int(os.Stdin.Fd()), oldState) - if err != nil { - return 0x00, fmt.Errorf("error restoring terminal state %s", err) +// BlockForCharacterWithEcho blocks for input and shows that input before it +// is returned. +// +// This function DOES NOT proxy to our registered console-input driver. +func (co *ConsoleIn) BlockForCharacterWithEcho() (byte, error) { + c, err := co.driver.BlockForCharacterNoEcho() + if err == nil { + fmt.Printf("%c", c) } - - fmt.Printf("%c", b[0]) - return b[0], nil + return c, err } -// ReadLine reads a line of input from the console, truncating to the -// length specified. -// -// Note: We should enable echo in this function. +// ReadLine handles the input of a single line of text. // -// NOTE: A user pressing Ctrl-C will be caught, and this will trigger the BDOS -// function to reboot. We have a variable holding the number of consecutive -// Ctrl-C characters are required to trigger this behaviour. -// -// NOTE: We erase the input buffer with ESC, and allow history movement via -// Ctrl-p and Ctrl-n. -func (ci *ConsoleIn) ReadLine(max uint8) (string, error) { - +// This function DOES NOT proxy to our registered console-input driver. +func (co *ConsoleIn) ReadLine(max uint8) (string, error) { // Text the user entered text := "" @@ -223,7 +182,7 @@ func (ci *ConsoleIn) ReadLine(max uint8) (string, error) { } } - // Wwe're expecting the user to enter a line of text, + // We're expecting the user to enter a line of text, // but we process their input in terms of characters. // // We do that so that we can react to special characters @@ -234,7 +193,7 @@ func (ci *ConsoleIn) ReadLine(max uint8) (string, error) { for { // Get a character, with no echo. - x, err := ci.BlockForCharacterNoEcho() + x, err := co.driver.BlockForCharacterNoEcho() if err != nil { return "", err } @@ -255,9 +214,9 @@ func (ci *ConsoleIn) ReadLine(max uint8) (string, error) { eraseInput() - if len(ci.history)-offset < len(ci.history) { + if len(history)-offset < len(history) { // replace with a suitable value, and show it - text = ci.history[len(ci.history)-offset] + text = history[len(history)-offset] fmt.Printf("%s", text) } } @@ -266,7 +225,7 @@ func (ci *ConsoleIn) ReadLine(max uint8) (string, error) { // Ctrl-P? if x == 16 { - if offset >= len(ci.history) { + if offset >= len(history) { continue } offset += 1 @@ -274,7 +233,7 @@ func (ci *ConsoleIn) ReadLine(max uint8) (string, error) { eraseInput() // replace with a suitable value, and show it - text = ci.history[len(ci.history)-offset] + text = history[len(history)-offset] fmt.Printf("%s", text) continue @@ -290,7 +249,7 @@ func (ci *ConsoleIn) ReadLine(max uint8) (string, error) { // If we've hit our limit of consecutive Ctrl-Cs // then we return the interrupted error-code - if ctrlCount == ci.InterruptCount { + if ctrlCount == interruptCount { return "", ErrInterrupted } } @@ -305,12 +264,12 @@ func (ci *ConsoleIn) ReadLine(max uint8) (string, error) { if text != "" { // If we have no history, save it. - if len(ci.history) == 0 { - ci.history = append(ci.history, text) + if len(history) == 0 { + history = append(history, text) } else { // otherwise only add if different to previous entry. - if text != ci.history[len(ci.history)-1] { - ci.history = append(ci.history, text) + if text != history[len(history)-1] { + history = append(history, text) } } } @@ -355,20 +314,3 @@ func (ci *ConsoleIn) ReadLine(max uint8) (string, error) { // Return the text return text, nil } - -// Reset restores echo. -func (ci *ConsoleIn) Reset() { - ci.enableEcho() -} - -// disableEcho is the single place where we disable echoing. -func (ci *ConsoleIn) disableEcho() { - _ = exec.Command("stty", "-F", "/dev/tty", "-echo").Run() - ci.State = NoEcho -} - -// enableEcho is the single place where we enable echoing. -func (ci *ConsoleIn) enableEcho() { - _ = exec.Command("stty", "-F", "/dev/tty", "echo").Run() - ci.State = Echo -} diff --git a/consolein/consolein_test.go b/consolein/consolein_test.go index a951280..b198e43 100644 --- a/consolein/consolein_test.go +++ b/consolein/consolein_test.go @@ -2,16 +2,19 @@ package consolein import "testing" -func TestReadline(t *testing.T) { +func TestReadlineSTTY(t *testing.T) { - x := New() - x.State = Echo + // Create a helper + x := STTYInput{} + + ch := ConsoleIn{} + ch.driver = &x // Simple readline // Here \x10 is the Ctrl-P which would use the previous history // as we're just created we have none so it is ignored. - x.stuffed = "s\x10teve\n" - out, err := x.ReadLine(20) + x.StuffInput("s\x10teve\n") + out, err := ch.ReadLine(20) if err != nil { t.Fatalf("unexpected error") } @@ -21,17 +24,15 @@ func TestReadline(t *testing.T) { // Ctrl-C at start of the line should trigger a reboot-error // x.stuffed = string([]byte{0x03, 0x03, 0x00}a) - x.stuffed = "\x03\x03steve" - x.State = Echo - _, err = x.ReadLine(20) + x.StuffInput("\x03\x03steve") + _, err = ch.ReadLine(20) if err != ErrInterrupted { t.Fatalf("unexpected error %s", err) } // Ctrl-C at the middle of a line should not - x.stuffed = "steve\x03\x03steve\n" - x.State = Echo - out, err = x.ReadLine(20) + x.StuffInput("steve\x03\x03steve\n") + out, err = ch.ReadLine(20) if err != nil { t.Fatalf("unexpected error %s", err) } @@ -40,9 +41,8 @@ func TestReadline(t *testing.T) { } // Ctrl-B overwrites - x.stuffed = "steve\b\b\b\b\bHello\n" - x.State = Echo - out, err = x.ReadLine(20) + x.StuffInput("steve\b\b\b\b\bHello\n") + out, err = ch.ReadLine(20) if err != nil { t.Fatalf("unexpected error %s", err) } @@ -51,9 +51,8 @@ func TestReadline(t *testing.T) { } // ESC resets input - x.stuffed = "steve\x1BHello\n" - x.State = Echo - out, err = x.ReadLine(20) + x.StuffInput("steve\x1BHello\n") + out, err = ch.ReadLine(20) if err != nil { t.Fatalf("unexpected error %s", err) } @@ -62,9 +61,8 @@ func TestReadline(t *testing.T) { } // Too much input? We truncate - x.stuffed = "I like to move it, move it\n" - x.State = Echo - out, err = x.ReadLine(5) + x.StuffInput("I like to move it, move it\n") + out, err = ch.ReadLine(5) if err != nil { t.Fatalf("unexpected error %s", err) } @@ -73,10 +71,9 @@ func TestReadline(t *testing.T) { } // Add some history, and return the last value - x.history = append(x.history, "I like to move it") - x.stuffed = "ste\x10\n" - x.State = Echo - out, err = x.ReadLine(5) + history = append(history, "I like to move it") + x.StuffInput("ste\x10\n") + out, err = ch.ReadLine(5) if err != nil { t.Fatalf("unexpected error %s", err) } @@ -84,37 +81,35 @@ func TestReadline(t *testing.T) { t.Fatalf("unexpected output %s", out) } - // Go back and forwardd in history - - x.stuffed = "\x10\x10\x10\x0e\n" - x.State = Echo - out, err = x.ReadLine(10) + // Go back and forward in history + x.StuffInput("\x10\x10\x10\x0e\n") + out, err = ch.ReadLine(10) if err != nil { t.Fatalf("unexpected error %s", err) } if out != "Hello" { t.Fatalf("unexpected output %s", out) } - } func TestCtrlC(t *testing.T) { - x := New() + ch := ConsoleIn{} - if x.InterruptCount != 2 { + if interruptCount != 1 { t.Fatalf("unexpected default interrupt count") } - x.SetInterruptCount((3)) - if x.GetInterruptCount() != 3 { + ch.SetInterruptCount((3)) + if ch.GetInterruptCount() != 3 { t.Fatalf("unexpected interrupt count") } } func TestPending(t *testing.T) { - x := New() + // Create a helper + x := STTYInput{} x.StuffInput("foo") if !x.PendingInput() { @@ -123,24 +118,53 @@ func TestPending(t *testing.T) { } -// TestExec is just here for coverate -func TestExec(t *testing.T) { +// TestDriverRegistration performs some sanity-check on our driver-registration. +func TestDriverRegistration(t *testing.T) { - x := New() + if len(handlers.m) != 2 { + t.Fatalf("wrong number of handlers") + } - // No echo - x.disableEcho() - if x.State != NoEcho { - t.Fatalf("unexpected state") + _, ok := handlers.m["term"] + if !ok { + t.Fatalf("failed to find expected handler, term") + } + _, err := New("term") + if err != nil { + t.Fatalf("failed to find expected handler, term") } - x.enableEcho() - if x.State != Echo { - t.Fatalf("unexpected state") + _, ok = handlers.m["stty"] + if !ok { + t.Fatalf("failed to find expected handler, stty") + } + _, err = New("stty") + if err != nil { + t.Fatalf("failed to find expected handler, term") } - x.Reset() - if x.State != Echo { - t.Fatalf("unexpected state") + _, ok = handlers.m["bogus"] + if ok { + t.Fatalf("found unexpected handler!") } + _, err = New("bogus") + if err == nil { + t.Fatalf("failed to find expected handler, term") + } + + obj, err2 := New("stty") + if err2 != nil { + t.Fatalf("error looking up driver") + } + drv := obj.GetDriver() + if drv.GetName() != "stty" { + t.Fatalf("naming mismatch on driver!") + } + if obj.GetName() != "stty" { + t.Fatalf("naming mismatch on driver!") + } + if len(obj.GetDrivers()) != 2 { + t.Fatalf("driver count is wrong") + } + } diff --git a/consolein/drv_stty.go b/consolein/drv_stty.go new file mode 100644 index 0000000..6abe42f --- /dev/null +++ b/consolein/drv_stty.go @@ -0,0 +1,157 @@ +// drv_stty creates a console input-driver which uses the +// `stty` binary to set our echo/no-echo state. +// +// This is obviously not portable outwith Unix-like systems. + +package consolein + +import ( + "fmt" + "os" + "os/exec" + + "golang.org/x/term" +) + +// EchoStatus is used to record our current state. +type EchoStatus int + +var ( + // Unknown means we don't know the status of echo/noecho + Unknown EchoStatus = 0 + + // Echo means that input will echo characters. + Echo EchoStatus = 1 + + // NoEcho means that input will not echo characters. + NoEcho EchoStatus = 2 +) + +// STTYInput is an input-driver that executes the 'stty' binary +// to toggle between echoing character input, and disabling the +// echo. +// +// This is slow, as you can imagine, and non-portable outwith Unix-like +// systems. To mitigate against the speed-issue we keep track of "echo" +// versus "noecho" states, to minimise the executions. +type STTYInput struct { + + // state holds our state + state EchoStatus + + // stuffed holds fake input which has been forced into the buffer used + // by ReadLine + stuffed string +} + +// Setup is a NOP. +func (si *STTYInput) Setup() { + // NOP +} + +// TearDown resets the state of the terminal. +func (si *STTYInput) TearDown() { + if si.state != Echo { + si.enableEcho() + } +} + +// PendingInput returns true if there is pending input from STDIN.. +// +// Note that we have to set RAW mode, without this input is laggy +// and zork doesn't run. +func (si *STTYInput) PendingInput() bool { + + // Do we have faked/stuffed input to process? + if len(si.stuffed) > 0 { + return true + } + + // switch stdin into 'raw' mode + oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + return false + } + + // Platform-specific code in select_XXXX.go + res := canSelect() + + // restore the state of the terminal to avoid mixing RAW/Cooked + err = term.Restore(int(os.Stdin.Fd()), oldState) + if err != nil { + return false + } + + // Return true if we have something ready to read. + return res +} + +// StuffInput inserts fake values into our input-buffer +func (si *STTYInput) StuffInput(input string) { + si.stuffed = input +} + +// BlockForCharacterNoEcho returns the next character from the console, blocking until +// one is available. +// +// NOTE: This function should not echo keystrokes which are entered. +func (si *STTYInput) BlockForCharacterNoEcho() (byte, error) { + + // Do we have faked/stuffed input to process? + if len(si.stuffed) > 0 { + c := si.stuffed[0] + si.stuffed = si.stuffed[1:] + return c, nil + } + + // Do we need to change state? If so then do it. + if si.state != NoEcho { + si.disableEcho() + } + + // switch stdin into 'raw' mode + oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + return 0x00, fmt.Errorf("error making raw terminal %s", err) + } + + // read only a single byte + b := make([]byte, 1) + _, err = os.Stdin.Read(b) + if err != nil { + return 0x00, fmt.Errorf("error reading a byte from stdin %s", err) + } + + // restore the state of the terminal to avoid mixing RAW/Cooked + err = term.Restore(int(os.Stdin.Fd()), oldState) + if err != nil { + return 0x00, fmt.Errorf("error restoring terminal state %s", err) + } + + // Return the character we read + return b[0], nil +} + +// disableEcho is the single place where we disable echoing. +func (si *STTYInput) disableEcho() { + _ = exec.Command("stty", "-F", "/dev/tty", "-echo").Run() + si.state = NoEcho +} + +// enableEcho is the single place where we enable echoing. +func (si *STTYInput) enableEcho() { + _ = exec.Command("stty", "-F", "/dev/tty", "echo").Run() + si.state = Echo +} + +// GetName is part of the module API, and returns the name of this driver. +func (si *STTYInput) GetName() string { + return "stty" +} + +// init registers our driver, by name. +func init() { + Register("stty", func() ConsoleInput { + return &STTYInput{} + }) +} diff --git a/consolein/drv_term.go b/consolein/drv_term.go new file mode 100644 index 0000000..95f7e23 --- /dev/null +++ b/consolein/drv_term.go @@ -0,0 +1,159 @@ +// drv_term.go uses the Termbox library to handle console-based input. +// +// A goroutine is launched which collects any keyboard input and +// saves that to a buffer where it can be peeled off on-demand. +// +// The portability of this solution is unknown, however this driver +// _seems_ reasonable and is the default. + +package consolein + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/nsf/termbox-go" + "golang.org/x/term" +) + +// TermboxInput is our input-driver, using termbox +type TermboxInput struct { + + // oldState contains the state of the terminal, before switching to RAW mode + oldState *term.State + + // Cancel holds a context which can be used to close our polling goroutine + Cancel context.CancelFunc + + // stuffed holds fake input which has been forced into the buffer used + // by ReadLine + stuffed string + + // keyBuffer builds up keys read "in the background", via termbox + keyBuffer []rune +} + +// Setup ensures that the termbox init functions are called, and our +// terminal is set into RAW mode. +func (ti *TermboxInput) Setup() { + + var err error + + // switch STDIN into 'raw' mode - we must do this before + // we setup termbox. + ti.oldState, err = term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + panic(err) + } + + // Setup the terminal. + err = termbox.Init() + if err != nil { + panic(err) + } + + // This is "Show Cursor" which termbox hides by default. + // + // Sigh. + fmt.Printf("\x1b[?25h") + + // Allow our polling of keyboard to be canceled + ctx, cancel := context.WithCancel(context.Background()) + ti.Cancel = cancel + + // Start polling for keyboard input "in the background". + go ti.pollKeyboard(ctx) +} + +// pollKeyboard runs in a goroutine and collects keyboard input +// into a buffer where it will be read from in the future. +func (ti *TermboxInput) pollKeyboard(ctx context.Context) { + for { + // Are we done? + select { + case <-ctx.Done(): + return + default: + // NOP + } + + // Now look for keyboard input + switch ev := termbox.PollEvent(); ev.Type { + case termbox.EventKey: + if ev.Ch != 0 { + ti.keyBuffer = append(ti.keyBuffer, ev.Ch) + } else { + ti.keyBuffer = append(ti.keyBuffer, rune(ev.Key)) + } + } + } +} + +// TearDown resets the state of the terminal, disables the background polling of characters +// and generally gets us ready for exit. +func (ti *TermboxInput) TearDown() { + + // Cancel the keyboard reading + ti.Cancel() + + // Terminate the GUI. + termbox.Close() + + // Restore the terminal + term.Restore(int(os.Stdin.Fd()), ti.oldState) +} + +// StuffInput inserts fake values into our input-buffer +func (ti *TermboxInput) StuffInput(input string) { + ti.stuffed = input +} + +// PendingInput returns true if there is pending input from STDIN. +func (ti *TermboxInput) PendingInput() bool { + + // Do we have faked/stuffed input to process? + if len(ti.stuffed) > 0 { + return true + } + + // Otherwise only if we've read stuff. + return len(ti.keyBuffer) > 0 +} + +// BlockForCharacterNoEcho returns the next character from the console, blocking until +// one is available. +// +// NOTE: This function should not echo keystrokes which are entered. +func (ti *TermboxInput) BlockForCharacterNoEcho() (byte, error) { + + // Do we have faked/stuffed input to process? + if len(ti.stuffed) > 0 { + c := ti.stuffed[0] + ti.stuffed = ti.stuffed[1:] + return c, nil + } + + // Otherwise only if we've read stuff. + for len(ti.keyBuffer) == 0 { + time.Sleep(1 * time.Millisecond) + } + + // Return the character + c := ti.keyBuffer[0] + ti.keyBuffer = ti.keyBuffer[1:] + return byte(c), nil +} + +// GetName is part of the module API, and returns the name of this driver. +func (ti *TermboxInput) GetName() string { + return "term" +} + +// init registers our driver, by name. +func init() { + Register("term", func() ConsoleInput { + return &TermboxInput{} + }) +} diff --git a/cpm/cpm.go b/cpm/cpm.go index 14bf8b6..af5d29a 100644 --- a/cpm/cpm.go +++ b/cpm/cpm.go @@ -233,6 +233,22 @@ func WithConsoleDriver(name string) cpmoption { } } +// WithInputDriver allows the console input driver to be created in our +// constructor. +func WithInputDriver(name string) cpmoption { + + return func(c *CPM) error { + + driver, err := consolein.New(name) + if err != nil { + return err + } + + c.input = driver + return nil + } +} + // New returns a new emulation object. We support default options, // and new defaults may be specified via WithConsoleDriver, etc, etc. func New(options ...cpmoption) (*CPM, error) { @@ -496,7 +512,13 @@ func New(options ...cpmoption) (*CPM, error) { } // Default output driver - driver, err := consoleout.New("adm-3a") + oDriver, err := consoleout.New("adm-3a") + if err != nil { + return nil, err + } + + // Default input driver + iDriver, err := consolein.New("term") if err != nil { return nil, err } @@ -509,8 +531,8 @@ func New(options ...cpmoption) (*CPM, error) { dma: 0x0080, drives: make(map[string]string), files: make(map[uint16]FileCache), - input: consolein.New(), - output: driver, // default + input: iDriver, // default + output: oDriver, // default prnPath: "printer.log", // default start: 0x0100, launchTime: time.Now(), @@ -527,9 +549,19 @@ func New(options ...cpmoption) (*CPM, error) { return tmp, nil } -// Cleanup cleans up the state of the terminal, if necessary. -func (cpm *CPM) Cleanup() { - cpm.input.Reset() +// IOTearDown cleans up the state of the terminal, if necessary. +func (cpm *CPM) IOSetup() { + cpm.input.Setup() +} + +// IOTearDown cleans up the state of the terminal, if necessary. +func (cpm *CPM) IOTearDown() { + cpm.input.TearDown() +} + +// GetInputDriver returns the configured input driver. +func (cpm *CPM) GetInputDriver() consolein.ConsoleInput { + return cpm.input.GetDriver() } // GetOutputDriver returns the configured output driver. diff --git a/cpm/cpm_bdos_test.go b/cpm/cpm_bdos_test.go index f1808bb..15b5c4a 100644 --- a/cpm/cpm_bdos_test.go +++ b/cpm/cpm_bdos_test.go @@ -12,7 +12,10 @@ import ( "github.com/skx/cpmulator/static" ) +// Flaky with our new implementation. func TestConsoleInput(t *testing.T) { + return + // Create a new helper c, err := New(WithPrinterPath("11.log")) if err != nil { @@ -28,10 +31,6 @@ func TestConsoleInput(t *testing.T) { if err != nil { t.Fatalf("failed to call CP/M") } - err = BdosSysCallReadChar(c) - if err == nil { - t.Fatalf("expected error, got none") - } if c.CPU.States.AF.Hi != 's' { t.Fatalf("got the wrong input") } diff --git a/go.mod b/go.mod index cbb2ede..ba7e304 100644 --- a/go.mod +++ b/go.mod @@ -8,3 +8,8 @@ require ( ) require golang.org/x/sys v0.22.0 + +require ( + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/nsf/termbox-go v1.1.1 // indirect +) diff --git a/go.sum b/go.sum index 5e2d4f7..d65a665 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,10 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/koron-go/z80 v0.10.1 h1:Jfb0esP/QFL4cvcr+eFECVG0Y/mA9JBLC4EKbMU5zAY= github.com/koron-go/z80 v0.10.1/go.mod h1:ry+Zl9kRKelzaDG9UzEtUpUnXy0Yv/kk1YEaX958xdk= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY= +github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= diff --git a/main.go b/main.go index d444099..c92e410 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "strings" cpmccp "github.com/skx/cpmulator/ccp" + "github.com/skx/cpmulator/consolein" "github.com/skx/cpmulator/consoleout" "github.com/skx/cpmulator/cpm" "github.com/skx/cpmulator/static" @@ -28,19 +29,21 @@ func main() { // // Parse the command-line flags for this driver-application // + ccp := flag.String("ccp", "ccpz", "The name of the CCP that we should run (ccp vs. ccpz).") cd := flag.String("cd", "", "Change to this directory before launching") - createDirectories := flag.Bool("create", false, "Create subdirectories on the host computer for each CP/M drive.") console := flag.String("console", "adm-3a", "The name of the console output driver to use (adm-3a or ansi).") - ccp := flag.String("ccp", "ccpz", "The name of the CCP that we should run (ccp vs. ccpz).") - useDirectories := flag.Bool("directories", false, "Use subdirectories on the host computer for CP/M drives.") - logPath := flag.String("log-path", "", "Specify the file to write debug logs to.") + createDirectories := flag.Bool("create", false, "Create subdirectories on the host computer for each CP/M drive.") + input := flag.String("input", "term", "The name of the console input driver to use (term or stty).") logAll := flag.Bool("log-all", false, "Log the output of all functions, including the noisy Console I/O ones.") + logPath := flag.String("log-path", "", "Specify the file to write debug logs to.") prnPath := flag.String("prn-path", "print.log", "Specify the file to write printer-output to.") showVersion := flag.Bool("version", false, "Report our version, and exit.") + useDirectories := flag.Bool("directories", false, "Use subdirectories on the host computer for CP/M drives.") // listing listCcps := flag.Bool("list-ccp", false, "Dump the list of embedded CCPs.") - listConsole := flag.Bool("list-console-drivers", false, "Dump the list of valid console drivers.") + listOutput := flag.Bool("list-output-drivers", false, "Dump the list of valid console output drivers.") + listInput := flag.Bool("list-input-drivers", false, "Dump the list of valid console input drivers.") listSyscalls := flag.Bool("list-syscalls", false, "Dump the list of implemented BIOS/BDOS syscall functions.") // drives @@ -73,8 +76,19 @@ func main() { return } - // Are we dumping console drivers? - if *listConsole { + // Are we dumping console input drivers? + if *listInput { + obj, _ := consolein.New("null") + valid := obj.GetDrivers() + + for _, name := range valid { + fmt.Printf("%s\n", name) + } + return + } + + // Are we dumping console output drivers? + if *listOutput { obj, _ := consoleout.New("null") valid := obj.GetDrivers() @@ -83,6 +97,7 @@ func main() { } return } + // Are we dumping syscalls? if *listSyscalls { @@ -176,6 +191,7 @@ func main() { // Create a new emulator. obj, err := cpm.New(cpm.WithPrinterPath(*prnPath), cpm.WithConsoleDriver(*console), + cpm.WithInputDriver(*input), cpm.WithCCP(*ccp)) if err != nil { fmt.Printf("error creating CPM object: %s\n", err) @@ -187,8 +203,12 @@ func main() { obj.LogNoisy() } + // I/O SETUP + obj.IOSetup() + + // I/O TearDown // When we're finishing we'll reset some (console) state. - defer obj.Cleanup() + defer obj.IOTearDown() // change directory? // @@ -296,7 +316,7 @@ func main() { } // Show a startup-banner. - fmt.Printf("\ncpmulator %s loaded CCP %s, with %s output driver\n", cpmver.GetVersionString(), obj.GetCCPName(), obj.GetOutputDriver().GetName()) + fmt.Printf("\ncpmulator %s CCP %s, input driver %s, output driver %s\n", cpmver.GetVersionString(), obj.GetCCPName(), obj.GetInputDriver().GetName(), obj.GetOutputDriver().GetName()) // We will load AUTOEXEC.SUB, once, if it exists (*) // From 416d605913abf92a334070ff24490aa6a7c8faf1 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Sun, 29 Dec 2024 19:40:54 +0200 Subject: [PATCH 02/21] Fixed tests to work once again. --- consolein/consolein.go | 2 +- consolein/consolein_test.go | 2 +- consolein/drv_stty.go | 2 +- consolein/drv_term.go | 11 +++++++---- cpm/cpm.go | 2 +- cpm/cpm_bdos_test.go | 19 +++++++------------ cpm/cpm_bios_test.go | 4 ++-- cpm/cpm_test.go | 2 +- 8 files changed, 21 insertions(+), 23 deletions(-) diff --git a/consolein/consolein.go b/consolein/consolein.go index 936366e..3591e46 100644 --- a/consolein/consolein.go +++ b/consolein/consolein.go @@ -47,7 +47,7 @@ var handlers = struct { }{m: make(map[string]Constructor)} // This is the count of Ctrl-C which we keep track of to allow "reboots" -var interruptCount int = 1 +var interruptCount int = 2 // history holds previous (line) input. var history []string diff --git a/consolein/consolein_test.go b/consolein/consolein_test.go index b198e43..4443e89 100644 --- a/consolein/consolein_test.go +++ b/consolein/consolein_test.go @@ -96,7 +96,7 @@ func TestCtrlC(t *testing.T) { ch := ConsoleIn{} - if interruptCount != 1 { + if interruptCount != 2 { t.Fatalf("unexpected default interrupt count") } diff --git a/consolein/drv_stty.go b/consolein/drv_stty.go index 6abe42f..838b1fa 100644 --- a/consolein/drv_stty.go +++ b/consolein/drv_stty.go @@ -152,6 +152,6 @@ func (si *STTYInput) GetName() string { // init registers our driver, by name. func init() { Register("stty", func() ConsoleInput { - return &STTYInput{} + return new(STTYInput) }) } diff --git a/consolein/drv_term.go b/consolein/drv_term.go index 95f7e23..4e4d884 100644 --- a/consolein/drv_term.go +++ b/consolein/drv_term.go @@ -94,15 +94,18 @@ func (ti *TermboxInput) pollKeyboard(ctx context.Context) { // TearDown resets the state of the terminal, disables the background polling of characters // and generally gets us ready for exit. func (ti *TermboxInput) TearDown() { - // Cancel the keyboard reading - ti.Cancel() + if ti.Cancel != nil { + ti.Cancel() + } // Terminate the GUI. termbox.Close() // Restore the terminal - term.Restore(int(os.Stdin.Fd()), ti.oldState) + if ti.oldState != nil { + term.Restore(int(os.Stdin.Fd()), ti.oldState) + } } // StuffInput inserts fake values into our input-buffer @@ -154,6 +157,6 @@ func (ti *TermboxInput) GetName() string { // init registers our driver, by name. func init() { Register("term", func() ConsoleInput { - return &TermboxInput{} + return new(TermboxInput) }) } diff --git a/cpm/cpm.go b/cpm/cpm.go index af5d29a..f360c77 100644 --- a/cpm/cpm.go +++ b/cpm/cpm.go @@ -549,7 +549,7 @@ func New(options ...cpmoption) (*CPM, error) { return tmp, nil } -// IOTearDown cleans up the state of the terminal, if necessary. +// IOSetup ensures that our I/O is ready. func (cpm *CPM) IOSetup() { cpm.input.Setup() } diff --git a/cpm/cpm_bdos_test.go b/cpm/cpm_bdos_test.go index 15b5c4a..c18ac42 100644 --- a/cpm/cpm_bdos_test.go +++ b/cpm/cpm_bdos_test.go @@ -14,16 +14,15 @@ import ( // Flaky with our new implementation. func TestConsoleInput(t *testing.T) { - return // Create a new helper - c, err := New(WithPrinterPath("11.log")) + c, err := New(WithPrinterPath("11.log"), WithInputDriver("term")) if err != nil { t.Fatalf("failed to create CPM") } c.Memory = new(memory.Memory) c.fixupRAM() - defer c.Cleanup() + defer c.IOTearDown() // ReadChar c.StuffText("s") @@ -44,10 +43,6 @@ func TestConsoleInput(t *testing.T) { if c.CPU.States.AF.Hi != 'k' { t.Fatalf("got the wrong input") } - err = BdosSysCallAuxRead(c) - if err == nil { - t.Fatalf("expected to get an error, but didn't") - } // RawIO c.StuffText("x") @@ -105,7 +100,7 @@ func TestUnimplemented(t *testing.T) { } c.Memory = new(memory.Memory) c.fixupRAM() - defer c.Cleanup() + defer c.IOTearDown() // Create a binary var file *os.File @@ -154,7 +149,7 @@ func TestBoot(t *testing.T) { } c.Memory = new(memory.Memory) c.fixupRAM() - defer c.Cleanup() + defer c.IOTearDown() // Create a binary var file *os.File @@ -207,7 +202,7 @@ func TestFind(t *testing.T) { c.fixupRAM() c.SetDrives(false) c.SetStaticFilesystem(static.GetContent()) - defer c.Cleanup() + defer c.IOTearDown() // Create a pattern in an FCB name := "*.GO" @@ -348,7 +343,7 @@ func TestReadLine(t *testing.T) { c.Memory = new(memory.Memory) // Stuff some fake input - c.input = consolein.New() + c.input, _ = consolein.New("stty") c.StuffText("steve\n") // Setup a buffer, so we can read 5 characters @@ -1162,7 +1157,7 @@ func TestFileOpen(t *testing.T) { c.Memory = new(memory.Memory) c.fixupRAM() c.SetDrives(false) - defer c.Cleanup() + defer c.IOTearDown() // Create a binary diff --git a/cpm/cpm_bios_test.go b/cpm/cpm_bios_test.go index 3884f10..563e3db 100644 --- a/cpm/cpm_bios_test.go +++ b/cpm/cpm_bios_test.go @@ -294,7 +294,7 @@ func TestBIOSConsoleInput(t *testing.T) { } c.Memory = new(memory.Memory) c.fixupRAM() - defer c.Cleanup() + defer c.IOTearDown() c.input.StuffInput("s") err = BiosSysCallConsoleInput(c) @@ -317,7 +317,7 @@ func TestBIOSError(t *testing.T) { } c.Memory = new(memory.Memory) c.fixupRAM() - defer c.Cleanup() + defer c.IOTearDown() c.simpleDebug = true diff --git a/cpm/cpm_test.go b/cpm/cpm_test.go index 95fa541..99ecd27 100644 --- a/cpm/cpm_test.go +++ b/cpm/cpm_test.go @@ -95,7 +95,7 @@ func TestSimple(t *testing.T) { t.Fatalf("failed to run binary!") } - defer obj.Cleanup() + defer obj.IOTearDown() } func TestBogusConstructor(t *testing.T) { From d4cf66874a5c4881f1eccd88859ce9dbe2dc85ca Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Sun, 29 Dec 2024 19:46:06 +0200 Subject: [PATCH 03/21] Report on failure to run term.Restore --- consolein/drv_term.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/consolein/drv_term.go b/consolein/drv_term.go index 4e4d884..636fc4b 100644 --- a/consolein/drv_term.go +++ b/consolein/drv_term.go @@ -104,7 +104,10 @@ func (ti *TermboxInput) TearDown() { // Restore the terminal if ti.oldState != nil { - term.Restore(int(os.Stdin.Fd()), ti.oldState) + err := term.Restore(int(os.Stdin.Fd()), ti.oldState) + if err != nil { + fmt.Printf("failed to restore terminal:%s\n", err) + } } } From afe8d3100eddb4ad77302ca5d82f8a7942c307a2 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Sun, 29 Dec 2024 20:07:20 +0200 Subject: [PATCH 04/21] Improve test-coverage on new wrapper.! --- consolein/consolein_test.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/consolein/consolein_test.go b/consolein/consolein_test.go index 4443e89..c6c0ff0 100644 --- a/consolein/consolein_test.go +++ b/consolein/consolein_test.go @@ -92,6 +92,41 @@ func TestReadlineSTTY(t *testing.T) { } } +// TestOverview just calls most of the methods, as an overview, to bump coverage. +func TestOverview(t *testing.T) { + + // Create a helper + x := STTYInput{} + + ch := ConsoleIn{} + ch.driver = &x + + ch.Setup() + defer ch.TearDown() + + ch.StuffInput("1.2.3.4.5.6.7.8.9.0\n") + + if !ch.PendingInput() { + t.Fatalf("should have pending input") + } + + c, err := ch.BlockForCharacterNoEcho() + if err != nil { + t.Fatalf("unexpected error") + } + if c != '1' { + t.Fatalf("wrong character") + } + + c, err = ch.BlockForCharacterWithEcho() + if err != nil { + t.Fatalf("unexpected error") + } + if c != '.' { + t.Fatalf("wrong character") + } +} + func TestCtrlC(t *testing.T) { ch := ConsoleIn{} From 998d252151df20ad7f2f76e5767b24c4c383e430 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Sun, 29 Dec 2024 20:21:01 +0200 Subject: [PATCH 05/21] Reordered sections on portability --- README.md | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 0790935..19db2dd 100644 --- a/README.md +++ b/README.md @@ -62,22 +62,6 @@ Releases will be made as/when features seem to justify it, but it should be note -# Portability - -The CP/M input handlers need to disable echoing when reading (single) characters from STDIN. There isn't a simple and portable solution for this in golang, although the appropriate primitives exist so building such support isn't impossible, it just relies upon writing per-environment support, using something like the [ReadPassword](https://pkg.go.dev/golang.org/x/term#ReadPassword) function from the standard-library. - -I sidestepped this whole problem initially, just invoking the `stty` binary to enable/disable the echoing of characters on-demand, but that only works on Linux, BSD, and Mac hosts. To be properly portable I had to use the [termbox](https://github.com/nsf/termbox-go) library for all input, but that means we get no scrollback/history so there's a tradeoff to be made. - -By default input will be read via `termbox` but you may you specify a different driver via the CLI arguments: - -* `cpmulator -input xxx` - * Use the input-driver named `xxx`. -* `cpmulator -list-input-drivers` - * List all available input-drivers. - - - - # Usage If you launch `cpmulator` with no arguments then the default CCP ("console command processor") will be launched, dropping you into a familiar shell: @@ -174,7 +158,7 @@ This allows you to customize the emulator, or perform other "one-time" setup via There are a small number of [extensions](EXTENSIONS.md) added to the BIOS functionality we provide, and these extensions allow changing the behaviour of the emulator at runtime. -The behaviour changing is achieved by having a small number of .COM files invoke the extension functions, and these binaries are embedded within our emulator to improve ease of use, via the [static/](static/) directory in our source-tree - This means no matter what you'll always find some binaries installed on A:, despite not being present in reality. +The behaviour changing is achieved by having a small number of .COM files invoke the extension functions, and these binaries are embedded within our emulator to improve ease of use, via the [static/](static/) directory in our source-tree. This means no matter what you'll always find some binaries installed on A:, despite not being present in reality. > **NOTE** To avoid naming collisions all our embedded binaries are named with a `!` prefix, except for `#.COM` which is designed to be used as a comment-binary. @@ -199,6 +183,8 @@ Run `A:!CONSOLE ansi` to disable the output emulation, or `A:!CONSOLE adm-3a` to You'll see that the [cpm-dist](https://github.com/skx/cpm-dist) repository contains a version of Wordstar, and that behaves differently depending on the selected output handler. Changing the handler at run-time is a neat bit of behaviour. +You'll note it is **not** possible to change the console _input_ driver at runtime, I think once you know which works best upon your system it doesn't make sense to change this interactively. + ### Debug Handling @@ -327,6 +313,22 @@ The implementation of the syscalls is the core of our emulator, and they can be +# Portability + +The CP/M input handlers need to disable echoing when reading (single) characters from STDIN. There isn't a simple and portable solution for this in golang, although the appropriate primitives exist so building such support isn't impossible, it just relies upon writing per-environment support, using something like the [ReadPassword](https://pkg.go.dev/golang.org/x/term#ReadPassword) function from the standard-library. + +I sidestepped this whole problem initially, just invoking the `stty` binary to enable/disable the echoing of characters on-demand, but that only works on Linux, BSD, and Mac hosts. To be properly portable I had to use the [termbox](https://github.com/nsf/termbox-go) library for all input, but that means we get no scrollback/history so there's a tradeoff to be made. + +By default input will be read via `termbox` but you may you specify a different driver via the CLI arguments: + +* `cpmulator -input xxx` + * Use the input-driver named `xxx`. +* `cpmulator -list-input-drivers` + * List all available input-drivers. + + + + # Debugging Failures & Tweaking Behaviour When an unimplemented BIOS call is attempted the program it will abort with a fatal error, for example: From 4b2756a43530fd32b42f80c48eeadc2c3754144f Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Sun, 29 Dec 2024 20:39:15 +0200 Subject: [PATCH 06/21] Rename ConsoleDriver to ConsoleOutput, to match ConsoleInput, and added minor comment updates. --- consolein/consolein.go | 4 ++++ consoleout/consoleout.go | 17 +++++++++++------ consoleout/drv_adm3a.go | 2 +- consoleout/drv_ansi.go | 2 +- consoleout/drv_logger.go | 2 +- consoleout/drv_null.go | 2 +- cpm/cpm.go | 2 +- 7 files changed, 20 insertions(+), 11 deletions(-) diff --git a/consolein/consolein.go b/consolein/consolein.go index 3591e46..739da4d 100644 --- a/consolein/consolein.go +++ b/consolein/consolein.go @@ -19,6 +19,10 @@ var ErrInterrupted error = fmt.Errorf("INTERRUPTED") // // Providing this interface is implemented an object may register itself, // by name, via the Register method. +// +// You can compare this interface to the corresponding ConsoleOutput one, +// that delegates everything to the drivers rather than having some wrapper +// methods building upon the drivers as we do here. type ConsoleInput interface { // Setup performs any specific setup which is required. diff --git a/consoleout/consoleout.go b/consoleout/consoleout.go index b750d1c..525c881 100644 --- a/consoleout/consoleout.go +++ b/consoleout/consoleout.go @@ -10,14 +10,19 @@ import ( "io" ) -// ConsoleDriver is the interface that must be implemented by anything +// ConsoleOutput is the interface that must be implemented by anything // that wishes to be used as a console driver. // // Providing this interface is implemented an object may register itself, // by name, via the Register method. -type ConsoleDriver interface { +// +// You can compare this to the ConsoleInput interface, which is similar, although +// in that case the wrapper which creates the instances also implements some common methods. +type ConsoleOutput interface { - // PutCharacter will output the specified character to STDOUT. + // PutCharacter will output the specified character to the defined writer. + // + // The writer will default to STDOUT, but can be changed, via SetWriter. PutCharacter(c uint8) // GetName will return the name of the driver. @@ -47,7 +52,7 @@ var handlers = struct { // Constructor is the signature of a constructor-function // which is used to instantiate an instance of a driver. -type Constructor func() ConsoleDriver +type Constructor func() ConsoleOutput // Register makes a console driver available, by name. // @@ -62,7 +67,7 @@ func Register(name string, obj Constructor) { type ConsoleOut struct { // driver is the thing that actually writes our output. - driver ConsoleDriver + driver ConsoleOutput } // New is our constructore, it creates an output device which uses @@ -82,7 +87,7 @@ func New(name string) (*ConsoleOut, error) { } // GetDriver allows getting our driver at runtime. -func (co *ConsoleOut) GetDriver() ConsoleDriver { +func (co *ConsoleOut) GetDriver() ConsoleOutput { return co.driver } diff --git a/consoleout/drv_adm3a.go b/consoleout/drv_adm3a.go index 018262b..1667f76 100644 --- a/consoleout/drv_adm3a.go +++ b/consoleout/drv_adm3a.go @@ -155,7 +155,7 @@ func (a3a *Adm3AOutputDriver) SetWriter(w io.Writer) { // init registers our driver, by name. func init() { - Register("adm-3a", func() ConsoleDriver { + Register("adm-3a", func() ConsoleOutput { return &Adm3AOutputDriver{ writer: os.Stdout, } diff --git a/consoleout/drv_ansi.go b/consoleout/drv_ansi.go index 07e44fb..f9bff37 100644 --- a/consoleout/drv_ansi.go +++ b/consoleout/drv_ansi.go @@ -33,7 +33,7 @@ func (ad *AnsiOutputDriver) SetWriter(w io.Writer) { // init registers our driver, by name. func init() { - Register("ansi", func() ConsoleDriver { + Register("ansi", func() ConsoleOutput { return &AnsiOutputDriver{ writer: os.Stdout, } diff --git a/consoleout/drv_logger.go b/consoleout/drv_logger.go index aeea693..91d29bd 100644 --- a/consoleout/drv_logger.go +++ b/consoleout/drv_logger.go @@ -52,7 +52,7 @@ func (ol *OutputLoggingDriver) Reset() { // init registers our driver, by name. func init() { - Register("logger", func() ConsoleDriver { + Register("logger", func() ConsoleOutput { return &OutputLoggingDriver{ writer: os.Stdout, } diff --git a/consoleout/drv_null.go b/consoleout/drv_null.go index 5513f6c..205530b 100644 --- a/consoleout/drv_null.go +++ b/consoleout/drv_null.go @@ -35,7 +35,7 @@ func (no *NullOutputDriver) SetWriter(w io.Writer) { // init registers our driver, by name. func init() { - Register("null", func() ConsoleDriver { + Register("null", func() ConsoleOutput { return &NullOutputDriver{ writer: os.Stdout, } diff --git a/cpm/cpm.go b/cpm/cpm.go index f360c77..b37008a 100644 --- a/cpm/cpm.go +++ b/cpm/cpm.go @@ -565,7 +565,7 @@ func (cpm *CPM) GetInputDriver() consolein.ConsoleInput { } // GetOutputDriver returns the configured output driver. -func (cpm *CPM) GetOutputDriver() consoleout.ConsoleDriver { +func (cpm *CPM) GetOutputDriver() consoleout.ConsoleOutput { return cpm.output.GetDriver() } From 7b43ce5230e05d105dcbddb89bdcfe1d33e215b8 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Sun, 29 Dec 2024 20:58:14 +0200 Subject: [PATCH 07/21] More minor improvements to test coverage --- cpm/cpm_test.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cpm/cpm_test.go b/cpm/cpm_test.go index 99ecd27..0e95811 100644 --- a/cpm/cpm_test.go +++ b/cpm/cpm_test.go @@ -11,7 +11,7 @@ import ( func TestSimple(t *testing.T) { // Create a new CP/M helper - obj, err := New(WithConsoleDriver("null")) + obj, err := New(WithConsoleDriver("null"), WithInputDriver("stty")) if err != nil { t.Fatalf("failed to create CPM") } @@ -51,6 +51,9 @@ func TestSimple(t *testing.T) { if obj.GetOutputDriver().GetName() != "null" { t.Fatalf("console driver name mismatch!") } + if obj.GetInputDriver().GetName() != "stty" { + t.Fatalf("console driver name mismatch!") + } if obj.GetCCPName() != "ccp" { t.Fatalf("ccp name mismatch!") } @@ -104,6 +107,15 @@ func TestBogusConstructor(t *testing.T) { if err == nil { t.Fatalf("expected error, bogus console driver, got none") } + + _, err = New(WithInputDriver("bogus")) + if err == nil { + t.Fatalf("expected error, bogus console driver, got none") + } + + x, _ := New(WithInputDriver("stty")) + x.IOSetup() + x.IOTearDown() } func TestLoadCCP(t *testing.T) { From ae136c65bcefdd22c2ab3f5db8e015c708d77326 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Sun, 29 Dec 2024 21:00:08 +0200 Subject: [PATCH 08/21] Rename WithConsoleDriver to be WithOutputDriver, to match the new flexible WithInputDriver. --- cpm/cpm.go | 6 +++--- cpm/cpm_test.go | 4 ++-- main.go | 2 +- main_test.go | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cpm/cpm.go b/cpm/cpm.go index b37008a..fa39d3b 100644 --- a/cpm/cpm.go +++ b/cpm/cpm.go @@ -217,9 +217,9 @@ func WithPrinterPath(path string) cpmoption { } } -// WithConsoleDriver allows the console driver to be created in our +// WithOutputDriver allows the console driver to be created in our // constructor. -func WithConsoleDriver(name string) cpmoption { +func WithOutputDriver(name string) cpmoption { return func(c *CPM) error { @@ -250,7 +250,7 @@ func WithInputDriver(name string) cpmoption { } // New returns a new emulation object. We support default options, -// and new defaults may be specified via WithConsoleDriver, etc, etc. +// and new defaults may be specified via WithOutputDriver, etc, etc. func New(options ...cpmoption) (*CPM, error) { // diff --git a/cpm/cpm_test.go b/cpm/cpm_test.go index 0e95811..febadc5 100644 --- a/cpm/cpm_test.go +++ b/cpm/cpm_test.go @@ -11,7 +11,7 @@ import ( func TestSimple(t *testing.T) { // Create a new CP/M helper - obj, err := New(WithConsoleDriver("null"), WithInputDriver("stty")) + obj, err := New(WithOutputDriver("null"), WithInputDriver("stty")) if err != nil { t.Fatalf("failed to create CPM") } @@ -103,7 +103,7 @@ func TestSimple(t *testing.T) { func TestBogusConstructor(t *testing.T) { - _, err := New(WithConsoleDriver("bogus")) + _, err := New(WithOutputDriver("bogus")) if err == nil { t.Fatalf("expected error, bogus console driver, got none") } diff --git a/main.go b/main.go index c92e410..ab5523b 100644 --- a/main.go +++ b/main.go @@ -190,7 +190,7 @@ func main() { // Create a new emulator. obj, err := cpm.New(cpm.WithPrinterPath(*prnPath), - cpm.WithConsoleDriver(*console), + cpm.WithOutputDriver(*console), cpm.WithInputDriver(*input), cpm.WithCCP(*ccp)) if err != nil { diff --git a/main_test.go b/main_test.go index f9ac300..e293dc5 100644 --- a/main_test.go +++ b/main_test.go @@ -16,7 +16,7 @@ import ( // changing drives. func TestDriveChange(t *testing.T) { - obj, err := cpm.New(cpm.WithConsoleDriver("logger")) + obj, err := cpm.New(cpm.WithOutputDriver("logger")) if err != nil { t.Fatalf("Create CP/M failed") } @@ -95,7 +95,7 @@ func TestReadWriteRand(t *testing.T) { // However it is a great test to see that things work as expected. func TestCompleteLighthouse(t *testing.T) { - obj, err := cpm.New(cpm.WithConsoleDriver("logger")) + obj, err := cpm.New(cpm.WithOutputDriver("logger")) if err != nil { t.Fatalf("Create CP/M failed") } From a1a33b6c06a373c7981ee9c0da1146b87028542e Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Sun, 29 Dec 2024 21:08:24 +0200 Subject: [PATCH 09/21] We now track the default input/output drivers in the CPM package. This means there is only one place to change, and we show the default option when listing available input/output drivers (which are now sorted). --- cpm/cpm.go | 16 ++++++++++------ main.go | 20 ++++++++++++++++---- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/cpm/cpm.go b/cpm/cpm.go index fa39d3b..8946ed8 100644 --- a/cpm/cpm.go +++ b/cpm/cpm.go @@ -47,6 +47,12 @@ var ( // // It should be handled and expected by callers. ErrUnimplemented = errors.New("UNIMPLEMENTED") + + // DefaultInputDriver contains the name of the default console input driver. + DefaultInputDriver string = "term" + + // DefaultOutputDriver contains the name of the default console output driver. + DefaultOutputDriver string = "adm-3a" ) // CPMHandlerType contains the signature of a function we use to @@ -217,8 +223,7 @@ func WithPrinterPath(path string) cpmoption { } } -// WithOutputDriver allows the console driver to be created in our -// constructor. +// WithOutputDriver allows the default console output driver to be changed in our constructor. func WithOutputDriver(name string) cpmoption { return func(c *CPM) error { @@ -233,8 +238,7 @@ func WithOutputDriver(name string) cpmoption { } } -// WithInputDriver allows the console input driver to be created in our -// constructor. +// WithInputDriver allows the default console input driver to be changed in our constructor. func WithInputDriver(name string) cpmoption { return func(c *CPM) error { @@ -512,13 +516,13 @@ func New(options ...cpmoption) (*CPM, error) { } // Default output driver - oDriver, err := consoleout.New("adm-3a") + oDriver, err := consoleout.New(DefaultOutputDriver) if err != nil { return nil, err } // Default input driver - iDriver, err := consolein.New("term") + iDriver, err := consolein.New(DefaultInputDriver) if err != nil { return nil, err } diff --git a/main.go b/main.go index ab5523b..09c362d 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "fmt" "log/slog" "os" + "slices" "sort" "strings" @@ -31,9 +32,9 @@ func main() { // ccp := flag.String("ccp", "ccpz", "The name of the CCP that we should run (ccp vs. ccpz).") cd := flag.String("cd", "", "Change to this directory before launching") - console := flag.String("console", "adm-3a", "The name of the console output driver to use (adm-3a or ansi).") + console := flag.String("console", cpm.DefaultOutputDriver, "The name of the console output driver to use (adm-3a or ansi).") createDirectories := flag.Bool("create", false, "Create subdirectories on the host computer for each CP/M drive.") - input := flag.String("input", "term", "The name of the console input driver to use (term or stty).") + input := flag.String("input", cpm.DefaultInputDriver, "The name of the console input driver to use (term or stty).") logAll := flag.Bool("log-all", false, "Log the output of all functions, including the noisy Console I/O ones.") logPath := flag.String("log-path", "", "Specify the file to write debug logs to.") prnPath := flag.String("prn-path", "print.log", "Specify the file to write printer-output to.") @@ -80,9 +81,14 @@ func main() { if *listInput { obj, _ := consolein.New("null") valid := obj.GetDrivers() + slices.Sort(valid) for _, name := range valid { - fmt.Printf("%s\n", name) + suffix := "" + if name == cpm.DefaultInputDriver { + suffix = "\t[default]" + } + fmt.Printf("%s%s\n", name, suffix) } return } @@ -92,8 +98,14 @@ func main() { obj, _ := consoleout.New("null") valid := obj.GetDrivers() + slices.Sort(valid) + for _, name := range valid { - fmt.Printf("%s\n", name) + suffix := "" + if name == cpm.DefaultOutputDriver { + suffix = "\t[default]" + } + fmt.Printf("%s%s\n", name, suffix) } return } From 53d7edd9b4338b875ad40659fd2aec02869a3fd6 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Mon, 30 Dec 2024 04:42:17 +0200 Subject: [PATCH 10/21] Reworded CLI flag-help a little, and fixed formatting for drive-warning(s). --- main.go | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/main.go b/main.go index 09c362d..23463ac 100644 --- a/main.go +++ b/main.go @@ -32,20 +32,20 @@ func main() { // ccp := flag.String("ccp", "ccpz", "The name of the CCP that we should run (ccp vs. ccpz).") cd := flag.String("cd", "", "Change to this directory before launching") - console := flag.String("console", cpm.DefaultOutputDriver, "The name of the console output driver to use (adm-3a or ansi).") + console := flag.String("console", cpm.DefaultOutputDriver, "The name of the console output driver to use (-list-output-drivers will show valid choices).") createDirectories := flag.Bool("create", false, "Create subdirectories on the host computer for each CP/M drive.") - input := flag.String("input", cpm.DefaultInputDriver, "The name of the console input driver to use (term or stty).") - logAll := flag.Bool("log-all", false, "Log the output of all functions, including the noisy Console I/O ones.") + input := flag.String("input", cpm.DefaultInputDriver, "The name of the console input driver to use (-list-input-drivers will show valid choices).") + logAll := flag.Bool("log-all", false, "Log all function invocations, including the noisy console I/O ones.") logPath := flag.String("log-path", "", "Specify the file to write debug logs to.") prnPath := flag.String("prn-path", "print.log", "Specify the file to write printer-output to.") showVersion := flag.Bool("version", false, "Report our version, and exit.") useDirectories := flag.Bool("directories", false, "Use subdirectories on the host computer for CP/M drives.") // listing - listCcps := flag.Bool("list-ccp", false, "Dump the list of embedded CCPs.") - listOutput := flag.Bool("list-output-drivers", false, "Dump the list of valid console output drivers.") - listInput := flag.Bool("list-input-drivers", false, "Dump the list of valid console input drivers.") - listSyscalls := flag.Bool("list-syscalls", false, "Dump the list of implemented BIOS/BDOS syscall functions.") + listCcps := flag.Bool("list-ccp", false, "Dump the list of embedded CCPs, and exit.") + listOutput := flag.Bool("list-output-drivers", false, "Dump the list of valid console output drivers, and exit.") + listInput := flag.Bool("list-input-drivers", false, "Dump the list of valid console input drivers, and exit.") + listSyscalls := flag.Bool("list-syscalls", false, "Dump the list of implemented BIOS/BDOS syscall functions, and exit.") // drives drive := make(map[string]*string) @@ -265,20 +265,20 @@ func main() { } } if found == 0 { - fmt.Printf("WARNING: You've chosen to use subdirectories as drives.\n") - fmt.Printf(" i.e. A/ would be used for the contents of A:\n") - fmt.Printf(" i.e. B/ would be used for the contents of B:\n") - fmt.Printf("\n") - fmt.Printf(" However no drive-directories are present!\n") - fmt.Printf("\n") - fmt.Printf("You could fix this, like so:\n") - fmt.Printf(" mkdir A\n") - fmt.Printf(" mkdir B\n") - fmt.Printf(" mkdir C\n") - fmt.Printf(" etc\n") - fmt.Printf("\n") - fmt.Printf("Or you could launch this program with the '-create' flag.\n") - fmt.Printf("That would automatically create directories for drives A-P.\n") + fmt.Printf("WARNING: You've chosen to use subdirectories as drives.\r\n") + fmt.Printf(" i.e. A/ would be used for the contents of A:\r\n") + fmt.Printf(" i.e. B/ would be used for the contents of B:\r\n") + fmt.Printf("\r\n") + fmt.Printf(" However no drive-directories are present!\r\n") + fmt.Printf("\r\n") + fmt.Printf("You could fix this, like so:\r\n") + fmt.Printf(" mkdir A\r\n") + fmt.Printf(" mkdir B\r\n") + fmt.Printf(" mkdir C\r\n") + fmt.Printf(" etc\r\n") + fmt.Printf("\r\n") + fmt.Printf("Or you could launch this program with the '-create' flag.\r\n") + fmt.Printf("That would automatically create directories for drives A-P.\r\n") } } From c5577ed4518e423fc8719424b128022baac736a9 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Mon, 30 Dec 2024 04:56:31 +0200 Subject: [PATCH 11/21] Minor comment updates --- ccp/ccp.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ccp/ccp.go b/ccp/ccp.go index 8b052b6..e798d80 100644 --- a/ccp/ccp.go +++ b/ccp/ccp.go @@ -15,19 +15,21 @@ import ( // Flavour contains details about a possible CCP the user might run. type Flavour struct { - // Name has the name of the CCP. + // Name contains the public-facing name of the CCP. // // NOTE: This name is visible to end-users, and will be used in the "-ccp" command-line flag, - // or as the name when changing at run-time via the "CCP.COM" utility. + // or as the name when changing at run-time via the "A:!CCP.COM" binary. Name string - // Description has the description of the CCP. + // Description contains the description of the CCP. Description string // Bytes contains the raw binary content. Bytes []uint8 - // Origin contains the start/load location of the CCP. + // Start specifies the memory-address, within RAM, to which the raw bytes should be loaded and to which control should be passed. + // + // (i.e. This must match the ORG specified in the CCP source code.) Start uint16 } From eede411a1beeafec1a019fb93d0e123867ad7ee1 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Mon, 30 Dec 2024 04:56:39 +0200 Subject: [PATCH 12/21] Only build the STTY-based input-driver on Unix systems. This means we can now build a Windows binary :rocket: --- consolein/drv_stty.go | 24 +++++++++++++++++++++++- consolein/select_unix.go | 28 ---------------------------- 2 files changed, 23 insertions(+), 29 deletions(-) delete mode 100644 consolein/select_unix.go diff --git a/consolein/drv_stty.go b/consolein/drv_stty.go index 838b1fa..1d51fd6 100644 --- a/consolein/drv_stty.go +++ b/consolein/drv_stty.go @@ -1,3 +1,5 @@ +//go:build unix + // drv_stty creates a console input-driver which uses the // `stty` binary to set our echo/no-echo state. // @@ -10,6 +12,7 @@ import ( "os" "os/exec" + "golang.org/x/sys/unix" "golang.org/x/term" ) @@ -56,6 +59,25 @@ func (si *STTYInput) TearDown() { } } +// canSelect contains a platform-specific implementation of code that tries to use +// SELECT to read from STDIN. +func canSelect() bool { + + fds := new(unix.FdSet) + fds.Set(int(os.Stdin.Fd())) + + // See if input is pending, for a while. + tv := unix.Timeval{Usec: 200} + + // via select with timeout + nRead, err := unix.Select(1, fds, nil, nil, &tv) + if err != nil { + return false + } + + return (nRead > 0) +} + // PendingInput returns true if there is pending input from STDIN.. // // Note that we have to set RAW mode, without this input is laggy @@ -73,7 +95,7 @@ func (si *STTYInput) PendingInput() bool { return false } - // Platform-specific code in select_XXXX.go + // Can we read from STDIN? res := canSelect() // restore the state of the terminal to avoid mixing RAW/Cooked diff --git a/consolein/select_unix.go b/consolein/select_unix.go deleted file mode 100644 index e968fad..0000000 --- a/consolein/select_unix.go +++ /dev/null @@ -1,28 +0,0 @@ -//go:build unix - -package consolein - -import ( - "os" - - "golang.org/x/sys/unix" -) - -// canSelect contains a platform-specific implementation of code that tries to use -// SELECT to read from STDIN. -func canSelect() bool { - - fds := new(unix.FdSet) - fds.Set(int(os.Stdin.Fd())) - - // See if input is pending, for a while. - tv := unix.Timeval{Usec: 200} - - // via select with timeout - nRead, err := unix.Select(1, fds, nil, nil, &tv) - if err != nil { - return false - } - - return (nRead > 0) -} From 6b135efce8dec86624c2ff937b480c889cb15240 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Mon, 30 Dec 2024 04:57:22 +0200 Subject: [PATCH 13/21] Ignore any generated .exe files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 64676f8..3ddeff4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ cpmulator cpmulator-* +*.exe From b6d240d5d1d052369acb4e25a9f6c3536792b60a Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Mon, 30 Dec 2024 04:58:24 +0200 Subject: [PATCH 14/21] Build for windows too, now we can. --- .github/build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/build b/.github/build index 40a18c5..3d4c247 100755 --- a/.github/build +++ b/.github/build @@ -12,9 +12,9 @@ if [ -d /github/workspace ] ; then fi # -# We build on only a single platform/arch. +# We build for many platforms. # -BUILD_PLATFORMS="linux darwin freebsd openbsd netbsd" +BUILD_PLATFORMS="linux darwin freebsd openbsd netbsd windows" BUILD_ARCHS="amd64 386" # For each platform From 686e2f9b921987e16c84f0d475b9b5e30cdbd760 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Mon, 30 Dec 2024 05:24:09 +0200 Subject: [PATCH 15/21] Added \!VERSION.COM to the embedded resources, as per feedback. --- README.md | 2 ++ cpm/cpm_bios.go | 1 + static/A/!VERSION.COM | Bin 0 -> 111 bytes static/Makefile | 4 ++- static/version.z80 | 65 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 static/A/!VERSION.COM create mode 100644 static/version.z80 diff --git a/README.md b/README.md index 19db2dd..8f79c09 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,8 @@ runtime. `A:!DEBUG.COM` will show the state of the flag, and it can be enabled with `A:!DEBUG 1` or disabled with `!DEBUG 0`. +Finally `A:!VERSION.COM` will show you the version of the emulator you're running, as would the startup banner itself. + diff --git a/cpm/cpm_bios.go b/cpm/cpm_bios.go index fb0754a..81d2730 100644 --- a/cpm/cpm_bios.go +++ b/cpm/cpm_bios.go @@ -173,6 +173,7 @@ func BiosSysCallReserved1(cpm *CPM) error { // Get our version vers := version.GetVersionBanner() + vers = strings.ReplaceAll(vers, "\n", "\n\r") // Fill the DMA area with NULL bytes addr := cpm.dma diff --git a/static/A/!VERSION.COM b/static/A/!VERSION.COM new file mode 100644 index 0000000000000000000000000000000000000000..51219d73307f7e841637f8b2808e077564f385b2 GIT binary patch literal 111 zcmY#nV6cO=DU)cqs9Kj{!&vnltio z0%?hlAsLy)3Q3uHiA9wPKvrIUi9%6nUS4Kix Date: Mon, 30 Dec 2024 05:30:14 +0200 Subject: [PATCH 16/21] Updated test-case now we have more embedded files --- cpm/cpm_bdos_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpm/cpm_bdos_test.go b/cpm/cpm_bdos_test.go index c18ac42..c0c7e8f 100644 --- a/cpm/cpm_bdos_test.go +++ b/cpm/cpm_bdos_test.go @@ -282,7 +282,7 @@ func TestFind(t *testing.T) { } - if found != 4 { + if found != 5 { t.Fatalf("found wrong number of embedded files, got %d", found) } From 923cfaf5ecd081b47ab9a34a3c8824685d13c0f2 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Mon, 30 Dec 2024 05:32:24 +0200 Subject: [PATCH 17/21] breakup our banner into two lines because that is more readable --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 23463ac..3558979 100644 --- a/main.go +++ b/main.go @@ -328,7 +328,7 @@ func main() { } // Show a startup-banner. - fmt.Printf("\ncpmulator %s CCP %s, input driver %s, output driver %s\n", cpmver.GetVersionString(), obj.GetCCPName(), obj.GetInputDriver().GetName(), obj.GetOutputDriver().GetName()) + fmt.Printf("\ncpmulator %s\r\nCCP:%s input driver:%s output driver:%s\n", cpmver.GetVersionString(), obj.GetCCPName(), obj.GetInputDriver().GetName(), obj.GetOutputDriver().GetName()) // We will load AUTOEXEC.SUB, once, if it exists (*) // From 49340bf594d7ab1a0d348ebca388cfe93002729d Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Mon, 30 Dec 2024 05:35:05 +0200 Subject: [PATCH 18/21] Lower-case our dynamic drivers, so we always find matches even if the user uses mixed-case. --- consolein/consolein.go | 6 ++++++ consoleout/consoleout.go | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/consolein/consolein.go b/consolein/consolein.go index 739da4d..23aebd8 100644 --- a/consolein/consolein.go +++ b/consolein/consolein.go @@ -65,6 +65,9 @@ type Constructor func() ConsoleInput // When one needs to be created the constructor can be called // to create an instance of it. func Register(name string, obj Constructor) { + // Downcase for consistency. + name = strings.ToLower(name) + handlers.m[name] = obj } @@ -80,6 +83,9 @@ type ConsoleIn struct { // the specified driver. func New(name string) (*ConsoleIn, error) { + // Downcase for consistency. + name = strings.ToLower(name) + // Do we have a constructor with the given name? ctor, ok := handlers.m[name] if !ok { diff --git a/consoleout/consoleout.go b/consoleout/consoleout.go index 525c881..1ca821f 100644 --- a/consoleout/consoleout.go +++ b/consoleout/consoleout.go @@ -8,6 +8,7 @@ package consoleout import ( "fmt" "io" + "strings" ) // ConsoleOutput is the interface that must be implemented by anything @@ -59,6 +60,9 @@ type Constructor func() ConsoleOutput // When one needs to be created the constructor can be called // to create an instance of it. func Register(name string, obj Constructor) { + // Downcase for consistency. + name = strings.ToLower(name) + handlers.m[name] = obj } @@ -73,6 +77,8 @@ type ConsoleOut struct { // New is our constructore, it creates an output device which uses // the specified driver. func New(name string) (*ConsoleOut, error) { + // Downcase for consistency. + name = strings.ToLower(name) // Do we have a constructor with the given name? ctor, ok := handlers.m[name] From 28df479f929d4c9deff4cfa346bbf0fa229503fb Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Mon, 30 Dec 2024 05:54:38 +0200 Subject: [PATCH 19/21] Move the "stuffed input" into the scaffolding, not the individual drivers This cuts down on knowledge, and simplifies the implementation. --- consolein/consolein.go | 35 +++++++++++++++++++++++++++++------ consolein/consolein_test.go | 28 +++++++++++++++++----------- consolein/drv_stty.go | 21 --------------------- consolein/drv_term.go | 23 ----------------------- cpm/cpm_bios_test.go | 4 ++-- 5 files changed, 48 insertions(+), 63 deletions(-) diff --git a/consolein/consolein.go b/consolein/consolein.go index 23aebd8..5530b91 100644 --- a/consolein/consolein.go +++ b/consolein/consolein.go @@ -31,9 +31,6 @@ type ConsoleInput interface { // TearDown performs any specific cleanup which is required. TearDown() - // StuffInput saves fake-input into the drivers' buffer, to be returned later. - StuffInput(input string) - // PendingInput returns true if there is pending input available to be read. PendingInput() bool @@ -50,9 +47,12 @@ var handlers = struct { m map[string]Constructor }{m: make(map[string]Constructor)} -// This is the count of Ctrl-C which we keep track of to allow "reboots" +// interruptCount is the count of consecutive Ctrl-Cs which will trigger a "reboot". var interruptCount int = 2 +// stuffed holds pending input +var stuffed string = "" + // history holds previous (line) input. var history []string @@ -130,7 +130,7 @@ func (co *ConsoleIn) TearDown() { // StuffInput proxies into our registered console-input driver. func (co *ConsoleIn) StuffInput(input string) { - co.driver.StuffInput(input) + stuffed = input } // SetInterruptCount sets the number of consecutive Ctrl-C characters @@ -150,11 +150,25 @@ func (co *ConsoleIn) GetInterruptCount() int { // PendingInput proxies into our registered console-input driver. func (co *ConsoleIn) PendingInput() bool { + + // if there is stuffed input we have something ready to read + if len(stuffed) > 0 { + return true + } + return co.driver.PendingInput() } // BlockForCharacterNoEcho proxies into our registered console-input driver. func (co *ConsoleIn) BlockForCharacterNoEcho() (byte, error) { + + // Do we have faked/stuffed input to process? + if len(stuffed) > 0 { + c := stuffed[0] + stuffed = stuffed[1:] + return c, nil + } + return co.driver.BlockForCharacterNoEcho() } @@ -163,6 +177,15 @@ func (co *ConsoleIn) BlockForCharacterNoEcho() (byte, error) { // // This function DOES NOT proxy to our registered console-input driver. func (co *ConsoleIn) BlockForCharacterWithEcho() (byte, error) { + + // Do we have faked/stuffed input to process? + if len(stuffed) > 0 { + c := stuffed[0] + stuffed = stuffed[1:] + fmt.Printf("%c", c) + return c, nil + } + c, err := co.driver.BlockForCharacterNoEcho() if err == nil { fmt.Printf("%c", c) @@ -203,7 +226,7 @@ func (co *ConsoleIn) ReadLine(max uint8) (string, error) { for { // Get a character, with no echo. - x, err := co.driver.BlockForCharacterNoEcho() + x, err := co.BlockForCharacterNoEcho() if err != nil { return "", err } diff --git a/consolein/consolein_test.go b/consolein/consolein_test.go index c6c0ff0..bd46e03 100644 --- a/consolein/consolein_test.go +++ b/consolein/consolein_test.go @@ -13,10 +13,10 @@ func TestReadlineSTTY(t *testing.T) { // Simple readline // Here \x10 is the Ctrl-P which would use the previous history // as we're just created we have none so it is ignored. - x.StuffInput("s\x10teve\n") + ch.StuffInput("s\x10teve\n") out, err := ch.ReadLine(20) if err != nil { - t.Fatalf("unexpected error") + t.Fatalf("unexpected error: %s", err) } if out != "steve" { t.Fatalf("Unexpected output '%s'", out) @@ -24,14 +24,14 @@ func TestReadlineSTTY(t *testing.T) { // Ctrl-C at start of the line should trigger a reboot-error // x.stuffed = string([]byte{0x03, 0x03, 0x00}a) - x.StuffInput("\x03\x03steve") + ch.StuffInput("\x03\x03steve") _, err = ch.ReadLine(20) if err != ErrInterrupted { t.Fatalf("unexpected error %s", err) } // Ctrl-C at the middle of a line should not - x.StuffInput("steve\x03\x03steve\n") + ch.StuffInput("steve\x03\x03steve\n") out, err = ch.ReadLine(20) if err != nil { t.Fatalf("unexpected error %s", err) @@ -41,7 +41,7 @@ func TestReadlineSTTY(t *testing.T) { } // Ctrl-B overwrites - x.StuffInput("steve\b\b\b\b\bHello\n") + ch.StuffInput("steve\b\b\b\b\bHello\n") out, err = ch.ReadLine(20) if err != nil { t.Fatalf("unexpected error %s", err) @@ -51,7 +51,7 @@ func TestReadlineSTTY(t *testing.T) { } // ESC resets input - x.StuffInput("steve\x1BHello\n") + ch.StuffInput("steve\x1BHello\n") out, err = ch.ReadLine(20) if err != nil { t.Fatalf("unexpected error %s", err) @@ -61,7 +61,7 @@ func TestReadlineSTTY(t *testing.T) { } // Too much input? We truncate - x.StuffInput("I like to move it, move it\n") + ch.StuffInput("I like to move it, move it\n") out, err = ch.ReadLine(5) if err != nil { t.Fatalf("unexpected error %s", err) @@ -72,7 +72,7 @@ func TestReadlineSTTY(t *testing.T) { // Add some history, and return the last value history = append(history, "I like to move it") - x.StuffInput("ste\x10\n") + ch.StuffInput("ste\x10\n") out, err = ch.ReadLine(5) if err != nil { t.Fatalf("unexpected error %s", err) @@ -82,7 +82,7 @@ func TestReadlineSTTY(t *testing.T) { } // Go back and forward in history - x.StuffInput("\x10\x10\x10\x0e\n") + ch.StuffInput("\x10\x10\x10\x0e\n") out, err = ch.ReadLine(10) if err != nil { t.Fatalf("unexpected error %s", err) @@ -146,8 +146,14 @@ func TestPending(t *testing.T) { // Create a helper x := STTYInput{} - x.StuffInput("foo") - if !x.PendingInput() { + ch := ConsoleIn{} + ch.driver = &x + + ch.Setup() + defer ch.TearDown() + + ch.StuffInput("foo") + if !ch.PendingInput() { t.Fatalf("we should have pending input, but see none") } diff --git a/consolein/drv_stty.go b/consolein/drv_stty.go index 1d51fd6..65ab44a 100644 --- a/consolein/drv_stty.go +++ b/consolein/drv_stty.go @@ -41,10 +41,6 @@ type STTYInput struct { // state holds our state state EchoStatus - - // stuffed holds fake input which has been forced into the buffer used - // by ReadLine - stuffed string } // Setup is a NOP. @@ -84,11 +80,6 @@ func canSelect() bool { // and zork doesn't run. func (si *STTYInput) PendingInput() bool { - // Do we have faked/stuffed input to process? - if len(si.stuffed) > 0 { - return true - } - // switch stdin into 'raw' mode oldState, err := term.MakeRaw(int(os.Stdin.Fd())) if err != nil { @@ -108,24 +99,12 @@ func (si *STTYInput) PendingInput() bool { return res } -// StuffInput inserts fake values into our input-buffer -func (si *STTYInput) StuffInput(input string) { - si.stuffed = input -} - // BlockForCharacterNoEcho returns the next character from the console, blocking until // one is available. // // NOTE: This function should not echo keystrokes which are entered. func (si *STTYInput) BlockForCharacterNoEcho() (byte, error) { - // Do we have faked/stuffed input to process? - if len(si.stuffed) > 0 { - c := si.stuffed[0] - si.stuffed = si.stuffed[1:] - return c, nil - } - // Do we need to change state? If so then do it. if si.state != NoEcho { si.disableEcho() diff --git a/consolein/drv_term.go b/consolein/drv_term.go index 636fc4b..4d25701 100644 --- a/consolein/drv_term.go +++ b/consolein/drv_term.go @@ -27,10 +27,6 @@ type TermboxInput struct { // Cancel holds a context which can be used to close our polling goroutine Cancel context.CancelFunc - // stuffed holds fake input which has been forced into the buffer used - // by ReadLine - stuffed string - // keyBuffer builds up keys read "in the background", via termbox keyBuffer []rune } @@ -111,20 +107,9 @@ func (ti *TermboxInput) TearDown() { } } -// StuffInput inserts fake values into our input-buffer -func (ti *TermboxInput) StuffInput(input string) { - ti.stuffed = input -} - // PendingInput returns true if there is pending input from STDIN. func (ti *TermboxInput) PendingInput() bool { - // Do we have faked/stuffed input to process? - if len(ti.stuffed) > 0 { - return true - } - - // Otherwise only if we've read stuff. return len(ti.keyBuffer) > 0 } @@ -134,14 +119,6 @@ func (ti *TermboxInput) PendingInput() bool { // NOTE: This function should not echo keystrokes which are entered. func (ti *TermboxInput) BlockForCharacterNoEcho() (byte, error) { - // Do we have faked/stuffed input to process? - if len(ti.stuffed) > 0 { - c := ti.stuffed[0] - ti.stuffed = ti.stuffed[1:] - return c, nil - } - - // Otherwise only if we've read stuff. for len(ti.keyBuffer) == 0 { time.Sleep(1 * time.Millisecond) } diff --git a/cpm/cpm_bios_test.go b/cpm/cpm_bios_test.go index 563e3db..a69d584 100644 --- a/cpm/cpm_bios_test.go +++ b/cpm/cpm_bios_test.go @@ -14,13 +14,13 @@ func TestStatus(t *testing.T) { if err != nil { t.Fatalf("failed to create CPM") } - + c.StuffText("") err = BiosSysCallConsoleStatus(c) if err != nil { t.Fatalf("failed to call CPM") } if c.CPU.States.AF.Hi != 0x00 { - t.Fatalf("console status was wrong") + t.Fatalf("console status was wrong %02X", c.CPU.States.AF.Hi) } c.input.StuffInput("S") From 3d00063320e385fdbd6754b16b2af32e223e4765 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Mon, 30 Dec 2024 06:55:29 +0200 Subject: [PATCH 20/21] bumped dependencies --- go.mod | 11 +++++++---- go.sum | 14 +++++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index ba7e304..5dce9e4 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,15 @@ go 1.22.2 require ( github.com/koron-go/z80 v0.10.1 - golang.org/x/term v0.22.0 + golang.org/x/term v0.27.0 ) -require golang.org/x/sys v0.22.0 +require ( + github.com/nsf/termbox-go v1.1.1 + golang.org/x/sys v0.28.0 +) require ( - github.com/mattn/go-runewidth v0.0.9 // indirect - github.com/nsf/termbox-go v1.1.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect ) diff --git a/go.sum b/go.sum index d65a665..0755092 100644 --- a/go.sum +++ b/go.sum @@ -2,11 +2,15 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/koron-go/z80 v0.10.1 h1:Jfb0esP/QFL4cvcr+eFECVG0Y/mA9JBLC4EKbMU5zAY= github.com/koron-go/z80 v0.10.1/go.mod h1:ry+Zl9kRKelzaDG9UzEtUpUnXy0Yv/kk1YEaX958xdk= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY= github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= From 21a91f548bb45927aa516ff54ce50ab9402cf799 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Mon, 30 Dec 2024 16:31:35 +0200 Subject: [PATCH 21/21] We prefer -output to -console --- README.md | 8 ++++---- main.go | 12 ++++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8f79c09..c91d4d9 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ There are many available command-line options, which are shown in the output of * All output which CP/M sends to the "printer" will be written to the given file. * `-list-syscalls` * Dump the list of implemented BDOS and BIOS syscalls. -* `-list-input-drivers` and `-list-output-drivers` to see the available I/O driver-names, which may be enabled via `-input` and `-output`. +* `-list-input-drivers` and `-list-output-drivers` to see the available I/O driver-names, which may then be selected via the `-input` and `-output` flags. * `-version` * Show the version number of the emulator, and exit. @@ -156,7 +156,7 @@ This allows you to customize the emulator, or perform other "one-time" setup via ## Runtime Behaviour Changes -There are a small number of [extensions](EXTENSIONS.md) added to the BIOS functionality we provide, and these extensions allow changing the behaviour of the emulator at runtime. +There are a small number of [extensions](EXTENSIONS.md) added to the BIOS functionality we provide, and these extensions allow changing some aspects of the emulator at runtime. The behaviour changing is achieved by having a small number of .COM files invoke the extension functions, and these binaries are embedded within our emulator to improve ease of use, via the [static/](static/) directory in our source-tree. This means no matter what you'll always find some binaries installed on A:, despite not being present in reality. @@ -177,7 +177,7 @@ The binary `A:!CTRLC.COM` which lets you change this at runtime. Run `A:!CTRLC ### Console Output -We default to pretending our output device is an ADM-3A terminal, this can be changed via the `-console` command-line flag at startup. Additionally it can be changed at runtime via `A:!CONSOLE.COM`. +We default to pretending our output device is an ADM-3A terminal, this can be changed via the `-output` command-line flag (previously `-console`) at startup. Additionally it can be changed at runtime via `A:!CONSOLE.COM`. Run `A:!CONSOLE ansi` to disable the output emulation, or `A:!CONSOLE adm-3a` to restore it. @@ -194,7 +194,7 @@ runtime. `A:!DEBUG.COM` will show the state of the flag, and it can be enabled with `A:!DEBUG 1` or disabled with `!DEBUG 0`. -Finally `A:!VERSION.COM` will show you the version of the emulator you're running, as would the startup banner itself. +Finally `A:!VERSION.COM` will show you the version of the emulator you're running. diff --git a/main.go b/main.go index 3558979..2346a15 100644 --- a/main.go +++ b/main.go @@ -32,9 +32,10 @@ func main() { // ccp := flag.String("ccp", "ccpz", "The name of the CCP that we should run (ccp vs. ccpz).") cd := flag.String("cd", "", "Change to this directory before launching") - console := flag.String("console", cpm.DefaultOutputDriver, "The name of the console output driver to use (-list-output-drivers will show valid choices).") + console := flag.String("console", "", "The name of the console output driver to use (-list-output-drivers will show valid choices).") createDirectories := flag.Bool("create", false, "Create subdirectories on the host computer for each CP/M drive.") input := flag.String("input", cpm.DefaultInputDriver, "The name of the console input driver to use (-list-input-drivers will show valid choices).") + output := flag.String("output", cpm.DefaultOutputDriver, "The name of the console output driver to use (-list-output-drivers will show valid choices).") logAll := flag.Bool("log-all", false, "Log all function invocations, including the noisy console I/O ones.") logPath := flag.String("log-path", "", "Specify the file to write debug logs to.") prnPath := flag.String("prn-path", "print.log", "Specify the file to write printer-output to.") @@ -200,9 +201,16 @@ func main() { // Set the logger now we've updated as appropriate. slog.SetDefault(log) + // We used to use "-console", but now we prefer "-output" to match with "-input". + out := *output + if *console != "" { + out = *console + fmt.Printf("WARNING: -console is a deprecated flag, prefer to use -output.\r\n") + } + // Create a new emulator. obj, err := cpm.New(cpm.WithPrinterPath(*prnPath), - cpm.WithOutputDriver(*console), + cpm.WithOutputDriver(out), cpm.WithInputDriver(*input), cpm.WithCCP(*ccp)) if err != nil {