Skip to content

Commit b93a075

Browse files
committed
fix(spin): preserve color output when --show-output is given
1 parent 7b9d51d commit b93a075

File tree

5 files changed

+112
-23
lines changed

5 files changed

+112
-23
lines changed

Diff for: go.mod

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ require (
1414
github.com/charmbracelet/x/ansi v0.8.0
1515
github.com/charmbracelet/x/editor v0.1.0
1616
github.com/charmbracelet/x/term v0.2.1
17+
github.com/charmbracelet/x/xpty v0.1.2
1718
github.com/muesli/roff v0.1.0
1819
github.com/muesli/termenv v0.15.3-0.20241211131612-0d230cb6eb15
1920
github.com/rivo/uniseg v0.4.7
@@ -26,6 +27,10 @@ require (
2627
github.com/atotto/clipboard v0.1.4 // indirect
2728
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
2829
github.com/aymerick/douceur v0.2.0 // indirect
30+
github.com/charmbracelet/x/conpty v0.1.0 // indirect
31+
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 // indirect
32+
github.com/charmbracelet/x/termios v0.1.1 // indirect
33+
github.com/creack/pty v1.1.24 // indirect
2934
github.com/dlclark/regexp2 v1.11.0 // indirect
3035
github.com/dustin/go-humanize v1.0.1 // indirect
3136
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect

Diff for: go.sum

+10
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,22 @@ github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8
3232
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
3333
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
3434
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
35+
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
36+
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
3537
github.com/charmbracelet/x/editor v0.1.0 h1:p69/dpvlwRTs9uYiPeAWruwsHqTFzHhTvQOd/WVSX98=
3638
github.com/charmbracelet/x/editor v0.1.0/go.mod h1:oivrEbcP/AYt/Hpvk5pwDXXrQ933gQS6UzL6fxqAGSA=
39+
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
40+
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
3741
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
3842
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
3943
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
4044
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
45+
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
46+
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
47+
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
48+
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
49+
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
50+
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
4151
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4252
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4353
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=

Diff for: spin/command.go

+18-11
Original file line numberDiff line numberDiff line change
@@ -48,22 +48,29 @@ func (o Options) Run() error {
4848
// If the command succeeds, and we are printing output and we are in a TTY then push the STDOUT we got to the actual
4949
// STDOUT for piping or other things.
5050
//nolint:nestif
51-
if m.status == 0 {
52-
if o.ShowOutput {
53-
// BubbleTea writes the View() to stderr.
54-
// If the program is being piped then put the accumulated output in stdout.
55-
if !isOutTTY {
56-
_, err := os.Stdout.WriteString(m.stdout)
57-
if err != nil {
58-
return fmt.Errorf("failed to write to stdout: %w", err)
59-
}
51+
if m.err != nil {
52+
if _, err := fmt.Fprintf(os.Stderr, "%s\n", m.err.Error()); err != nil {
53+
return fmt.Errorf("failed to write to stdout: %w", err)
54+
}
55+
return exit.ErrExit(1)
56+
} else if m.status == 0 {
57+
var output string
58+
if o.ShowOutput || (o.ShowStdout && o.ShowStderr) {
59+
output = m.output
60+
} else if o.ShowStdout {
61+
output = m.stdout
62+
} else if o.ShowStderr {
63+
output = m.stderr
64+
}
65+
if output != "" {
66+
if _, err := os.Stdout.WriteString(output); err != nil {
67+
return fmt.Errorf("failed to write to stdout: %w", err)
6068
}
6169
}
6270
} else if o.ShowError {
6371
// Otherwise if we are showing errors and the command did not exit with a 0 status code then push all of the command
6472
// output to the terminal. This way failed commands can be debugged.
65-
_, err := os.Stdout.WriteString(m.output)
66-
if err != nil {
73+
if _, err := os.Stdout.WriteString(m.output); err != nil {
6774
return fmt.Errorf("failed to write to stdout: %w", err)
6875
}
6976
}

Diff for: spin/pty.go

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package spin
2+
3+
import (
4+
"os"
5+
6+
"github.com/charmbracelet/x/term"
7+
"github.com/charmbracelet/x/xpty"
8+
)
9+
10+
func openPty(f *os.File) (pty xpty.Pty, err error) {
11+
width, height, err := term.GetSize(f.Fd())
12+
if err != nil {
13+
return nil, err
14+
}
15+
16+
pty, err = xpty.NewPty(width, height)
17+
if err != nil {
18+
return nil, err
19+
}
20+
21+
return pty, nil
22+
}

Diff for: spin/spin.go

+57-12
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,18 @@
1515
package spin
1616

1717
import (
18+
"bytes"
19+
"context"
1820
"io"
1921
"os"
2022
"os/exec"
21-
"strings"
23+
"runtime"
2224
"syscall"
2325

2426
"github.com/charmbracelet/bubbles/spinner"
2527
tea "github.com/charmbracelet/bubbletea"
2628
"github.com/charmbracelet/x/term"
29+
"github.com/charmbracelet/x/xpty"
2730
)
2831

2932
type model struct {
@@ -40,16 +43,19 @@ type model struct {
4043
showStdout bool
4144
showStderr bool
4245
showError bool
46+
err error
4347
}
4448

4549
var (
46-
bothbuf strings.Builder
47-
outbuf strings.Builder
48-
errbuf strings.Builder
50+
bothbuf bytes.Buffer
51+
outbuf bytes.Buffer
52+
errbuf bytes.Buffer
4953

5054
executing *exec.Cmd
5155
)
5256

57+
type errorMsg error
58+
5359
type finishCommandMsg struct {
5460
stdout string
5561
stderr string
@@ -65,15 +71,50 @@ func commandStart(command []string) tea.Cmd {
6571
}
6672

6773
executing = exec.Command(command[0], args...) //nolint:gosec
68-
if term.IsTerminal(os.Stdout.Fd()) {
74+
executing.Stdin = os.Stdin
75+
76+
isTerminal := term.IsTerminal(os.Stdout.Fd())
77+
78+
//nolint:nestif
79+
if isTerminal && runtime.GOOS == "windows" {
6980
executing.Stdout = io.MultiWriter(&bothbuf, &outbuf)
7081
executing.Stderr = io.MultiWriter(&bothbuf, &errbuf)
82+
_ = executing.Run()
83+
} else if isTerminal {
84+
stdoutPty, err := openPty(os.Stdout)
85+
if err != nil {
86+
return errorMsg(err)
87+
}
88+
defer stdoutPty.Close() //nolint:errcheck
89+
90+
stderrPty, err := openPty(os.Stderr)
91+
if err != nil {
92+
return errorMsg(err)
93+
}
94+
defer stderrPty.Close() //nolint:errcheck
95+
96+
outUnixPty, isOutUnixPty := stdoutPty.(*xpty.UnixPty)
97+
errUnixPty, isErrUnixPty := stderrPty.(*xpty.UnixPty)
98+
if isOutUnixPty && isErrUnixPty {
99+
executing.Stdout = outUnixPty.Slave()
100+
executing.Stderr = errUnixPty.Slave()
101+
}
102+
103+
go io.Copy(io.MultiWriter(&bothbuf, &outbuf), stdoutPty) //nolint:errcheck
104+
go io.Copy(io.MultiWriter(&bothbuf, &errbuf), stderrPty) //nolint:errcheck
105+
106+
if err = stdoutPty.Start(executing); err != nil {
107+
return errorMsg(err)
108+
}
109+
if err = xpty.WaitProcess(context.Background(), executing); err != nil {
110+
return errorMsg(err)
111+
}
71112
} else {
72113
executing.Stdout = os.Stdout
73114
executing.Stderr = os.Stderr
115+
_ = executing.Run()
74116
}
75-
executing.Stdin = os.Stdin
76-
_ = executing.Run()
117+
77118
status := executing.ProcessState.ExitCode()
78119
if status == -1 {
79120
status = 1
@@ -103,6 +144,10 @@ func (m model) Init() tea.Cmd {
103144
}
104145

105146
func (m model) View() string {
147+
if m.quitting {
148+
return ""
149+
}
150+
106151
var out string
107152
if m.showStderr {
108153
out += errbuf.String()
@@ -111,10 +156,6 @@ func (m model) View() string {
111156
out += outbuf.String()
112157
}
113158

114-
if m.quitting {
115-
return out
116-
}
117-
118159
if !m.isTTY {
119160
return m.title
120161
}
@@ -129,7 +170,6 @@ func (m model) View() string {
129170
}
130171

131172
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
132-
var cmd tea.Cmd
133173
switch msg := msg.(type) {
134174
case finishCommandMsg:
135175
m.stdout = msg.stdout
@@ -143,8 +183,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
143183
case "ctrl+c":
144184
return m, commandAbort
145185
}
186+
case errorMsg:
187+
m.err = msg
188+
m.quitting = true
189+
return m, tea.Quit
146190
}
147191

192+
var cmd tea.Cmd
148193
m.spinner, cmd = m.spinner.Update(msg)
149194
return m, cmd
150195
}

0 commit comments

Comments
 (0)