add a spinner for long running commands

This commit is contained in:
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.