Files
five/hbrtl/rawtty.go
Charles KWON OhJun 272576f6ce feat: Harbour-compatible VM shutdown sequence
Implements full cleanup on program exit (normal, Ctrl+C, crash):
- EXIT PROCEDURE auto-execution (reverse module order)
- AtExit callback registry (LIFO order)
- All WorkAreas auto-close (child before parent)
- Terminal restore (raw → normal) on signal/exit
- Static variables clear
- Signal handlers (SIGINT, SIGTERM) for clean shutdown
- shutdown.go: Harbour hb_vmQuit() 25-step sequence adapted for Five
- vm.go: Run() now calls Shutdown() via defer
- rawtty.go: terminal restore registered with shutdown system

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 10:07:42 +09:00

138 lines
3.1 KiB
Go

// 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)
}