From 616fc6a4120f585946b13fe35089ebc958cc70e2 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Sun, 26 Mar 2017 21:35:17 +0100 Subject: [PATCH] Implement simple terminal The terminal uses console.Vga as its output device. A proper terminal implementation would be using a console.Console interface as its output. However, at this point we cannot use Go interfaces as the fn pointers in the itables have not been yet initialized. The Go runtime bits that set up the itables need access to a memory allocator, a facility which is not yet provided by the kernel. --- kernel/driver/tty/vt.go | 118 ++++++++++++++++++++++++ kernel/driver/tty/vt_test.go | 70 ++++++++++++++ kernel/driver/video/console/vga.go | 7 ++ kernel/driver/video/console/vga_test.go | 12 +++ 4 files changed, 207 insertions(+) create mode 100644 kernel/driver/tty/vt.go create mode 100644 kernel/driver/tty/vt_test.go 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") + } +}