Skip to content

Commit

Permalink
Merge branch 'main' into command_mode
Browse files Browse the repository at this point in the history
  • Loading branch information
ksysoev committed Nov 5, 2023
2 parents a939033 + a74ddac commit ef3f83e
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 91 deletions.
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,44 @@ go install github.com/ksysoev/wsget/cmd/wsget@latest

## Usage

To use wsget, you need to specify the WebSocket URL using the -u flag:
To use wsget, you need to specify the WebSocket URL:

```
wsget -u "wss://ws.postman-echo.com/raw"
wsget wss://ws.postman-echo.com/raw
```


You also can pass initial request as part command line argument by using flag -r

```
wsget wss://ws.postman-echo.com/raw -r "Hello world!"
```


By default, wsget will print the data received from the WebSocket server only to the console. You can also save the data to a file using the -o flag:

```
wsget -u "wss://ws.postman-echo.com/raw" -o output.txt
wsget wss://ws.postman-echo.com/raw -o output.txt
```

![Example of usage](./example.png)
Example:

```
wsget "wss://ws.derivws.com/websockets/v3?app_id=1" -r '{"time":1}'
Use Enter to input request and send it, Ctrl+C to exit
->
{
"time": 1
}
<-
{
"echo_req": {
"time": 1
},
"msg_type": "time",
"time": 1698555261
}
```

## License

Expand Down
44 changes: 28 additions & 16 deletions cmd/wsget/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package main

import (
"fmt"
"log"
"os"

"github.com/fatih/color"
"github.com/ksysoev/wsget/pkg/cli"
"github.com/ksysoev/wsget/pkg/ws"
"github.com/spf13/cobra"
Expand All @@ -13,6 +13,8 @@ import (
var insecure bool
var request string
var outputFile string
var headers []string
var waitResponse int

const (
LongDescription = `A command-line tool for interacting with WebSocket servers.
Expand All @@ -21,17 +23,18 @@ The tool have severl modes of operation:
1. Request mode. The tool will start in interactive mode if no request is provided:
- You can type resquest and press Ctrl+S to send it to the server.
- It supports multiline input.
- You can type resquest and press Enter to send it to the server.
- Request editor allows to input multiline request. the last sybmol of line should be \(backslash) to indicate that the request is not finished yet.
- You can use Ctrl+U to clear the input.
- You can use Ctrl+C or Ctrl+D to exit the tool.
- You can use Esc to exit Request mode and switch to connection mode.
- You can use Esc to cancel input and return to connection mod.
2. Connection mode. The tool will start in connection mode if request is provided.
In this request mode the tool will send the request to the server and print responses.
- You can use Enter to switch to request input mode.
- You can use Esc to exit connection
- You can use Ctrl+C or Ctrl+D to exit the tool.
- You can use Esc to switch to Request mode.
`
)

Expand All @@ -43,13 +46,15 @@ func main() {
Example: `wsget wss://ws.postman-echo.com/raw -r "Hello, world!"`,
Args: cobra.ExactArgs(1),
ArgAliases: []string{"url"},
Version: "0.1.4",
Version: "0.2.0",
Run: run,
}

cmd.Flags().BoolVarP(&insecure, "insecure", "k", false, "Skip SSL certificate verification")
cmd.Flags().StringVarP(&request, "request", "r", "", "WebSocket request that will be sent to the server")
cmd.Flags().StringVarP(&outputFile, "output", "o", "", "Output file for saving all request and responses")
cmd.Flags().IntVarP(&waitResponse, "wait-resp", "w", -1, "Timeout for single response in seconds, 0 means no timeout. If this option is set, the tool will exit after receiving the first response")
cmd.Flags().StringSliceVarP(&headers, "header", "H", []string{}, "HTTP headers to attach to the request")

err := cmd.Execute()
if err != nil {
Expand All @@ -62,42 +67,49 @@ func run(cmd *cobra.Command, args []string) {
wsURL := args[0]
if wsURL == "" {
_ = cmd.Help()
return
}

os.Exit(1)
if waitResponse >= 0 && request == "" {
color.New(color.FgRed).Println("Single response timeout could be used only with request")
return
}

wsInsp, err := ws.NewWS(wsURL, ws.Options{SkipSSLVerification: insecure})
wsConn, err := ws.NewWS(wsURL, ws.Options{SkipSSLVerification: insecure, Headers: headers, WaitForResp: waitResponse})
if err != nil {
log.Fatal(err)
color.New(color.FgRed).Println("Unable to connect to the server: ", err)
return
}

defer wsInsp.Close()
defer wsConn.Close()

input := cli.NewKeyboard()

client := cli.NewCLI(wsInsp, input, os.Stdout)
client, err := cli.NewCLI(wsConn, input, os.Stdout)
if err != nil {
color.New(color.FgRed).Println("Unable to start CLI: ", err)
}

opts := cli.RunOptions{StartEditor: true}

if request != "" {
opts.StartEditor = false

go func() {
err = wsInsp.Send(request)
if err != nil {
fmt.Println("Fail to send request:", err)
if err = wsConn.Send(request); err != nil {
color.New(color.FgRed).Println("Fail to send request: ", err)
}
}()
}

if outputFile != "" {
if opts.OutputFile, err = os.Create(outputFile); err != nil {
log.Println(err)
color.New(color.FgRed).Println("Fail to open output file: ", err)
return
}
}

if err = client.Run(opts); err != nil {
log.Println("Error:", err)
color.New(color.FgRed).Println(err)
}
}
Binary file removed example.png
Binary file not shown.
75 changes: 53 additions & 22 deletions pkg/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package cli
import (
"fmt"
"io"
"log"
"os"
"os/user"

"github.com/eiannone/keyboard"
"github.com/fatih/color"
"github.com/ksysoev/wsget/pkg/formater"
"github.com/ksysoev/wsget/pkg/ws"
)
Expand All @@ -18,6 +18,9 @@ const (

MacOSDeleteKey = 127

HideCursor = "\x1b[?25l"
ShowCursor = "\x1b[?25h"

Bell = "\a"
)

Expand All @@ -39,10 +42,10 @@ type Inputer interface {
Close()
}

func NewCLI(wsConn *ws.Connection, input Inputer, output io.Writer) *CLI {
func NewCLI(wsConn *ws.Connection, input Inputer, output io.Writer) (*CLI, error) {
currentUser, err := user.Current()
if err != nil {
log.Fatal(err)
return nil, fmt.Errorf("fail to get current user: %s", err)
}

homeDir := currentUser.HomeDir
Expand All @@ -55,49 +58,58 @@ func NewCLI(wsConn *ws.Connection, input Inputer, output io.Writer) *CLI {
wsConn: wsConn,
input: input,
output: output,
}
}, nil
}

func (c *CLI) Run(opts RunOptions) error {
defer func() {
c.showCursor()
err := c.editor.History.SaveToFile()

if err != nil {
fmt.Fprintln(c.output, "Fail to save history:", err)
color.New(color.FgRed).Fprint(c.output, "Fail to save history:", err)
}
}()

c.hideCursor()

keysEvents, err := c.input.GetKeys()
if err != nil {
return err
}
defer c.input.Close()

fmt.Fprintln(c.output, "Use Esc to switch between modes, Ctrl+C to exit")
fmt.Fprintln(c.output, "Use Enter to input request and send it, Ctrl+C to exit")

if opts.StartEditor {
if err := c.RequestMod(keysEvents); err != nil {
if err.Error() == "interrupted" {
switch err.Error() {
case "interrupted":
return nil
case "empty request":
default:
return err
}

return err
}
}

for {
select {
case event := <-keysEvents:
switch event.Key {
case keyboard.KeyCtrlC, keyboard.KeyCtrlD:
case keyboard.KeyEsc, keyboard.KeyCtrlC, keyboard.KeyCtrlD:
return nil

case keyboard.KeyEsc:
case keyboard.KeyEnter:
if err := c.RequestMod(keysEvents); err != nil {
if err.Error() == "interrupted" {
switch err.Error() {
case "interrupted":
return nil
case "empty request":
continue
default:
return err
}

return err
}

default:
Expand Down Expand Up @@ -132,39 +144,58 @@ func (c *CLI) Run(opts RunOptions) error {

output, err := c.formater.FormatMessage(msg)
if err != nil {
log.Printf("Fail to format message: %s, %s\n", err, msg.Data)
return fmt.Errorf("fail to format for output file: %s, data: %q", err, msg.Data)
}

switch msg.Type {
case ws.Request:
color.New(color.FgGreen).Fprint(c.output, "->\n")
case ws.Response:
color.New(color.FgRed).Fprint(c.output, "<-\n")
default:
return fmt.Errorf("unknown message type: %s, data: %q", msg.Type, msg.Data)
}

fmt.Fprintf(c.output, "%s\n\n", output)
fmt.Fprintf(c.output, "%s\n", output)

if opts.OutputFile != nil {
output, err := c.formater.FormatForFile(msg)
if err != nil {
log.Printf("Fail to format message for file: %s, %s\n", err, msg.Data)
return fmt.Errorf("fail to write to output file: %s", err)
}

fmt.Fprintln(c.output, opts.OutputFile, output)
fmt.Fprintln(opts.OutputFile, output)
}
}
}
}

func (c *CLI) RequestMod(keysEvents <-chan keyboard.KeyEvent) error {
fmt.Fprintln(c.output, "Ctrl+S to send>")
color.New(color.FgGreen).Fprint(c.output, "->\n")

c.showCursor()
req, err := c.editor.EditRequest(keysEvents, "")
fmt.Fprint(c.output, LineUp+LineClear)
c.hideCursor()

if err != nil {
return err
}

if req != "" {
err = c.wsConn.Send(req)
if err != nil {
fmt.Fprintln(c.output, "Fail to send request:", err)
return fmt.Errorf("fail to send request: %s", err)
}
}

fmt.Fprint(c.output, LineUp+LineClear)

return nil
}

func (c *CLI) hideCursor() {
fmt.Fprint(c.output, HideCursor)
}

func (c *CLI) showCursor() {
fmt.Fprint(c.output, ShowCursor)
}
6 changes: 5 additions & 1 deletion pkg/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ func TestNewCLI(t *testing.T) {
}

output := os.Stdout
cli := NewCLI(wsConn, &mockInput{}, output)
cli, err := NewCLI(wsConn, &mockInput{}, output)

if err != nil {
t.Fatalf("Unexpected error: %v", err)
}

if cli.formater == nil {
t.Error("Expected non-nil formater")
Expand Down
8 changes: 8 additions & 0 deletions pkg/cli/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,14 @@ func (c *Content) GetLinesAfterPosition(pos int) (startOfLine int, lines []strin
return startOfLine, strings.Split(string(c.text[startOfLine:]), string(NewLine))
}

func (c *Content) PrevSymbol() rune {
if c.pos <= 0 {
return 0
}

return c.text[c.pos-1]
}

func lastIndexOf(buffer []rune, pos int, search rune) int {
for i := pos; i >= 0; i-- {
if buffer[i] == search {
Expand Down
Loading

0 comments on commit ef3f83e

Please sign in to comment.