Skip to content

Commit

Permalink
Make the writing of characters to the console pluggable. (#86)
Browse files Browse the repository at this point in the history
* Make the writing of characters to the console pluggable.

Once complete this pull-request will close #85 by allowing us to
change output drivers at runtime.  We'll also remove the messy
ADM-3A output handling from our core CPM package.

* Make the console output driver selectable via CLI flag
* Working selectable output, via BIOS.
  • Loading branch information
skx authored May 22, 2024
1 parent bc5a54c commit 717313a
Show file tree
Hide file tree
Showing 13 changed files with 319 additions and 72 deletions.
12 changes: 10 additions & 2 deletions EXTENSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Currently we have one function implemented, demonstrated in [samples/ctrlc.z80](



## Function 01
## Function 0x01

* If C == 0xFF return the value of the Ctrl-C count in A.
* IF C != 0xFF set the Ctrl-C count to be C.
Expand All @@ -37,6 +37,14 @@ Example:



## Function 0xx
## Function 0x02

On entry DE points to a text-string, terminated by NULL, which represents the name of the
console output driver to use.

Demonstrated in [samples/console.z80](samples/console.z80)



## Function 0x00
* TODO
28 changes: 18 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,6 @@ You can terminate the CCP by typing `EXIT`. The following built-in commands are
</details>


Traditionally pressing `Ctrl-C` would reload the CCP, via a soft boot. I think that combination is likely to be entered by accident, so in `cpmulator` pressing Ctrl-C _twice_ will reboot the CCP.

> I've added a binary `samples/ctrlc.com` which lets you change this at runtime, via an internal [BIOS extension](EXTENSIONS.md). Run `ctrlc 0` to disable the Ctrl-C behaviour, or `ctrlc N` to require N consecutive Ctrl-C keystrokes to trigger the restart-behaviour. Neat.
There are currently a pair of CCP implementations included within the emulator, and they can be selected via the `-ccp` command-line flag:

* "ccp"
Expand Down Expand Up @@ -157,6 +153,24 @@ Other options are shown in the output of `cpmulator -help`, but in brief:



## Runtime Changes

Traditionally pressing `Ctrl-C` would reload the CCP, via a soft boot. I think that combination is likely to be entered by accident, so in `cpmulator` we default to requiring you to press Ctrl-C _twice_ to reboot the CCP.

> I've added a binary `samples/ctrlc.com` which lets you change this at runtime, via an internal [BIOS extension](EXTENSIONS.md).
> Run `ctrlc 0` to disable the Ctrl-C behaviour, or `ctrlc N` to require N consecutive Ctrl-C keystrokes to trigger the restart-behaviour. Neat.
Similarly we default to using emulation to pretend our output device is an ADM-3A terminal, this can be changed via a command-line flag at startup.

> I've added a binary `samples/console.com` which lets you change this at runtime, via an internal [BIOS extension](EXTENSIONS.md).
> Run `console ansi` to disable the output emulation, or `console adm-3a` to restore it.
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.

> The `cpm-dist` repository also includes both CTRLC.COM and CONSOLE.COM on the A: drive, for ease of use.



# Sample Binaries

Expand Down Expand Up @@ -292,12 +306,6 @@ If things are _mostly_ working, but something is not quite producing the correct

* [DEBUGGING.md](DEBUGGING.md)

The following environmental variables influence runtime behaviour:

| Variable | Purpose |
|-------------|---------------------------------------------------------------|
| SIMPLE_CHAR | Avoid the attempted VT52 output conversion. |

For reference the memory map of our CP/M looks like this:

* 0x0000 - Start of RAM
Expand Down
85 changes: 51 additions & 34 deletions cpm/outc.go → consoleout/console_adm3a.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
package cpm
package consoleout

import (
"fmt"
"os"
)
import "fmt"

// outC attempts to write a single character output, but converting to
// ANSI from vt. This means tracking state and handling multi-byte
// output properly.
// Adm3AOutputDriver holds our state.
type Adm3AOutputDriver struct {

// status contains our state, in the state-machine
status int

// x stores the cursor X
x uint8

// y stores the cursor Y
y uint8
}

// GetName returns the name of this driver.
//
// This is all a bit sleazy.
func (cpm *CPM) outC(c uint8) {
// This is part of the OutputDriver interface.
func (a3a *Adm3AOutputDriver) GetName() string {
return "adm-3a"
}

if os.Getenv("SIMPLE_CHAR") != "" {
fmt.Printf("%c", c)
return
}
// PutCharacter writes the character to the console.
//
// This is part of the OutputDriver interface.
func (a3a *Adm3AOutputDriver) PutCharacter(c uint8) {

switch cpm.auxStatus {
switch a3a.status {
case 0:
switch c {
case 0x07: /* BEL: flash screen */
Expand All @@ -31,9 +41,9 @@ func (cpm *CPM) outC(c uint8) {
case 0x1E: /* adm3a cursor home */
fmt.Printf("\033[H")
case 0x1B:
cpm.auxStatus = 1 /* esc-prefix */
a3a.status = 1 /* esc-prefix */
case 1:
cpm.auxStatus = 2 /* cursor motion prefix */
a3a.status = 2 /* cursor motion prefix */
case 2: /* insert line */
fmt.Printf("\033[L")
case 3: /* delete line */
Expand All @@ -50,32 +60,32 @@ func (cpm *CPM) outC(c uint8) {
case 0x1B:
fmt.Printf("%c", c)
case '=', 'Y':
cpm.auxStatus = 2
a3a.status = 2
case 'E': /* insert line */
fmt.Printf("\033[L")
case 'R': /* delete line */
fmt.Printf("\033[M")
case 'B': /* enable attribute */
cpm.auxStatus = 4
a3a.status = 4
case 'C': /* disable attribute */
cpm.auxStatus = 5
a3a.status = 5
case 'L', 'D': /* set line */ /* delete line */
cpm.auxStatus = 6
a3a.status = 6
case '*', ' ': /* set pixel */ /* clear pixel */
cpm.auxStatus = 8
a3a.status = 8
default: /* some true ANSI sequence? */
cpm.auxStatus = 0
a3a.status = 0
fmt.Printf("%c%c", 0x1B, c)
}
case 2:
cpm.y = c - ' ' + 1
cpm.auxStatus = 3
a3a.y = c - ' ' + 1
a3a.status = 3
case 3:
cpm.x = c - ' ' + 1
cpm.auxStatus = 0
fmt.Printf("\033[%d;%dH", cpm.y, cpm.x)
a3a.x = c - ' ' + 1
a3a.status = 0
fmt.Printf("\033[%d;%dH", a3a.y, a3a.x)
case 4: /* <ESC>+B prefix */
cpm.auxStatus = 0
a3a.status = 0
switch c {
case '0': /* start reverse video */
fmt.Printf("\033[7m")
Expand All @@ -97,7 +107,7 @@ func (cpm *CPM) outC(c uint8) {
fmt.Printf("%cB%c", 0x1B, c)
}
case 5: /* <ESC>+C prefix */
cpm.auxStatus = 0
a3a.status = 0
switch c {
case '0': /* stop reverse video */
fmt.Printf("\033[27m")
Expand All @@ -120,13 +130,20 @@ func (cpm *CPM) outC(c uint8) {
}
/* set/clear line/point */
case 6:
cpm.auxStatus++
a3a.status++
case 7:
cpm.auxStatus++
a3a.status++
case 8:
cpm.auxStatus++
a3a.status++
case 9:
cpm.auxStatus = 0
a3a.status = 0
}

}

// init registers our driver, by name.
func init() {
Register("adm-3a", func() ConsoleDriver {
return &Adm3AOutputDriver{}
})
}
28 changes: 28 additions & 0 deletions consoleout/console_ansi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package consoleout

import "fmt"

// AnsiOutputDriver holds our state.
type AnsiOutputDriver struct {
}

// GetName returns the name of this driver.
//
// This is part of the OutputDriver interface.
func (ad *AnsiOutputDriver) GetName() string {
return "ansi"
}

// PutCharacter writes the specified character to the console.
//
// This is part of the OutputDriver interface.
func (ad *AnsiOutputDriver) PutCharacter(c uint8) {
fmt.Printf("%c", c)
}

// init registers our driver, by name.
func init() {
Register("ansi", func() ConsoleDriver {
return &AnsiOutputDriver{}
})
}
87 changes: 87 additions & 0 deletions consoleout/consoleout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Package consoleout is an abstruction over console output.
//
// We know we need an ANSI/RAW output, and we have an ADM-3A driver,
// so we want to create a factory that can instantiate and change a driver,
// given just a name.
package consoleout

import "fmt"

// ConsoleDriver 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 {

// PutCharacter will output the specified character to STDOUT.
PutCharacter(c uint8)

// GetName will return the name of the driver.
GetName() string
}

// This is a map of known-drivers
var handlers = struct {
m map[string]Constructor
}{m: make(map[string]Constructor)}

// Constructor is the signature of a constructor-function
// which is used to instantiate an instance of a driver.
type Constructor func() ConsoleDriver

// Register makes a console driver available, by name.
//
// 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
}

// ConsoleOut holds our state, which is basically just a
// pointer to the object handling our output.
type ConsoleOut struct {

// driver is the thing that actually writes our output.
driver ConsoleDriver
}

// New is our constructore, it creates an output device which uses
// the specified driver.
func New(name string) (*ConsoleOut, error) {

// 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)
}

// OK we do, return ourselves with that driver.
return &ConsoleOut{
driver: ctor(),
}, nil
}

// ChangeDriver allows changing our driver at runtime.
func (co *ConsoleOut) ChangeDriver(name string) error {

// Do we have a constructor with the given name?
ctor, ok := handlers.m[name]
if !ok {
return fmt.Errorf("failed to lookup driver by name '%s'", name)
}

// change the driver by creating a new object
co.driver = ctor()
return nil
}

// GetName returns the name of our selected driver.
func (co *ConsoleOut) GetName() string {
return co.driver.GetName()
}

// PutCharacter outputs a character, using our selected driver.
func (co *ConsoleOut) PutCharacter(c byte) {
co.driver.PutCharacter(c)
}
Loading

0 comments on commit 717313a

Please sign in to comment.