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 }