// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. //go:build windows // Windows console equivalent of the Unix termios helpers. Rather than // dealing with ReadConsoleInput's VK_* keycodes we turn on // ENABLE_VIRTUAL_TERMINAL_INPUT / PROCESSING on modern Windows (10+), // which makes the console speak ANSI — same byte stream the Unix path // sees. Everything above this file stays platform-neutral. package hbrt import ( "os" "syscall" "unsafe" ) const ( enableProcessedInput = 0x0001 enableLineInput = 0x0002 enableEchoInput = 0x0004 enableVirtualTerminalInput = 0x0200 enableProcessedOutput = 0x0001 enableVirtualTerminalProcessing = 0x0004 ) var ( kernel32 = syscall.NewLazyDLL("kernel32.dll") procGetConsoleMode = kernel32.NewProc("GetConsoleMode") procSetConsoleMode = kernel32.NewProc("SetConsoleMode") procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") ) type smallRect struct { Left, Top, Right, Bottom int16 } type consoleScreenBufferInfo struct { Size struct{ X, Y int16 } CursorPosition struct{ X, Y int16 } Attributes uint16 Window smallRect MaximumWindowSize struct{ X, Y int16 } } func getConsoleMode(h syscall.Handle) (uint32, bool) { var mode uint32 r, _, _ := procGetConsoleMode.Call(uintptr(h), uintptr(unsafe.Pointer(&mode))) return mode, r != 0 } func setConsoleMode(h syscall.Handle, mode uint32) { _, _, _ = procSetConsoleMode.Call(uintptr(h), uintptr(mode)) } // Remember the input mode at program start so we can restore it. var ( savedInMode uint32 savedOutMode uint32 termSaved bool ) func restoreCooked() { hIn := syscall.Handle(os.Stdin.Fd()) hOut := syscall.Handle(os.Stdout.Fd()) if !termSaved { if m, ok := getConsoleMode(hIn); ok { savedInMode = m } if m, ok := getConsoleMode(hOut); ok { savedOutMode = m } termSaved = true } // Cooked: line input + echo, plus VT so our ANSI rendering still works. inMode := (savedInMode | enableProcessedInput | enableLineInput | enableEchoInput | enableVirtualTerminalInput) setConsoleMode(hIn, inMode) outMode := savedOutMode | enableProcessedOutput | enableVirtualTerminalProcessing setConsoleMode(hOut, outMode) } func reenterRaw() { hIn := syscall.Handle(os.Stdin.Fd()) hOut := syscall.Handle(os.Stdout.Fd()) // Raw: no line input, no echo, but keep VT so F-keys arrive as // ESC[15~ etc. and our ANSI writes still render. inMode := (savedInMode &^ (enableLineInput | enableEchoInput | enableProcessedInput)) | enableVirtualTerminalInput setConsoleMode(hIn, inMode) outMode := savedOutMode | enableProcessedOutput | enableVirtualTerminalProcessing setConsoleMode(hOut, outMode) } func termSize() (int, int) { hOut := syscall.Handle(os.Stdout.Fd()) var info consoleScreenBufferInfo r, _, _ := procGetConsoleScreenBufferInfo.Call(uintptr(hOut), uintptr(unsafe.Pointer(&info))) if r == 0 { return 24, 80 } cols := int(info.Window.Right - info.Window.Left + 1) rows := int(info.Window.Bottom - info.Window.Top + 1) return rows, cols } // readDebugKey reads a single key with raw console mode. VT input is // enabled so F-keys / arrows arrive as the same ANSI escape sequences // the Unix build expects, and decodeDebugKey classifies them. func readDebugKey() int { hIn := syscall.Handle(os.Stdin.Fd()) var before uint32 if m, ok := getConsoleMode(hIn); ok { before = m } rawMode := (before &^ (enableLineInput | enableEchoInput | enableProcessedInput)) | enableVirtualTerminalInput setConsoleMode(hIn, rawMode) defer setConsoleMode(hIn, before) buf := make([]byte, 8) n, _ := os.Stdin.Read(buf) return decodeDebugKey(buf, n) }