Bladeren bron

Working tests, -race ok.

Next: cleanup code, there's lots of debug output with this.
Steve Thielemann 2 jaren geleden
bovenliggende
commit
9a5ad0a854
6 gewijzigde bestanden met toevoegingen van 511 en 163 verwijderingen
  1. 20 14
      door/door.go
  2. 86 0
      door/extended.go
  3. 34 142
      door/input.go
  4. 352 4
      door/input_linux.go
  5. 3 3
      door/input_test.go
  6. 16 0
      door/utilities.go

+ 20 - 14
door/door.go

@@ -105,23 +105,29 @@ type DropfileConfig struct {
 	Node           int    // BBS Node number
 }
 
+type ReaderData struct {
+	R   rune
+	Ex  Extended
+	Err error
+}
+
 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 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
+	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 ReaderData // 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
@@ -385,7 +391,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 rune, 16) // was 8 ?
+	d.readerChannel = make(chan ReaderData, 16) // was 8 ?
 	/*
 		Ok, here's the issue.  This blocks the go reader when this is full.
 		It seems like it would be better to have a channel that receives

+ 86 - 0
door/extended.go

@@ -0,0 +1,86 @@
+package door
+
+type Extended int8
+
+const (
+	NOP Extended = iota
+	UP_ARROW
+	DOWN_ARROW
+	RIGHT_ARROW
+	LEFT_ARROW
+	HOME
+	END
+	PAGE_UP
+	PAGE_DOWN
+	INSERT
+	DELETE
+	F1
+	F2
+	F3
+	F4
+	F5
+	F6
+	F7
+	F8
+	F9
+	F10
+	F11
+	F12
+	MOUSE
+	CURSOR
+	ALT_a
+	ALT_b
+	ALT_c
+	ALT_d
+	ALT_e
+	ALT_f
+	ALT_g
+	ALT_h
+	ALT_i
+	ALT_j
+	ALT_k
+	ALT_l
+	ALT_m
+	ALT_n
+	ALT_o
+	ALT_p
+	ALT_q
+	ALT_r
+	ALT_s
+	ALT_t
+	ALT_u
+	ALT_v
+	ALT_w
+	ALT_x
+	ALT_y
+	ALT_z
+	ALT_A
+	ALT_B
+	ALT_C
+	ALT_D
+	ALT_E
+	ALT_F
+	ALT_G
+	ALT_H
+	ALT_I
+	ALT_J
+	ALT_K
+	ALT_L
+	ALT_M
+	ALT_N
+	ALT_O
+	ALT_P
+	ALT_Q
+	ALT_R
+	ALT_S
+	ALT_T
+	ALT_U
+	ALT_V
+	ALT_W
+	ALT_X
+	ALT_Y
+	ALT_Z
+	UNKNOWN
+)
+
+//go:generate stringer -type=Extended

+ 34 - 142
door/input.go

@@ -9,135 +9,15 @@ import (
 	"unicode"
 )
 
-type Extended int8
-
-const (
-	NOP Extended = iota
-	UP_ARROW
-	DOWN_ARROW
-	RIGHT_ARROW
-	LEFT_ARROW
-	HOME
-	END
-	PAGE_UP
-	PAGE_DOWN
-	INSERT
-	DELETE
-	F1
-	F2
-	F3
-	F4
-	F5
-	F6
-	F7
-	F8
-	F9
-	F10
-	F11
-	F12
-	MOUSE
-	CURSOR
-	ALT_a
-	ALT_b
-	ALT_c
-	ALT_d
-	ALT_e
-	ALT_f
-	ALT_g
-	ALT_h
-	ALT_i
-	ALT_j
-	ALT_k
-	ALT_l
-	ALT_m
-	ALT_n
-	ALT_o
-	ALT_p
-	ALT_q
-	ALT_r
-	ALT_s
-	ALT_t
-	ALT_u
-	ALT_v
-	ALT_w
-	ALT_x
-	ALT_y
-	ALT_z
-	ALT_A
-	ALT_B
-	ALT_C
-	ALT_D
-	ALT_E
-	ALT_F
-	ALT_G
-	ALT_H
-	ALT_I
-	ALT_J
-	ALT_K
-	ALT_L
-	ALT_M
-	ALT_N
-	ALT_O
-	ALT_P
-	ALT_Q
-	ALT_R
-	ALT_S
-	ALT_T
-	ALT_U
-	ALT_V
-	ALT_W
-	ALT_X
-	ALT_Y
-	ALT_Z
-	UNKNOWN
-)
-
-//go:generate stringer -type=Extended
-
 const DEBUG_INPUT = true
 
-func extended_output(buffer []rune) string {
-	var output string = string(buffer)
-	output = strings.Replace(output, "\x1b", "^[", -1)
-	return output
-}
-
 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
-}
-
+/*
 // ReadRune fails on IAC AYT => unicode.ReplacementChar X 2
 
 // getch -> ReadRune  Low Level
@@ -399,33 +279,44 @@ func (d *Door) GetKey() (rune, Extended, error) {
 		return rune(0), UNKNOWN, nil
 	}
 
-	// AYT (Telnet Are You There?)
+	return r, ex, nil
+}
+
+*/
+// "[1":   XKEY_UNKNOWN, // Syncterm is lost, could be F1..F5?
+
+func (d *Door) WaitKey(timeout time.Duration) (rune, Extended, error) {
 	/*
-		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()
-			}
+		d.readerMutex.Lock()
+		if d.ReaderClosed {
+			d.readerMutex.Unlock()
+			return 0, NOP, ErrDisconnected
 		}
+		d.readerMutex.Unlock()
 	*/
 
-	return r, ex, nil
-}
+	// Probably faster to just read from closed channel and get ok = false.
 
-// "[1":   XKEY_UNKNOWN, // Syncterm is lost, could be F1..F5?
+	select {
+	case r, ok := <-d.readerChannel:
+		if ok {
+			if DEBUG_INPUT {
+				log.Println("WaitKey:", r)
+			}
+			// return bio.GetKey()
+			return r.R, r.Ex, r.Err
+		} else {
+			log.Println("WaitKey: Disconnected")
+			// Reader has closed.
+			// Disconnected = true
+			return 0, NOP, ErrDisconnected
+		}
+	case <-time.After(timeout):
+		return 0, NOP, ErrTimeout
+	}
+}
 
+/*
 // port over WaitKey
 func (d *Door) WaitKey(timeout time.Duration) (rune, Extended, error) {
 	// disconnected test
@@ -472,6 +363,7 @@ func (d *Door) WaitKey(timeout time.Duration) (rune, Extended, error) {
 		return rune(0), Extended(0), ErrTimeout
 	}
 }
+*/
 
 // Outputs spaces and backspaces
 // If you have set a background color, this shows the input area.

+ 352 - 4
door/input_linux.go

@@ -5,6 +5,7 @@ import (
 	"bytes"
 	"io"
 	"log"
+	"strings"
 	"syscall"
 	"time"
 	"unicode"
@@ -13,6 +14,13 @@ import (
 var ReaderInterval = time.Duration(200) * time.Millisecond
 var ReaderTimeval syscall.Timeval = syscall.Timeval{0, 200}
 
+// Output a nice string representation of the rune buffer.
+func extended_output(buffer []rune) string {
+	var output string = string(buffer)
+	output = strings.Replace(output, "\x1b", "^[", -1)
+	return output
+}
+
 // syscall.FdSet clear all
 func clearAll(fdSetPtr *syscall.FdSet) {
 	for index := range (*fdSetPtr).Bits {
@@ -25,13 +33,346 @@ func set(fdSetPtr *syscall.FdSet, fd int) {
 	(*fdSetPtr).Bits[fd/64] |= 1 << uint64(fd%64)
 }
 
+func RuneToInt8(r rune) int8 {
+	return int8(r)
+}
+
 const READ_SIZE = 16 // Size of read buffer
 
+var readerBuffer []rune
+
+// Translate these extended codes into these Extended values.
+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
+}
+
+func process(d *Door, newRune bool) {
+	// This is like GetKey
+	var rlen int
+	var r, r2 rune
+	var has2 bool
+	var done bool = false
+
+	for !done {
+		rlen = len(readerBuffer)
+		if rlen == 0 {
+			// Nothing to do here
+			return
+		}
+
+		log.Println("rlen:", rlen, "readerBuffer:", readerBuffer, "newRune:", newRune)
+
+		r = readerBuffer[0]
+		if rlen >= 2 {
+			r2 = readerBuffer[1]
+			has2 = true
+		} else {
+			r2 = unicode.ReplacementChar
+			has2 = false
+		}
+
+		// if !has2 and !newRune, then we're "done" (received everything,
+		// and there's nothing else coming...)
+
+		// fyneterm CR
+		if r == '\x0a' {
+			if !has2 && !newRune {
+				ArrayPop(&readerBuffer, 1)
+				d.readerChannel <- ReaderData{R: '\x0d', Ex: NOP, Err: nil}
+				return
+			}
+
+			if has2 {
+				ArrayPop(&readerBuffer, 1)
+				if r2 == '\x00' || r2 == '\x0a' {
+					ArrayPop(&readerBuffer, 1)
+				}
+				d.readerChannel <- ReaderData{R: '\x0d', Ex: NOP, Err: nil}
+			} else {
+				// We don't have a 2nd rune, and we haven't timed out.
+				return
+			}
+
+			continue
+		}
+
+		// We get 0x0d, 0x00, or 0x0d 0x0a from syncterm
+		if r == '\x0d' {
+			if !has2 && !newRune {
+				ArrayPop(&readerBuffer, 1)
+				d.readerChannel <- ReaderData{R: r, Ex: NOP, Err: nil}
+				return
+			}
+
+			if has2 {
+				ArrayPop(&readerBuffer, 1)
+				if r2 == '\x00' || r2 == '\x0a' {
+					ArrayPop(&readerBuffer, 1)
+				}
+				d.readerChannel <- ReaderData{R: r, Ex: NOP, Err: nil}
+			} else {
+				return
+			}
+
+			continue
+		}
+
+		if r == '\x00' {
+			// Possibly doorway mode - deprecated?
+			// syncterm does support this.
+
+			if !has2 && !newRune {
+				// timeout
+				ArrayPop(&readerBuffer, 1)
+				d.readerChannel <- ReaderData{R: r, Ex: NOP, Err: nil}
+				return
+			}
+
+			if has2 {
+				ArrayPop(&readerBuffer, 2)
+				switch r2 {
+				case '\x50':
+					d.readerChannel <- ReaderData{0, DOWN_ARROW, nil}
+				case '\x48':
+					d.readerChannel <- ReaderData{0, UP_ARROW, nil}
+				case '\x4b':
+					d.readerChannel <- ReaderData{0, LEFT_ARROW, nil}
+				case 0x4d:
+					d.readerChannel <- ReaderData{0, RIGHT_ARROW, nil}
+				case 0x47:
+					d.readerChannel <- ReaderData{0, HOME, nil}
+				case 0x4f:
+					d.readerChannel <- ReaderData{0, END, nil}
+				case 0x49:
+					d.readerChannel <- ReaderData{0, PAGE_UP, nil}
+				case 0x51:
+					d.readerChannel <- ReaderData{0, PAGE_DOWN, nil}
+				case 0x3b:
+					d.readerChannel <- ReaderData{0, F1, nil}
+				case 0x3c:
+					d.readerChannel <- ReaderData{0, F2, nil}
+				case 0x3d:
+					d.readerChannel <- ReaderData{0, F3, nil}
+				case 0x3e:
+					d.readerChannel <- ReaderData{0, F4, nil}
+				case 0x3f:
+					d.readerChannel <- ReaderData{0, F5, nil}
+				case 0x40:
+					d.readerChannel <- ReaderData{0, F6, nil}
+				case 0x41:
+					d.readerChannel <- ReaderData{0, F7, nil}
+				case 0x42:
+					d.readerChannel <- ReaderData{0, F8, nil}
+				case 0x43:
+					d.readerChannel <- ReaderData{0, F9, nil}
+				case 0x44:
+					d.readerChannel <- ReaderData{0, F10, nil}
+
+				case 0x45:
+					d.readerChannel <- ReaderData{0, F11, nil}
+				case 0x46:
+					d.readerChannel <- ReaderData{0, F12, nil}
+
+				case 0x52:
+					d.readerChannel <- ReaderData{0, INSERT, nil}
+				case 0x53:
+					d.readerChannel <- ReaderData{0, DELETE, nil}
+				default:
+					log.Printf("ERROR Doorway mode: 0x00 %x\n", r2)
+					d.readerChannel <- ReaderData{0, UNKNOWN, nil}
+
+				}
+			} else {
+				return
+			}
+			continue
+		} // end doorway mode
+
+		if r == '\x1b' {
+			// Escape key, or ?
+			// This is a little harder, since we don't know how many we need.
+			if !has2 && !newRune {
+				ArrayPop(&readerBuffer, 1)
+				d.readerChannel <- ReaderData{r, NOP, nil}
+				return
+			}
+
+			if has2 {
+				// We at least have the 2nd one...
+				if r2 == '\x1b' {
+					ArrayPop(&readerBuffer, 1)
+					d.readerChannel <- ReaderData{r, NOP, nil}
+					continue
+				}
+
+				// Can't distinguish between \x1bO and \x1bOP (F1)
+				/*
+					if unicode.IsLetter(r2) {
+						// ALT-KEY
+						if unicode.IsLower(r2) {
+							ArrayPop(&readerBuffer, 2)
+							ex := Extended(int(ALT_a) + int(r2-'a'))
+							d.readerChannel <- ReaderData{0, ex, nil}
+						} else {
+							// Must be upper
+							ArrayPop(&readerBuffer, 2)
+							ex := Extended(int(ALT_A) + int(r2-'A'))
+							d.readerChannel <- ReaderData{0, ex, nil}
+						}
+						continue
+					}
+				*/
+
+				var extended []rune = make([]rune, 0, 10)
+				extended = append(extended, r2)
+				var extlen = 2 // Length of codes to remove on successful match.
+				var pos = 2    // Where we get the next rune from
+				var isMouse bool = false
+				var found bool = false
+
+				log.Println(pos, readerBuffer)
+
+				for pos < rlen {
+					r2 = readerBuffer[pos]
+
+					pos++
+					extlen++
+
+					extended = append(extended, r2)
+
+					log.Println("0x1b LOOP pos:", pos, "extlen:", extlen, "extended:", string(extended))
+
+					ext, has := extendedKeys[string(extended)]
+					if has {
+						// Found it!
+						log.Println("Found Extended Match:", ext.String())
+						ArrayPop(&readerBuffer, extlen)
+						d.readerChannel <- ReaderData{0, ext, nil}
+						found = true
+						break
+					}
+
+					// Mouse codes can also contain letters.
+					if !isMouse && unicode.IsLetter(r2) {
+						// end of extended code, unless mouse!
+						log.Println("not mouse, is letter...")
+						if string(extended) == "[M" {
+							isMouse = true
+						} else {
+							break
+						}
+					}
+
+					if isMouse && len(extended) == 5 {
+						break
+					}
+				}
+
+				if found {
+					continue
+				}
+
+				log.Println("POS:", pos, "EXLEN:", extlen, "Extended:", extended, "readerBuffer:", readerBuffer)
+
+				log.Printf("(possible) Extended Code: [%s]", extended_output(extended))
+
+				var exString string = string(extended)
+				if strings.HasPrefix(exString, "[M") && len(extended) == 5 {
+					// Yes, "valid"
+					ArrayPop(&readerBuffer, extlen)
+					// 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()
+					d.LastMouse = append(d.LastMouse, mouse)
+					d.mcMutex.Unlock()
+					log.Println("Mouse:", mouse)
+					d.readerChannel <- ReaderData{0, MOUSE, nil}
+					continue
+				}
+
+				if strings.HasSuffix(exString, "R") {
+					// yes, "valid"
+					ArrayPop(&readerBuffer, extlen)
+					// Cursor Position information (or Shift-F3)
+					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()
+						d.LastCursor = append(d.LastCursor, cursor)
+						d.mcMutex.Unlock()
+						log.Println("Cursor Pos:", cursor)
+						d.readerChannel <- ReaderData{0, CURSOR, nil}
+						continue
+					} else {
+						log.Println("ERROR Cursor Pos:", extended)
+						d.readerChannel <- ReaderData{0, UNKNOWN, nil}
+						continue
+					}
+				}
+
+				// Ok, this LOOKS like something invalid...
+				if !newRune {
+					// Yes, this is invalid.
+					ArrayPop(&readerBuffer, extlen)
+					log.Println("ERROR Extended:", extended)
+					d.readerChannel <- ReaderData{0, UNKNOWN, nil}
+				} else {
+					log.Println("(Possibly) invalid extended:", extended)
+					return
+				}
+
+			} else {
+				return
+			}
+			continue
+		}
+
+		ArrayPop(&readerBuffer, 1)
+		d.readerChannel <- ReaderData{r, NOP, nil}
+		continue
+	}
+}
+
 // go routine Reader for input
 // This "times out" every ReaderTimeval
 func Reader(d *Door) {
 	// I need non-blocking reads here.
 
+	readerBuffer = make([]rune, 0, READ_SIZE*2)
+
 	defer func() {
 		log.Printf("~Reader\n")
 		if d.ReaderCanClose {
@@ -73,6 +414,7 @@ func Reader(d *Door) {
 
 		if v == 0 {
 			// timeout
+			process(d, false)
 			continue
 		}
 
@@ -118,12 +460,12 @@ func Reader(d *Door) {
 			input, _, err = runeread.ReadRune()
 
 			if err == io.EOF {
-				continue
+				break // continue
 			}
 			if err != nil {
 				log.Printf("ReadRune: %#v\n", err)
 				// errors EOF
-				continue // break for loop
+				break // continue // break for loop
 			}
 			if input == unicode.ReplacementChar {
 				runeread.UnreadRune()
@@ -131,15 +473,21 @@ func Reader(d *Door) {
 				if DEBUG_INPUT {
 					log.Printf("Reader (byte) >> %x\n", b)
 				}
-				d.readerChannel <- rune(b)
+				readerBuffer = append(readerBuffer, rune(b))
+				// d.readerChannel <- rune(b)
 			} else {
 				if DEBUG_INPUT {
 					log.Printf("Reader >> %x\n", input)
 				}
-				d.readerChannel <- input
+				readerBuffer = append(readerBuffer, input)
+				// d.readerChannel <- input
 			}
+			// process(d, true)
+
 		} // goto RuneRead
 
+		process(d, true)
+
 		// buffer = append(buffer, readone[0])
 
 		/*

+ 3 - 3
door/input_test.go

@@ -379,7 +379,7 @@ func TestDoorInputConnection(t *testing.T) {
 		var result string = string(buffer[:r])
 	*/
 
-	expected := "     \x08\x08\x08\x08\x0812345\x07\x07\x07\x07\x07"
+	expected := "     \x08\x08\x08\x08\x0812345\x07\x07\x07\x07"
 	if result != expected {
 		t.Errorf("Buffer Input(5): Expected %#v, got %#v\n", expected, result)
 	}
@@ -436,7 +436,7 @@ func TestDoorInputConnection(t *testing.T) {
 		}
 	*/
 
-	_, err = d.ReadRune() // GetKey()
+	_, _, err = d.WaitKey(time.Millisecond) // GetKey()
 
 	if err != ErrDisconnected {
 		t.Errorf("Expected ErrDisconnected, got %#v", err)
@@ -453,7 +453,7 @@ func TestDoorInputConnection(t *testing.T) {
 		t.Errorf("Input should return blank (hangup).")
 	}
 
-	_, err = d.ReadRune() // getch()
+	_, _, err = d.WaitKey(time.Millisecond) // getch()
 
 	if err != ErrDisconnected {
 		t.Errorf("Expected ErrDisconnected, got %#v", err)

+ 16 - 0
door/utilities.go

@@ -33,6 +33,22 @@ func ArrayDelete[T any](stack *[]T, pos int) (T, bool) {
 	return result, true
 }
 
+func ArrayPop[T any](stack *[]T, count int) bool {
+	/*
+	   https://stackoverflow.com/questions/33834742/remove-and-adding-elements-to-array-in-go-lang
+	   https://github.com/golang/go/wiki/SliceTricks
+	*/
+	if count < 0 || count > len(*stack) {
+		return false
+	}
+
+	copy((*stack)[0:], (*stack)[count:])
+	// var temp T
+	// (*stack)[len(*stack)-1] = temp
+	*stack = (*stack)[:len(*stack)-count]
+	return true
+}
+
 func SplitToInt(input string, sep string) []int {
 	var result []int
 	for _, number := range strings.Split(input, sep) {