// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // Raw terminal for Five. Uses /dev/tty opened fresh each ReadKey call. package hbrtl import ( "five/hbrt" "os" "syscall" "unsafe" ) var ( origTermios syscall.Termios rawModeOn bool stdinFd int ) // InitRawTerminal sets raw mode on stdin. func InitRawTerminal() { if rawModeOn { return } stdinFd = int(os.Stdin.Fd()) // Save original _, _, e := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(stdinFd), 0x5401, uintptr(unsafe.Pointer(&origTermios)), 0, 0, 0) if e != 0 { return } // Set raw on stdin raw := origTermios raw.Lflag &^= syscall.ICANON | syscall.ECHO | syscall.ISIG raw.Oflag &^= syscall.OPOST raw.Cc[syscall.VMIN] = 1 raw.Cc[syscall.VTIME] = 0 syscall.Syscall6(syscall.SYS_IOCTL, uintptr(stdinFd), 0x5402, uintptr(unsafe.Pointer(&raw)), 0, 0, 0) rawModeOn = true // Register terminal restore with VM shutdown system hbrt.SetTerminalRestore(RestoreTerminal) } // RestoreTerminal restores original terminal. func RestoreTerminal() { if !rawModeOn { return } syscall.Syscall6(syscall.SYS_IOCTL, uintptr(stdinFd), 0x5402, uintptr(unsafe.Pointer(&origTermios)), 0, 0, 0) rawModeOn = false } // IsRawMode returns true if raw mode is active. func IsRawMode() bool { return rawModeOn } // ReadKey reads one key. Opens /dev/tty fresh each time to avoid buffered data. func ReadKey() int { if !rawModeOn { InitRawTerminal() } os.Stdout.Sync() // Open /dev/tty fresh — no stale data possible fd, err := syscall.Open("/dev/tty", syscall.O_RDONLY, 0) if err != nil { return 27 // ESC } defer syscall.Close(fd) // Set this fd to raw too var t syscall.Termios syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5401, uintptr(unsafe.Pointer(&t)), 0, 0, 0) t.Lflag &^= syscall.ICANON | syscall.ECHO | syscall.ISIG t.Cc[syscall.VMIN] = 1 t.Cc[syscall.VTIME] = 0 syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5402, uintptr(unsafe.Pointer(&t)), 0, 0, 0) // Flush input buffer (TCFLSH = 0x540B, TCIFLUSH = 0) syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), 0x540B, 0) buf := make([]byte, 1) n, e := syscall.Read(fd, buf) if e != nil || n == 0 { return 27 } b := buf[0] if b == 27 { // ESC — bare ESC or sequence? // Short timeout to check for sequence var t2 syscall.Termios syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5401, uintptr(unsafe.Pointer(&t2)), 0, 0, 0) t2.Cc[syscall.VMIN] = 0 t2.Cc[syscall.VTIME] = 1 // 100ms syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5402, uintptr(unsafe.Pointer(&t2)), 0, 0, 0) n2, _ := syscall.Read(fd, buf) if n2 == 0 { return 27 // bare ESC } if buf[0] == '[' { n3, _ := syscall.Read(fd, buf) if n3 == 0 { return 27 } switch buf[0] { case 'A': return 'A' case 'B': return 'B' case 'C': return 'C' case 'D': return 'D' case '5': syscall.Read(fd, buf) return '5' case '6': syscall.Read(fd, buf) return '6' case 'H': return 'H' case 'F': return 'F' default: return 27 } } return 27 } return int(b) }