add a spinner for long running commands

This commit is contained in:
onyx-and-iris 2026-02-21 01:58:26 +00:00
parent b1161e1e97
commit cf82b29de5
3 changed files with 82 additions and 33 deletions

View File

@ -12,6 +12,7 @@ import (
"time"
"github.com/charmbracelet/log"
"github.com/chelnak/ysmrr"
"github.com/peterbourgon/ff/v4"
"github.com/peterbourgon/ff/v4/ffhelp"
@ -67,6 +68,13 @@ func main() {
}
}
type context struct {
client *q3rcon.Rcon
timeouts map[string]time.Duration
in io.Reader
sm ysmrr.SpinnerManager
}
// run executes the main logic of the application and returns a cleanup function and an error if any.
func run() (func(), error) {
var flags Flags
@ -124,14 +132,31 @@ func run() (func(), error) {
}
log.SetLevel(level)
client, closer, err := connectRcon(flags.Host, flags.Port, flags.Rconpass)
timeouts := map[string]time.Duration{
"map": time.Second,
"map_rotate": time.Second,
"map_restart": time.Second,
}
log.Debugf("using timeouts: %v", timeouts)
client, closer, err := connectRcon(flags.Host, flags.Port, flags.Rconpass, timeouts)
if err != nil {
return nil, fmt.Errorf("failed to connect to rcon: %w", err)
}
sm := ysmrr.NewSpinnerManager()
sm.AddSpinner("")
ctx := &context{
client: client,
timeouts: timeouts,
in: os.Stdin,
sm: sm,
}
if flags.Interactive {
fmt.Printf("Enter 'Q' to exit.\n>> ")
err := interactiveMode(client, os.Stdin)
err := interactiveMode(ctx)
if err != nil {
return closer, fmt.Errorf("interactive mode error: %w", err)
}
@ -143,7 +168,7 @@ func run() (func(), error) {
log.Debug("no commands provided, defaulting to 'status'")
commands = append(commands, "status")
}
runCommands(client, commands)
runCommands(ctx, commands)
return closer, nil
}
@ -161,12 +186,13 @@ func versionFromBuild() string {
return strings.Split(info.Main.Version, "-")[0]
}
func connectRcon(host string, port int, password string) (*q3rcon.Rcon, func(), error) {
client, err := q3rcon.New(host, port, password, q3rcon.WithTimeouts(map[string]time.Duration{
"map": time.Second,
"map_rotate": time.Second,
"map_restart": time.Second,
}))
func connectRcon(
host string,
port int,
password string,
timeouts map[string]time.Duration,
) (*q3rcon.Rcon, func(), error) {
client, err := q3rcon.New(host, port, password, q3rcon.WithTimeouts(timeouts))
if err != nil {
return nil, nil, err
}
@ -180,35 +206,19 @@ func connectRcon(host string, port int, password string) (*q3rcon.Rcon, func(),
return client, closer, nil
}
// runCommands runs the commands given in the flag.Args slice.
// If no commands are given, it defaults to running the "status" command.
func runCommands(client *q3rcon.Rcon, commands []string) {
for _, cmd := range commands {
resp, err := client.Send(cmd)
if err != nil {
log.Error(err)
continue
}
fmt.Print(removeColourCodes(resp))
}
}
// interactiveMode continuously reads from input until a quit signal is given.
func interactiveMode(client *q3rcon.Rcon, input io.Reader) error {
scanner := bufio.NewScanner(input)
func interactiveMode(ctx *context) error {
scanner := bufio.NewScanner(ctx.in)
for scanner.Scan() {
cmd := scanner.Text()
if strings.EqualFold(cmd, "Q") {
return nil
}
resp, err := client.Send(cmd)
if err != nil {
log.Error(err)
fmt.Print(">> ")
continue
if err := runCommand(ctx, cmd); err != nil {
fmt.Printf("Error: %v\n", err)
}
fmt.Printf("%s>> ", removeColourCodes(resp))
fmt.Printf(">> ")
}
if scanner.Err() != nil {
@ -217,6 +227,33 @@ func interactiveMode(client *q3rcon.Rcon, input io.Reader) error {
return nil
}
// runCommands executes a list of commands sequentially and prints any errors encountered.
func runCommands(ctx *context, commands []string) {
for _, cmd := range commands {
if err := runCommand(ctx, cmd); err != nil {
fmt.Printf("Error: %v\n", err)
}
}
}
// runCommand sends a command to the RCON client and prints the response.
// If the command is in the timeouts map, it starts a spinner until the response is received.
func runCommand(ctx *context, cmd string) error {
before, _, _ := strings.Cut(cmd, " ")
_, ok := ctx.timeouts[before]
if ok {
ctx.sm.Start()
}
resp, err := ctx.client.Send(cmd)
if err != nil {
return fmt.Errorf("failed to run command '%s': %w", cmd, err)
}
ctx.sm.Stop()
fmt.Print(removeColourCodes(resp))
return nil
}
var colourCodeRegex = regexp.MustCompile(`\^[0-9]`)
// removeColourCodes removes '\^[0-9]' colour codes from the input string.

6
go.mod
View File

@ -4,6 +4,7 @@ go 1.25.0
require (
github.com/charmbracelet/log v0.4.2
github.com/chelnak/ysmrr v0.6.0
github.com/peterbourgon/ff/v4 v4.0.0-beta.1
)
@ -14,13 +15,16 @@ require (
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.40.0 // indirect
)

12
go.sum
View File

@ -12,12 +12,18 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/chelnak/ysmrr v0.6.0 h1:kMhO0oI02tl/9szvxrOE0yeImtrK4KQhER0oXu1K/iM=
github.com/chelnak/ysmrr v0.6.0/go.mod h1:56JSrmQgb7/7xoMvuD87h3PE/qW6K1+BQcrgWtVLTUo=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
@ -40,8 +46,10 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=