mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-26 09:35:52 +00:00
155 lines
5 KiB
Go
155 lines
5 KiB
Go
package session
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"syscall"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// TerminalMode represents terminal mode settings
|
|
type TerminalMode struct {
|
|
Raw bool
|
|
Echo bool
|
|
LineMode bool
|
|
FlowControl bool
|
|
}
|
|
|
|
// configurePTYTerminal configures the PTY terminal attributes to match node-pty behavior
|
|
// This ensures proper terminal behavior with flow control, signal handling, and line editing
|
|
func configurePTYTerminal(ptyFile *os.File) error {
|
|
fd := int(ptyFile.Fd())
|
|
|
|
// Get current terminal attributes
|
|
termios, err := unix.IoctlGetTermios(fd, ioctlGetTermios)
|
|
if err != nil {
|
|
// Non-fatal: some systems may not support this
|
|
debugLog("[DEBUG] Could not get terminal attributes, using defaults: %v", err)
|
|
return nil
|
|
}
|
|
|
|
// Match node-pty's default behavior: keep most settings from the parent terminal
|
|
// but ensure proper signal handling and character processing
|
|
|
|
// Ensure proper input processing to match node-pty behavior
|
|
// ICRNL: Map CR to NL on input (important for Enter key)
|
|
// IXON: Enable XON/XOFF flow control on output
|
|
// IXANY: Allow any character to restart output
|
|
// IMAXBEL: Ring bell on input queue full
|
|
// BRKINT: Send SIGINT on break
|
|
termios.Iflag |= unix.ICRNL | unix.IXON | unix.IXANY | unix.IMAXBEL | unix.BRKINT
|
|
// Note: We KEEP flow control enabled to match node-pty behavior
|
|
|
|
// Configure output flags
|
|
// OPOST: Enable output processing
|
|
// ONLCR: Map NL to CR-NL on output (important for proper line endings)
|
|
termios.Oflag |= unix.OPOST | unix.ONLCR
|
|
|
|
// Configure control flags to match node-pty
|
|
// CS8: 8-bit characters
|
|
// CREAD: Enable receiver
|
|
// HUPCL: Hang up on last close
|
|
termios.Cflag |= unix.CS8 | unix.CREAD | unix.HUPCL
|
|
termios.Cflag &^= unix.PARENB // Disable parity
|
|
|
|
// Configure local flags to match node-pty behavior exactly
|
|
// ISIG: Enable signal generation (SIGINT on Ctrl+C, etc)
|
|
// ICANON: Enable canonical mode (line editing)
|
|
// IEXTEN: Enable extended functions
|
|
// ECHO: Enable echo (node-pty enables this!)
|
|
// ECHOE: Echo erase character as BS-SP-BS
|
|
// ECHOK: Echo KILL by erasing line
|
|
// ECHOKE: BS-SP-BS entire line on KILL
|
|
// ECHOCTL: Echo control characters as ^X
|
|
termios.Lflag |= unix.ISIG | unix.ICANON | unix.IEXTEN | unix.ECHO | unix.ECHOE | unix.ECHOK | unix.ECHOKE | unix.ECHOCTL
|
|
|
|
// Set control characters to match node-pty defaults exactly
|
|
termios.Cc[unix.VEOF] = 4 // Ctrl+D
|
|
termios.Cc[unix.VERASE] = 0x7f // DEL (127)
|
|
termios.Cc[unix.VWERASE] = 23 // Ctrl+W (word erase)
|
|
termios.Cc[unix.VKILL] = 21 // Ctrl+U
|
|
termios.Cc[unix.VREPRINT] = 18 // Ctrl+R (reprint line)
|
|
termios.Cc[unix.VINTR] = 3 // Ctrl+C
|
|
termios.Cc[unix.VQUIT] = 0x1c // Ctrl+\ (28)
|
|
termios.Cc[unix.VSUSP] = 26 // Ctrl+Z
|
|
termios.Cc[unix.VSTART] = 17 // Ctrl+Q (XON)
|
|
termios.Cc[unix.VSTOP] = 19 // Ctrl+S (XOFF)
|
|
termios.Cc[unix.VLNEXT] = 22 // Ctrl+V (literal next)
|
|
termios.Cc[unix.VDISCARD] = 15 // Ctrl+O (discard output)
|
|
termios.Cc[unix.VMIN] = 1 // Minimum characters for read
|
|
termios.Cc[unix.VTIME] = 0 // Timeout for read
|
|
|
|
// Set platform-specific control characters for macOS
|
|
// These constants might not exist on all platforms, so we check
|
|
if vdsusp, ok := getControlCharConstant("VDSUSP"); ok {
|
|
termios.Cc[vdsusp] = 25 // Ctrl+Y (delayed suspend)
|
|
}
|
|
if vstatus, ok := getControlCharConstant("VSTATUS"); ok {
|
|
termios.Cc[vstatus] = 20 // Ctrl+T (status)
|
|
}
|
|
|
|
// Apply the terminal attributes
|
|
if err := unix.IoctlSetTermios(fd, ioctlSetTermios, termios); err != nil {
|
|
// Non-fatal: log but continue
|
|
debugLog("[DEBUG] Could not set terminal attributes: %v", err)
|
|
return nil
|
|
}
|
|
|
|
debugLog("[DEBUG] PTY terminal configured to match node-pty defaults with echo and flow control enabled")
|
|
return nil
|
|
}
|
|
|
|
// setPTYSize sets the window size of the PTY
|
|
func setPTYSize(ptyFile *os.File, cols, rows uint16) error {
|
|
fd := int(ptyFile.Fd())
|
|
|
|
ws := &unix.Winsize{
|
|
Row: rows,
|
|
Col: cols,
|
|
Xpixel: 0,
|
|
Ypixel: 0,
|
|
}
|
|
|
|
if err := unix.IoctlSetWinsize(fd, unix.TIOCSWINSZ, ws); err != nil {
|
|
return fmt.Errorf("failed to set PTY size: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getPTYSize gets the current window size of the PTY
|
|
func getPTYSize(ptyFile *os.File) (cols, rows uint16, err error) {
|
|
fd := int(ptyFile.Fd())
|
|
|
|
ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ)
|
|
if err != nil {
|
|
return 0, 0, fmt.Errorf("failed to get PTY size: %w", err)
|
|
}
|
|
|
|
return ws.Col, ws.Row, nil
|
|
}
|
|
|
|
// sendSignalToPTY sends a signal to the PTY process group
|
|
func sendSignalToPTY(ptyFile *os.File, signal syscall.Signal) error {
|
|
fd := int(ptyFile.Fd())
|
|
|
|
// Get the process group ID of the PTY
|
|
pgid, err := unix.IoctlGetInt(fd, unix.TIOCGPGRP)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get PTY process group: %w", err)
|
|
}
|
|
|
|
// Send signal to the process group
|
|
if err := syscall.Kill(-pgid, signal); err != nil {
|
|
return fmt.Errorf("failed to send signal to PTY process group: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// isTerminal checks if a file descriptor is a terminal
|
|
func isTerminal(fd int) bool {
|
|
_, err := unix.IoctlGetTermios(fd, ioctlGetTermios)
|
|
return err == nil
|
|
}
|