230 lines
5.6 KiB
Go
230 lines
5.6 KiB
Go
package session
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Input error sentinels. These replace the TIMEOUT and NO_CARRIER
|
|
// return codes that the original TAG-BBS passed through every function.
|
|
// In Go, we use the context for cancellation and return these as errors
|
|
// only when the caller needs to distinguish the reason.
|
|
var (
|
|
ErrTimeout = fmt.Errorf("input timeout")
|
|
ErrDisconnected = fmt.Errorf("disconnected")
|
|
)
|
|
|
|
// ReadChar reads a single byte from the client with a timeout.
|
|
// This is the direct replacement for ReadChar() in CONSOLE.C, which
|
|
// was the central input function that Wait()'d on serial, console,
|
|
// and timer signals simultaneously.
|
|
//
|
|
// In Go, the equivalent is a select on the input channel (fed by the
|
|
// readLoop goroutine), a timer, and the context's Done channel.
|
|
//
|
|
// A timeout of 0 means no timeout (wait forever, or until disconnect).
|
|
func (s *Session) ReadChar(timeout time.Duration) (byte, error) {
|
|
// Check context first — if the session is already cancelled,
|
|
// don't block on input.
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return 0, ErrDisconnected
|
|
default:
|
|
}
|
|
|
|
if timeout == 0 {
|
|
// No timeout — block until input or disconnect
|
|
select {
|
|
case b, ok := <-s.inputCh:
|
|
if !ok {
|
|
return 0, ErrDisconnected
|
|
}
|
|
return b, nil
|
|
case <-s.ctx.Done():
|
|
return 0, ErrDisconnected
|
|
}
|
|
}
|
|
|
|
// With timeout — the original used SetTimer() and waited on
|
|
// TimerSig alongside the input signals. Same concept here.
|
|
timer := time.NewTimer(timeout)
|
|
defer timer.Stop()
|
|
|
|
select {
|
|
case b, ok := <-s.inputCh:
|
|
if !ok {
|
|
return 0, ErrDisconnected
|
|
}
|
|
return b, nil
|
|
case <-timer.C:
|
|
return 0, ErrTimeout
|
|
case <-s.ctx.Done():
|
|
return 0, ErrDisconnected
|
|
}
|
|
}
|
|
|
|
// ReadLine reads a full line of input (terminated by Enter) with echo
|
|
// and basic line editing. Returns the entered string, or an error on
|
|
// timeout/disconnect.
|
|
//
|
|
// This replaces LineInput() from the original, which handled echo,
|
|
// backspace, and had a pre-fill capability (the first argument was
|
|
// a default string displayed in the input field). We keep the same
|
|
// semantics.
|
|
//
|
|
// maxLen limits the input length (like the original's fixed char arrays).
|
|
// A value of 0 means no limit.
|
|
// timeout is the idle timeout — resets on each keypress.
|
|
func (s *Session) ReadLine(prefill string, maxLen int, timeout time.Duration) (string, error) {
|
|
buf := []byte(prefill)
|
|
|
|
// Display the prefill text if any
|
|
if len(prefill) > 0 {
|
|
if err := s.WriteString(prefill); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
for {
|
|
ch, err := s.ReadChar(timeout)
|
|
if err != nil {
|
|
return string(buf), err
|
|
}
|
|
|
|
switch {
|
|
case ch == '\r' || ch == '\n':
|
|
// Enter — return the line
|
|
s.WriteString("\r\n")
|
|
return string(buf), nil
|
|
|
|
case ch == 127 || ch == 8:
|
|
// Backspace or DEL — erase last character
|
|
// Same as the original: PutStr("\b \b")
|
|
if len(buf) > 0 {
|
|
buf = buf[:len(buf)-1]
|
|
s.WriteString("\b \b")
|
|
}
|
|
|
|
case ch == 24: // Ctrl-X — erase entire line
|
|
// Same as the original editor's line-kill
|
|
for len(buf) > 0 {
|
|
buf = buf[:len(buf)-1]
|
|
s.WriteString("\b \b")
|
|
}
|
|
|
|
case ch == 23: // Ctrl-W — erase last word
|
|
// Delete trailing spaces first, then back to previous space.
|
|
// Same logic as the original EDIT.C's Ctrl-W handler.
|
|
for len(buf) > 0 && buf[len(buf)-1] == ' ' {
|
|
buf = buf[:len(buf)-1]
|
|
s.WriteString("\b \b")
|
|
}
|
|
for len(buf) > 0 && buf[len(buf)-1] != ' ' {
|
|
buf = buf[:len(buf)-1]
|
|
s.WriteString("\b \b")
|
|
}
|
|
|
|
case ch >= 32 && ch < 127:
|
|
// Printable character
|
|
if maxLen > 0 && len(buf) >= maxLen {
|
|
continue // At limit, ignore
|
|
}
|
|
buf = append(buf, ch)
|
|
s.Write([]byte{ch})
|
|
|
|
default:
|
|
// Control characters — ignore
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
// ReadLineNoEcho reads a line of input without echoing characters.
|
|
// Used for password entry. Displays asterisks instead of the actual
|
|
// characters.
|
|
func (s *Session) ReadLineNoEcho(maxLen int, timeout time.Duration) (string, error) {
|
|
var buf []byte
|
|
|
|
for {
|
|
ch, err := s.ReadChar(timeout)
|
|
if err != nil {
|
|
return string(buf), err
|
|
}
|
|
|
|
switch {
|
|
case ch == '\r' || ch == '\n':
|
|
s.WriteString("\r\n")
|
|
return string(buf), nil
|
|
|
|
case ch == 127 || ch == 8:
|
|
if len(buf) > 0 {
|
|
buf = buf[:len(buf)-1]
|
|
s.WriteString("\b \b")
|
|
}
|
|
|
|
case ch >= 32 && ch < 127:
|
|
if maxLen > 0 && len(buf) >= maxLen {
|
|
continue
|
|
}
|
|
buf = append(buf, ch)
|
|
s.Write([]byte{'*'})
|
|
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
// ReadKey reads a single keypress and returns it immediately without
|
|
// waiting for Enter. Used for single-character menu selections.
|
|
// This is the classic BBS "press a key" input — the original's
|
|
// ReadChar(120L) in the menu dispatch loops.
|
|
func (s *Session) ReadKey(timeout time.Duration) (byte, error) {
|
|
for {
|
|
ch, err := s.ReadChar(timeout)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Skip null bytes (can come from function key sequences)
|
|
if ch == 0 {
|
|
continue
|
|
}
|
|
|
|
return ch, nil
|
|
}
|
|
}
|
|
|
|
// WaitForKey displays a prompt and waits for any keypress.
|
|
// The classic "Press any key to continue..." pattern.
|
|
func (s *Session) WaitForKey(prompt string, timeout time.Duration) error {
|
|
if prompt != "" {
|
|
if err := s.WriteString(prompt); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err := s.ReadKey(timeout)
|
|
return err
|
|
}
|
|
|
|
// Confirm asks a yes/no question and returns the result.
|
|
// Keeps prompting until Y or N is pressed.
|
|
func (s *Session) Confirm(prompt string, timeout time.Duration) (bool, error) {
|
|
s.WriteString(prompt)
|
|
for {
|
|
ch, err := s.ReadKey(timeout)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
switch ch {
|
|
case 'y', 'Y':
|
|
s.WriteString("Yes\r\n")
|
|
return true, nil
|
|
case 'n', 'N', '\r', '\n':
|
|
s.WriteString("No\r\n")
|
|
return false, nil
|
|
}
|
|
}
|
|
}
|