izapple2/apple2/ansiConsoleFrontend.go

174 lines
3.6 KiB
Go

package apple2
import (
"bufio"
"fmt"
"os"
"strings"
"time"
)
/*
Uses the console standard input and output to interface with the machine.
Input is buffered until the next CR. This avoids working in place, a line
for input is added at the end.
Outut is done in place using ANSI escape sequences.
Those tricks do not work with the Apple2e ROM
*/
type ansiConsoleFrontend struct {
apple2 *Apple2
keyChannel chan uint8
extraLineFeeds chan int
stdinKeyboard bool
lastContent string
}
func newAnsiConsoleFrontend(a *Apple2, stdinKeyboard bool) *ansiConsoleFrontend {
var fe ansiConsoleFrontend
fe.apple2 = a
fe.stdinKeyboard = stdinKeyboard
return &fe
}
const refreshDelayMs = 100
func (fe *ansiConsoleFrontend) GetKey(strobed bool) (key uint8, ok bool) {
// Init the first time
if fe.keyChannel == nil {
stdinReader := func(c chan uint8) {
reader := bufio.NewReader(os.Stdin)
for {
key, err := reader.ReadByte()
if err != nil {
fmt.Println(err)
return
}
c <- key
}
}
fe.keyChannel = make(chan uint8, 100)
go stdinReader(fe.keyChannel)
}
if !strobed {
// We must use the strobe to control the flow from stdin
ok = false
return
}
select {
case key = <-fe.keyChannel:
if key == 10 {
key = 13
if fe.extraLineFeeds != nil {
fe.extraLineFeeds <- 1
}
}
ok = true
default:
ok = false
}
return
}
func ansiCursorUp(steps int) string {
return fmt.Sprintf("\033[%vA", steps)
}
func (fe *ansiConsoleFrontend) textModeGoRoutine() {
fe.extraLineFeeds = make(chan int, 100)
fmt.Printf(strings.Repeat("\n", textLines+3))
for {
// Go up
content := ansiCursorUp(textLines + 3)
done := false
for !done {
select {
case lineFeeds := <-fe.extraLineFeeds:
content += ansiCursorUp(lineFeeds)
default:
done = true
}
}
content += "\n"
content += fmt.Sprintln(strings.Repeat("#", textColumns+4))
pageIndex := 0
if fe.apple2.io.isSoftSwitchActive(ioFlagSecondPage) {
pageIndex = 1
}
isAltText := fe.apple2.isApple2e && fe.apple2.io.isSoftSwitchActive(ioFlagAltChar)
for l := 0; l < textLines; l++ {
line := ""
for c := 0; c < textColumns; c++ {
char := getTextChar(fe.apple2, c, l, pageIndex)
line += textMemoryByteToString(char, isAltText)
}
content += fmt.Sprintf("# %v #\n", line)
}
content += fmt.Sprintln(strings.Repeat("#", textColumns+4))
if fe.stdinKeyboard {
content += "\033[KLine: "
}
if content != fe.lastContent {
fmt.Print(content)
fe.lastContent = content
}
time.Sleep(refreshDelayMs * time.Millisecond)
}
}
func textMemoryByteToString(value uint8, isAltCharSet bool) string {
// See https://en.wikipedia.org/wiki/Apple_II_character_set
// Supports the new lowercase characters in the Apple2e
// Only ascii from 0x20 to 0x5F is visible
topBits := value >> 6
isInverse := topBits == 0
isFlash := topBits == 1
if isFlash && isAltCharSet {
// On the Apple2e with lowercase chars there is not flash mode.
isFlash = false
isInverse = true
}
if isAltCharSet {
value = value & 0x7F
} else {
value = value & 0x3F
}
if value < 0x20 {
value += 0x40
}
if value == 0x7f {
// DEL is full box
value = '_'
}
if isFlash {
if value == ' ' {
// Flashing space in Apple is the full box. It can't be done with ANSI codes
value = '_'
}
return fmt.Sprintf("\033[5m%v\033[0m", string(value))
} else if isInverse {
return fmt.Sprintf("\033[7m%v\033[0m", string(value))
} else {
return string(value)
}
}
func textMemoryByteToStringHex(value uint8, _ bool) string {
return fmt.Sprintf("%02x ", value)
}