Skip to content

Commit

Permalink
vm: move some of the terminal handling code from the vm to the client
Browse files Browse the repository at this point in the history
  • Loading branch information
db47h committed Aug 8, 2016
1 parent 30c7d66 commit bbc6572
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 94 deletions.
8 changes: 2 additions & 6 deletions cmd/retro/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,12 @@ func main() {

// buffer input if not raw tty
if rawtty {
opts = append(opts, vm.Input(os.Stdin), vm.Output(os.Stdout, true))
opts = append(opts, vm.Input(os.Stdin), vm.Output(os.Stdout, nil, consoleSize(os.Stdout), true))
} else {
output := bufio.NewWriter(os.Stdout)
opts = append(opts,
vm.Input(bufio.NewReader(os.Stdin)),
vm.Output(output, false),
vm.BindOutHandler(3, func(v, port vm.Cell) error {
output.Flush()
return nil
}))
vm.Output(output, output.Flush, consoleSize(os.Stdout), false))
}

// append withFile to the input stack
Expand Down
25 changes: 25 additions & 0 deletions cmd/retro/term.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
package main

import (
"os"
"syscall"
"unsafe"

"github.com/pkg/term/termios"
)
Expand Down Expand Up @@ -49,3 +51,26 @@ func setRawIO() (func(), error) {
termios.Tcsetattr(0, termios.TCSANOW, &tios)
}, nil
}

type winsize struct {
row, col, xpixel, ypixel uint16
}

func ioctl(fd uintptr, request, argp uintptr) (err error) {
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, request, argp)
if errno != 0 {
err = errno
}
return err
}

func consoleSize(f *os.File) func() (int, int) {
return func() (int, int) {
var w winsize
err := ioctl(f.Fd(), syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&w)))
if err != nil {
return 0, 0
}
return int(w.col), int(w.row)
}
}
4 changes: 4 additions & 0 deletions cmd/retro/term_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@ import "errors"
func setRawIO() (func(), error) {
return nil, errors.New("raw IO not supported")
}

func consoleSize() (int, int) {
return 0, 0
}
2 changes: 1 addition & 1 deletion vm/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func (i *Instance) Run() (err error) {
return err
}
} else {
i.Ports[port] = v
i.Out(v, port)
}
i.PC++
case OpWait:
Expand Down
32 changes: 15 additions & 17 deletions vm/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package vm_test

import (
"bufio"
"bytes"
"fmt"
"io"
Expand All @@ -43,7 +42,7 @@ func ExampleInstance_Run() {
i, err := vm.New(img, imageFile,
vm.Input(os.Stdin),
vm.Input(strings.NewReader("\"testdata/core.rx\" :include\n")),
vm.Output(output, false))
vm.Output(output, nil, nil, false))

// run it
if err == nil {
Expand All @@ -67,38 +66,37 @@ func ExampleInstance_Run() {

// Shows a common use of OUT port handlers.
func ExampleBindOutHandler() {
var i *vm.Instance
imageFile := "testdata/retroImage"
img, err := vm.Load(imageFile, 0)
if err != nil {
panic(err)
}

// we will use a buffered output
output := bufio.NewWriter(os.Stdout)
// so according to the spec, we should flush the output as soon as port 3
// is written to:
// Our out handler, will just take the written value, and return its square.
// The result will be stored in the bound port, so we can read it back with
// IN.
outputHandler := func(v, port vm.Cell) error {
// flush output
output.Flush()
i.Ports[port] = v * v
return nil
}
// create the VM instance with our port handler bound to port 3.
// note that we do net wire any input, we just want to see the prompt and
// exit.
i, err := vm.New(img, imageFile,
vm.Output(output, false),
vm.BindOutHandler(3, outputHandler))
// Create the VM instance with our port handler bound to port 42.
// We do not wire any output, we'll just read the results from the stack.
i, err = vm.New(img, imageFile,
vm.Input(strings.NewReader(": square 42 out 42 in ; 7 square bye\n")),
vm.BindOutHandler(42, outputHandler))
if err != nil {
panic(err)
}

if err = i.Run(); err != nil && err != io.EOF {
fmt.Fprintf(os.Stderr, "%v\n", err)
}

fmt.Println(i.Data())

// Output:
// Retro 11.7.1
//
// ok
// [49]
}

// A simple WAIT handler that overrides the default implementation. It's used
Expand Down
43 changes: 27 additions & 16 deletions vm/io.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ import (
"fmt"
"io"
"os"
"syscall"
"time"
"unicode/utf8"
"unsafe"
)

type winsize struct {
row, col, xpixel, ypixel uint16
// output wraps the output device and maps some of its capabilities
type output struct {
runeWriter
flush func() error
consoleSize func() (with int, height int)
rawtty bool
}

// PushInput sets r as the current input RuneReader for the VM. When this reader
Expand All @@ -53,6 +56,10 @@ func (i *Instance) In(port Cell) error {

// Out is the default OUT handler for all ports.
func (i *Instance) Out(v, port Cell) error {
if port == 3 && i.output != nil && i.output.flush != nil {
i.output.flush()
return nil
}
i.Ports[port] = v
return nil
}
Expand All @@ -79,7 +86,7 @@ func (i *Instance) Wait(v, port Cell) error {
}
r, size, err := i.input.ReadRune()
if size > 0 {
if i.tty && r == 4 { // CTRL-D
if i.output != nil && i.output.rawtty && r == 4 { // CTRL-D
return io.EOF
}
i.WaitReply(Cell(r), 1)
Expand All @@ -95,12 +102,12 @@ func (i *Instance) Wait(v, port Cell) error {
r := rune(i.Pop())
if i.output != nil {
var err error
if r < 0 && i.tty {
if r < 0 {
_, err = io.WriteString(i.output, "\033[2J\033[1;1H")
} else {
_, err = i.output.WriteRune(r)
// Erase last char if backspace
if r == 8 && err == nil && i.tty {
if r == 8 && err == nil && i.output.rawtty {
_, err = i.output.Write([]byte{32, 8})
}
}
Expand Down Expand Up @@ -157,26 +164,30 @@ func (i *Instance) Wait(v, port Cell) error {
i.Ports[5] = 0
case -11:
// console width
var w winsize
err := ioctl(i.output, syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&w)))
if err != nil {
if i.output != nil && i.output.consoleSize != nil {
w, _ := i.output.consoleSize()
i.Ports[5] = Cell(w)
} else {
i.Ports[5] = 0
}
i.Ports[5] = Cell(w.col)
case -12:
// console height
var w winsize
err := ioctl(i.output, syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&w)))
if err != nil {
if i.output != nil && i.output.consoleSize != nil {
_, h := i.output.consoleSize()
i.Ports[5] = Cell(h)
} else {
i.Ports[5] = 0
}
i.Ports[5] = Cell(w.row)
case -13:
i.Ports[5] = Cell(unsafe.Sizeof(Cell(0)) * 8)
// -14: endianness
case -15:
// port 8 enabled
i.Ports[5] = -1
if i.output != nil {
i.Ports[5] = -1
} else {
i.Ports[5] = 0
}
case -16:
i.Ports[5] = Cell(len(i.data))
case -17:
Expand All @@ -187,7 +198,7 @@ func (i *Instance) Wait(v, port Cell) error {
i.Ports[0] = 1
}
case 8:
if v := i.Ports[8]; v != 0 {
if v := i.Ports[8]; v != 0 && i.output != nil {
switch i.Ports[8] {
case 1:
fmt.Fprintf(i.output, "\033[%d;%dH", i.data[i.sp-1], i.data[i.sp])
Expand Down
47 changes: 1 addition & 46 deletions vm/io_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,9 @@ package vm

import (
"io"
"syscall"
"unicode/utf8"
)

// fder wraps the Fd method
type fder interface {
Fd() uintptr
}

// readWriter wraps the WriteRune method. Works the same ad bufio.Writer.WriteRune.
type runeWriter interface {
io.Writer
Expand All @@ -38,28 +32,10 @@ type runeWriterWrapper struct {
io.Writer
}

// UnwrapFd checks if the wrapped io.Writer implements fder and returns its Fd
// method or nil.
func (w *runeWriterWrapper) UnwrapFd() func() uintptr {
if f, ok := w.Writer.(fder); ok {
return f.Fd
}
return nil
}

func (w *runeWriterWrapper) WriteByte(c byte) (err error) {
_, err = w.Writer.Write([]byte{c})
return
}

func (w *runeWriterWrapper) WriteRune(r rune) (size int, err error) {
b := [utf8.UTFMax]byte{}
if r < utf8.RuneSelf {
err = w.WriteByte(byte(r))
if err != nil {
return 0, err
}
return 1, nil
return w.Write([]byte{byte(r)})
}
l := utf8.EncodeRune(b[:], r)
return w.Writer.Write(b[0:l])
Expand Down Expand Up @@ -146,24 +122,3 @@ func (mr *multiRuneReader) ReadRune() (r rune, size int, err error) {
func (mr *multiRuneReader) pushReader(r io.Reader) {
mr.readers = append([]io.RuneReader{newRuneReader(r)}, mr.readers...)
}

// ioctl to control i.output
// TODO: Need to test ioctl on windows
func ioctl(w runeWriter, request, argp uintptr) (err error) {
var fd func() uintptr
switch o := w.(type) {
case *runeWriterWrapper:
fd = o.UnwrapFd()
case fder:
fd = o.Fd
default:
err = syscall.Errno(syscall.EBADF)
}
if fd != nil {
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd(), request, argp)
if errno != 0 {
err = errno
}
}
return err
}
22 changes: 14 additions & 8 deletions vm/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,20 @@ func Input(r io.Reader) Option {
return func(i *Instance) error { i.PushInput(r); return nil }
}

// Output sets the output Writer. If the isatty flag is set to true, the output
// will be treated as a raw terminal and special handling of some control
// characters will apply. This will also enable the extended terminal support.
func Output(w io.Writer, isatty bool) Option {
// Output configures the output Writer.
//
// If non-nil, the flush function should write any buffered output data to the
// underlying Writer.
//
// If non-nil, the consoleSize function should return the width and height of the
// console window.
//
// If the rawtty flag is set to true, the output will be treated as a raw
// terminal and special handling of some control characters will apply
// (backspace and CTRL-D).
func Output(w io.Writer, flush func() error, consoleSize func() (int, int), rawtty bool) Option {
return func(i *Instance) error {
i.tty = isatty
i.output = newWriter(w)
i.output = &output{newWriter(w), flush, consoleSize, rawtty}
return nil
}
}
Expand Down Expand Up @@ -154,8 +161,7 @@ type Instance struct {
imageFile string
shrink bool
input io.RuneReader
output runeWriter
tty bool
output *output
}

// New creates a new Ngaro Virtual Machine instance.
Expand Down

0 comments on commit bbc6572

Please sign in to comment.