Skip to content

Commit af2aecd

Browse files
authored
Merge pull request cli#4146 from cli/force-tty
Add ability to force terminal-style output even when redirected
2 parents 8129fb3 + 0701f8a commit af2aecd

File tree

6 files changed

+146
-0
lines changed

6 files changed

+146
-0
lines changed

cmd/gh/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ func mainRun() exitCode {
6161

6262
cmdFactory := factory.New(buildVersion)
6363
stderr := cmdFactory.IOStreams.ErrOut
64+
65+
if spec := os.Getenv("GH_FORCE_TTY"); spec != "" {
66+
cmdFactory.IOStreams.ForceTerminal(spec)
67+
}
6468
if !cmdFactory.IOStreams.ColorEnabled() {
6569
surveyCore.DisableColor = true
6670
} else {

pkg/cmd/root/help_topic.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ var HelpTopics = map[string]map[string]string{
6262
CLICOLOR_FORCE: set to a value other than "0" to keep ANSI colors in output
6363
even when the output is piped.
6464
65+
GH_FORCE_TTY: set to any value to force terminal-style output even when the output is
66+
redirected. When the value is a number, it is interpreted as the number of columns
67+
available in the viewport. When the value is a percentage, it will be applied against
68+
the number of columns available in the current viewport.
69+
6570
GH_NO_UPDATE_NOTIFIER: set to any value to disable update notifications. By default, gh
6671
checks for new releases once every 24 hours and displays an upgrade notice on standard
6772
error if a newer version was found.

pkg/iostreams/iostreams.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package iostreams
22

33
import (
44
"bytes"
5+
"errors"
56
"fmt"
67
"io"
78
"io/ioutil"
@@ -40,6 +41,8 @@ type IOStreams struct {
4041
stdoutIsTTY bool
4142
stderrTTYOverride bool
4243
stderrIsTTY bool
44+
termWidthOverride int
45+
ttySize func() (int, int, error)
4346

4447
pagerCommand string
4548
pagerProcess *os.Process
@@ -232,6 +235,10 @@ func (s *IOStreams) StopProgressIndicator() {
232235
}
233236

234237
func (s *IOStreams) TerminalWidth() int {
238+
if s.termWidthOverride > 0 {
239+
return s.termWidthOverride
240+
}
241+
235242
defaultWidth := 80
236243
out := s.Out
237244
if s.originalOut != nil {
@@ -259,6 +266,28 @@ func (s *IOStreams) TerminalWidth() int {
259266
return defaultWidth
260267
}
261268

269+
func (s *IOStreams) ForceTerminal(spec string) {
270+
s.colorEnabled = !EnvColorDisabled()
271+
s.SetStdoutTTY(true)
272+
273+
if w, err := strconv.Atoi(spec); err == nil {
274+
s.termWidthOverride = w
275+
return
276+
}
277+
278+
ttyWidth, _, err := s.ttySize()
279+
if err != nil {
280+
return
281+
}
282+
s.termWidthOverride = ttyWidth
283+
284+
if strings.HasSuffix(spec, "%") {
285+
if p, err := strconv.Atoi(spec[:len(spec)-1]); err == nil {
286+
s.termWidthOverride = int(float64(s.termWidthOverride) * (float64(p) / 100))
287+
}
288+
}
289+
}
290+
262291
func (s *IOStreams) ColorScheme() *ColorScheme {
263292
return NewColorScheme(s.ColorEnabled(), s.ColorSupport256())
264293
}
@@ -297,6 +326,7 @@ func System() *IOStreams {
297326
colorEnabled: EnvColorForced() || (!EnvColorDisabled() && stdoutIsTTY),
298327
is256enabled: Is256ColorSupported(),
299328
pagerCommand: os.Getenv("PAGER"),
329+
ttySize: ttySize,
300330
}
301331

302332
if stdoutIsTTY && stderrIsTTY {
@@ -317,6 +347,9 @@ func Test() (*IOStreams, *bytes.Buffer, *bytes.Buffer, *bytes.Buffer) {
317347
In: ioutil.NopCloser(in),
318348
Out: out,
319349
ErrOut: errOut,
350+
ttySize: func() (int, int, error) {
351+
return -1, -1, errors.New("ttySize not implemented in tests")
352+
},
320353
}, in, out, errOut
321354
}
322355

@@ -331,6 +364,7 @@ func isCygwinTerminal(w io.Writer) bool {
331364
return false
332365
}
333366

367+
// terminalSize measures the viewport of the terminal that the output stream is connected to
334368
func terminalSize(w io.Writer) (int, int, error) {
335369
if f, isFile := w.(*os.File); isFile {
336370
return term.GetSize(int(f.Fd()))

pkg/iostreams/iostreams_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package iostreams
2+
3+
import (
4+
"errors"
5+
"testing"
6+
)
7+
8+
func TestIOStreams_ForceTerminal(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
iostreams *IOStreams
12+
arg string
13+
wantTTY bool
14+
wantWidth int
15+
}{
16+
{
17+
name: "explicit width",
18+
iostreams: &IOStreams{},
19+
arg: "72",
20+
wantTTY: true,
21+
wantWidth: 72,
22+
},
23+
{
24+
name: "measure width",
25+
iostreams: &IOStreams{
26+
ttySize: func() (int, int, error) {
27+
return 72, 0, nil
28+
},
29+
},
30+
arg: "true",
31+
wantTTY: true,
32+
wantWidth: 72,
33+
},
34+
{
35+
name: "measure width fails",
36+
iostreams: &IOStreams{
37+
ttySize: func() (int, int, error) {
38+
return -1, -1, errors.New("ttySize sabotage!")
39+
},
40+
},
41+
arg: "true",
42+
wantTTY: true,
43+
wantWidth: 80,
44+
},
45+
{
46+
name: "apply percentage",
47+
iostreams: &IOStreams{
48+
ttySize: func() (int, int, error) {
49+
return 72, 0, nil
50+
},
51+
},
52+
arg: "50%",
53+
wantTTY: true,
54+
wantWidth: 36,
55+
},
56+
}
57+
for _, tt := range tests {
58+
t.Run(tt.name, func(t *testing.T) {
59+
tt.iostreams.ForceTerminal(tt.arg)
60+
if isTTY := tt.iostreams.IsStdoutTTY(); isTTY != tt.wantTTY {
61+
t.Errorf("IOStreams.IsStdoutTTY() = %v, want %v", isTTY, tt.wantTTY)
62+
}
63+
if tw := tt.iostreams.TerminalWidth(); tw != tt.wantWidth {
64+
t.Errorf("IOStreams.TerminalWidth() = %v, want %v", tw, tt.wantWidth)
65+
}
66+
})
67+
}
68+
}

pkg/iostreams/tty_size.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//+build !windows
2+
3+
package iostreams
4+
5+
import (
6+
"os"
7+
8+
"golang.org/x/term"
9+
)
10+
11+
// ttySize measures the size of the controlling terminal for the current process
12+
func ttySize() (int, int, error) {
13+
f, err := os.Open("/dev/tty")
14+
if err != nil {
15+
return -1, -1, err
16+
}
17+
defer f.Close()
18+
return term.GetSize(int(f.Fd()))
19+
}

pkg/iostreams/tty_size_windows.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package iostreams
2+
3+
import (
4+
"os"
5+
6+
"golang.org/x/term"
7+
)
8+
9+
func ttySize() (int, int, error) {
10+
f, err := os.Open("CONOUT$")
11+
if err != nil {
12+
return -1, -1, err
13+
}
14+
defer f.Close()
15+
return term.GetSize(int(f.Fd()))
16+
}

0 commit comments

Comments
 (0)