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 }