diff --git a/kernel/driver/tty/tty.go b/kernel/driver/tty/tty.go new file mode 100644 index 0000000..2f74a7c --- /dev/null +++ b/kernel/driver/tty/tty.go @@ -0,0 +1,18 @@ +package tty + +import "io" + +// Tty is implemented by objects that can register themselves as ttys. +type Tty interface { + io.Writer + + // Position returns the current cursor position (x, y). + Position() (uint16, uint16) + + // SetPosition sets the current cursor position to (x,y). Console implementations + // must clip the provided cursor position if it exceeds the console dimensions. + SetPosition(x, y uint16) + + // Clear clears the terminal. + Clear() +} diff --git a/kernel/driver/tty/vt.go b/kernel/driver/tty/vt.go new file mode 100644 index 0000000..20afa6e --- /dev/null +++ b/kernel/driver/tty/vt.go @@ -0,0 +1,118 @@ +package tty + +import "github.com/achilleasa/gopher-os/kernel/driver/video/console" + +const ( + defaultFg = console.LightGrey + defaultBg = console.Black +) + +// 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.Vga + + width uint16 + height uint16 + + curX uint16 + curY uint16 + curAttr console.Attr +} + +func (t *Vt) Init(cons *console.Vga) { + 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.cons.Lock() + defer t.cons.Unlock() + + t.clear() +} + +// Position returns the current cursor position (x, y). +func (t *Vt) Position() (uint16, uint16) { + t.cons.Lock() + defer t.cons.Unlock() + + return t.curX, t.curY +} + +// SetPosition sets the current cursor position to (x,y). +func (t *Vt) SetPosition(x, y uint16) { + t.cons.Lock() + defer t.cons.Unlock() + + 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) { + t.cons.Lock() + defer t.cons.Unlock() + + attr := t.curAttr + for _, b := range data { + switch b { + case '\r': + t.cr() + case '\n': + t.cr() + t.lf() + default: + t.cons.Write(b, attr, t.curX, t.curY) + t.curX++ + if t.curX == t.width { + t.lf() + } + } + } + + return len(data), 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) + return +} + +func makeAttr(fg, bg console.Attr) console.Attr { + return (bg << 4) | (fg & 0xF) +} diff --git a/kernel/driver/tty/vt_test.go b/kernel/driver/tty/vt_test.go new file mode 100644 index 0000000..9d6f4f9 --- /dev/null +++ b/kernel/driver/tty/vt_test.go @@ -0,0 +1,70 @@ +package tty + +import ( + "testing" + + "github.com/achilleasa/gopher-os/kernel/driver/video/console" +) + +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}, + } + + var cons console.Vga + cons.Init() + + var vt Vt + vt.Init(&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) + cons := &console.Vga{} + cons.OverrideFb(fb) + cons.Init() + + var vt Vt + vt.Init(cons) + + vt.Clear() + vt.SetPosition(0, 1) + vt.Write([]byte("12\n3\n4\r56")) + + // Trigger scroll + vt.SetPosition(79, 24) + vt.Write([]byte{'!'}) + + specs := []struct { + x, y uint16 + expChar byte + }{ + {0, 0, '1'}, + {1, 0, '2'}, + {0, 1, '3'}, + {0, 2, '5'}, + {1, 2, '6'}, + {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) + } + } +} diff --git a/kernel/driver/video/console/vga.go b/kernel/driver/video/console/vga.go index f562b8b..6eab30d 100644 --- a/kernel/driver/video/console/vga.go +++ b/kernel/driver/video/console/vga.go @@ -42,6 +42,13 @@ func (cons *Vga) Init() { })) } +// OverrideFb overrides the console framebuffer slice with the supplied slice. +// This is a temporary function used by tests that will be removed once we can work +// with interfaces. +func (cons *Vga) OverrideFb(fb []uint16) { + cons.fb = fb +} + // Clear clears the specified rectangular region func (cons *Vga) Clear(x, y, width, height uint16) { var ( diff --git a/kernel/driver/video/console/vga_test.go b/kernel/driver/video/console/vga_test.go index 06bda9b..918acff 100644 --- a/kernel/driver/video/console/vga_test.go +++ b/kernel/driver/video/console/vga_test.go @@ -202,3 +202,15 @@ func TestVgaWrite(t *testing.T) { t.Errorf("expected call to Write() to set fb[0] to %d; got %d", expVal, got) } } + +func TestVgaOverrideFb(t *testing.T) { + var cons = Vga{} + cons.Init() + + fb := []uint16{} + cons.OverrideFb(fb) + + if len(cons.fb) != len(fb) { + t.Fatalf("expected calling OverrideFb to change the framebuffer for the console") + } +}