//go:build windows // +build windows package terminal import ( "fmt" "os" "syscall" "golang.org/x/sys/windows" // We're continuing to use this third-party library on Windows because it // has the additional IsCygwinTerminal function, which includes some useful // heuristics for recognizing when a pipe seems to be connected to a // legacy terminal emulator on Windows versions that lack true pty support. // We now use golang.org/x/term's functionality on other platforms. isatty "github.com/mattn/go-isatty" ) func configureOutputHandle(f *os.File) (*OutputStream, error) { ret := &OutputStream{ File: f, } if fd := f.Fd(); isatty.IsTerminal(fd) { // We have a few things to deal with here: // - Activating UTF-8 output support (mandatory) // - Activating virtual terminal support (optional) // These will not succeed on Windows 8 or early versions of Windows 10. // UTF-8 support means switching the console "code page" to CP_UTF8. // Notice that this doesn't take the specific file descriptor, because // the console is just ambiently associated with our process. err := SetConsoleOutputCP(CP_UTF8) if err != nil { return nil, fmt.Errorf("failed to set the console to UTF-8 mode; you may need to use a newer version of Windows: %s", err) } // If the console also allows us to turn on // ENABLE_VIRTUAL_TERMINAL_PROCESSING then we can potentially use VT // output, although the methods of Settings will make the final // determination on that because we might have some handles pointing at // terminals and other handles pointing at files/pipes. ret.getColumns = getColumnsWindowsConsole var mode uint32 err = windows.GetConsoleMode(windows.Handle(fd), &mode) if err != nil { return ret, nil // We'll treat this as success but without VT support } mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING err = windows.SetConsoleMode(windows.Handle(fd), mode) if err != nil { return ret, nil // We'll treat this as success but without VT support } // If we get here then we've successfully turned on VT processing, so // we can return an OutputStream that answers true when asked if it // is a Terminal. ret.isTerminal = staticTrue return ret, nil } else if isatty.IsCygwinTerminal(fd) { // Cygwin terminals -- and other VT100 "fakers" for older versions of // Windows -- are not really terminals in the usual sense, but rather // are pipes between the child process (Terraform) and the terminal // emulator. isatty.IsCygwinTerminal uses some heuristics to // distinguish those pipes from other pipes we might see if the user // were, for example, using the | operator on the command line. // If we get in here then we'll assume that we can send VT100 sequences // to this stream, even though it isn't a terminal in the usual sense. ret.isTerminal = staticTrue // TODO: Is it possible to detect the width of these fake terminals? return ret, nil } // If we fall out here then we have a non-terminal filehandle, so we'll // just accept all of the default OutputStream behaviors return ret, nil } func configureInputHandle(f *os.File) (*InputStream, error) { ret := &InputStream{ File: f, } if fd := f.Fd(); isatty.IsTerminal(fd) { // We have to activate UTF-8 input, or else we fail. This will not // succeed on Windows 8 or early versions of Windows 10. // Notice that this doesn't take the specific file descriptor, because // the console is just ambiently associated with our process. err := SetConsoleCP(CP_UTF8) if err != nil { return nil, fmt.Errorf("failed to set the console to UTF-8 mode; you may need to use a newer version of Windows: %s", err) } ret.isTerminal = staticTrue return ret, nil } else if isatty.IsCygwinTerminal(fd) { // As with the output handles above, we'll use isatty's heuristic to // pretend that a pipe from mintty or a similar userspace terminal // emulator is actually a terminal. ret.isTerminal = staticTrue return ret, nil } // If we fall out here then we have a non-terminal filehandle, so we'll // just accept all of the default InputStream behaviors return ret, nil } func getColumnsWindowsConsole(f *os.File) int { // We'll just unconditionally ask the given file for its console buffer // info here, and let it fail if the file isn't actually a console. // (In practice, the init functions above only hook up this function // if the handle looks like a console, so this should succeed.) var info windows.ConsoleScreenBufferInfo err := windows.GetConsoleScreenBufferInfo(windows.Handle(f.Fd()), &info) if err != nil { return defaultColumns } return int(info.Size.X) } // Unfortunately not all of the Windows kernel functions we need are in // x/sys/windows at the time of writing, so we need to call some of them // directly. (If you're maintaining this in future and have the capacity to // test it well, consider checking if these functions have been added upstream // yet and switch to their wrapper stubs if so. var modkernel32 = windows.NewLazySystemDLL("kernel32.dll") var procSetConsoleCP = modkernel32.NewProc("SetConsoleCP") var procSetConsoleOutputCP = modkernel32.NewProc("SetConsoleOutputCP") const CP_UTF8 = 65001 // (These are written in the style of the stubs in x/sys/windows, which is // a little non-idiomatic just due to the awkwardness of the low-level syscall // interface.) func SetConsoleCP(codepageID uint32) (err error) { r1, _, e1 := syscall.Syscall(procSetConsoleCP.Addr(), 1, uintptr(codepageID), 0, 0) if r1 == 0 { err = e1 } return } func SetConsoleOutputCP(codepageID uint32) (err error) { r1, _, e1 := syscall.Syscall(procSetConsoleOutputCP.Addr(), 1, uintptr(codepageID), 0, 0) if r1 == 0 { err = e1 } return } func staticTrue(f *os.File) bool { return true } func staticFalse(f *os.File) bool { return false }