Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the writing of characters to the console pluggable. #86

Merged
merged 8 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading