1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
// SPDX-License-Identifier: GPL-3.0-or-later
// Package tui is krino's terminal layer: colour policy, paging a plan
// through $PAGER when it does not fit the screen, and single-key input for
// the interactive review prompt. See docs/design.md §8.2.
package tui
import (
"io"
"os"
"os/exec"
"strings"
"golang.org/x/term"
)
// isTerminal and termSize hold term.IsTerminal and term.GetSize so tests
// can replace them; that is the only way to test this package without a
// pty.
var (
isTerminal = term.IsTerminal
termSize = term.GetSize
)
// defaultPager is used when $PAGER is unset.
const defaultPager = "less -FRX"
// Colour reports whether to emit ANSI colour: w is a terminal and NO_COLOR
// is unset (spec §8.2).
func Colour(w io.Writer) bool {
f, ok := w.(*os.File)
if !ok || !isTerminal(int(f.Fd())) {
return false
}
_, noColour := os.LookupEnv("NO_COLOR")
return !noColour
}
// Height is the terminal's row count, 0 when it is not a terminal or the
// size cannot be read.
func Height(w io.Writer) int {
f, ok := w.(*os.File)
if !ok || !isTerminal(int(f.Fd())) {
return 0
}
_, h, err := termSize(int(f.Fd()))
if err != nil {
return 0
}
return h
}
// Page writes text through $PAGER (default "less -FRX") when it is taller
// than the terminal, and directly otherwise. Height is judged from
// os.Stdout regardless of which writer w is: that is the terminal a
// spawned pager would inherit, not necessarily w. A missing or broken
// pager never loses the plan: Page falls back to writing directly when
// the pager cannot start.
func Page(w io.Writer, text string) error {
if fitsWithoutPaging(text) {
_, err := io.WriteString(w, text)
return err
}
if runPager(text) {
return nil
}
_, err := io.WriteString(w, text)
return err
}
// fitsWithoutPaging reports whether text has no more lines than the
// terminal's height. It looks at the process's own stdout, since that is
// the terminal the pager would inherit, not the writer text is otherwise
// sent to.
func fitsWithoutPaging(text string) bool {
h := Height(os.Stdout)
if h <= 0 {
return true
}
lines := strings.Count(text, "\n")
if text != "" && !strings.HasSuffix(text, "\n") {
lines++ // the final, unterminated line still occupies a row
}
return lines <= h
}
// runPager sends text through $PAGER (default "less -FRX") and reports
// whether it started. $PAGER is split with strings.Fields, not a shell.
func runPager(text string) bool {
spec := os.Getenv("PAGER")
if spec == "" {
spec = defaultPager
}
fields := strings.Fields(spec)
if len(fields) == 0 {
return false
}
cmd := exec.Command(fields[0], fields[1:]...)
cmd.Stdin = strings.NewReader(text)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
return false
}
_ = cmd.Wait()
return true
}
|