diff --git a/src/gopheros/device/tty/vt.go b/src/gopheros/device/tty/vt.go new file mode 100644 index 0000000..ea63238 --- /dev/null +++ b/src/gopheros/device/tty/vt.go @@ -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 } diff --git a/src/gopheros/device/tty/vt_test.go b/src/gopheros/device/tty/vt_test.go new file mode 100644 index 0000000..678acba --- /dev/null +++ b/src/gopheros/device/tty/vt_test.go @@ -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++ +} diff --git a/src/gopheros/kernel/driver/tty/vt.go b/src/gopheros/kernel/driver/tty/vt.go deleted file mode 100644 index f18cef2..0000000 --- a/src/gopheros/kernel/driver/tty/vt.go +++ /dev/null @@ -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) -} diff --git a/src/gopheros/kernel/driver/tty/vt_test.go b/src/gopheros/kernel/driver/tty/vt_test.go deleted file mode 100644 index 4648266..0000000 --- a/src/gopheros/kernel/driver/tty/vt_test.go +++ /dev/null @@ -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) - } - } -}