189 lines
5.6 KiB
Go
189 lines
5.6 KiB
Go
package session
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ANSI escape code constants for terminal formatting.
|
|
// The original TAG-BBS used raw escape codes inline (e.g., "\033c\14\2331m").
|
|
// These named constants are easier to work with and self-documenting.
|
|
const (
|
|
AnsiReset = "\033[0m"
|
|
AnsiBold = "\033[1m"
|
|
AnsiDim = "\033[2m"
|
|
AnsiUnderline = "\033[4m"
|
|
AnsiBlink = "\033[5m"
|
|
AnsiReverse = "\033[7m"
|
|
|
|
// Foreground colors
|
|
AnsiFgBlack = "\033[30m"
|
|
AnsiFgRed = "\033[31m"
|
|
AnsiFgGreen = "\033[32m"
|
|
AnsiFgYellow = "\033[33m"
|
|
AnsiFgBlue = "\033[34m"
|
|
AnsiFgMagenta = "\033[35m"
|
|
AnsiFgCyan = "\033[36m"
|
|
AnsiFgWhite = "\033[37m"
|
|
|
|
// Bright foreground colors
|
|
AnsiFgBrightBlack = "\033[90m"
|
|
AnsiFgBrightRed = "\033[91m"
|
|
AnsiFgBrightGreen = "\033[92m"
|
|
AnsiFgBrightYellow = "\033[93m"
|
|
AnsiFgBrightBlue = "\033[94m"
|
|
AnsiFgBrightMagenta = "\033[95m"
|
|
AnsiFgBrightCyan = "\033[96m"
|
|
AnsiFgBrightWhite = "\033[97m"
|
|
|
|
// Background colors
|
|
AnsiBgBlack = "\033[40m"
|
|
AnsiBgRed = "\033[41m"
|
|
AnsiBgGreen = "\033[42m"
|
|
AnsiBgYellow = "\033[43m"
|
|
AnsiBgBlue = "\033[44m"
|
|
AnsiBgMagenta = "\033[45m"
|
|
AnsiBgCyan = "\033[46m"
|
|
AnsiBgWhite = "\033[47m"
|
|
|
|
// Cursor and screen control
|
|
AnsiClearScreen = "\033[2J"
|
|
AnsiCursorHome = "\033[H"
|
|
AnsiClearLine = "\033[2K"
|
|
AnsiSaveCursor = "\033[s"
|
|
AnsiLoadCursor = "\033[u"
|
|
)
|
|
|
|
// Write sends raw bytes to the client. This is the lowest-level output
|
|
// function — everything else builds on it.
|
|
//
|
|
// In the original, this was split across SerPutStr/SerPutChar (serial)
|
|
// and ConPutStr/ConPutChar (console), with IO_Flags controlling which
|
|
// outputs were active. Here we just write to the connection; the single
|
|
// output destination (the network connection) replaces the flag-based
|
|
// multiplexing.
|
|
func (s *Session) Write(data []byte) (int, error) {
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return 0, ErrDisconnected
|
|
default:
|
|
}
|
|
return s.conn.Write(data)
|
|
}
|
|
|
|
// WriteString sends a string to the client.
|
|
// This is the direct replacement for PutStr() in the original.
|
|
func (s *Session) WriteString(str string) error {
|
|
_, err := s.Write([]byte(str))
|
|
return err
|
|
}
|
|
|
|
// Printf formats and sends a string to the client.
|
|
// Convenience wrapper for the common sprintf+PutStr pattern that
|
|
// appeared throughout the original code.
|
|
func (s *Session) Printf(format string, args ...interface{}) error {
|
|
return s.WriteString(fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// NewLine sends a CR+LF. Telnet protocol requires \r\n for line endings,
|
|
// not just \n.
|
|
func (s *Session) NewLine() error {
|
|
return s.WriteString("\r\n")
|
|
}
|
|
|
|
// ClearScreen clears the terminal and moves cursor to home position.
|
|
// The original used "\014" (form feed) for this on the Amiga console;
|
|
// standard ANSI escape sequences are more portable.
|
|
func (s *Session) ClearScreen() error {
|
|
return s.WriteString(AnsiClearScreen + AnsiCursorHome)
|
|
}
|
|
|
|
// MoveCursor positions the cursor at the given column and row (1-based).
|
|
// Replaces StatCursorTo() from the original.
|
|
func (s *Session) MoveCursor(col, row int) error {
|
|
return s.Printf("\033[%d;%dH", row, col)
|
|
}
|
|
|
|
// Color sets the text color using ANSI codes. Accepts one or more
|
|
// ANSI constants which are concatenated. Reset with AnsiReset.
|
|
func (s *Session) Color(codes ...string) error {
|
|
return s.WriteString(strings.Join(codes, ""))
|
|
}
|
|
|
|
// HorizontalRule draws a line across the terminal width.
|
|
func (s *Session) HorizontalRule(ch rune) error {
|
|
width, _ := s.TerminalSize()
|
|
return s.WriteString(strings.Repeat(string(ch), width) + "\r\n")
|
|
}
|
|
|
|
// SendFile reads a file from disk and sends its contents to the client.
|
|
// This replaces MenuSend() from the original, which loaded text files
|
|
// (Logon.Text, MainMenu.Help, etc.) and sent them to the user.
|
|
//
|
|
// For ANSI art files (.ans), the raw bytes are sent directly — ANSI
|
|
// escape codes embedded in the file are interpreted by the client's
|
|
// terminal emulator.
|
|
//
|
|
// Returns nil if the file doesn't exist (missing screen files are not
|
|
// an error — the sysop simply hasn't installed one).
|
|
func (s *Session) SendFile(path string) error {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil // Missing screen files are silently skipped
|
|
}
|
|
return fmt.Errorf("reading screen file %s: %w", path, err)
|
|
}
|
|
|
|
// Ensure proper line endings for telnet — convert bare \n to \r\n
|
|
// but don't double-convert existing \r\n sequences.
|
|
content := strings.ReplaceAll(string(data), "\r\n", "\n")
|
|
content = strings.ReplaceAll(content, "\n", "\r\n")
|
|
|
|
_, err = s.Write([]byte(content))
|
|
return err
|
|
}
|
|
|
|
// Paginate sends text through a "more" pager. After each screenful
|
|
// (based on terminal height), it pauses and waits for a keypress.
|
|
// Press Enter or space to continue, Q to stop.
|
|
//
|
|
// This improves on the original's behavior where long output scrolled
|
|
// off screen with only Ctrl-S (pause) and Ctrl-Q (resume) for flow
|
|
// control — a serial-era convention that doesn't apply to TCP.
|
|
func (s *Session) Paginate(text string, timeout time.Duration) error {
|
|
_, height := s.TerminalSize()
|
|
pageLines := height - 2 // Leave room for the prompt
|
|
|
|
lines := strings.Split(text, "\n")
|
|
lineCount := 0
|
|
|
|
for _, line := range lines {
|
|
// Ensure telnet line ending
|
|
cleaned := strings.TrimRight(line, "\r")
|
|
if err := s.WriteString(cleaned + "\r\n"); err != nil {
|
|
return err
|
|
}
|
|
|
|
lineCount++
|
|
if lineCount >= pageLines {
|
|
s.WriteString("\r\n" + AnsiBold + "--- More ---" + AnsiReset + " ")
|
|
ch, err := s.ReadKey(timeout)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Clear the "more" prompt
|
|
s.WriteString("\r" + AnsiClearLine)
|
|
|
|
if ch == 'q' || ch == 'Q' {
|
|
return nil
|
|
}
|
|
lineCount = 0
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|