2022-07-13 15:55:36 +02:00
|
|
|
// Package spin provides a shell script interface for the spinner bubble.
|
|
|
|
// https://github.com/charmbracelet/bubbles/tree/master/spinner
|
|
|
|
//
|
|
|
|
// It is useful for displaying that some task is running in the background
|
|
|
|
// while consuming it's output so that it is not shown to the user.
|
|
|
|
//
|
|
|
|
// For example, let's do a long running task: $ sleep 5
|
|
|
|
//
|
|
|
|
// We can simply prepend a spinner to this task to show it to the user, while
|
|
|
|
// performing the task / command in the background.
|
|
|
|
//
|
2022-08-05 00:45:19 +02:00
|
|
|
// $ gum spin -t "Taking a nap..." -- sleep 5
|
2022-07-13 15:55:36 +02:00
|
|
|
//
|
|
|
|
// The spinner will automatically exit when the task is complete.
|
2022-07-06 18:08:17 +02:00
|
|
|
package spin
|
|
|
|
|
|
|
|
import (
|
2024-03-28 18:11:07 +01:00
|
|
|
"io"
|
2023-12-21 21:09:00 +01:00
|
|
|
"os"
|
2022-07-06 18:08:17 +02:00
|
|
|
"os/exec"
|
2022-08-01 19:24:01 +02:00
|
|
|
"strings"
|
2023-06-30 15:17:09 +02:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/charmbracelet/gum/internal/exit"
|
|
|
|
"github.com/charmbracelet/gum/timeout"
|
2023-12-21 21:09:00 +01:00
|
|
|
"github.com/mattn/go-isatty"
|
2022-07-06 18:08:17 +02:00
|
|
|
|
|
|
|
"github.com/charmbracelet/bubbles/spinner"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
|
)
|
|
|
|
|
|
|
|
type model struct {
|
2023-06-27 16:31:54 +02:00
|
|
|
spinner spinner.Model
|
|
|
|
title string
|
|
|
|
align string
|
|
|
|
command []string
|
2023-12-13 18:26:10 +01:00
|
|
|
quitting bool
|
2023-06-27 16:31:54 +02:00
|
|
|
aborted bool
|
|
|
|
status int
|
|
|
|
stdout string
|
2024-03-28 18:11:07 +01:00
|
|
|
stderr string
|
|
|
|
output string
|
2023-06-27 16:31:54 +02:00
|
|
|
showOutput bool
|
2024-03-28 18:11:07 +01:00
|
|
|
showError bool
|
2023-06-30 15:17:09 +02:00
|
|
|
timeout time.Duration
|
|
|
|
hasTimeout bool
|
2022-07-06 18:08:17 +02:00
|
|
|
}
|
|
|
|
|
2024-03-28 18:11:07 +01:00
|
|
|
var bothbuf strings.Builder
|
2023-06-27 16:31:54 +02:00
|
|
|
var outbuf strings.Builder
|
|
|
|
var errbuf strings.Builder
|
|
|
|
|
2022-07-30 23:34:25 +02:00
|
|
|
type finishCommandMsg struct {
|
2022-08-01 19:24:01 +02:00
|
|
|
stdout string
|
2024-03-28 18:11:07 +01:00
|
|
|
stderr string
|
|
|
|
output string
|
2022-07-30 23:34:25 +02:00
|
|
|
status int
|
|
|
|
}
|
2022-07-06 18:08:17 +02:00
|
|
|
|
|
|
|
func commandStart(command []string) tea.Cmd {
|
|
|
|
return func() tea.Msg {
|
|
|
|
var args []string
|
|
|
|
if len(command) > 1 {
|
|
|
|
args = command[1:]
|
|
|
|
}
|
2022-07-30 23:34:25 +02:00
|
|
|
cmd := exec.Command(command[0], args...) //nolint:gosec
|
2022-08-01 19:24:01 +02:00
|
|
|
|
2023-12-21 21:09:00 +01:00
|
|
|
if isatty.IsTerminal(os.Stdout.Fd()) {
|
2024-03-28 18:11:07 +01:00
|
|
|
stdout := io.MultiWriter(&bothbuf, &errbuf)
|
|
|
|
stderr := io.MultiWriter(&bothbuf, &outbuf)
|
|
|
|
|
|
|
|
cmd.Stdout = stdout
|
|
|
|
cmd.Stderr = stderr
|
2023-12-21 21:09:00 +01:00
|
|
|
} else {
|
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
}
|
2022-08-01 19:24:01 +02:00
|
|
|
|
|
|
|
_ = cmd.Run()
|
2022-07-30 23:34:25 +02:00
|
|
|
|
|
|
|
status := cmd.ProcessState.ExitCode()
|
|
|
|
|
|
|
|
if status == -1 {
|
|
|
|
status = 1
|
|
|
|
}
|
|
|
|
|
|
|
|
return finishCommandMsg{
|
2022-08-01 19:24:01 +02:00
|
|
|
stdout: outbuf.String(),
|
2024-03-28 18:11:07 +01:00
|
|
|
stderr: errbuf.String(),
|
|
|
|
output: bothbuf.String(),
|
2022-07-30 23:34:25 +02:00
|
|
|
status: status,
|
|
|
|
}
|
2022-07-06 18:08:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m model) Init() tea.Cmd {
|
|
|
|
return tea.Batch(
|
|
|
|
m.spinner.Tick,
|
|
|
|
commandStart(m.command),
|
2023-06-30 15:17:09 +02:00
|
|
|
timeout.Init(m.timeout, nil),
|
2022-07-06 18:08:17 +02:00
|
|
|
)
|
|
|
|
}
|
2022-10-02 11:18:37 +02:00
|
|
|
func (m model) View() string {
|
2023-12-21 21:09:00 +01:00
|
|
|
if m.quitting && m.showOutput {
|
|
|
|
return strings.TrimPrefix(errbuf.String()+"\n"+outbuf.String(), "\n")
|
2023-12-13 18:26:10 +01:00
|
|
|
}
|
|
|
|
|
2023-06-30 15:17:09 +02:00
|
|
|
var str string
|
|
|
|
if m.hasTimeout {
|
|
|
|
str = timeout.Str(m.timeout)
|
|
|
|
}
|
2023-06-27 16:31:54 +02:00
|
|
|
var header string
|
2022-10-02 11:18:37 +02:00
|
|
|
if m.align == "left" {
|
2023-06-30 15:17:09 +02:00
|
|
|
header = m.spinner.View() + str + " " + m.title
|
2023-06-27 16:31:54 +02:00
|
|
|
} else {
|
2023-06-30 15:17:09 +02:00
|
|
|
header = str + " " + m.title + " " + m.spinner.View()
|
2022-10-02 11:18:37 +02:00
|
|
|
}
|
2023-06-27 16:31:54 +02:00
|
|
|
if !m.showOutput {
|
|
|
|
return header
|
|
|
|
}
|
2023-06-27 16:36:33 +02:00
|
|
|
return header + errbuf.String() + "\n" + outbuf.String()
|
2022-10-02 11:18:37 +02:00
|
|
|
}
|
2022-07-06 18:08:17 +02:00
|
|
|
|
|
|
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
|
var cmd tea.Cmd
|
|
|
|
switch msg := msg.(type) {
|
2023-06-30 15:17:09 +02:00
|
|
|
case timeout.TickTimeoutMsg:
|
|
|
|
if msg.TimeoutValue <= 0 {
|
2023-12-13 19:54:14 +01:00
|
|
|
// grab current output before closing for piped instances
|
|
|
|
m.stdout = outbuf.String()
|
|
|
|
|
2023-06-30 15:17:09 +02:00
|
|
|
m.status = exit.StatusAborted
|
|
|
|
return m, tea.Quit
|
|
|
|
}
|
|
|
|
m.timeout = msg.TimeoutValue
|
|
|
|
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
|
2022-07-06 18:08:17 +02:00
|
|
|
case finishCommandMsg:
|
2022-08-01 19:24:01 +02:00
|
|
|
m.stdout = msg.stdout
|
2024-03-28 18:11:07 +01:00
|
|
|
m.stderr = msg.stderr
|
|
|
|
m.output = msg.output
|
2022-07-30 23:34:25 +02:00
|
|
|
m.status = msg.status
|
2023-12-13 18:26:10 +01:00
|
|
|
m.quitting = true
|
2022-07-06 18:08:17 +02:00
|
|
|
return m, tea.Quit
|
|
|
|
case tea.KeyMsg:
|
|
|
|
switch msg.String() {
|
|
|
|
case "ctrl+c":
|
2022-07-31 03:41:18 +02:00
|
|
|
m.aborted = true
|
2022-07-06 18:08:17 +02:00
|
|
|
return m, tea.Quit
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
m.spinner, cmd = m.spinner.Update(msg)
|
|
|
|
return m, cmd
|
|
|
|
}
|