github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/terminal/impl_windows.go (about)

     1  //go:build windows
     2  // +build windows
     3  
     4  package terminal
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"syscall"
    10  
    11  	"golang.org/x/sys/windows"
    12  
    13  	// We're continuing to use this third-party library on Windows because it
    14  	// has the additional IsCygwinTerminal function, which includes some useful
    15  	// heuristics for recognizing when a pipe seems to be connected to a
    16  	// legacy terminal emulator on Windows versions that lack true pty support.
    17  	// We now use golang.org/x/term's functionality on other platforms.
    18  	isatty "github.com/mattn/go-isatty"
    19  )
    20  
    21  func configureOutputHandle(f *os.File) (*OutputStream, error) {
    22  	ret := &OutputStream{
    23  		File: f,
    24  	}
    25  
    26  	if fd := f.Fd(); isatty.IsTerminal(fd) {
    27  		// We have a few things to deal with here:
    28  		// - Activating UTF-8 output support (mandatory)
    29  		// - Activating virtual terminal support (optional)
    30  		// These will not succeed on Windows 8 or early versions of Windows 10.
    31  
    32  		// UTF-8 support means switching the console "code page" to CP_UTF8.
    33  		// Notice that this doesn't take the specific file descriptor, because
    34  		// the console is just ambiently associated with our process.
    35  		err := SetConsoleOutputCP(CP_UTF8)
    36  		if err != nil {
    37  			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)
    38  		}
    39  
    40  		// If the console also allows us to turn on
    41  		// ENABLE_VIRTUAL_TERMINAL_PROCESSING then we can potentially use VT
    42  		// output, although the methods of Settings will make the final
    43  		// determination on that because we might have some handles pointing at
    44  		// terminals and other handles pointing at files/pipes.
    45  		ret.getColumns = getColumnsWindowsConsole
    46  		var mode uint32
    47  		err = windows.GetConsoleMode(windows.Handle(fd), &mode)
    48  		if err != nil {
    49  			return ret, nil // We'll treat this as success but without VT support
    50  		}
    51  		mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
    52  		err = windows.SetConsoleMode(windows.Handle(fd), mode)
    53  		if err != nil {
    54  			return ret, nil // We'll treat this as success but without VT support
    55  		}
    56  
    57  		// If we get here then we've successfully turned on VT processing, so
    58  		// we can return an OutputStream that answers true when asked if it
    59  		// is a Terminal.
    60  		ret.isTerminal = staticTrue
    61  		return ret, nil
    62  
    63  	} else if isatty.IsCygwinTerminal(fd) {
    64  		// Cygwin terminals -- and other VT100 "fakers" for older versions of
    65  		// Windows -- are not really terminals in the usual sense, but rather
    66  		// are pipes between the child process (Durgaform) and the terminal
    67  		// emulator. isatty.IsCygwinTerminal uses some heuristics to
    68  		// distinguish those pipes from other pipes we might see if the user
    69  		// were, for example, using the | operator on the command line.
    70  		// If we get in here then we'll assume that we can send VT100 sequences
    71  		// to this stream, even though it isn't a terminal in the usual sense.
    72  
    73  		ret.isTerminal = staticTrue
    74  		// TODO: Is it possible to detect the width of these fake terminals?
    75  		return ret, nil
    76  	}
    77  
    78  	// If we fall out here then we have a non-terminal filehandle, so we'll
    79  	// just accept all of the default OutputStream behaviors
    80  	return ret, nil
    81  }
    82  
    83  func configureInputHandle(f *os.File) (*InputStream, error) {
    84  	ret := &InputStream{
    85  		File: f,
    86  	}
    87  
    88  	if fd := f.Fd(); isatty.IsTerminal(fd) {
    89  		// We have to activate UTF-8 input, or else we fail. This will not
    90  		// succeed on Windows 8 or early versions of Windows 10.
    91  		// Notice that this doesn't take the specific file descriptor, because
    92  		// the console is just ambiently associated with our process.
    93  		err := SetConsoleCP(CP_UTF8)
    94  		if err != nil {
    95  			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)
    96  		}
    97  		ret.isTerminal = staticTrue
    98  		return ret, nil
    99  	} else if isatty.IsCygwinTerminal(fd) {
   100  		// As with the output handles above, we'll use isatty's heuristic to
   101  		// pretend that a pipe from mintty or a similar userspace terminal
   102  		// emulator is actually a terminal.
   103  		ret.isTerminal = staticTrue
   104  		return ret, nil
   105  	}
   106  
   107  	// If we fall out here then we have a non-terminal filehandle, so we'll
   108  	// just accept all of the default InputStream behaviors
   109  	return ret, nil
   110  }
   111  
   112  func getColumnsWindowsConsole(f *os.File) int {
   113  	// We'll just unconditionally ask the given file for its console buffer
   114  	// info here, and let it fail if the file isn't actually a console.
   115  	// (In practice, the init functions above only hook up this function
   116  	// if the handle looks like a console, so this should succeed.)
   117  	var info windows.ConsoleScreenBufferInfo
   118  	err := windows.GetConsoleScreenBufferInfo(windows.Handle(f.Fd()), &info)
   119  	if err != nil {
   120  		return defaultColumns
   121  	}
   122  	return int(info.Size.X)
   123  }
   124  
   125  // Unfortunately not all of the Windows kernel functions we need are in
   126  // x/sys/windows at the time of writing, so we need to call some of them
   127  // directly. (If you're maintaining this in future and have the capacity to
   128  // test it well, consider checking if these functions have been added upstream
   129  // yet and switch to their wrapper stubs if so.
   130  var modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
   131  var procSetConsoleCP = modkernel32.NewProc("SetConsoleCP")
   132  var procSetConsoleOutputCP = modkernel32.NewProc("SetConsoleOutputCP")
   133  
   134  const CP_UTF8 = 65001
   135  
   136  // (These are written in the style of the stubs in x/sys/windows, which is
   137  // a little non-idiomatic just due to the awkwardness of the low-level syscall
   138  // interface.)
   139  
   140  func SetConsoleCP(codepageID uint32) (err error) {
   141  	r1, _, e1 := syscall.Syscall(procSetConsoleCP.Addr(), 1, uintptr(codepageID), 0, 0)
   142  	if r1 == 0 {
   143  		err = e1
   144  	}
   145  	return
   146  }
   147  
   148  func SetConsoleOutputCP(codepageID uint32) (err error) {
   149  	r1, _, e1 := syscall.Syscall(procSetConsoleOutputCP.Addr(), 1, uintptr(codepageID), 0, 0)
   150  	if r1 == 0 {
   151  		err = e1
   152  	}
   153  	return
   154  }
   155  
   156  func staticTrue(f *os.File) bool {
   157  	return true
   158  }
   159  
   160  func staticFalse(f *os.File) bool {
   161  	return false
   162  }