package door import ( "fmt" "log" "strconv" "strings" "time" "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 UNKNOWN ) //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 } // ReadRune fails on IAC AYT => unicode.ReplacementChar X 2 // getch -> ReadRune Low Level func (d *Door) ReadRune() (rune, error) { var r rune if d.ReaderClosed { return r, ErrDisconnected } for { select { case r = <-d.readerChannel: return r, nil case <-time.After(ReaderInterval): return r, ErrTimeout } } } func (d *Door) RunePushback(r rune) { d.Pushback.Push(r) } // 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? // Low level read key function. // 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: if ok { return int(res) } else { d.Disconnected = true // atomic.StoreInt32(&d.Disconnected, 1) return -2 } case <-time.After(time.Duration(100) * time.Millisecond): return -1 } } func (d *Door) getkey_or_pushback() int { if !d.Pushback.Empty() { return d.Pushback.Pop() } if false { var key int = d.getch() log.Printf("%d / %X\n", key, key) return key } else { return d.getch() } } // Return key received, or XKEY_* values. // -1 timeout/no key // -2 hangup // -3 out of time func (d *Door) GetKey() int { var c, c2 int if d.Disconnect() { return -2 } if d.TimeLeft() < 0 { return -3 } c = d.getkey_or_pushback() if c < 0 { return c } // We get 0x0d 0x00, or 0x0d 0x0a from syncterm. if c == 0x0d { c2 = d.getkey_or_pushback() if c2 > 0 { // wasn't an error if c2 != 0x00 && c2 != 0x0a { // wasn't 0x00 or 0x0a d.Pushback.Push(c2) // log.Printf("Push 0x0d trailer %d / %x\n", c2, c2) } } return c } if c == 0 { // possibly doorway mode var tries int = 0 c2 = d.getkey_or_pushback() for c2 < 0 { if tries > 7 { return c } c2 = d.getkey_or_pushback() tries++ } switch c2 { case 0x50: return DOWN_ARROW case 0x48: return UP_ARROW case 0x4b: return LEFT_ARROW case 0x4d: return RIGHT_ARROW case 0x47: return HOME case 0x4f: return END case 0x49: return PAGE_UP case 0x51: return PAGE_DOWN case 0x3b: return F1 case 0x3c: return F2 case 0x3d: return F3 case 0x3e: return F4 case 0x3f: return F5 case 0x40: return F6 case 0x41: return F7 case 0x42: return F8 case 0x43: return F9 case 0x44: return F10 case 0x52: return INSERT case 0x53: return DELETE default: log.Printf("ERROR Doorway mode: 0x00 %x\n", c2) return XKEY_UNKNOWN } } if c == 0x1b { // Escape key? c2 = d.getkey_or_pushback() if c2 < 0 { // Just escape key return c } var extended []byte = make([]byte, 1) extended[0] = byte(c2) // string = string(byte(c2)) c2 = d.getkey_or_pushback() for c2 > 0 { if c2 == 0x1b { d.Pushback.Push(c2) break } extended = append(extended, byte(c2)) // += string(byte(c2)) var has bool c2, has = extendedKeys[string(extended)] if has { // break out here if \x1b[ + letter or @ // break out if \x1b[ + digits + ~ // break out if \x1bO + letter return c2 } c2 = d.getkey_or_pushback() } if strings.HasPrefix(string(extended), "[M") && len(extended) == 5 { // log.Printf("MOUSE Extended %#v\n", extended) var mouse Mouse = Mouse{Button: int8(extended[2]) - ' ' + 1, X: int8(extended[3]) - '!' + 1, Y: int8(extended[4]) - '!' + 1} d.AddMouse(mouse) log.Printf("MOUSE %d (%d,%d)\n", mouse.Button, mouse.X, mouse.Y) return XKEY_MOUSE } log.Printf("ERROR Extended %#v\n", extended) return XKEY_UNKNOWN } return c } func (d *Door) Key() int { return d.WaitKey(Inactivity, 0) } // usecs = Microseconds func (d *Door) WaitKey(secs int64, usecs int64) int { if d.Disconnect() { return -2 } if !d.Pushback.Empty() { return d.GetKey() } var timeout time.Duration = time.Duration(secs)*time.Second + time.Duration(usecs)*time.Microsecond select { case res, ok := <-d.readerChannel: if ok { d.Pushback.Push(int(res)) return d.GetKey() } else { // Reader Closed d.Disconnected = true // If I wrap this with if !d.WriterClosed .. races ? // Why can't I do this? This isn't a go routine... if !d.WriterClosed { d.writerChannel <- "" } // d.closeChannel <- struct{}{} // atomic.StoreInt32(&d.Disconnected, 1) return -2 } case <-time.After(timeout): 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. func DisplayInput(max int) string { return strings.Repeat(" ", max) + strings.Repeat("\x08", max) } // Input a string of max length. // This displays the input area if a bg color was set. // This handles timeout, input, backspace, and enter. func (d *Door) Input(max int) string { var line string // draw input area d.Write(DisplayInput(max)) var r rune var ex Extended var err error for { r, ex, err = d.WaitKey(DefaultTimeout) if err != nil { // timeout/hangup return "" } if ex != NOP { continue } if strconv.IsPrint(r) { if len(line) < max { d.Write(string(r)) line += string(r) } else { d.Write("\x07") } } else { // Non-print switch r { case 0x7f, 0x08: if len(line) > 0 { d.Write("\x08 \x08") line = line[:len(line)-1] } case 0x0d: return line } } } } func (d *Door) GetOneOf(possible string) rune { var r rune var err error for { r, _, err = d.WaitKey(DefaultTimeout) if err != nil { return rune(0) } r := unicode.ToUpper(r) if strings.ContainsRune(possible, r) { // return upper case rune return r } /* c = strings.IndexRune(possible, r) if c != -1 { return c } */ } }