diff --git a/cmd/q3rcon/main.go b/cmd/q3rcon/main.go index 6db314a..d1585ea 100644 --- a/cmd/q3rcon/main.go +++ b/cmd/q3rcon/main.go @@ -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. diff --git a/go.mod b/go.mod index 5a60077..87779f0 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index c4b44a6..5edddca 100644 --- a/go.sum +++ b/go.sum @@ -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=