Screen video modes refactor. Video debug mode

This commit is contained in:
Ivan Izaguirre 2020-08-08 13:44:45 +02:00
parent caabdf4781
commit aa198877ec
8 changed files with 283 additions and 158 deletions

View File

@ -42,6 +42,7 @@ Portable emulator of an Apple II+ or //e. Written in Go.
- NTSC Color TV (extracting the phase from the mono signal)
- RGB for Super High Resolution and RGB card
- ANSI Console, avoiding the SDL2 dependency
- Debug mode: shows four panels with actual screen, page1, page2 and extra info dependant of the video mode
- Other features:
- Sound
- Joystick support. Up to two joysticks or four paddles
@ -131,10 +132,10 @@ Line:
- F5: Toggle speed between real and fastest
- Ctrl-F5: Show current speed in Mhz
- F6: Toggle between NTSC color TV and green phosphor monochrome monitor
- Ctrl-F6: Show a split screen with the views for NTSC color TV, monochrome monitor, HGR page 1 and HGR page 2.
- Ctrl-F6: Show the video mode and a split screen with the views for NTSC color TV, page 1, page 2 and extra info.
- F7: Save current state to disk (incomplete)
- F8: Restore state from disk (incomplete)
- F10: Cycle character generator codepages. Only if the character generator ROM has more than one 2Kb page.
- F10: Cycle character generator code pages. Only if the character generator ROM has more than one 2Kb page.
- F11: Toggle on and off the trace to console of the CPU execution
- F12: Save a screen snapshot to a file `snapshot.png`
- Pause: Pause the emulation

View File

@ -80,9 +80,10 @@ func SDLRun(a *apple2.Apple2) {
if !a.IsPaused() {
var img *image.RGBA
if kp.showPages {
img = apple2.SnapshotHGRModes(a)
img = a.SnapshotParts()
window.SetTitle(a.Name + " " + a.VideoModeName())
} else {
img = apple2.Snapshot(a)
img = a.Snapshot()
}
if img != nil {
surface, err := sdl.CreateRGBSurfaceFrom(unsafe.Pointer(&img.Pix[0]),

208
screen.go
View File

@ -15,28 +15,89 @@ References:
- "More Colors for your Apple", https://archive.org/details/byte-magazine-1979-06/page/n61
*/
// Snapshot the currently visible screen
func Snapshot(a *Apple2) *image.RGBA {
return activeSnapshot(a, false)
}
const (
videoText40 uint8 = 0x01
videoGR uint8 = 0x02
videoHGR uint8 = 0x03
func activeSnapshot(a *Apple2, raw bool) *image.RGBA {
isColor := a.isColor
videoText80 uint8 = 0x08
videoDGR uint8 = 0x09
videoDHGR uint8 = 0x0a
videoMono560 uint8 = 0x10
videoRGBMix uint8 = 0x11
videoSHR uint8 = 0x12
// Modifiers
videoBaseMask uint8 = 0x1f
videoSecondPage uint8 = 0x20
videoMixText40 uint8 = 0x40
videoMixText80 uint8 = 0x80
)
func getCurrentVideoMode(a *Apple2) uint8 {
isTextMode := a.io.isSoftSwitchActive(ioFlagText)
isHiResMode := a.io.isSoftSwitchActive(ioFlagHiRes)
isMixMode := a.io.isSoftSwitchActive(ioFlagMixed)
is80Columns := a.io.isSoftSwitchActive(ioFlag80Col)
isDoubleResMode := !isTextMode && is80Columns && !a.io.isSoftSwitchActive(ioFlagAnnunciator3)
isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active
isSuperHighResMode := a.io.isSoftSwitchActive(ioDataNewVideo)
rgbFlag1 := a.io.isSoftSwitchActive(ioFlag1RGBCard)
rgbFlag2 := a.io.isSoftSwitchActive(ioFlag2RGBCard)
isMono560 := isDoubleResMode && !rgbFlag1 && !rgbFlag2
isRGBMixMode := isDoubleResMode && !rgbFlag1 && rgbFlag2
isMixMode := a.io.isSoftSwitchActive(ioFlagMixed)
mode := uint8(0)
if isSuperHighResMode {
mode = videoSHR
isMixMode = false
} else if isTextMode {
if is80Columns {
mode = videoText80
} else {
mode = videoText40
}
isMixMode = false
} else if isHiResMode {
if !isDoubleResMode {
mode = videoHGR
} else if isMono560 {
mode = videoMono560
} else if isRGBMixMode {
mode = videoRGBMix
} else {
mode = videoDHGR
}
} else if isDoubleResMode {
mode = videoDGR
} else {
mode = videoGR
}
// Modifiers
if isMixMode {
if is80Columns {
mode |= videoMixText80
} else {
mode |= videoMixText40
}
}
isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active
if isSecondPage {
mode |= videoSecondPage
}
return mode
}
func snapshotByMode(a *Apple2, videoMode uint8) *image.RGBA {
videoBase := videoMode & videoBaseMask
isSecondPage := (videoMode & videoSecondPage) != 0
isMixMode := (videoMode & (videoMixText40 | videoMixText80)) != 0
var lightColor color.Color
if isColor {
if a.isColor {
lightColor = color.White
} else {
// Color for typical Apple ][ period green P1 phosphor monitors
@ -45,101 +106,80 @@ func activeSnapshot(a *Apple2, raw bool) *image.RGBA {
}
applyNTSCFilter := a.isColor
var snap *image.RGBA
var ntscMask *image.Alpha
if isSuperHighResMode { // Has to be first and disables the rest
switch videoBase {
case videoText40:
snap = snapshotTextMode(a, false /*is80Columns*/, isSecondPage, lightColor)
applyNTSCFilter = false
case videoText80:
snap = snapshotTextMode(a, true /*is80Columns*/, isSecondPage, lightColor)
applyNTSCFilter = false
case videoGR:
snap = snapshotLoResModeMono(a, false /*isDoubleResMode*/, isSecondPage, lightColor)
case videoDGR:
snap = snapshotLoResModeMono(a, true /*isDoubleResMode*/, isSecondPage, lightColor)
case videoHGR:
snap = snapshotHiResModeMono(a, isSecondPage, lightColor)
case videoDHGR:
snap, _ = snapshotDoubleHiResModeMono(a, isSecondPage, false /*isRGBMixMode*/, lightColor)
case videoMono560:
snap, _ = snapshotDoubleHiResModeMono(a, isSecondPage, false /*isRGBMixMode*/, lightColor)
applyNTSCFilter = false
case videoRGBMix:
snap, ntscMask = snapshotDoubleHiResModeMono(a, isSecondPage, true /*isRGBMixMode*/, lightColor)
case videoSHR:
snap = snapshotSuperHiResMode(a)
} else if isTextMode {
snap = snapshotTextMode(a, is80Columns, isSecondPage, false /*isMixMode*/, lightColor)
} else {
if isHiResMode {
if isDoubleResMode {
snap, ntscMask = snapshotDoubleHiResModeMono(a, isSecondPage, isMixMode, isRGBMixMode, lightColor)
} else {
snap = snapshotHiResModeMono(a, isSecondPage, isMixMode, lightColor)
}
} else {
snap = snapshotLoResModeMono(a, isDoubleResMode, isSecondPage, isMixMode, lightColor)
}
if isMixMode {
snapText := snapshotTextMode(a, is80Columns, false /*isSecondPage*/, true /*isMixMode*/, lightColor)
snap = mixSnapshots(snap, snapText)
}
if isColor && !(raw || isMono560) {
snap = filterNTSCColor(snap, ntscMask)
}
applyNTSCFilter = false
}
if !raw && !isSuperHighResMode {
snap = linesSeparatedFilter(snap)
if isMixMode {
isMix80 := (videoMode & videoMixText80) != 0
bottom := snapshotTextMode(a, isMix80, isSecondPage, lightColor)
snap = mixSnapshots(snap, bottom)
}
if applyNTSCFilter {
snap = filterNTSCColor(snap, ntscMask)
}
return snap
}
// SnapshotHGRModes to get all modes mixed
func SnapshotHGRModes(a *Apple2) *image.RGBA {
bwSnap := activeSnapshot(a, true)
if bwSnap.Bounds().Dx() == hiResWidth {
bwSnap = doubleWidthFilter(bwSnap)
}
colorSnap := filterNTSCColor(bwSnap, nil)
page1Snap := filterNTSCColor(snapshotHiResModeMono(a, false /*2nd page*/, false /*mix*/, color.White), nil) // HGR 1
page2Snap := filterNTSCColor(snapshotHiResModeMono(a, true /*2nd page*/, false /*mix*/, color.White), nil) // HGR 2
// Snapshot the currently visible screen
func (a *Apple2) Snapshot() *image.RGBA {
videoMode := getCurrentVideoMode(a)
snap := snapshotByMode(a, videoMode)
size := image.Rect(0, 0, hiResWidth*4, hiResHeight*2)
out := image.NewRGBA(size)
for y := 0; y < hiResHeight; y++ {
for x := 0; x < hiResWidth*2; x++ {
out.Set(x, y, colorSnap.At(x, y))
out.Set(x+hiResWidth*2, y, bwSnap.At(x, y))
out.Set(x, y+hiResHeight, page1Snap.At(x, y))
out.Set(x+hiResWidth*2, y+hiResHeight, page2Snap.At(x, y))
}
if snap.Bounds().Dy() == hiResHeight {
// Apply the filter to regular CRT snapshots with 192 lines. Not to SHR
snap = linesSeparatedFilter(snap)
}
return out
return snap
}
func mixSnapshots(top, bottom *image.RGBA) *image.RGBA {
topBounds := top.Bounds()
topWidth := topBounds.Dx()
topHeight := topBounds.Dy()
bottomBounds := bottom.Bounds()
bottomWidth := bottomBounds.Dx()
bottomHeight := bottomBounds.Dy()
topWidth := top.Bounds().Dx()
bottomWidth := bottom.Bounds().Dx()
factor := topWidth / bottomWidth
size := image.Rect(0, 0, topWidth, topHeight+bottomHeight)
out := image.NewRGBA(size)
// Copy top
for y := topBounds.Min.Y; y < topBounds.Max.Y; y++ {
for x := topBounds.Min.X; x < topBounds.Max.X; x++ {
c := top.At(x, y)
out.Set(x, y, c)
}
}
// Copy bottom, applying the factor
for y := bottomBounds.Min.Y; y < bottomBounds.Max.Y; y++ {
for x := bottomBounds.Min.X; x < bottomBounds.Max.X; x++ {
// Copy bottom's bottom on top's bottom, applying the factor
for y := hiResHeightMixed; y < hiResHeight; y++ {
for x := 0; x < topWidth; x++ {
c := bottom.At(x, y)
for f := 0; f < factor; f++ {
out.Set(x*factor+f, topHeight+y, c)
top.Set(x*factor+f, y, c)
}
}
}
return out
return top
}
// SaveSnapshot saves a snapshot of the screen to a png file
func SaveSnapshot(a *Apple2, filename string) error {
img := Snapshot(a)
img := a.Snapshot()
img = squarishPixelsFilter(img)
f, err := os.Create(filename)
@ -184,17 +224,3 @@ func linesSeparatedFilter(in *image.RGBA) *image.RGBA {
}
return out
}
func doubleWidthFilter(in *image.RGBA) *image.RGBA {
b := in.Bounds()
size := image.Rect(0, 0, 2*b.Dx(), b.Dy())
out := image.NewRGBA(size)
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
c := in.At(x, y)
out.Set(2*x, y, c)
out.Set(2*x+1, y, c)
}
}
return out
}

120
screenDebugParts.go Normal file
View File

@ -0,0 +1,120 @@
package apple2
import (
"image"
)
// SnapshotParts the currently visible screen
func (a *Apple2) SnapshotParts() *image.RGBA {
videoMode := getCurrentVideoMode(a)
snapScreen := snapshotByMode(a, videoMode)
snapPage1 := snapshotByMode(a, videoMode&^videoSecondPage)
snapPage2 := snapshotByMode(a, videoMode|videoSecondPage)
var snapAux *image.RGBA
/*videoBase := videoMode & videoBaseMask
if videoBase == videoRGBMix {
isSecondPage := (videoMode & videoSecondPage) != 0
_, mask := snapshotDoubleHiResModeMono(a, isSecondPage, true /*isRGBMixMode*/ /*, color.White)
snapAux = filterMask(mask)
}*/
if snapAux == nil && (videoMode&videoMixText80) != 0 {
snapAux = snapshotByMode(a, videoText80)
}
if snapAux == nil {
snapAux = snapshotByMode(a, videoText40)
}
return mixFourSnapshots([]*image.RGBA{snapScreen, snapAux, snapPage1, snapPage2})
}
// VideoModeName returns the name of the current video mode
func (a *Apple2) VideoModeName() string {
videoMode := getCurrentVideoMode(a)
videoBase := videoMode & videoBaseMask
var name string
applyNTSCFilter := a.isColor
switch videoBase {
case videoText40:
name = "TEXT40COL"
applyNTSCFilter = false
case videoText80:
name = "TEXT80COL"
applyNTSCFilter = false
case videoGR:
name = "GR"
case videoDGR:
name = "DGR"
case videoHGR:
name = "HGR"
case videoDHGR:
name = "DHGR"
case videoMono560:
name = "Mono560"
applyNTSCFilter = false
case videoRGBMix:
name = "RGMMIX"
case videoSHR:
name = "SHR"
applyNTSCFilter = false
default:
name = "Unknown video mode"
}
if (videoMode & videoSecondPage) != 0 {
name += "-PAGE2"
}
if (videoMode & videoMixText40) != 0 {
name += "-MIX40"
}
if (videoMode & videoMixText80) != 0 {
name += "-MIX80"
}
if applyNTSCFilter {
name += "-NTSC"
}
return name
}
func mixFourSnapshots(snaps []*image.RGBA) *image.RGBA {
size := image.Rect(0, 0, hiResWidth*4, hiResHeight*2)
out := image.NewRGBA(size)
for i := 0; i < 4; i++ {
if snaps[i].Bounds().Dx() < hiResWidth*2 {
snaps[i] = doubleWidthFilter(snaps[i])
}
}
for y := 0; y < hiResHeight; y++ {
for x := 0; x < hiResWidth*2; x++ {
out.Set(x, y, snaps[0].At(x, y))
out.Set(x+hiResWidth*2, y, snaps[1].At(x, y))
out.Set(x, y+hiResHeight, snaps[2].At(x, y))
out.Set(x+hiResWidth*2, y+hiResHeight, snaps[3].At(x, y))
}
}
return out
}
func doubleWidthFilter(in *image.RGBA) *image.RGBA {
b := in.Bounds()
size := image.Rect(0, 0, 2*b.Dx(), b.Dy())
out := image.NewRGBA(size)
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
c := in.At(x, y)
out.Set(2*x, y, c)
out.Set(2*x+1, y, c)
}
}
return out
}

View File

@ -9,29 +9,25 @@ const (
doubleHiResWidth = 2 * hiResWidth
)
func snapshotDoubleHiResModeMono(a *Apple2, isSecondPage bool, mixedMode bool, getNTSCMask bool, light color.Color) (*image.RGBA, *image.Alpha) {
func snapshotDoubleHiResModeMono(a *Apple2, isSecondPage bool, getNTSCMask bool, light color.Color) (*image.RGBA, *image.Alpha) {
// As described in "Inside the Apple IIe"
height := hiResHeight
if mixedMode {
height = hiResHeightMixed
}
size := image.Rect(0, 0, doubleHiResWidth, height)
size := image.Rect(0, 0, doubleHiResWidth, hiResHeight)
img := image.NewRGBA(size)
// To support RGB-mode14 we will have a mask to mark where we should not have the NTSC filter applied
// To support RGB-mode 14 we will have a mask to mark where we should not have the NTSC filter applied
// See: https://apple2online.com/web_documents/Video-7%20Manual%20KB.pdf
var ntscMask *image.Alpha
if getNTSCMask {
ntscMask = image.NewAlpha(size)
}
img := image.NewRGBA(size)
for y := 0; y < height; y++ {
for y := 0; y < hiResHeight; y++ {
lineParts := [][]uint8{
getHiResLine(a, y, isSecondPage, true),
getHiResLine(a, y, isSecondPage, false),
getHiResLine(a, y, isSecondPage, true /*auxmem*/),
getHiResLine(a, y, isSecondPage, false /*auxmem*/),
}
x := 0
// For the NTSC filter to work we have to insert an initial black pixel and skip the last one
// For the NTSC filter to work we have to insert an initial black pixel and skip the last one ¿?
img.Set(x, y, color.Black)
if getNTSCMask {
ntscMask.Set(x, y, color.Opaque)
@ -40,10 +36,12 @@ func snapshotDoubleHiResModeMono(a *Apple2, isSecondPage bool, mixedMode bool, g
for iByte := 0; iByte < hiResLineBytes; iByte++ {
for iPart := 0; iPart < 2; iPart++ {
b := lineParts[iPart][iByte]
mask := color.Transparent // Apply the NTSC filter
if getNTSCMask && b&0x80 == 0 {
mask = color.Opaque // Do not apply the NTSC filter
}
for j := uint(0); j < 7; j++ {
// Set color
bit := (b >> j) & 1

View File

@ -19,9 +19,9 @@ func getHiResLineOffset(line int) uint16 {
// See "Understanding the Apple II", page 5-14
// http://www.applelogic.org/files/UNDERSTANDINGTHEAII.pdf
section := line >> 6 // Top, middle and bottom
outerEigth := (line >> 3) & 0x07
innerEigth := line & 0x07
return uint16(section*40 + outerEigth*0x80 + innerEigth*0x400)
outerEighth := (line >> 3) & 0x07
innerEighth := line & 0x07
return uint16(section*40 + outerEighth*0x80 + innerEighth*0x400)
}
func getHiResLine(a *Apple2, line int, isSecondPage bool, auxMem bool) []uint8 {
@ -34,18 +34,12 @@ func getHiResLine(a *Apple2, line int, isSecondPage bool, auxMem bool) []uint8 {
return a.mmu.getPhysicalMainRAM(auxMem).subRange(address, address+hiResLineBytes)
}
func snapshotHiResModeMono(a *Apple2, isSecondPage bool, mixedMode bool, light color.Color) *image.RGBA {
func snapshotHiResModeMono(a *Apple2, isSecondPage bool, light color.Color) *image.RGBA {
// As described in "Undertanding the Apple II", with half pixel shifts
height := hiResHeight
if mixedMode {
height = hiResHeightMixed
}
size := image.Rect(0, 0, 2*hiResWidth, height)
size := image.Rect(0, 0, 2*hiResWidth, hiResHeight)
img := image.NewRGBA(size)
for y := 0; y < height; y++ {
for y := 0; y < hiResHeight; y++ {
bytes := getHiResLine(a, y, isSecondPage, false /*auxMem*/)
x := 0
var previousColour color.Color = color.Black

View File

@ -38,11 +38,8 @@ func getColorPatterns(light color.Color) [16][16]color.Color {
}
func snapshotLoResModeMono(a *Apple2, isDoubleResMode bool, isSecondPage bool, isMixMode bool, light color.Color) *image.RGBA {
text, columns, lines := getActiveText(a, isDoubleResMode, isSecondPage, false)
if isMixMode {
lines -= textLinesMix
}
func snapshotLoResModeMono(a *Apple2, isDoubleResMode bool, isSecondPage bool, light color.Color) *image.RGBA {
text, columns, lines := getActiveText(a, isDoubleResMode, isSecondPage)
grLines := lines * 2
pixelWidth := loResPixelWidth
if isDoubleResMode {
@ -61,7 +58,7 @@ func snapshotLoResModeMono(a *Apple2, isDoubleResMode bool, isSecondPage bool, i
grPixel = char & 0xf
}
// We place pixelWidth mono pixels per graphic pixel.
// The groups of 4 mono pixels need to be alligned with an offset to get plain surfaces
// The groups of 4 mono pixels need to be aligned with an offset to get plain surfaces
offset := (c * pixelWidth) % 4
if isDoubleResMode && ((c % 2) == 0) {

View File

@ -9,11 +9,11 @@ import (
)
const (
charWidth = 7
charHeight = 8
textColumns = 40
textLines = 24
textLinesMix = 4
charWidth = 7
charHeight = 8
textColumns = 40
textLines = 24
textPage1Address = uint16(0x0400)
textPage2Address = uint16(0x0800)
textPageSize = uint16(0x0400)
@ -24,44 +24,33 @@ func getTextCharOffset(col int, line int) uint16 {
// See "Understanding the Apple II", page 5-10
// http://www.applelogic.org/files/UNDERSTANDINGTHEAII.pdf
section := line / 8 // Top, middle and bottom
eigth := line % 8
return uint16(section*40 + eigth*0x80 + col)
eighth := line % 8
return uint16(section*40 + eighth*0x80 + col)
}
func snapshotTextMode(a *Apple2, is80Columns bool, isSecondPage bool, isMixMode bool, light color.Color) *image.RGBA {
text, columns, lines := getActiveText(a, is80Columns, isSecondPage, isMixMode)
func snapshotTextMode(a *Apple2, is80Columns bool, isSecondPage bool, light color.Color) *image.RGBA {
text, columns, lines := getActiveText(a, is80Columns, isSecondPage)
return renderTextMode(a, text, columns, lines, light)
}
func getActiveText(a *Apple2, is80Columns bool, isSecondPage bool, isMixMode bool) ([]uint8, int, int) {
lines := textLines
if isMixMode {
lines = textLinesMix
}
func getActiveText(a *Apple2, is80Columns bool, isSecondPage bool) ([]uint8, int, int) {
if !is80Columns {
text40Columns := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage, isMixMode)
return text40Columns, textColumns, lines
text40Columns := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage)
return text40Columns, textColumns, textLines
}
text40Columns := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage, isMixMode)
text40ColumnsAlt := getTextFromMemory(a.mmu.physicalMainRAMAlt, isSecondPage, isMixMode)
text40Columns := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage)
text40ColumnsAlt := getTextFromMemory(a.mmu.physicalMainRAMAlt, isSecondPage)
// Merge the two 40 cols to return 80 cols
text80Columns := make([]uint8, 2*len(text40Columns))
for i := 0; i < len(text40Columns); i++ {
text80Columns[2*i] = text40ColumnsAlt[i]
text80Columns[2*i+1] = text40Columns[i]
}
return text80Columns, textColumns * 2, lines
return text80Columns, textColumns * 2, textLines
}
func getTextFromMemory(mem *memoryRange, isSecondPage bool, isMixMode bool) []uint8 {
lineStart := 0
if isMixMode {
lineStart = textLines - textLinesMix
}
func getTextFromMemory(mem *memoryRange, isSecondPage bool) []uint8 {
addressStart := textPage1Address
if isSecondPage {
addressStart = textPage2Address
@ -69,11 +58,10 @@ func getTextFromMemory(mem *memoryRange, isSecondPage bool, isMixMode bool) []ui
addressEnd := addressStart + textPageSize
data := mem.subRange(addressStart, addressEnd)
lines := textLines - lineStart
text := make([]uint8, lines*textColumns)
for l := 0; l < lines; l++ {
text := make([]uint8, textLines*textColumns)
for l := 0; l < textLines; l++ {
for c := 0; c < textColumns; c++ {
char := data[getTextCharOffset(c, l+lineStart)]
char := data[getTextCharOffset(c, l)]
text[textColumns*l+c] = char
}
}
@ -81,7 +69,7 @@ func getTextFromMemory(mem *memoryRange, isSecondPage bool, isMixMode bool) []ui
}
func renderTextMode(a *Apple2, text []uint8, columns int, lines int, light color.Color) *image.RGBA {
// Flash mode is 2Hz
// Flash mode is 2Hz (host time)
isFlashedFrame := time.Now().Nanosecond() > (1 * 1000 * 1000 * 1000 / 2)
width := columns * charWidth
@ -137,7 +125,7 @@ func DumpTextModeAnsi(a *Apple2) string {
is80Columns := a.io.isSoftSwitchActive(ioFlag80Col)
isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active
text, columns, lines := getActiveText(a, is80Columns, isSecondPage, false /*isMixedMode*/)
text, columns, lines := getActiveText(a, is80Columns, isSecondPage)
content := "\n"
content += fmt.Sprintln(strings.Repeat("#", columns+4))