Bläddra i källkod

Tests passing, no race.

This added cursor and mouse support.
TODO:  unicode input detection.
Steve Thielemann 2 år sedan
förälder
incheckning
67b0d014cb
12 ändrade filer med 1033 tillägg och 292 borttagningar
  1. 130 17
      door/door.go
  2. 2 1
      door/door_linux.go
  3. 4 0
      door/door_test.go
  4. 48 0
      door/extended_string.go
  5. 4 4
      door/fifobuffer.go
  6. 375 105
      door/input.go
  7. 117 38
      door/input_linux.go
  8. 227 79
      door/input_test.go
  9. 44 37
      door/menu.go
  10. 16 0
      door/utilities.go
  11. 25 2
      door/write.go
  12. 41 9
      door/write_linux.go

+ 130 - 17
door/door.go

@@ -28,7 +28,6 @@ import (
 	"path/filepath"
 	"runtime/debug"
 	"strconv"
-	"strings"
 	"sync"
 	"time"
 )
@@ -41,6 +40,19 @@ const HideCursor = "\x1b[?25l"        // Hide Cursor
 const ShowCursor = "\x1b[?25h"        // Show Cursor
 var Reset string = Color(0)           // ANSI Color Reset
 
+const CURSOR_POS = "\x1b[6n"
+
+const SAVE_POS = "\x1b[s"
+const RESTORE_POS = "\x1b[u"
+
+// Mouse Clicks (On Click)
+const MOUSE_X10 = "\x1b[?9h"
+const MOUSE_X10_OFF = "\x1b[?9l"
+
+// Mouse Drags (Up/Down events)
+const MOUSE_DRAG = "\x1b[?1000h"
+const MOUSE_DRAG_OFF = "\x1b[?1000l"
+
 var Unicode bool           // Unicode support detected
 var CP437 bool             // CP437 support detected
 var Full_CP437 bool        // Full CP437 support detected (handles control codes properly)
@@ -93,16 +105,19 @@ type Door struct {
 	Config         DropfileConfig
 	READFD         int
 	WRITEFD        int
-	Disconnected   bool        // int32         // atomic bool      // Has User disconnected/Hung up?
-	TimeOut        time.Time   // Fixed point in time, when time expires
-	StartTime      time.Time   // Time when User started door
-	Pushback       FIFOBuffer  // Key buffer
-	LastColor      []int       // Track the last color sent for restore color
-	ReaderClosed   bool        // Reader close
-	readerChannel  chan byte   // Reading from the User
-	ReaderCanClose bool        // We can close the reader (in tests)
-	WriterClosed   bool        // Writer closed
-	writerChannel  chan string // Writing to the User
+	Disconnected   bool          // int32         // atomic bool      // Has User disconnected/Hung up?
+	TimeOut        time.Time     // Fixed point in time, when time expires
+	StartTime      time.Time     // Time when User started door
+	Pushback       FIFOBuffer    // Key buffer
+	LastColor      []int         // Track the last color sent for restore color
+	ReaderClosed   bool          // Reader close
+	readerChannel  chan rune     // Reading from the User
+	readerMutex    sync.Mutex    // Reader close mutex
+	readerFile     *os.File      // fd to File
+	runereader     *bufio.Reader // Rune Reader
+	ReaderCanClose bool          // We can close the reader (in tests)
+	WriterClosed   bool          // Writer closed
+	writerChannel  chan string   // Writing to the User
 	writerMutex    sync.RWMutex
 	LastMouse      []Mouse     // Store Mouse information
 	LastCursor     []CursorPos // Store Cursor pos information
@@ -228,10 +243,104 @@ func (d *Door) ReadDropfile(filename string) {
 	d.TimeOut = time.Now().Add(time.Duration(d.Config.Time_left) * time.Minute)
 }
 
+func (d *Door) Detect() bool {
+	// detect is destructive ... make it non-destructive
+	// destructive: clears/trashes the screen.
+	var detect string = "\r\x03\x04" + CURSOR_POS + "\b \b\b \b" +
+		"\r\u2615" + CURSOR_POS + "\b \b\b \b\b \b" +
+		SAVE_POS + "\x1b[999C\x1b[999B" + CURSOR_POS + RESTORE_POS
+	// hot beverage is 3 bytes long -- need 3 "\b \b" to erase.
+
+	d.Write(detect)
+
+	var info []CursorPos = make([]CursorPos, 0, 3)
+	var done bool
+
+	for !done {
+		_, ex, err := d.WaitKey(time.Second)
+		log.Println("WaitKey:", ex, err)
+
+		if ex == CURSOR {
+			cursor, ok := d.GetCursorPos()
+			if ok {
+				info = append(info, cursor)
+				if len(info) == 3 {
+					done = true
+				}
+			}
+		}
+		if err != nil {
+			done = true
+		}
+	}
+
+	if len(info) != 3 {
+		// Detection FAILED.
+		log.Println("Detect FAILED:", info)
+		return false
+	}
+
+	// Ok!  Let's see what we've got...
+	var valid bool
+
+	// Where did I get these numbers from?
+
+	// linux term (telnet/ssh) [0].X = 1, [1].X = 3
+	// VS Code terminal: 1, 3
+	// https://docs.python.org/3/library/unicodedata.html
+	// \u2615 is a fullwidth (2 char) unicode symbol!
+	// SO, [1].X == 3 // and not 2.
+
+	// syncterm [0].X = 3, [1].X = 4
+	// Magiterm [0].X = 3, [1].X = 4
+	// cp437 + telnet [0].X = 1, [1].X = 4  FullCP437 = False
+	// ^ Fails FullCP437 test - characters codes ignored.
+	// cp437plus + telnet 3, 4.  (Has FullCP437 support.)
+
+	// if (info[0].X == 1 || info[0].X == 3) &&
+	//	(info[1].X == 2 || info[1].X == 3) {
+
+	// Breakdown by detected type:
+	// Unicode \x03 \x04 (control codes ignored)
+	// Unicode \u2615 = fullwidth takes up 2 characters
+
+	// CP437 \x03 \x04 Hearts Diamonds Symbols 2 characters
+	// ^ Only works for FullCP437
+
+	// CP437 \u2615 = b'\xe2\x98\x95' 3 bytes
+	// So info[1].X = 4
+
+	if info[0].X == 1 && info[1].X == 3 {
+		Unicode = true
+		valid = true
+	} else {
+		// info[1].X = 4
+		Unicode = false
+		CP437 = true
+		valid = true
+	}
+	if info[0].X == 3 {
+		Full_CP437 = true
+	}
+
+	if !valid {
+		log.Println("Detect FAILED (not valid):", info)
+		return false
+	}
+	Width, Height = info[2].X, info[2].Y
+
+	// Putty doesn't seem to restoring the cursor position (when detecting).
+	d.Write(Goto(1, info[0].Y))
+	log.Printf("Unicode: %t, CP437: %t, Full %t, Screen %v\n", Unicode, CP437, Full_CP437, info[2])
+
+	return true
+}
+
 // Detect client terminal capabilities, Unicode, CP437, Full_CP437,
 // screen Height and Width.
 // Full_CP437 means the terminal understands control codes as CP437.
-func (d *Door) detect() {
+/*
+func (d *Door) old_detect() {
 	d.Write("\x1b[0;30;40m\x1b[2J\x1b[H")                          // black on black, clrscr, go home
 	d.Write("\x03\x04\x1b[6n")                                     // hearts and diamonds does CP437 work?
 	d.Write(CRNL + "\u2615\x1b[6n")                                // hot beverage
@@ -299,6 +408,7 @@ func (d *Door) detect() {
 	}
 	log.Printf("Unicode %v Screen: %d X %d\n", Unicode, Width, Height)
 }
+*/
 
 // Initialize door framework.  Parse commandline, read dropfile,
 // detect terminal capabilities.
@@ -335,7 +445,7 @@ func (d *Door) Init(doorname string) {
 	log.Printf("Loading dropfile %s\n", dropfile)
 	log.Printf("BBS %s, User %s / Handle %s / File %d\n", d.Config.BBSID, d.Config.Real_name, d.Config.Handle, d.Config.Comm_handle)
 
-	d.readerChannel = make(chan byte, 8)
+	d.readerChannel = make(chan rune, 8)
 	d.writerChannel = make(chan string) // unbuffered
 
 	// changing this to unbound/sync hangs tests.
@@ -343,7 +453,7 @@ func (d *Door) Init(doorname string) {
 
 	d.setupChannels()
 
-	d.detect()
+	d.Detect()
 	if Unicode {
 		BOXES = BOXES_UNICODE
 		BARS = BARS_UNICODE
@@ -364,9 +474,12 @@ func (d *Door) Close() {
 
 	log.Println("Closing...")
 	// d.closeChannel <- struct{}{}
-	if !d.WriterClosed {
-		d.writerChannel <- ""
-	}
+	close(d.writerChannel)
+	/*
+		if !d.WriterClosed {
+			d.writerChannel <- ""
+		}
+	*/
 
 	// CloseReader(d.Config.Comm_handle)
 

+ 2 - 1
door/door_linux.go

@@ -8,6 +8,7 @@ func (d *Door) setupChannels() {
 		// Default:  Only wait for the Writer to stop
 		d.wg.Add(1)
 	}
-	go Reader(d.Config.Comm_handle, d)
+
+	go Reader(d)
 	go Writer(d)
 }

+ 4 - 0
door/door_test.go

@@ -163,6 +163,7 @@ func TestDetectFail(t *testing.T) {
 	}
 
 	// Remember to clean up the file afterwards
+
 	defer os.Remove(tmpFile.Name())
 
 	// establish network socket connection to set Comm_handle
@@ -208,6 +209,7 @@ func TestDetectFail(t *testing.T) {
 	os.Args = []string{"door", "-d", tmpFile.Name()}
 	d.Init("menu-test")
 
+	t.Logf("Init completed.")
 	defer d.Close()
 	// clean up log file
 	// I don't need to.  Tests are run in /tmp/go-buildNNNN.
@@ -221,8 +223,10 @@ func TestDetectFail(t *testing.T) {
 		t.Errorf("Expected 0, 0, got Width %d, Height %d", Width, Height)
 	}
 
+	t.Logf("Closing server and client...")
 	server.Close()
 	client.Close()
 
 	// time.Sleep(time.Duration(1) * time.Second)
+	t.Logf("Done.")
 }

+ 48 - 0
door/extended_string.go

@@ -0,0 +1,48 @@
+// Code generated by "stringer -type=Extended"; DO NOT EDIT.
+
+package door
+
+import "strconv"
+
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[NOP-0]
+	_ = x[UP_ARROW-1]
+	_ = x[DOWN_ARROW-2]
+	_ = x[RIGHT_ARROW-3]
+	_ = x[LEFT_ARROW-4]
+	_ = x[HOME-5]
+	_ = x[END-6]
+	_ = x[PAGE_UP-7]
+	_ = x[PAGE_DOWN-8]
+	_ = x[INSERT-9]
+	_ = x[DELETE-10]
+	_ = x[F1-11]
+	_ = x[F2-12]
+	_ = x[F3-13]
+	_ = x[F4-14]
+	_ = x[F5-15]
+	_ = x[F6-16]
+	_ = x[F7-17]
+	_ = x[F8-18]
+	_ = x[F9-19]
+	_ = x[F10-20]
+	_ = x[F11-21]
+	_ = x[F12-22]
+	_ = x[MOUSE-23]
+	_ = x[CURSOR-24]
+	_ = x[UNKNOWN-25]
+}
+
+const _Extended_name = "NOPUP_ARROWDOWN_ARROWRIGHT_ARROWLEFT_ARROWHOMEENDPAGE_UPPAGE_DOWNINSERTDELETEF1F2F3F4F5F6F7F8F9F10F11F12MOUSECURSORUNKNOWN"
+
+var _Extended_index = [...]uint8{0, 3, 11, 21, 32, 42, 46, 49, 56, 65, 71, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 98, 101, 104, 109, 115, 122}
+
+func (i Extended) String() string {
+	if i < 0 || i >= Extended(len(_Extended_index)-1) {
+		return "Extended(" + strconv.FormatInt(int64(i), 10) + ")"
+	}
+	return _Extended_name[_Extended_index[i]:_Extended_index[i+1]]
+}

+ 4 - 4
door/fifobuffer.go

@@ -3,19 +3,19 @@ package door
 import "log"
 
 type FIFOBuffer struct {
-	data  []int
+	data  []rune
 	index int
 }
 
 func NewFIFOBuffer(maxsize int) FIFOBuffer {
-	return FIFOBuffer{data: make([]int, maxsize)}
+	return FIFOBuffer{data: make([]rune, maxsize)}
 }
 
 func (f *FIFOBuffer) Empty() bool {
 	return f.index == 0
 }
 
-func (f *FIFOBuffer) Push(value int) {
+func (f *FIFOBuffer) Push(value rune) {
 	if f.index >= len(f.data) {
 		log.Panicf("Exceeded FIFOBuffer index %d size %d %#v", f.index, len(f.data), f.data)
 	}
@@ -23,7 +23,7 @@ func (f *FIFOBuffer) Push(value int) {
 	f.index++
 }
 
-func (f *FIFOBuffer) Pop() int {
+func (f *FIFOBuffer) Pop() rune {
 	if f.index == 0 {
 		log.Panicf("Pop from empty FIFOBuffer (size %d).", len(f.data))
 	}

+ 375 - 105
door/input.go

@@ -1,6 +1,7 @@
 package door
 
 import (
+	"fmt"
 	"log"
 	"strconv"
 	"strings"
@@ -8,7 +9,6 @@ import (
 	"unicode"
 )
 
-// This is neat, would be neater if it WORKED! go:generate stringer -type=Extended
 type Extended int8
 
 const (
@@ -40,76 +40,300 @@ const (
 	UNKNOWN
 )
 
-// To rebuild this, copy the above into it's own file and generate.
-// Then, copy the generated code to below:
+//go:generate stringer -type=Extended
+
+const DEBUG_INPUT = true
+
+var ErrInactivity error = fmt.Errorf("Inactivity")
+var ErrTimeout error = fmt.Errorf("Timeout")
+var ErrDisconnected error = fmt.Errorf("Disconnected")
+
+var DefaultTimeout time.Duration = time.Duration(60) * time.Second
+
+var extendedKeys map[string]Extended = map[string]Extended{
+	"[A":   UP_ARROW,
+	"[B":   DOWN_ARROW,
+	"[C":   RIGHT_ARROW,
+	"[D":   LEFT_ARROW,
+	"[H":   HOME,
+	"[F":   END, // terminal
+	"[K":   END,
+	"[V":   PAGE_UP,
+	"[U":   PAGE_DOWN,
+	"[@":   INSERT,
+	"[2~":  INSERT,    // terminal
+	"[3~":  DELETE,    // terminal
+	"[5~":  PAGE_UP,   // terminal
+	"[6~":  PAGE_DOWN, // terminal
+	"[15~": F5,        // terminal
+	"[17~": F6,        // terminal
+	"[18~": F7,        // terminal
+	"[19~": F8,        // terminal
+	"[20~": F9,        // terminal
+	"[21~": F10,       // terminal
+	"[23~": F11,
+	"[24~": F12, // terminal
+	"OP":   F1,
+	"OQ":   F2,
+	"OR":   F3,
+	"OS":   F4, // syncterm "[1" = F1,F2,F3, and F4)
+	"Ot":   F5, // syncterm
+}
 
-const _Extended_name = "NOPUP_ARROWDOWN_ARROWRIGHT_ARROWLEFT_ARROWHOMEENDPAGE_UPPAGE_DOWNINSERTDELETEF1F2F3F4F5F6F7F8F9F10F11F12MOUSECURSORUNKNOWN"
+// ReadRune fails on IAC AYT => unicode.ReplacementChar X 2
 
-var _Extended_index = [...]uint8{0, 3, 11, 21, 32, 42, 46, 49, 56, 65, 71, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 98, 101, 104, 109, 115, 122}
+// getch -> ReadRune  Low Level
+func (d *Door) ReadRune() (rune, error) {
+	var r rune
 
-func (i Extended) String() string {
-	if i < 0 || i >= Extended(len(_Extended_index)-1) {
-		return "Extended(" + strconv.FormatInt(int64(i), 10) + ")"
+	if d.ReaderClosed {
+		return r, ErrDisconnected
+	}
+
+	for {
+		select {
+		case r = <-d.readerChannel:
+			return r, nil
+		case <-time.After(ReaderInterval):
+			return r, ErrTimeout
+		}
 	}
-	return _Extended_name[_Extended_index[i]:_Extended_index[i+1]]
 }
 
-// This is the current list of Extended keys we support:
-const (
-	XKEY_UP_ARROW    = 0x1001
-	XKEY_DOWN_ARROW  = 0x1002
-	XKEY_RIGHT_ARROW = 0x1003
-	XKEY_LEFT_ARROW  = 0x1004
-	XKEY_HOME        = 0x1010
-	XKEY_END         = 0x1011
-	XKEY_PGUP        = 0x1012
-	XKEY_PGDN        = 0x1023
-	XKEY_INSERT      = 0x1024
-	XKEY_DELETE      = 0x7f
-	XKEY_F1          = 0x1021
-	XKEY_F2          = 0x1022
-	XKEY_F3          = 0x1023
-	XKEY_F4          = 0x1024
-	XKEY_F5          = 0x1025
-	XKEY_F6          = 0x1026
-	XKEY_F7          = 0x1027
-	XKEY_F8          = 0x1028
-	XKEY_F9          = 0x1029
-	XKEY_F10         = 0x102a
-	XKEY_F11         = 0x102b
-	XKEY_F12         = 0x102c
-	XKEY_MOUSE       = 0x1100
-	XKEY_UNKNOWN     = 0x1111
-)
+func (d *Door) RunePushback(r rune) {
+	d.Pushback.Push(r)
+}
 
-var extendedKeys map[string]int = map[string]int{
-	"[A":   XKEY_UP_ARROW,
-	"[B":   XKEY_DOWN_ARROW,
-	"[C":   XKEY_RIGHT_ARROW,
-	"[D":   XKEY_LEFT_ARROW,
-	"[H":   XKEY_HOME,
-	"[F":   XKEY_END, // terminal
-	"[K":   XKEY_END,
-	"[V":   XKEY_PGUP,
-	"[U":   XKEY_PGDN,
-	"[@":   XKEY_INSERT,
-	"[2~":  XKEY_INSERT, // terminal
-	"[3~":  XKEY_DELETE, // terminal
-	"[5~":  XKEY_PGUP,   // terminal
-	"[6~":  XKEY_PGDN,   // terminal
-	"[15~": XKEY_F5,     // terminal
-	"[17~": XKEY_F6,     // terminal
-	"[18~": XKEY_F7,     // terminal
-	"[19~": XKEY_F8,     // terminal
-	"[20~": XKEY_F9,     // terminal
-	"[21~": XKEY_F10,    // terminal
-	"[23~": XKEY_F11,
-	"[24~": XKEY_F12, // terminal
-	"OP":   XKEY_F1,
-	"OQ":   XKEY_F2,
-	"OR":   XKEY_F3,
-	"OS":   XKEY_F4,
-	"Ot":   XKEY_F5, // syncterm
+// getkey_or_pushback -> ReadRunePushback()
+func (d *Door) ReadRunePushback() (rune, error) {
+	if !d.Pushback.Empty() {
+		if DEBUG_INPUT {
+			log.Println("Read From PushBack")
+		}
+		return d.Pushback.Pop(), nil
+	}
+	return d.ReadRune()
+}
+
+func RuneToInt8(r rune) int8 {
+	return int8(r)
+}
+
+// Confusion - It's possible to return a null rune
+// that we received.  So it's a little sloppy having
+// to check Extended == NOP.  :(
+
+// High Level function - returns rune or extended
+
+func (d *Door) GetKey() (rune, Extended, error) {
+	var r, r2 rune
+	var err, err2 error
+	var ex Extended
+
+	if d.ReaderClosed {
+		return r, ex, ErrDisconnected
+	}
+
+	// if bio.Disconnected() {
+	//	return r, ex, DisconnectedError
+	// }
+
+	r, err = d.ReadRunePushback()
+
+	if err != nil {
+		return r, ex, err
+	}
+
+	// fyneterm CR
+	if r == '\x0a' {
+		r2, err2 = d.ReadRunePushback()
+		if err2 == nil {
+			// Not an error
+			if r2 != '\x00' && r2 != '\x0a' {
+				// Wasn't 0x00 or 0x0a
+				d.RunePushback(r2)
+			}
+		}
+		return '\x0d', ex, nil
+	}
+
+	// We get 0x0d, 0x00, or 0x0d 0x0a from syncterm
+	if r == '\x0d' {
+		r2, err2 = d.ReadRunePushback()
+		if err2 == nil {
+			// Not an error
+			if r2 != '\x00' && r2 != '\x0a' {
+				// Wasn't 0x00 or 0x0a
+				d.RunePushback(r2)
+			}
+		}
+		return r, ex, nil
+	}
+
+	if r == '\x00' {
+		// Possibly doorway mode - deprecated?
+		// syncterm does support this, so it isn't entirely dead (NNY!)
+		r2, _ = d.ReadRunePushback()
+
+		r = rune(0)
+
+		switch r2 {
+		case '\x50':
+			return r, DOWN_ARROW, nil
+		case '\x48':
+			return r, UP_ARROW, nil
+		case '\x4b':
+			return r, LEFT_ARROW, nil
+		case 0x4d:
+			return r, RIGHT_ARROW, nil
+		case 0x47:
+			return r, HOME, nil
+		case 0x4f:
+			return r, END, nil
+		case 0x49:
+			return r, PAGE_UP, nil
+		case 0x51:
+			return r, PAGE_DOWN, nil
+		case 0x3b:
+			return r, F1, nil
+		case 0x3c:
+			return r, F2, nil
+		case 0x3d:
+			return r, F3, nil
+		case 0x3e:
+			return r, F4, nil
+		case 0x3f:
+			return r, F5, nil
+		case 0x40:
+			return r, F6, nil
+		case 0x41:
+			return r, F7, nil
+		case 0x42:
+			return r, F8, nil
+		case 0x43:
+			return r, F9, nil
+		case 0x44:
+			return r, F10, nil
+		/*
+			case 0x45:
+				return F11
+			case 0x46:
+				return F12
+		*/
+		case 0x52:
+			return r, INSERT, nil
+		case 0x53:
+			return r, DELETE, nil
+		default:
+			log.Printf("ERROR Doorway mode: 0x00 %x\n", r2)
+			return r, UNKNOWN, nil
+		}
+	} // End doorway
+
+	if r == '\x1b' {
+		// Escape key?
+		r2, err2 = d.ReadRunePushback()
+		if err2 != nil {
+			// Just escape key
+			return r, ex, nil
+		}
+
+		if r2 == '\x1b' {
+			d.RunePushback(r2)
+			return r, ex, nil
+		}
+
+		var extended []rune = make([]rune, 1)
+		extended[0] = r2
+
+		r2, err2 = d.ReadRunePushback()
+		for err2 == nil {
+			if r2 == '\x1b' {
+				// This is the end of the extended code.
+				d.RunePushback(r2)
+				break
+			}
+			extended = append(extended, r2)
+
+			ext, has := extendedKeys[string(extended)]
+			if has {
+				return rune(0), ext, nil
+			}
+
+			if unicode.IsLetter(r2) {
+				// The end of the extended code
+				break
+			}
+			r2, err2 = d.ReadRunePushback()
+		}
+
+		var exString string = string(extended)
+		if strings.HasPrefix(exString, "[M") && len(extended) == 5 {
+			// Mouse Extended - input zero based (I add +1 to X, Y, and button)
+			var mouse Mouse = Mouse{Button: RuneToInt8(extended[2]) - ' ' + 1,
+				X: RuneToInt8(extended[3]) - '!' + 1,
+				Y: RuneToInt8(extended[4]) - '!' + 1}
+
+			d.mcMutex.Lock()
+			defer d.mcMutex.Unlock()
+			d.LastMouse = append(d.LastMouse, mouse)
+			log.Println("Mouse:", mouse)
+			return rune(0), MOUSE, nil
+		}
+
+		if strings.HasSuffix(exString, "R") {
+			// Cursor Position information
+			var cursor CursorPos
+			// ^[[1;1R^[[2;3R^[[41;173R
+
+			// Y;X
+			exString = exString[1 : len(exString)-1] // Remove [ and R
+			pos := SplitToInt(exString, ";")
+			if len(pos) == 2 {
+				cursor.X = pos[1]
+				cursor.Y = pos[0]
+				d.mcMutex.Lock()
+				defer d.mcMutex.Unlock()
+				d.LastCursor = append(d.LastCursor, cursor)
+				log.Println("Cursor Pos:", cursor)
+				return rune(0), CURSOR, nil
+			} else {
+				log.Println("ERROR Cursor:", extended)
+				return rune(0), UNKNOWN, nil
+			}
+
+		}
+		if exString == "\x1b" {
+			return extended[0], Extended(0), nil
+		}
+
+		log.Println("ERROR Extended:", extended)
+		return rune(0), UNKNOWN, nil
+	}
+
+	// AYT (Telnet Are You There?)
+	/*
+		if r == '\xff' {
+			log.Printf("IAC")
+			r2, err2 = bio.ReadRunePushback()
+			if err2 != nil {
+				// Return value
+				return r, ex, nil
+			}
+			if r2 == '\xf6' {
+				// Are You There?
+				log.Println("AYT? - Yes")
+				bio.Write("Yes?")
+				// bio.Write(string([]byte{0x00})) // 0xff, 0xf2}))
+				// Eat these keys, and re-call ourselves...
+				// Or goto very top?
+
+				return bio.GetKey()
+			}
+		}
+	*/
+
+	return r, ex, nil
 }
 
 // "[1":   XKEY_UNKNOWN, // Syncterm is lost, could be F1..F5?
@@ -118,6 +342,7 @@ var extendedKeys map[string]int = map[string]int{
 // This gets the raw keys from the client, it doesn't handle extended keys,
 // functions, arrows.
 // Return key, or -1 (Timeout/No key available), -2 hangup
+/*
 func (d *Door) getch() int {
 	select {
 	case res, ok := <-d.readerChannel:
@@ -195,51 +420,45 @@ func (d *Door) GetKey() int {
 
 		switch c2 {
 		case 0x50:
-			return XKEY_DOWN_ARROW
+			return DOWN_ARROW
 		case 0x48:
-			return XKEY_UP_ARROW
+			return UP_ARROW
 		case 0x4b:
-			return XKEY_LEFT_ARROW
+			return LEFT_ARROW
 		case 0x4d:
-			return XKEY_RIGHT_ARROW
+			return RIGHT_ARROW
 		case 0x47:
-			return XKEY_HOME
+			return HOME
 		case 0x4f:
-			return XKEY_END
+			return END
 		case 0x49:
-			return XKEY_PGUP
+			return PAGE_UP
 		case 0x51:
-			return XKEY_PGDN
+			return PAGE_DOWN
 		case 0x3b:
-			return XKEY_F1
+			return F1
 		case 0x3c:
-			return XKEY_F2
+			return F2
 		case 0x3d:
-			return XKEY_F3
+			return F3
 		case 0x3e:
-			return XKEY_F4
+			return F4
 		case 0x3f:
-			return XKEY_F5
+			return F5
 		case 0x40:
-			return XKEY_F6
+			return F6
 		case 0x41:
-			return XKEY_F7
+			return F7
 		case 0x42:
-			return XKEY_F8
+			return F8
 		case 0x43:
-			return XKEY_F9
+			return F9
 		case 0x44:
-			return XKEY_F10
-		/*
-			case 0x45:
-				return XKEY_F11
-			case 0x46:
-				return XKEY_F12
-		*/
+			return F10
 		case 0x52:
-			return XKEY_INSERT
+			return INSERT
 		case 0x53:
-			return XKEY_DELETE
+			return DELETE
 		default:
 			log.Printf("ERROR Doorway mode: 0x00 %x\n", c2)
 			return XKEY_UNKNOWN
@@ -334,6 +553,54 @@ func (d *Door) WaitKey(secs int64, usecs int64) int {
 		return -1
 	}
 }
+*/
+
+// port over WaitKey
+func (d *Door) WaitKey(timeout time.Duration) (rune, Extended, error) {
+	// disconnected test
+
+	d.readerMutex.Lock()
+	if d.ReaderClosed {
+		d.readerMutex.Unlock()
+		return rune(0), Extended(0), ErrDisconnected
+	}
+	d.readerMutex.Unlock()
+
+	if !d.Pushback.Empty() {
+		log.Println("WaitKey: Pushback ! empty.")
+		r, ex, e := d.GetKey()
+		if DEBUG_INPUT {
+			log.Println("WaitKey:", r, ex, e)
+		}
+		// return bio.GetKey()
+		return r, ex, e
+	}
+
+	// expiration := time.NewTicker(timeout)
+	// defer expiration.Stop()
+
+	select {
+	case r, ok := <-d.readerChannel:
+		if ok {
+			// Is this blocking? Ignoring expiration?
+			d.RunePushback(r)
+			log.Printf("readerChannel %#v ...\n", r)
+			r, ex, e := d.GetKey()
+			if DEBUG_INPUT {
+				log.Println("WaitKey:", r, ex, e)
+			}
+			// return bio.GetKey()
+			return r, ex, e
+		} else {
+			log.Println("WaitKey: Disconnected")
+			// Reader has closed.
+			// Disconnected = true
+			return rune(0), Extended(0), ErrDisconnected
+		}
+	case <-time.After(timeout):
+		return rune(0), Extended(0), ErrTimeout
+	}
+}
 
 // Outputs spaces and backspaces
 // If you have set a background color, this shows the input area.
@@ -350,29 +617,31 @@ func (d *Door) Input(max int) string {
 	// draw input area
 	d.Write(DisplayInput(max))
 
-	var c int
+	var r rune
+	var ex Extended
+	var err error
 
 	for {
-		c = d.WaitKey(Inactivity, 0)
-		if c < 0 {
+		r, ex, err = d.WaitKey(DefaultTimeout)
+		if err != nil {
 			// timeout/hangup
 			return ""
 		}
 
-		if c > 1000 {
+		if ex != NOP {
 			continue
 		}
 
-		if strconv.IsPrint(rune(c)) {
+		if strconv.IsPrint(r) {
 			if len(line) < max {
-				d.Write(string(byte(c)))
-				line += string(byte(c))
+				d.Write(string(r))
+				line += string(r)
 			} else {
 				d.Write("\x07")
 			}
 		} else {
 			// Non-print
-			switch c {
+			switch r {
 			case 0x7f, 0x08:
 				if len(line) > 0 {
 					d.Write("\x08 \x08")
@@ -385,18 +654,19 @@ func (d *Door) Input(max int) string {
 	}
 }
 
-func (d *Door) GetOneOf(possible string) int {
-	var c int
+func (d *Door) GetOneOf(possible string) rune {
+	var r rune
+	var err error
 
 	for {
-		c = d.WaitKey(Inactivity, 0)
-		if c < 0 {
-			return c
+		r, _, err = d.WaitKey(DefaultTimeout)
+		if err != nil {
+			return rune(0)
 		}
-		r := unicode.ToUpper(rune(c))
+		r := unicode.ToUpper(r)
 		if strings.ContainsRune(possible, r) {
 			// return upper case rune
-			return int(r)
+			return r
 		}
 		/*
 			c = strings.IndexRune(possible, r)

+ 117 - 38
door/input_linux.go

@@ -3,13 +3,32 @@ package door
 import (
 	"log"
 	"syscall"
+	"time"
 )
 
-func Reader(handle int, d *Door) {
-	// I don't need the select anymore.  Let the read block.
-	// defer d.wg.Done()
+var ReaderInterval = time.Duration(200) * time.Millisecond
+var ReaderTimeval syscall.Timeval = syscall.Timeval{0, 200}
+
+func clearAll(fdSetPtr *syscall.FdSet) {
+	for index := range (*fdSetPtr).Bits {
+		(*fdSetPtr).Bits[index] = 0
+	}
+}
+
+func set(fdSetPtr *syscall.FdSet, fd int) {
+	(*fdSetPtr).Bits[fd/64] |= 1 << uint64(fd%64)
+}
+
+func Reader(d *Door) {
+	// I need non-blocking reads here.
+
+	// I'm not really using either of these things.
+
+	// d.readerFile = os.NewFile(uintptr(d.READFD), "Door FD")
+	// d.runereader = bufio.NewReaderSize(d.readerFile, 1)
+
 	defer func() {
-		log.Printf("~Reader2\n")
+		log.Printf("~Reader\n")
 		if d.ReaderCanClose {
 			d.wg.Done()
 		}
@@ -20,47 +39,107 @@ func Reader(handle int, d *Door) {
 		}
 	}()
 
-	var buffer []byte = make([]byte, 1)
+	var fdset syscall.FdSet
+
 	for {
-		var read int
-		var err error
-		read, err = syscall.Read(handle, buffer)
-		if err != nil {
+		clearAll(&fdset)
+		set(&fdset, d.READFD)
+
+		v, err := syscall.Select(d.READFD+1, &fdset, nil, nil, &ReaderTimeval)
+
+		if err == syscall.EINTR {
+			continue
+		}
+		// log.Printf("Select: %#v / %#v\n", v, err)
+
+		if v == -1 {
 			log.Printf("Reader ERR: %#v\n", err)
-			d.ReaderClosed = true
-			close(d.readerChannel)
-			/*
-				if !d.Disconnect() {
-					log.Println("Reader close writerChannel")
-					d.Disconnected = true
-					// atomic.StoreInt32(&d.Disconnected, 1)
-					d.closeChannel <- struct{}{}
-					// close(d.writerChannel)
-					return
-				}
-				// break
-			*/
+			d.readerMutex.Lock()
+
+			if !d.ReaderClosed {
+				d.ReaderClosed = true
+				close(d.readerChannel)
+			}
 			return
 		}
-		if read == 1 {
-			d.readerChannel <- buffer[0]
-		} else {
-			log.Printf("READ FAILED %d\n", read)
+
+		if v == 0 {
+			// timeout
+			continue
+		}
+
+		// d.readerFile.SetReadDeadline(time.Now().Add(ReaderInterval))
+		// not on a os.File you won't. :P
+
+		// r, _, err := d.runereader.ReadRune()
+
+		buffer := make([]byte, 1)
+		r, err := syscall.Read(d.READFD, buffer)
+		if r == -1 {
+			log.Println("Read -1 (closed)")
+			d.readerMutex.Lock()
+			defer d.readerMutex.Unlock()
+			if !d.ReaderClosed {
+				d.ReaderClosed = true
+				d.Disconnected = true
+				close(d.readerChannel)
+			}
+			return
+		}
+		if r != 1 {
+			log.Printf("Select said ready, but: %#v %#v\n", r, err)
+			d.readerMutex.Lock()
+			defer d.readerMutex.Unlock()
+			if !d.ReaderClosed {
+				d.ReaderClosed = true
+				d.Disconnected = true
+				close(d.readerChannel)
+			}
+			return
+		}
+
+		if DEBUG_INPUT {
+			log.Printf("Reader (byte): %#v\n", buffer[0])
+		}
+
+		d.readerChannel <- rune(buffer[0])
+
+		/*
+			if r == unicode.ReplacementChar {
+				_ = d.runereader.UnreadRune()
+				b, _ := d.runereader.ReadByte()
+				if DEBUG_INPUT {
+					log.Printf("Reader (byte): %#v\n", b)
+				}
+				d.readerChannel <- rune(b)
+				continue
+			}
+
+			if err == nil {
+				if DEBUG_INPUT {
+					log.Printf("Reader (rune): %#v\n", r)
+				}
+				d.readerChannel <- r
+				continue
+			}
+		*/
+
+		// I'm not sure I care what the error is we're getting here...
+
+		/*
+			if e, ok := err.(net.Error); ok && e.Timeout() {
+
+			} else {
+
+			}
+		*/
+
+		/*
+			log.Printf("Reader ERR: %#v\n", err)
 			d.ReaderClosed = true
 			close(d.readerChannel)
-			/*
-				if !d.Disconnect() {
-					log.Println("Reader close writerChannel")
-					d.Disconnected = true
-					// atomic.StoreInt32(&d.Disconnected, 1)
-					d.closeChannel <- struct{}{}
-					//close(d.writerChannel)
-					return
-				}
-				// break
-			*/
 			return
-		}
+		*/
 	}
 }
 

+ 227 - 79
door/input_test.go

@@ -4,13 +4,38 @@ import (
 	"flag"
 	"fmt"
 
+	"log"
 	"net"
 	"os"
-	"strings"
 	"testing"
 	"time"
 )
 
+const KEEP_LOGS = true
+
+func clear_socket(socket net.Conn, t *testing.T) string {
+	// Clear out the data that's pending in the socket.
+
+	buffer := make([]byte, 1204)
+	var r int
+	var err error
+
+	err = socket.SetReadDeadline(time.Now().Add(time.Millisecond * 20))
+	if err != nil {
+		t.Error("socket.SetReadDeadLine:", err)
+	}
+	r, err = socket.Read(buffer)
+	if err != nil {
+		t.Errorf("socket.Read: %#v", err)
+	}
+	// t.Errorf("Buffer : %#v\n", buffer[:r])
+	err = socket.SetReadDeadline(time.Time{})
+	if err != nil {
+		t.Error("socket.SetReadDeadLine:", err)
+	}
+	return string(buffer[:r])
+}
+
 func TestDoorInputConnection(t *testing.T) {
 	var tmpFile *os.File
 	var err error
@@ -20,7 +45,11 @@ func TestDoorInputConnection(t *testing.T) {
 	}
 
 	// Remember to clean up the file afterwards
-	defer os.Remove(tmpFile.Name())
+	if !KEEP_LOGS {
+		defer os.Remove(tmpFile.Name())
+	} else {
+		t.Logf("See: %s\n", tmpFile.Name())
+	}
 
 	// establish network socket connection to set Comm_handle
 	var server, client net.Conn
@@ -73,8 +102,10 @@ func TestDoorInputConnection(t *testing.T) {
 	d.Init("input-test")
 
 	defer d.Close()
-	// clean up logfile
-	defer os.Remove("input-test-12.log")
+	// clean up logfile - (filename is from Init name + node #)
+	if !KEEP_LOGS {
+		defer os.Remove("input-test-12.log")
+	}
 
 	// Ok!
 	if !Unicode {
@@ -89,43 +120,55 @@ func TestDoorInputConnection(t *testing.T) {
 		t.Errorf("Height not 43: %d", Height)
 	}
 
-	// These are the commands sent to detect ... throw this all away.
-	buffer = make([]byte, 128)
-	err = server.SetReadDeadline(time.Now().Add(time.Millisecond * 20))
-	if err != nil {
-		t.Error("server.SetReadDeadLine:", err)
-	}
-	_, err = server.Read(buffer)
-	if err != nil {
-		t.Errorf("server.Read: %#v", err)
-	}
-	// t.Errorf("Buffer : %#v\n", buffer[:r])
-	err = server.SetReadDeadline(time.Time{})
-	if err != nil {
-		t.Error("server.SetReadDeadLine:", err)
+	clear_socket(server, t)
+	// This should all be received by above (I think)...
+	/*
+		// These are the commands sent to detect ... throw this all away.
+		buffer = make([]byte, 128)
+		err = server.SetReadDeadline(time.Now().Add(time.Millisecond * 20))
+		if err != nil {
+			t.Error("server.SetReadDeadLine:", err)
+		}
+		_, err = server.Read(buffer)
+		if err != nil {
+			t.Errorf("server.Read: %#v", err)
+		}
+		// t.Errorf("Buffer : %#v\n", buffer[:r])
+		err = server.SetReadDeadline(time.Time{})
+		if err != nil {
+			t.Error("server.SetReadDeadLine:", err)
+		}
+	*/
+
+	keytest := map[string][]rune{
+		"\x1b":     []rune{0x1b},
+		"\x0d\x00": []rune{0x0d},
+		"\x0d\x0a": []rune{0x0d},
+		"\x0dCQ":   []rune{0x0d, 'C', 'Q'},
+		"\x0dA":    []rune{0x0d, 'A'},
+		"\x0dCAT":  []rune{0x0d, 'C', 'A', 'T'},
 	}
 
-	keytest := map[string][]int{
-		"\x1b":                             []int{0x1b},
-		"\x0d\x00":                         []int{0x0d},
-		"\x0d\x0a":                         []int{0x0d},
-		"\x0dCQ":                           []int{0x0d, 'C', 'Q'},
-		"\x0dA":                            []int{0x0d, 'A'},
-		"\x0dCAT":                          []int{0x0d, 'C', 'A', 'T'},
-		"\x00\x50\x00\x48\x00\x4b\x00\x4d": []int{XKEY_DOWN_ARROW, XKEY_UP_ARROW, XKEY_LEFT_ARROW, XKEY_RIGHT_ARROW},
-		"\x00\x47\x00\x4f\x00\x49\x00\x51": []int{XKEY_HOME, XKEY_END, XKEY_PGUP, XKEY_PGDN},
-		"\x00\x3b\x00\x3c\x00\x3d\x00\x3e": []int{XKEY_F1, XKEY_F2, XKEY_F3, XKEY_F4},
-		"\x00\x3f\x00\x40\x00\x41\x00\x42": []int{XKEY_F5, XKEY_F6, XKEY_F7, XKEY_F8},
-		"\x00\x43\x00\x44\x00\x52\x00\x53": []int{XKEY_F9, XKEY_F10, XKEY_INSERT, XKEY_DELETE},
-		"\x1b[A\x1b[B\x1b[C\x1b[D":         []int{XKEY_UP_ARROW, XKEY_DOWN_ARROW, XKEY_RIGHT_ARROW, XKEY_LEFT_ARROW},
-		"\x1b[H\x1b[F\x1b[K\x1b[V\x1b[U":   []int{XKEY_HOME, XKEY_END, XKEY_END, XKEY_PGUP, XKEY_PGDN},
-		"\x1b[5~\x1b[6~":                   []int{XKEY_PGUP, XKEY_PGDN},
-		"\x1b[@\x1b[2~\x1b[3~":             []int{XKEY_INSERT, XKEY_INSERT, XKEY_DELETE},
-		"\x1bOP\x1bOQ\x1bOR\x1bOS":         []int{XKEY_F1, XKEY_F2, XKEY_F3, XKEY_F4},
-		"\x1b[15~\x1b[17~\x1b[18~\x1b[19~": []int{XKEY_F5, XKEY_F6, XKEY_F7, XKEY_F8},
-		"\x1b[20~\x1b[21~\x1b[23~\x1b[24~": []int{XKEY_F9, XKEY_F10, XKEY_F11, XKEY_F12},
+	keyextest := map[string][]Extended{
+		"\x00\x50\x00\x48\x00\x4b\x00\x4d": []Extended{DOWN_ARROW, UP_ARROW, LEFT_ARROW, RIGHT_ARROW},
+		"\x00\x47\x00\x4f\x00\x49\x00\x51": []Extended{HOME, END, PAGE_UP, PAGE_DOWN},
+		"\x00\x3b\x00\x3c\x00\x3d\x00\x3e": []Extended{F1, F2, F3, F4},
+		"\x00\x3f\x00\x40\x00\x41\x00\x42": []Extended{F5, F6, F7, F8},
+		"\x00\x43\x00\x44\x00\x52\x00\x53": []Extended{F9, F10, INSERT, DELETE},
+		"\x1b[A\x1b[B\x1b[C\x1b[D":         []Extended{UP_ARROW, DOWN_ARROW, RIGHT_ARROW, LEFT_ARROW},
+		"\x1b[H\x1b[F\x1b[K\x1b[V\x1b[U":   []Extended{HOME, END, END, PAGE_UP, PAGE_DOWN},
+		"\x1b[5~\x1b[6~":                   []Extended{PAGE_UP, PAGE_DOWN},
+		"\x1b[@\x1b[2~\x1b[3~":             []Extended{INSERT, INSERT, DELETE},
+		"\x1bOP\x1bOQ\x1bOR\x1bOS":         []Extended{F1, F2, F3, F4},
+		"\x1b[15~\x1b[17~\x1b[18~\x1b[19~": []Extended{F5, F6, F7, F8},
+		"\x1b[20~\x1b[21~\x1b[23~\x1b[24~": []Extended{F9, F10, F11, F12},
 	}
 
+	t.Logf("Starting keytest\n")
+
+	keyWait := time.Duration(50 * time.Millisecond)
+	// 5 * time.Second) //50 * time.Millisecond)
+
 	for send, get := range keytest {
 		var buffer []byte = []byte(send)
 		_, err = server.Write(buffer)
@@ -134,7 +177,7 @@ func TestDoorInputConnection(t *testing.T) {
 		}
 		time.Sleep(time.Millisecond)
 
-		var recv []int = make([]int, 0)
+		var recv []rune = make([]rune, 0)
 		var retries int = 0
 		for {
 			// input := d.WaitKey(0, 50)
@@ -142,21 +185,35 @@ func TestDoorInputConnection(t *testing.T) {
 			// not getting all of the data we Write above.
 			// data shows up on next read.
 			// input := d.WaitKey(0, 100) // because we are retrying, reduce the wait time
-			input := d.WaitKey(0, 50)
+			input, _, err := d.WaitKey(keyWait)
+			t.Logf(">> %#v, %#v\n", input, err)
 
-			if input != -1 {
+			if err == nil {
 				recv = append(recv, input)
-			} else {
+				// } else {
 				// sometimes, running test using local loopback takes awhile for the characters
 				// to arrive.
+				if len(recv) != len(get) {
+					continue
+					/*
+						if retries < 5 {
+							retries++
+							t.Logf("Retry %d want %d got %d\n", retries, len(get), len(recv))
+							continue
+						}
+					*/
+				}
+				break
+			} else {
+				t.Logf("WaitKey Err: %#v\n", err)
 				if len(recv) != len(get) {
 					if retries < 5 {
 						retries++
 						t.Logf("Retry %d want %d got %d\n", retries, len(get), len(recv))
 						continue
 					}
+					break
 				}
-				break
 			}
 		}
 
@@ -176,30 +233,104 @@ func TestDoorInputConnection(t *testing.T) {
 		}
 	}
 
-	buffer = make([]byte, 128)
-	err = server.SetReadDeadline(time.Now().Add(time.Millisecond * 20))
-	if err != nil {
-		t.Error("server.SetReadDeadLine:", err)
-	}
-	r, err := server.Read(buffer)
-	// input_test.go:131: server.Read: &net.OpError{Op:"read", Net:"tcp", Source:(*net.TCPAddr)(0xc000012f00), Addr:(*net.TCPAddr)(0xc000012f30), Err:(*poll.DeadlineExceededError)(0x6c39c0)}
-	if !strings.Contains(err.Error(), "i/o timeout") {
-		t.Errorf("Expected poll.DeadlineExceededError: %s / %#v", err.Error(), err)
-	}
-	if r != 0 {
-		t.Errorf("Buffer After KeyTest: %#v\n", buffer[:r])
-	}
-	err = server.SetReadDeadline(time.Time{})
-	if err != nil {
-		t.Error("server.SetReadDeadLine:", err)
+	t.Logf("Starting keyextest\n")
+
+	for send, get := range keyextest {
+		var buffer []byte = []byte(send)
+		_, err = server.Write(buffer)
+		if err != nil {
+			t.Error("server.Write:", err)
+		}
+		time.Sleep(time.Millisecond)
+
+		var recv []Extended = make([]Extended, 0)
+		var retries int = 0
+		for {
+			// input := d.WaitKey(0, 50)
+			// running go test -count > 1 sometimes fails --
+			// not getting all of the data we Write above.
+			// data shows up on next read.
+			// input := d.WaitKey(0, 100) // because we are retrying, reduce the wait time
+			_, ex, err := d.WaitKey(keyWait)
+
+			if err == nil {
+				recv = append(recv, ex)
+
+				// sometimes, running test using local loopback takes awhile for the characters
+				// to arrive.
+				if len(recv) != len(get) {
+					continue
+					/*
+						if retries < 5 {
+							retries++
+							t.Logf("Retry %d want %d got %d\n", retries, len(get), len(recv))
+							continue
+						}
+					*/
+				}
+				break
+			} else {
+				t.Logf("WaitKey err: #%v\n", err)
+				if len(recv) != len(get) {
+					if retries < 5 {
+						retries++
+						t.Logf("Retry %d want %d got %d\n", retries, len(get), len(recv))
+						continue
+					}
+
+					break
+
+				}
+			}
+		}
+
+		if len(recv) != len(get) {
+			t.Errorf("Send %#v, LEN expected %#v, got %#v", send, get, recv)
+		} else {
+			matches := true
+			for idx, i := range get {
+				if recv[idx] != i {
+					matches = false
+					break
+				}
+			}
+			if !matches {
+				t.Errorf("Send %#v, MATCH expected %#v, got %#v", send, get, recv)
+			}
+		}
 	}
-	timeout := d.WaitKey(0, 50)
-	if timeout != -1 {
+
+	clear_socket(server, t)
+
+	/*
+		buffer = make([]byte, 128)
+		err = server.SetReadDeadline(time.Now().Add(time.Millisecond * 20))
+		if err != nil {
+			t.Error("server.SetReadDeadLine:", err)
+		}
+		r, err := server.Read(buffer)
+		// input_test.go:131: server.Read: &net.OpError{Op:"read", Net:"tcp", Source:(*net.TCPAddr)(0xc000012f00), Addr:(*net.TCPAddr)(0xc000012f30), Err:(*poll.DeadlineExceededError)(0x6c39c0)}
+		if !strings.Contains(err.Error(), "i/o timeout") {
+			t.Errorf("Expected poll.DeadlineExceededError: %s / %#v", err.Error(), err)
+		}
+		if r != 0 {
+			t.Errorf("Buffer After KeyTest: %#v\n", buffer[:r])
+		}
+		err = server.SetReadDeadline(time.Time{})
+		if err != nil {
+			t.Error("server.SetReadDeadLine:", err)
+		}
+	*/
+
+	t.Logf("Starting WaitKey timeout...\n")
+	_, _, timeout := d.WaitKey(keyWait)
+	if timeout == nil {
 		t.Errorf("Expected timeout, got %d / %X", timeout, timeout)
 	} else {
 		t.Logf("Ok! Buffer should be empty!  -1 (timeout)")
 	}
 
+	t.Logf("Starting input test\n")
 	// Input test
 	buffer = []byte("1234567890\r")
 	_, err = server.Write(buffer)
@@ -214,17 +345,26 @@ func TestDoorInputConnection(t *testing.T) {
 		t.Errorf("Expected Input(5) = 12345, but got %#v", input)
 	}
 
+	// What's a good way to clear out the receive buffer here??
+
 	// I'm not sure what they extra characters are in the buffer here.
-	buffer = make([]byte, 128)
-	err = server.SetReadDeadline(time.Now().Add(time.Millisecond * 20))
-	if err != nil {
-		t.Error("server.SetReadDeadLine:", err)
-	}
-	r, err = server.Read(buffer)
-	if err != nil {
-		t.Errorf("server.Read: %#v", err)
-	}
-	var result string = string(buffer[:r])
+	var result string = clear_socket(server, t)
+
+	/*
+		buffer = make([]byte, 128)
+		err = server.SetReadDeadline(time.Now().Add(time.Millisecond * 20))
+		if err != nil {
+			t.Error("server.SetReadDeadLine:", err)
+		}
+		var r int
+
+		r, err = server.Read(buffer)
+		if err != nil {
+			t.Errorf("server.Read: %#v", err)
+		}
+		var result string = string(buffer[:r])
+	*/
+
 	expected := "     \x08\x08\x08\x08\x0812345\x07\x07\x07\x07\x07"
 	if result != expected {
 		t.Errorf("Buffer Input(5): Expected %#v, got %#v\n", expected, result)
@@ -260,11 +400,16 @@ func TestDoorInputConnection(t *testing.T) {
 		t.Errorf("Expected Input(5) = 98765, but got %#v", input)
 	}
 
+	t.Logf("server close")
+	log.Println("server close")
+
 	server.Close()
 
-	var hungup int = d.WaitKey(1, 0)
-	if hungup != -2 {
-		t.Errorf("Expected -2 (hangup), got %d", hungup)
+	time.Sleep(time.Millisecond)
+
+	_, _, err = d.WaitKey(keyWait)
+	if err != ErrDisconnected {
+		t.Errorf("Expected ErrDisconnected, got %#v", err)
 	}
 
 	if !d.Disconnect() {
@@ -277,24 +422,27 @@ func TestDoorInputConnection(t *testing.T) {
 		}
 	*/
 
-	hungup = d.GetKey()
+	_, err = d.ReadRune() // GetKey()
 
-	if hungup != -2 {
-		t.Errorf("Expected -2 (hangup), got %d", hungup)
+	if err != ErrDisconnected {
+		t.Errorf("Expected ErrDisconnected, got %#v", err)
 	}
+
+	t.Logf("client close")
 	client.Close()
 	time.Sleep(time.Millisecond)
 
+	t.Logf("Input on closed server and client")
 	var blank string = d.Input(5)
 
 	if blank != "" {
 		t.Errorf("Input should return blank (hangup).")
 	}
 
-	hungup = d.getch()
+	_, err = d.ReadRune() // getch()
 
-	if hungup != -2 {
-		t.Errorf("Expected -2 (hangup), got %d", hungup)
+	if err != ErrDisconnected {
+		t.Errorf("Expected ErrDisconnected, got %#v", err)
 	}
 
 	d.Write("\x00")

+ 44 - 37
door/menu.go

@@ -140,10 +140,13 @@ func (m *Menu) Choose(d *Door) int {
 
 		updated = false
 
-		var event int = d.Key()
+		var r rune
+		var ex Extended
+		var err error
+		r, ex, err = d.WaitKey(DefaultTimeout)
 
-		if event < 0 {
-			return event
+		if err != nil {
+			return -1
 		}
 		previous_choice := m.Chosen
 		changed = make([]int, 0)
@@ -155,16 +158,16 @@ func (m *Menu) Choose(d *Door) int {
 			}
 		}
 
-		if event == XKEY_MOUSE {
+		if ex == MOUSE {
 			mouse, ok := d.GetMouse()
 			if ok {
 				if mouse.Button == 65 {
 					// Translate Mouse Wheel Up
-					event = XKEY_UP_ARROW
+					ex = UP_ARROW
 				}
 				if mouse.Button == 66 {
 					// Translate Mouse Wheel Down
-					event = XKEY_DOWN_ARROW
+					ex = DOWN_ARROW
 				}
 				// Look at Mouse X/Y to determine where they clicked
 				if mouse.Button == 1 || mouse.Button == 2 {
@@ -173,61 +176,65 @@ func (m *Menu) Choose(d *Door) int {
 			}
 		}
 
-		switch event {
-		case '8':
-			if use_numberpad {
-				goto use8
-			}
-			break
-		use8:
-			fallthrough
-		case XKEY_UP_ARROW:
+		switch ex {
+		case UP_ARROW:
 			if m.Chosen > 0 {
 				m.Chosen--
 				updated = true
 			}
-		case '2':
-			if use_numberpad {
-				goto use2
-			}
-			break
-		use2:
-			fallthrough
-		case XKEY_DOWN_ARROW:
+		case DOWN_ARROW:
 			if m.Chosen < len(m.Lines)-1 {
 				m.Chosen++
 				updated = true
 			}
-		case XKEY_HOME:
+		case HOME:
 			if m.Chosen != 0 {
 				m.Chosen = 0
 				updated = true
 			}
-		case XKEY_END:
+		case END:
 			if m.Chosen != len(m.Lines)-1 {
 				m.Chosen = len(m.Lines) - 1
 				updated = true
 			}
+		}
+
+		switch r {
+		case '8':
+			if use_numberpad {
+				if m.Chosen > 0 {
+					m.Chosen--
+					updated = true
+				}
+			}
+		case '2':
+			if use_numberpad {
+
+				if m.Chosen < len(m.Lines)-1 {
+					m.Chosen++
+					updated = true
+				}
+			}
 		case 0x0d:
 			// use current selection
 			return m.Chosen + 1
 		default:
 			// Is the key in the list of options?
-			if event < 0x1000 {
-				// fmt.Printf("Event: %d\n", event)
-				for x, option := range m.Options {
-					// fmt.Printf("Checking %d, %d\n", x, option)
-					if unicode.ToUpper(option) == unicode.ToUpper(rune(event)) {
-						if m.Chosen == x {
-							return x + 1
-						}
-
-						updated = true
-						m.Chosen = x
-						update_and_exit = true
+
+			// fmt.Printf("Event: %d\n", event)
+			for x, option := range m.Options {
+				// fmt.Printf("Checking %d, %d\n", x, option)
+				if unicode.ToUpper(option) == unicode.ToUpper(r) {
+					if m.Chosen == x {
+						return x + 1
 					}
+
+					updated = true
+					m.Chosen = x
+					update_and_exit = true
 				}
 			}
+
 		}
 
 		if previous_choice != m.Chosen {

+ 16 - 0
door/utilities.go

@@ -1,5 +1,10 @@
 package door
 
+import (
+	"strconv"
+	"strings"
+)
+
 func ArrayDelete[T any](stack *[]T, pos int) (T, bool) {
 	var result T
 	/*
@@ -16,3 +21,14 @@ func ArrayDelete[T any](stack *[]T, pos int) (T, bool) {
 	*stack = (*stack)[:len(*stack)-1]
 	return result, true
 }
+
+func SplitToInt(input string, sep string) []int {
+	var result []int
+	for _, number := range strings.Split(input, sep) {
+		v, err := strconv.Atoi(number)
+		if err == nil {
+			result = append(result, v)
+		}
+	}
+	return result
+}

+ 25 - 2
door/write.go

@@ -136,6 +136,7 @@ func (d *Door) Write(output string) {
 	if output == "" {
 		return
 	}
+
 	/*
 		if d.Disconnect() {
 			return
@@ -165,7 +166,29 @@ func (d *Door) Write(output string) {
 	// signal doesn't work either ... (called from go routine)
 	// The channel <- is synced, the access to the bool isn't. :(
 	//if !d.WriterClosed {
-	if !d.WriterIsClosed() {
+	// Apparently, when I get to the next line, there's
+	// already a data race here...
+	/*
+		if !d.WriterIsClosed() {
+			d.writerChannel <- output
+		}
+	*/
+
+	defer func() {
+		if err := recover(); err != nil {
+			log.Println("Write failed.", err)
+		}
+	}()
+
+	//if !d.WriterIsClosed() {
+	d.writerChannel <- output
+	//}
+	/*
+		d.writerMutex.Lock()
+		defer d.writerMutex.Unlock()
+		if d.WriterClosed {
+			return
+		}
 		d.writerChannel <- output
-	}
+	*/
 }

+ 41 - 9
door/write_linux.go

@@ -12,6 +12,8 @@ func Writer(d *Door) {
 	var handle int = d.Config.Comm_handle
 	log.Println("Writer")
 	defer d.wg.Done()
+	var Closed bool = false
+
 	for {
 		/*
 			select {
@@ -41,9 +43,14 @@ func Writer(d *Door) {
 		case output, ok := <-d.writerChannel:
 			if !ok {
 				log.Println("closeChannel")
-				// Safe from data races, writerChannel unbuffered
-				d.WriterClosed = true
-				close(d.writerChannel)
+
+				d.writerMutex.Lock()
+				if !d.WriterClosed {
+					// Safe from data races, writerChannel unbuffered
+					d.WriterClosed = true
+					// close(d.writerChannel)
+				}
+				d.writerMutex.Unlock()
 				/*
 					if !d.Disconnect() {
 						// d.Disconnected = true
@@ -70,11 +77,26 @@ func Writer(d *Door) {
 							close(d.writerChannel)
 						}
 					*/
-					d.SafeWriterClose()
-					log.Println("~Writer")
-					return
+					/*
+						d.writerMutex.Lock()
+						if !d.WriterClosed {
+							d.WriterClosed = true
+							close(d.writerChannel)
+							//
+						}
+					*/
+					Closed = true
+					// d.SafeWriterClose()
+					// log.Println("~Writer")
+					// return
+					continue
+				}
 
+				if Closed {
+					log.Println("ignoring write output.")
+					continue
 				}
+
 				if strings.HasSuffix(output, RestorePos) {
 					output += Color(d.LastColor...)
 
@@ -83,10 +105,19 @@ func Writer(d *Door) {
 				}
 				// log.Println("write output")
 				buffer := []byte(output)
+
 				n, err := syscall.Write(handle, buffer)
 				if (err != nil) || (n != len(buffer)) {
 					log.Println("closeChannel")
-					d.SafeWriterClose()
+					Closed = true
+					d.writerMutex.Lock()
+					if !d.WriterClosed {
+						d.WriterClosed = true
+						// close(d.writerChannel)
+					}
+					d.writerMutex.Unlock()
+					//d.SafeWriterClose()
+
 					/*
 						d.WriterClosed = true
 						close(d.writerChannel)
@@ -98,9 +129,10 @@ func Writer(d *Door) {
 							close(d.writerChannel)
 						}
 					*/
-					log.Println("~Writer")
-					return
+					// log.Println("~Writer")
+					// return
 				}
+
 			}
 		}
 	}