github.com/grailbio/base@v0.0.11/status/term.go (about)

     1  // Copyright 2018 GRAIL, Inc. All rights reserved.
     2  // Use of this source code is governed by the Apache 2.0
     3  // license that can be found in the LICENSE file.
     4  
     5  package status
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"syscall"
    13  	"unsafe"
    14  
    15  	gotty "github.com/Nvveen/Gotty"
    16  	"golang.org/x/sys/unix"
    17  )
    18  
    19  // Fd() is implemented by *os.File
    20  type fder interface {
    21  	Fd() uintptr
    22  }
    23  
    24  // term represents a small set of terminal capabilities used to
    25  // render status updates.
    26  type term struct {
    27  	info *gotty.TermInfo
    28  	fd   uintptr
    29  }
    30  
    31  // openTerm attempts to derive a term from the provided
    32  // file descriptor and the $TERM environment variable.
    33  //
    34  // if the file descriptor does not represent a terminal, or
    35  // if the value of $TERM indicates no terminal capabilities
    36  // are present or known, an error is returned.
    37  func openTerm(w io.Writer) (*term, error) {
    38  	fder, ok := w.(fder)
    39  	if !ok {
    40  		return nil, errors.New("cannot get file descriptor from writer")
    41  	}
    42  	if !isTerminal(fder.Fd()) {
    43  		return nil, errors.New("writer is not a terminal")
    44  	}
    45  	switch env := os.Getenv("TERM"); env {
    46  	case "":
    47  		return nil, errors.New("no $TERM defined")
    48  	case "dumb":
    49  		return nil, errors.New("dumb terminal")
    50  	default:
    51  		// In this case, we'll make a best-effort, at least to do vt102.
    52  		t := new(term)
    53  		t.info = safeOpenTermInfo(env)
    54  		t.fd = fder.Fd()
    55  		return t, nil
    56  	}
    57  }
    58  
    59  // safeOpenTermInfo wraps gotty's OpenTermInfo to recover from
    60  // panics. We can safely revert to at least vt102 in this case.
    61  func safeOpenTermInfo(env string) (info *gotty.TermInfo) {
    62  	defer func() {
    63  		if recover() != nil {
    64  			info = nil
    65  		}
    66  	}()
    67  	info, _ = gotty.OpenTermInfo(env)
    68  	return
    69  }
    70  
    71  func (t *term) print(w io.Writer, which string, params ...interface{}) bool {
    72  	if t.info == nil {
    73  		return false
    74  	}
    75  	attr, err := t.info.Parse(which, params...)
    76  	if err != nil {
    77  		return false
    78  	}
    79  	io.WriteString(w, attr)
    80  	return true
    81  }
    82  
    83  // Move moves the terminal cursor by n: if n is negative, we move up
    84  // by -n, when it's positive we move down by n.
    85  func (t *term) Move(w io.Writer, n int) {
    86  	switch {
    87  	case n > 0:
    88  		if t.print(w, "cud", n) {
    89  			return
    90  		}
    91  		fmt.Fprintf(w, "\x1b[%dB", n)
    92  	case n < 0:
    93  		if t.print(w, "cuu", -n) {
    94  			return
    95  		}
    96  		fmt.Fprintf(w, "\x1b[%dA", -n)
    97  	}
    98  }
    99  
   100  // Clear clears the current line of the terminal.
   101  func (t *term) Clear(w io.Writer) {
   102  	if !t.print(w, "el1") {
   103  		io.WriteString(w, "\x1b[1K")
   104  	}
   105  	if !t.print(w, "el") {
   106  		fmt.Fprintf(w, "\x1b[K")
   107  	}
   108  }
   109  
   110  // Dim returns the current dimensions of the terminal.
   111  func (t *term) Dim() (width, height int) {
   112  	ws, err := unix.IoctlGetWinsize(int(t.fd), unix.TIOCGWINSZ)
   113  	if err != nil {
   114  		return 80, 20
   115  	}
   116  	return int(ws.Col), int(ws.Row)
   117  }
   118  
   119  func isTerminal(fd uintptr) bool {
   120  	var t syscall.Termios
   121  	_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), termios, uintptr(unsafe.Pointer(&t)), 0, 0, 0)
   122  	return err == 0
   123  }