Files
opentf/internal/terminal/impl_windows.go
Martin Atkins 068df07d11 various: Remove legacy "+build" comments
Go 1.17 and earlier used a different syntax for build constraint comments,
starting with "+build". Go 1.18 changed this to the modern "go:build" form
as part of standardizing the structure of toolchain directive comments,
and so for a while it was convention to include comments in both styles
to allow building with both old and new Go compilers.

However, Go 1.17 is no longer supported, and regardless of that we only
expect OpenTofu to be built with the specific version we have selected
in "go.mod" and ".go-version" anyway, so we no longer need the legacy form
of these comments: the all supported Go toolchains now support the new
form, which this commit retains.

golangci-lint v2.6.0 includes a check for this legacy form, so removing
this will also allow us to upgrade to a newer version of that linter
aggregator in a future commit.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-11-01 08:00:01 -03:00

167 lines
5.9 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
//go: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: %w", 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 (OpenTofu) 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: %w", 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
}