mirror of
https://github.com/taigrr/gopher-os
synced 2025-01-18 04:43:13 -08:00
Refactor VT implementation
This commit refactors the old VT implementation to work with the revised TTY interface and adds support for: - scrollback - terminal state handling When a terminal becomes activated, it overwrites the attached console contents with the contents of its viewport.
This commit is contained in:
parent
78c87ab165
commit
0f3af2e78d
260
src/gopheros/device/tty/vt.go
Normal file
260
src/gopheros/device/tty/vt.go
Normal file
@ -0,0 +1,260 @@
|
||||
package tty
|
||||
|
||||
import (
|
||||
"gopheros/device/video/console"
|
||||
"gopheros/kernel"
|
||||
"io"
|
||||
)
|
||||
|
||||
// VT implements a terminal supporting scrollback. The terminal interprets the
|
||||
// following special characters:
|
||||
// - \r (carriage-return)
|
||||
// - \n (line-feed)
|
||||
// - \b (backspace)
|
||||
// - \t (tab; expanded to tabWidth spaces)
|
||||
type VT struct {
|
||||
cons console.Device
|
||||
|
||||
// Terminal dimensions
|
||||
termWidth uint16
|
||||
termHeight uint16
|
||||
viewportWidth uint16
|
||||
viewportHeight uint16
|
||||
|
||||
// The number of additional lines of output that are buffered by the
|
||||
// terminal to support scrolling up.
|
||||
scrollback uint16
|
||||
|
||||
// The terminal contents. Each character occupies 3 bytes and uses the
|
||||
// format: (ASCII char, fg, bg)
|
||||
data []uint8
|
||||
|
||||
// Terminal state.
|
||||
tabWidth uint8
|
||||
defaultFg, curFg uint8
|
||||
defaultBg, curBg uint8
|
||||
cursorX uint16
|
||||
cursorY uint16
|
||||
viewportY uint16
|
||||
dataOffset uint
|
||||
state State
|
||||
}
|
||||
|
||||
// NewVT creates a new virtual terminal device. The tabWidth parameter controls
|
||||
// tab expansion whereas the scrollback parameter defines the line count that
|
||||
// gets buffered by the terminal to provide scrolling beyond the console
|
||||
// height.
|
||||
func NewVT(tabWidth uint8, scrollback uint16) *VT {
|
||||
return &VT{
|
||||
tabWidth: tabWidth,
|
||||
scrollback: scrollback,
|
||||
cursorX: 1,
|
||||
cursorY: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// AttachTo connects a TTY to a console instance.
|
||||
func (t *VT) AttachTo(cons console.Device) {
|
||||
if cons == nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.cons = cons
|
||||
t.viewportWidth, t.viewportHeight = cons.Dimensions()
|
||||
t.viewportY = 0
|
||||
t.defaultFg, t.defaultBg = cons.DefaultColors()
|
||||
t.curFg, t.curBg = t.defaultFg, t.defaultBg
|
||||
t.termWidth, t.termHeight = t.viewportWidth, t.viewportHeight+t.scrollback
|
||||
t.cursorX, t.cursorY = 1, 1
|
||||
|
||||
// Allocate space for the contents and fill it with empty characters
|
||||
// using the default fg/bg colors for the attached console.
|
||||
t.data = make([]uint8, t.termWidth*t.termHeight*3)
|
||||
for i := 0; i < len(t.data); i += 3 {
|
||||
t.data[i] = ' '
|
||||
t.data[i+1] = t.defaultFg
|
||||
t.data[i+2] = t.defaultBg
|
||||
}
|
||||
}
|
||||
|
||||
// State returns the TTY's state.
|
||||
func (t *VT) State() State {
|
||||
return t.state
|
||||
}
|
||||
|
||||
// SetState updates the TTY's state.
|
||||
func (t *VT) SetState(newState State) {
|
||||
if t.state == newState {
|
||||
return
|
||||
}
|
||||
|
||||
t.state = newState
|
||||
|
||||
// If the terminal became active, update the console with its contents
|
||||
if t.state == StateActive && t.cons != nil {
|
||||
for y := uint16(1); y <= t.viewportHeight; y++ {
|
||||
offset := (y - 1 + t.viewportY) * (t.viewportWidth * 3)
|
||||
for x := uint16(1); x <= t.viewportWidth; x, offset = x+1, offset+3 {
|
||||
t.cons.Write(t.data[offset], t.data[offset+1], t.data[offset+2], x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CursorPosition returns the current cursor position.
|
||||
func (t *VT) CursorPosition() (uint16, uint16) {
|
||||
return t.cursorX, t.cursorY
|
||||
}
|
||||
|
||||
// SetCursorPosition sets the current cursor position to (x,y).
|
||||
func (t *VT) SetCursorPosition(x, y uint16) {
|
||||
if t.cons == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if x < 1 {
|
||||
x = 1
|
||||
} else if x > t.viewportWidth {
|
||||
x = t.viewportWidth
|
||||
}
|
||||
|
||||
if y < 1 {
|
||||
y = 1
|
||||
} else if y > t.viewportHeight {
|
||||
y = t.viewportHeight
|
||||
}
|
||||
|
||||
t.cursorX, t.cursorY = x, y
|
||||
t.updateDataOffset()
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (t *VT) Write(data []byte) (int, error) {
|
||||
for count, b := range data {
|
||||
err := t.WriteByte(b)
|
||||
if err != nil {
|
||||
return count, err
|
||||
}
|
||||
}
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
// WriteByte implements io.ByteWriter.
|
||||
func (t *VT) WriteByte(b byte) error {
|
||||
if t.cons == nil {
|
||||
return io.ErrClosedPipe
|
||||
}
|
||||
|
||||
switch b {
|
||||
case '\r':
|
||||
t.cr()
|
||||
case '\n':
|
||||
t.lf(true)
|
||||
case '\b':
|
||||
if t.cursorX > 1 {
|
||||
t.SetCursorPosition(t.cursorX-1, t.cursorY)
|
||||
t.doWrite(' ', false)
|
||||
}
|
||||
case '\t':
|
||||
for i := uint8(0); i < t.tabWidth; i++ {
|
||||
t.doWrite(' ', true)
|
||||
}
|
||||
default:
|
||||
t.doWrite(b, true)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// doWrite writes the specified character together with the current fg/bg
|
||||
// attributes at the current data offset advancing the cursor position if
|
||||
// advanceCursor is true. If the terminal is active, then doWrite also writes
|
||||
// the character to the attached console.
|
||||
func (t *VT) doWrite(b byte, advanceCursor bool) {
|
||||
if t.state == StateActive {
|
||||
t.cons.Write(b, t.curFg, t.curBg, t.cursorX, t.cursorY)
|
||||
}
|
||||
|
||||
t.data[t.dataOffset] = b
|
||||
t.data[t.dataOffset+1] = t.curFg
|
||||
t.data[t.dataOffset+2] = t.curBg
|
||||
|
||||
if advanceCursor {
|
||||
// Advance x position and handle wrapping when the cursor reaches the
|
||||
// end of the current line
|
||||
t.dataOffset += 3
|
||||
t.cursorX++
|
||||
if t.cursorX > t.viewportWidth {
|
||||
t.lf(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cr resets the x coordinate of the terminal cursor to 0.
|
||||
func (t *VT) cr() {
|
||||
t.cursorX = 1
|
||||
t.updateDataOffset()
|
||||
}
|
||||
|
||||
// lf advances the y coordinate of the terminal cursor by one line scrolling
|
||||
// the terminal contents if the end of the last terminal line is reached.
|
||||
func (t *VT) lf(withCR bool) {
|
||||
if withCR {
|
||||
t.cursorX = 1
|
||||
}
|
||||
|
||||
switch {
|
||||
// Cursor has not reached the end of the viewport
|
||||
case t.cursorY+1 <= t.viewportHeight:
|
||||
t.cursorY++
|
||||
default:
|
||||
// Check if the viewport can be scrolled down
|
||||
if t.viewportY+t.viewportHeight < t.termHeight {
|
||||
t.viewportY++
|
||||
} else {
|
||||
// We have reached the bottom of the terminal buffer.
|
||||
// We need to scroll its contents up and clear the last line
|
||||
var stride = int(t.viewportWidth * 3)
|
||||
var startOffset = int(t.viewportY) * stride
|
||||
var endOffset = int(t.viewportY+t.viewportHeight-1) * stride
|
||||
|
||||
for offset := startOffset; offset < endOffset; offset++ {
|
||||
t.data[offset] = t.data[offset+stride]
|
||||
}
|
||||
|
||||
for offset := endOffset; offset < endOffset+stride; offset += 3 {
|
||||
t.data[offset+0] = ' '
|
||||
t.data[offset+1] = t.defaultFg
|
||||
t.data[offset+2] = t.defaultBg
|
||||
}
|
||||
}
|
||||
|
||||
// Sync console
|
||||
if t.state == StateActive {
|
||||
t.cons.Scroll(console.ScrollDirUp, 1)
|
||||
t.cons.Fill(1, t.cursorY, t.termWidth, 1, t.defaultFg, t.defaultBg)
|
||||
}
|
||||
}
|
||||
|
||||
t.updateDataOffset()
|
||||
}
|
||||
|
||||
// updateDataOffset calculates the offset in the data buffer taking into account
|
||||
// the cursor position and the viewportY value.
|
||||
func (t *VT) updateDataOffset() {
|
||||
t.dataOffset = uint((t.viewportY+(t.cursorY-1))*(t.viewportWidth*3) + ((t.cursorX - 1) * 3))
|
||||
}
|
||||
|
||||
// DriverName returns the name of this driver.
|
||||
func (t *VT) DriverName() string {
|
||||
return "vt"
|
||||
}
|
||||
|
||||
// DriverVersion returns the version of this driver.
|
||||
func (t *VT) DriverVersion() (uint16, uint16, uint16) {
|
||||
return 0, 0, 1
|
||||
}
|
||||
|
||||
// DriverInit initializes this driver.
|
||||
func (t *VT) DriverInit() *kernel.Error { return nil }
|
407
src/gopheros/device/tty/vt_test.go
Normal file
407
src/gopheros/device/tty/vt_test.go
Normal file
@ -0,0 +1,407 @@
|
||||
package tty
|
||||
|
||||
import (
|
||||
"gopheros/device"
|
||||
"gopheros/device/video/console"
|
||||
"image/color"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVtPosition(t *testing.T) {
|
||||
specs := []struct {
|
||||
inX, inY uint16
|
||||
expX, expY uint16
|
||||
}{
|
||||
{20, 20, 20, 20},
|
||||
{100, 20, 80, 20},
|
||||
{10, 200, 10, 25},
|
||||
{10, 200, 10, 25},
|
||||
{100, 100, 80, 25},
|
||||
}
|
||||
|
||||
term := NewVT(4, 0)
|
||||
|
||||
// SetCursorPosition without an attached console is a no-op
|
||||
term.SetCursorPosition(2, 2)
|
||||
|
||||
if curX, curY := term.CursorPosition(); curX != 1 || curY != 1 {
|
||||
t.Fatalf("expected terminal initial position to be (1, 1); got (%d, %d)", curX, curY)
|
||||
}
|
||||
|
||||
cons := newMockConsole(80, 25)
|
||||
term.AttachTo(cons)
|
||||
|
||||
for specIndex, spec := range specs {
|
||||
term.SetCursorPosition(spec.inX, spec.inY)
|
||||
if x, y := term.CursorPosition(); x != spec.expX || y != spec.expY {
|
||||
t.Errorf("[spec %d] expected setting position to (%d, %d) to update the position to (%d, %d); got (%d, %d)", specIndex, spec.inX, spec.inY, spec.expX, spec.expY, x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVtWrite(t *testing.T) {
|
||||
t.Run("inactive terminal", func(t *testing.T) {
|
||||
cons := newMockConsole(80, 25)
|
||||
|
||||
term := NewVT(4, 0)
|
||||
if _, err := term.Write([]byte("foo")); err != io.ErrClosedPipe {
|
||||
t.Fatal("expected calling Write on a terminal without an attached console to return ErrClosedPipe")
|
||||
}
|
||||
|
||||
term.AttachTo(cons)
|
||||
|
||||
term.curFg = 2
|
||||
term.curBg = 3
|
||||
|
||||
data := []byte("\b123\b4\t5\n67\r68")
|
||||
count, err := term.Write(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if count != len(data) {
|
||||
t.Fatalf("expected to write %d bytes; wrote %d", len(data), count)
|
||||
}
|
||||
|
||||
if cons.bytesWritten != 0 {
|
||||
t.Fatalf("expected writes not to be synced with console when terminal is inactive; %d bytes written", cons.bytesWritten)
|
||||
}
|
||||
|
||||
specs := []struct {
|
||||
x, y uint16
|
||||
expByte uint8
|
||||
}{
|
||||
{1, 1, '1'},
|
||||
{2, 1, '2'},
|
||||
{3, 1, '4'},
|
||||
{8, 1, '5'}, // 2 + tabWidth + 1
|
||||
{1, 2, '6'},
|
||||
{2, 2, '8'},
|
||||
}
|
||||
|
||||
for specIndex, spec := range specs {
|
||||
offset := ((spec.y - 1) * term.viewportWidth * 3) + ((spec.x - 1) * 3)
|
||||
if term.data[offset] != spec.expByte {
|
||||
t.Errorf("[spec %d] expected char at (%d, %d) to be %q; got %q", specIndex, spec.x, spec.y, spec.expByte, term.data[offset])
|
||||
}
|
||||
|
||||
if term.data[offset+1] != term.curFg {
|
||||
t.Errorf("[spec %d] expected fg attribute at (%d, %d) to be %d; got %d", specIndex, spec.x, spec.y, term.curFg, term.data[offset+1])
|
||||
}
|
||||
|
||||
if term.data[offset+2] != term.curBg {
|
||||
t.Errorf("[spec %d] expected bg attribute at (%d, %d) to be %d; got %d", specIndex, spec.x, spec.y, term.curBg, term.data[offset+2])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("active terminal", func(t *testing.T) {
|
||||
cons := newMockConsole(80, 25)
|
||||
|
||||
term := NewVT(4, 0)
|
||||
term.SetState(StateActive)
|
||||
term.SetState(StateActive) // calling SetState with the same state is a no-op
|
||||
|
||||
if got := term.State(); got != StateActive {
|
||||
t.Fatalf("expected terminal state to be %d; got %d", StateActive, got)
|
||||
}
|
||||
|
||||
term.AttachTo(cons)
|
||||
|
||||
term.curFg = 2
|
||||
term.curBg = 3
|
||||
|
||||
data := []byte("\b123\b4\t5\n67\r68")
|
||||
term.Write(data)
|
||||
|
||||
if expCount := len(data); cons.bytesWritten != expCount {
|
||||
t.Fatalf("expected writes to be synced with console when terminal is active. %d bytes written; expected %d", cons.bytesWritten, expCount)
|
||||
}
|
||||
|
||||
specs := []struct {
|
||||
x, y uint16
|
||||
expByte uint8
|
||||
}{
|
||||
{1, 1, '1'},
|
||||
{2, 1, '2'},
|
||||
{3, 1, '4'},
|
||||
{8, 1, '5'}, // 2 + tabWidth + 1
|
||||
{1, 2, '6'},
|
||||
{2, 2, '8'},
|
||||
}
|
||||
|
||||
for specIndex, spec := range specs {
|
||||
offset := ((spec.y - 1) * cons.width) + (spec.x - 1)
|
||||
if cons.chars[offset] != spec.expByte {
|
||||
t.Errorf("[spec %d] expected console char at (%d, %d) to be %q; got %q", specIndex, spec.x, spec.y, spec.expByte, cons.chars[offset])
|
||||
}
|
||||
|
||||
if cons.fgAttrs[offset] != term.curFg {
|
||||
t.Errorf("[spec %d] expected console fg attribute at (%d, %d) to be %d; got %d", specIndex, spec.x, spec.y, term.curFg, cons.fgAttrs[offset])
|
||||
}
|
||||
|
||||
if cons.bgAttrs[offset] != term.curBg {
|
||||
t.Errorf("[spec %d] expected console bg attribute at (%d, %d) to be %d; got %d", specIndex, spec.x, spec.y, term.curBg, cons.bgAttrs[offset])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVtLineFeedHandling(t *testing.T) {
|
||||
t.Run("viewport at end of terminal", func(t *testing.T) {
|
||||
cons := newMockConsole(80, 25)
|
||||
|
||||
term := NewVT(4, 0)
|
||||
term.SetState(StateActive)
|
||||
term.AttachTo(cons)
|
||||
|
||||
// Fill last line except the last column which will trigger a
|
||||
// line feed. Cursor position will be automatically clipped to
|
||||
// the viewport bounds
|
||||
term.SetCursorPosition(1, term.viewportHeight+1)
|
||||
for i := uint16(0); i < term.viewportWidth-1; i++ {
|
||||
term.WriteByte(byte('0' + (i % 10)))
|
||||
}
|
||||
|
||||
// Emulate viewportHeight line feeds. The last one should cause a scroll
|
||||
term.SetCursorPosition(0, 0) // cursor is set to (1,1)
|
||||
for i := uint16(0); i < term.viewportHeight; i++ {
|
||||
term.lf(true)
|
||||
}
|
||||
|
||||
if cons.scrollUpCount != 1 {
|
||||
t.Fatalf("expected console to be scrolled up 1 time; got %d", cons.scrollUpCount)
|
||||
}
|
||||
|
||||
// Set cursor one line above the last; this line should now
|
||||
// contain the scrolled contents
|
||||
term.SetCursorPosition(1, term.viewportHeight-1)
|
||||
for col, offset := uint16(1), term.dataOffset; col <= term.viewportWidth; col, offset = col+1, offset+3 {
|
||||
expByte := byte('0' + ((col - 1) % 10))
|
||||
if col == term.viewportWidth {
|
||||
expByte = ' '
|
||||
}
|
||||
|
||||
if term.data[offset] != expByte {
|
||||
t.Errorf("expected char at (%d, %d) to be %q; got %q", col, term.viewportHeight-1, expByte, term.data[offset])
|
||||
}
|
||||
}
|
||||
|
||||
// Set cursor to the last line. This line should now be cleared
|
||||
term.SetCursorPosition(1, term.viewportHeight)
|
||||
for col, offset := uint16(1), term.dataOffset; col <= term.viewportWidth; col, offset = col+1, offset+3 {
|
||||
expByte := uint8(' ')
|
||||
if term.data[offset] != expByte {
|
||||
t.Errorf("expected char at (%d, %d) to be %q; got %q", col, term.viewportHeight, expByte, term.data[offset])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("viewport not at end of terminal", func(t *testing.T) {
|
||||
cons := newMockConsole(80, 25)
|
||||
|
||||
term := NewVT(4, 1)
|
||||
term.SetState(StateActive)
|
||||
term.AttachTo(cons)
|
||||
|
||||
// Fill last line except the last column which will trigger a
|
||||
// line feed. Cursor position will be automatically clipped to
|
||||
// the viewport bounds
|
||||
term.SetCursorPosition(1, term.viewportHeight+1)
|
||||
for i := uint16(0); i < term.viewportWidth-1; i++ {
|
||||
term.WriteByte(byte('0' + (i % 10)))
|
||||
}
|
||||
|
||||
// Fill first line including the last column
|
||||
term.SetCursorPosition(1, 1)
|
||||
for i := uint16(0); i < term.viewportWidth; i++ {
|
||||
term.WriteByte(byte('0' + (i % 10)))
|
||||
}
|
||||
|
||||
// Emulate viewportHeight line feeds. The last one should cause a scroll
|
||||
// in the console but only a viewport adjustment in the terminal
|
||||
term.SetCursorPosition(0, 0) // cursor is set to (1,1)
|
||||
for i := uint16(0); i < term.viewportHeight; i++ {
|
||||
term.lf(true)
|
||||
}
|
||||
|
||||
if cons.scrollUpCount != 1 {
|
||||
t.Fatalf("expected console to be scrolled up 1 time; got %d", cons.scrollUpCount)
|
||||
}
|
||||
|
||||
if expViewportY := uint16(1); term.viewportY != expViewportY {
|
||||
t.Fatalf("expected terminal viewportY to be adjusted to %d; got %d", expViewportY, term.viewportY)
|
||||
}
|
||||
|
||||
// Check that first line is still available in the terminal buffer
|
||||
// that is not currently visible
|
||||
term.SetCursorPosition(1, 1)
|
||||
offset := term.dataOffset - uint(term.viewportWidth*3)
|
||||
|
||||
for col := uint16(1); col <= term.viewportWidth; col, offset = col+1, offset+3 {
|
||||
expByte := byte('0' + ((col - 1) % 10))
|
||||
|
||||
if term.data[offset] != expByte {
|
||||
t.Errorf("expected char at hidden region (%d, -1) to be %q; got %q", col, expByte, term.data[offset])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVtAttach(t *testing.T) {
|
||||
cons := newMockConsole(80, 25)
|
||||
|
||||
term := NewVT(4, 1)
|
||||
|
||||
// AttachTo with a nil console should be a no-op
|
||||
term.AttachTo(nil)
|
||||
if term.termWidth != 0 || term.termHeight != 0 || term.viewportWidth != 0 || term.viewportHeight != 0 {
|
||||
t.Fatal("expected attaching a nil console to be a no-op")
|
||||
}
|
||||
|
||||
term.AttachTo(cons)
|
||||
if term.termWidth != cons.width ||
|
||||
term.termHeight != cons.height+term.scrollback ||
|
||||
term.viewportWidth != cons.width ||
|
||||
term.viewportHeight != cons.height ||
|
||||
term.data == nil {
|
||||
t.Fatal("expected the terminal to initialize using the attached console info")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVtSetState(t *testing.T) {
|
||||
cons := newMockConsole(80, 25)
|
||||
term := NewVT(4, 1)
|
||||
term.AttachTo(cons)
|
||||
|
||||
// Fill terminal viewport using a rotating pattern. Writing the last
|
||||
// character will cause a scroll operation moving the viewport down
|
||||
row := 0
|
||||
for index := 0; index < int(term.viewportWidth*term.viewportHeight); index++ {
|
||||
if index != 0 && index%int(term.viewportWidth) == 0 {
|
||||
row++
|
||||
}
|
||||
term.curFg = uint8((row + index + 1) % 10)
|
||||
term.curBg = uint8((row + index + 2) % 10)
|
||||
term.WriteByte(byte('0' + (row+index)%10))
|
||||
}
|
||||
|
||||
// Activating this terminal should trigger a copy of the terminal viewport
|
||||
// contents to the console.
|
||||
term.SetState(StateActive)
|
||||
row = 1
|
||||
for index := 0; index < len(cons.chars); index++ {
|
||||
if index != 0 && index%int(cons.width) == 0 {
|
||||
row++
|
||||
}
|
||||
|
||||
expCh := uint8('0' + (row+index)%10)
|
||||
expFg := uint8((row + index + 1) % 10)
|
||||
expBg := uint8((row + index + 2) % 10)
|
||||
|
||||
// last line should be cleared due to the scroll operation
|
||||
if row == int(cons.height) {
|
||||
expCh = ' '
|
||||
expFg = 7
|
||||
expBg = 0
|
||||
}
|
||||
|
||||
if cons.chars[index] != expCh {
|
||||
t.Errorf("expected console char at index %d to be %q; got %q", index, expCh, cons.chars[index])
|
||||
}
|
||||
|
||||
if cons.fgAttrs[index] != expFg {
|
||||
t.Errorf("expected console fg attr at index %d to be %d; got %d", index, expFg, cons.fgAttrs[index])
|
||||
}
|
||||
|
||||
if cons.bgAttrs[index] != expBg {
|
||||
t.Errorf("expected console bg attr at index %d to be %d; got %d", index, expBg, cons.bgAttrs[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVTDriverInterface(t *testing.T) {
|
||||
var dev device.Driver = NewVT(0, 0)
|
||||
|
||||
if err := dev.DriverInit(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if dev.DriverName() == "" {
|
||||
t.Fatal("DriverName() returned an empty string")
|
||||
}
|
||||
|
||||
if major, minor, patch := dev.DriverVersion(); major+minor+patch == 0 {
|
||||
t.Fatal("DriverVersion() returned an invalid version number")
|
||||
}
|
||||
}
|
||||
|
||||
type mockConsole struct {
|
||||
width, height uint16
|
||||
fg, bg uint8
|
||||
chars []uint8
|
||||
fgAttrs []uint8
|
||||
bgAttrs []uint8
|
||||
bytesWritten int
|
||||
scrollUpCount int
|
||||
scrollDownCount int
|
||||
}
|
||||
|
||||
func newMockConsole(w, h uint16) *mockConsole {
|
||||
return &mockConsole{
|
||||
width: w,
|
||||
height: h,
|
||||
fg: 7,
|
||||
bg: 0,
|
||||
chars: make([]uint8, w*h),
|
||||
fgAttrs: make([]uint8, w*h),
|
||||
bgAttrs: make([]uint8, w*h),
|
||||
}
|
||||
}
|
||||
|
||||
func (cons *mockConsole) Dimensions() (uint16, uint16) {
|
||||
return cons.width, cons.height
|
||||
}
|
||||
|
||||
func (cons *mockConsole) DefaultColors() (uint8, uint8) {
|
||||
return cons.fg, cons.bg
|
||||
}
|
||||
|
||||
func (cons *mockConsole) Fill(x, y, width, height uint16, fg, bg uint8) {
|
||||
yEnd := y + height - 1
|
||||
xEnd := x + width - 1
|
||||
|
||||
for fy := y; fy <= yEnd; fy++ {
|
||||
offset := ((fy - 1) * cons.width)
|
||||
for fx := x; fx <= xEnd; fx, offset = fx+1, offset+1 {
|
||||
cons.chars[offset] = ' '
|
||||
cons.fgAttrs[offset] = fg
|
||||
cons.bgAttrs[offset] = bg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cons *mockConsole) Scroll(dir console.ScrollDir, lines uint16) {
|
||||
switch dir {
|
||||
case console.ScrollDirUp:
|
||||
cons.scrollUpCount++
|
||||
case console.ScrollDirDown:
|
||||
cons.scrollDownCount++
|
||||
}
|
||||
}
|
||||
|
||||
func (cons *mockConsole) Palette() color.Palette {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cons *mockConsole) SetPaletteColor(index uint8, color color.RGBA) {
|
||||
}
|
||||
|
||||
func (cons *mockConsole) Write(b byte, fg, bg uint8, x, y uint16) {
|
||||
offset := ((y - 1) * cons.width) + (x - 1)
|
||||
cons.chars[offset] = b
|
||||
cons.fgAttrs[offset] = fg
|
||||
cons.bgAttrs[offset] = bg
|
||||
cons.bytesWritten++
|
||||
}
|
@ -1,128 +0,0 @@
|
||||
package tty
|
||||
|
||||
import "gopheros/kernel/driver/video/console"
|
||||
|
||||
const (
|
||||
defaultFg = console.LightGrey
|
||||
defaultBg = console.Black
|
||||
tabWidth = 4
|
||||
)
|
||||
|
||||
// Vt implements a simple terminal that can process LF and CR characters. The
|
||||
// terminal uses a console device for its output.
|
||||
type Vt struct {
|
||||
// Go interfaces will not work before we can get memory allocation working.
|
||||
// Till then we need to use concrete types instead.
|
||||
cons *console.Ega
|
||||
|
||||
width uint16
|
||||
height uint16
|
||||
|
||||
curX uint16
|
||||
curY uint16
|
||||
curAttr console.Attr
|
||||
}
|
||||
|
||||
// AttachTo links the terminal with the specified console device and updates
|
||||
// the terminal's dimensions to match the ones reported by the attached device.
|
||||
func (t *Vt) AttachTo(cons *console.Ega) {
|
||||
t.cons = cons
|
||||
t.width, t.height = cons.Dimensions()
|
||||
t.curX = 0
|
||||
t.curY = 0
|
||||
|
||||
// Default to lightgrey on black text.
|
||||
t.curAttr = makeAttr(defaultFg, defaultBg)
|
||||
}
|
||||
|
||||
// Clear clears the terminal.
|
||||
func (t *Vt) Clear() {
|
||||
t.clear()
|
||||
}
|
||||
|
||||
// Position returns the current cursor position (x, y).
|
||||
func (t *Vt) Position() (uint16, uint16) {
|
||||
return t.curX, t.curY
|
||||
}
|
||||
|
||||
// SetPosition sets the current cursor position to (x,y).
|
||||
func (t *Vt) SetPosition(x, y uint16) {
|
||||
if x >= t.width {
|
||||
x = t.width - 1
|
||||
}
|
||||
|
||||
if y >= t.height {
|
||||
y = t.height - 1
|
||||
}
|
||||
|
||||
t.curX, t.curY = x, y
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (t *Vt) Write(data []byte) (int, error) {
|
||||
for _, b := range data {
|
||||
t.WriteByte(b)
|
||||
}
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
// WriteByte implements io.ByteWriter.
|
||||
func (t *Vt) WriteByte(b byte) error {
|
||||
switch b {
|
||||
case '\r':
|
||||
t.cr()
|
||||
case '\n':
|
||||
t.cr()
|
||||
t.lf()
|
||||
case '\b':
|
||||
if t.curX > 0 {
|
||||
t.cons.Write(' ', t.curAttr, t.curX, t.curY)
|
||||
t.curX--
|
||||
}
|
||||
case '\t':
|
||||
for i := 0; i < tabWidth; i++ {
|
||||
t.cons.Write(' ', t.curAttr, t.curX, t.curY)
|
||||
t.curX++
|
||||
if t.curX == t.width {
|
||||
t.cr()
|
||||
t.lf()
|
||||
}
|
||||
}
|
||||
default:
|
||||
t.cons.Write(b, t.curAttr, t.curX, t.curY)
|
||||
t.curX++
|
||||
if t.curX == t.width {
|
||||
t.cr()
|
||||
t.lf()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cls clears the terminal.
|
||||
func (t *Vt) clear() {
|
||||
t.cons.Clear(0, 0, t.width, t.height)
|
||||
}
|
||||
|
||||
// cr resets the x coordinate of the terminal cursor to 0.
|
||||
func (t *Vt) cr() {
|
||||
t.curX = 0
|
||||
}
|
||||
|
||||
// lf advances the y coordinate of the terminal cursor by one line scrolling
|
||||
// the terminal contents if the end of the last terminal line is reached.
|
||||
func (t *Vt) lf() {
|
||||
if t.curY+1 < t.height {
|
||||
t.curY++
|
||||
return
|
||||
}
|
||||
|
||||
t.cons.Scroll(console.Up, 1)
|
||||
t.cons.Clear(0, t.height-1, t.width, 1)
|
||||
}
|
||||
|
||||
func makeAttr(fg, bg console.Attr) console.Attr {
|
||||
return (bg << 4) | (fg & 0xF)
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
package tty
|
||||
|
||||
import (
|
||||
"gopheros/kernel/driver/video/console"
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func TestVtPosition(t *testing.T) {
|
||||
specs := []struct {
|
||||
inX, inY uint16
|
||||
expX, expY uint16
|
||||
}{
|
||||
{20, 20, 20, 20},
|
||||
{100, 20, 79, 20},
|
||||
{10, 200, 10, 24},
|
||||
{10, 200, 10, 24},
|
||||
{100, 100, 79, 24},
|
||||
}
|
||||
|
||||
fb := make([]uint16, 80*25)
|
||||
var cons console.Ega
|
||||
cons.Init(80, 25, uintptr(unsafe.Pointer(&fb[0])))
|
||||
|
||||
var vt Vt
|
||||
vt.AttachTo(&cons)
|
||||
|
||||
for specIndex, spec := range specs {
|
||||
vt.SetPosition(spec.inX, spec.inY)
|
||||
if x, y := vt.Position(); x != spec.expX || y != spec.expY {
|
||||
t.Errorf("[spec %d] expected setting position to (%d, %d) to update the position to (%d, %d); got (%d, %d)", specIndex, spec.inX, spec.inY, spec.expX, spec.expY, x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
fb := make([]uint16, 80*25)
|
||||
var cons console.Ega
|
||||
cons.Init(80, 25, uintptr(unsafe.Pointer(&fb[0])))
|
||||
|
||||
var vt Vt
|
||||
vt.AttachTo(&cons)
|
||||
|
||||
vt.Clear()
|
||||
vt.SetPosition(0, 1)
|
||||
vt.Write([]byte("12\n\t3\n4\r567\b8"))
|
||||
|
||||
// Tab spanning rows
|
||||
vt.SetPosition(78, 4)
|
||||
vt.WriteByte('\t')
|
||||
vt.WriteByte('9')
|
||||
|
||||
// Trigger scroll
|
||||
vt.SetPosition(79, 24)
|
||||
vt.Write([]byte{'!'})
|
||||
|
||||
specs := []struct {
|
||||
x, y uint16
|
||||
expChar byte
|
||||
}{
|
||||
{0, 0, '1'},
|
||||
{1, 0, '2'},
|
||||
// tabs
|
||||
{0, 1, ' '},
|
||||
{1, 1, ' '},
|
||||
{2, 1, ' '},
|
||||
{3, 1, ' '},
|
||||
{4, 1, '3'},
|
||||
// tab spanning 2 rows
|
||||
{78, 3, ' '},
|
||||
{79, 3, ' '},
|
||||
{0, 4, ' '},
|
||||
{1, 4, ' '},
|
||||
{2, 4, '9'},
|
||||
//
|
||||
{0, 2, '5'},
|
||||
{1, 2, '6'},
|
||||
{2, 2, '8'}, // overwritten by BS
|
||||
{79, 23, '!'},
|
||||
}
|
||||
|
||||
for specIndex, spec := range specs {
|
||||
ch := (byte)(fb[(spec.y*vt.width)+spec.x] & 0xFF)
|
||||
if ch != spec.expChar {
|
||||
t.Errorf("[spec %d] expected char at (%d, %d) to be %c; got %c", specIndex, spec.x, spec.y, spec.expChar, ch)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user