diff --git a/internal/aws/sigv4.go b/internal/aws/sigv4.go index d50e0c8..70ba7f4 100644 --- a/internal/aws/sigv4.go +++ b/internal/aws/sigv4.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "github.com/ryanfowler/fetch/internal/vars" + "github.com/ryanfowler/fetch/internal/core" ) const ( @@ -113,9 +113,13 @@ func getPayloadHash(req *http.Request, service string) (string, error) { return hexSha256Reader(bytes.NewReader(body)) } -func getSignedHeaders(req *http.Request) []vars.KeyVal { - out := make([]vars.KeyVal, 0, len(req.Header)+1) - out = append(out, vars.KeyVal{Key: "host", Val: req.URL.Host}) +func getSignedHeaders(req *http.Request) []core.KeyVal { + out := make([]core.KeyVal, 0, len(req.Header)+1) + + if _, ok := req.Header["Host"]; !ok { + out = append(out, core.KeyVal{Key: "host", Val: req.URL.Host}) + } + for key, vals := range req.Header { switch key { case "Authorization", "Content-Length", "User-Agent": @@ -123,15 +127,15 @@ func getSignedHeaders(req *http.Request) []vars.KeyVal { } key = strings.ToLower(strings.TrimSpace(key)) val := strings.TrimSpace(strings.Join(vals, ",")) - out = append(out, vars.KeyVal{Key: key, Val: val}) + out = append(out, core.KeyVal{Key: key, Val: val}) } - slices.SortFunc(out, func(a, b vars.KeyVal) int { + slices.SortFunc(out, func(a, b core.KeyVal) int { return strings.Compare(a.Key, b.Key) }) return out } -func buildCanonicalRequest(req *http.Request, headers []vars.KeyVal, payload string) []byte { +func buildCanonicalRequest(req *http.Request, headers []core.KeyVal, payload string) []byte { var buf bytes.Buffer buf.Grow(512) diff --git a/internal/cli/app.go b/internal/cli/app.go index 2fe4f7b..bbd63f4 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -13,37 +13,36 @@ import ( "github.com/ryanfowler/fetch/internal/aws" "github.com/ryanfowler/fetch/internal/client" - "github.com/ryanfowler/fetch/internal/fetch" + "github.com/ryanfowler/fetch/internal/core" "github.com/ryanfowler/fetch/internal/printer" - "github.com/ryanfowler/fetch/internal/vars" ) type App struct { URL *url.URL AWSSigv4 *aws.Config - Basic *vars.KeyVal + Basic *core.KeyVal Bearer string - Color printer.Color + Color core.Color Data io.Reader DNSServer string DryRun bool Edit bool - Form []vars.KeyVal - Format fetch.Format - Headers []vars.KeyVal + Form []core.KeyVal + Format core.Format + Headers []core.KeyVal Help bool HTTP client.HTTPVersion IgnoreStatus bool Insecure bool JSON bool Method string - Multipart []vars.KeyVal + Multipart []core.KeyVal NoEncode bool NoPager bool Output string Proxy *url.URL - QueryParams []vars.KeyVal + QueryParams []core.KeyVal Silent bool Timeout time.Duration TLS uint16 @@ -168,7 +167,7 @@ func (a *App) CLI() *CLI { const usage = "format must be " return flagValueError("basic", value, usage) } - a.Basic = &vars.KeyVal{Key: user, Val: pass} + a.Basic = &core.KeyVal{Key: user, Val: pass} return nil }, }, @@ -194,16 +193,16 @@ func (a *App) CLI() *CLI { Default: "", Values: []string{"auto", "off", "on"}, IsSet: func() bool { - return a.Color != printer.ColorUnknown + return a.Color != core.ColorUnknown }, Fn: func(value string) error { switch value { case "auto": - a.Color = printer.ColorAuto + a.Color = core.ColorAuto case "off": - a.Color = printer.ColorOff + a.Color = core.ColorOff case "on": - a.Color = printer.ColorOn + a.Color = core.ColorOn default: const usage = "must be one of [auto, off, on]" return flagValueError("color", value, usage) @@ -314,7 +313,7 @@ func (a *App) CLI() *CLI { }, Fn: func(value string) error { key, val, _ := cut(value, "=") - a.Form = append(a.Form, vars.KeyVal{Key: key, Val: val}) + a.Form = append(a.Form, core.KeyVal{Key: key, Val: val}) return nil }, }, @@ -326,16 +325,16 @@ func (a *App) CLI() *CLI { Default: "", Values: []string{"auto", "off", "on"}, IsSet: func() bool { - return a.Format != fetch.FormatUnknown + return a.Format != core.FormatUnknown }, Fn: func(value string) error { switch value { case "auto": - a.Format = fetch.FormatAuto + a.Format = core.FormatAuto case "off": - a.Format = fetch.FormatOff + a.Format = core.FormatOff case "on": - a.Format = fetch.FormatOn + a.Format = core.FormatOn default: const usage = "must be one of [auto, off, on]" return flagValueError("format", value, usage) @@ -354,7 +353,7 @@ func (a *App) CLI() *CLI { }, Fn: func(value string) error { key, val, _ := cut(value, ":") - a.Headers = append(a.Headers, vars.KeyVal{Key: key, Val: val}) + a.Headers = append(a.Headers, core.KeyVal{Key: key, Val: val}) return nil }, }, @@ -475,7 +474,7 @@ func (a *App) CLI() *CLI { return fmt.Errorf("file is a directory: '%s'", val[1:]) } } - a.Multipart = append(a.Multipart, vars.KeyVal{Key: key, Val: val}) + a.Multipart = append(a.Multipart, core.KeyVal{Key: key, Val: val}) return nil }, }, @@ -550,7 +549,7 @@ func (a *App) CLI() *CLI { }, Fn: func(value string) error { key, val, _ := cut(value, "=") - a.QueryParams = append(a.QueryParams, vars.KeyVal{Key: key, Val: val}) + a.QueryParams = append(a.QueryParams, core.KeyVal{Key: key, Val: val}) return nil }, }, diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 4b3b781..71bd708 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -5,6 +5,7 @@ import ( "testing" "unicode/utf8" + "github.com/ryanfowler/fetch/internal/core" "github.com/ryanfowler/fetch/internal/printer" ) @@ -13,7 +14,7 @@ func TestCLI(t *testing.T) { if err != nil { t.Fatalf("unable to parse cli: %s", err.Error()) } - p := printer.NewHandle(printer.ColorOff).Stdout() + p := printer.NewHandle(core.ColorOff).Stdout() // Verify that no line of the help command is over 80 characters. app.PrintHelp(p) diff --git a/internal/client/client.go b/internal/client/client.go index 7b4eaed..f694482 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -12,8 +12,8 @@ import ( "time" "github.com/ryanfowler/fetch/internal/aws" + "github.com/ryanfowler/fetch/internal/core" "github.com/ryanfowler/fetch/internal/multipart" - "github.com/ryanfowler/fetch/internal/vars" ) type HTTPVersion int @@ -91,14 +91,14 @@ func NewClient(cfg ClientConfig) *Client { type RequestConfig struct { Method string URL *url.URL - Form []vars.KeyVal + Form []core.KeyVal Multipart *multipart.Multipart - Headers []vars.KeyVal - QueryParams []vars.KeyVal + Headers []core.KeyVal + QueryParams []core.KeyVal Body io.Reader NoEncode bool AWSSigV4 *aws.Config - Basic *vars.KeyVal + Basic *core.KeyVal Bearer string JSON bool XML bool @@ -129,7 +129,7 @@ func (c *Client) NewRequest(ctx context.Context, cfg RequestConfig) (*http.Reque } req.Header.Set("Accept", "application/json,application/xml,image/webp,*/*") - req.Header.Set("User-Agent", vars.UserAgent) + req.Header.Set("User-Agent", core.UserAgent) switch { case cfg.JSON: diff --git a/internal/core/core.go b/internal/core/core.go new file mode 100644 index 0000000..ddc06bd --- /dev/null +++ b/internal/core/core.go @@ -0,0 +1,35 @@ +package core + +// Color represents the options for enabling or disabling color output. +type Color int + +const ( + ColorUnknown Color = iota + ColorAuto + ColorOn + ColorOff +) + +// Format represents the options for enabling or disabling formatting. +type Format int + +const ( + FormatUnknown Format = iota + FormatAuto + FormatOff + FormatOn +) + +// Verbosity represents how verbose the output should be. +type Verbosity int + +const ( + VSilent Verbosity = iota + VNormal + VVerbose + VExtraVerbose +) + +type KeyVal struct { + Key, Val string +} diff --git a/internal/core/errors.go b/internal/core/errors.go new file mode 100644 index 0000000..6990547 --- /dev/null +++ b/internal/core/errors.go @@ -0,0 +1,22 @@ +package core + +import ( + "fmt" + "time" +) + +// ErrRequestTimedOut represents the error when the request times out. +type ErrRequestTimedOut struct { + Timeout time.Duration +} + +func (err ErrRequestTimedOut) Error() string { + return fmt.Sprintf("request timed out after %s", err.Timeout) +} + +// SignalError represents the error when a signal is caught. +type SignalError string + +func (err SignalError) Error() string { + return fmt.Sprintf("received signal: %s", string(err)) +} diff --git a/internal/vars/vars.go b/internal/core/vars.go similarity index 78% rename from internal/vars/vars.go rename to internal/core/vars.go index d9347b5..0f7574d 100644 --- a/internal/vars/vars.go +++ b/internal/core/vars.go @@ -1,11 +1,9 @@ -package vars +package core import ( "encoding/json" - "fmt" "os" "runtime/debug" - "time" "golang.org/x/term" ) @@ -37,24 +35,6 @@ func getVersion() string { return buildInfo.Main.Version } -type KeyVal struct { - Key, Val string -} - -type ErrRequestTimedOut struct { - Timeout time.Duration -} - -func (err ErrRequestTimedOut) Error() string { - return fmt.Sprintf("request timed out after %s", err.Timeout) -} - -type SignalError string - -func (err SignalError) Error() string { - return fmt.Sprintf("received signal: %s", string(err)) -} - func GetVersions() []byte { type Versions struct { Fetch string `json:"fetch"` diff --git a/internal/fetch/fetch.go b/internal/fetch/fetch.go index 3ebcd08..a8e244c 100644 --- a/internal/fetch/fetch.go +++ b/internal/fetch/fetch.go @@ -9,19 +9,17 @@ import ( "net/url" "os" "os/exec" - "strconv" + "slices" "strings" "time" - "unicode" - "unicode/utf8" "github.com/ryanfowler/fetch/internal/aws" "github.com/ryanfowler/fetch/internal/client" + "github.com/ryanfowler/fetch/internal/core" "github.com/ryanfowler/fetch/internal/format" "github.com/ryanfowler/fetch/internal/image" "github.com/ryanfowler/fetch/internal/multipart" "github.com/ryanfowler/fetch/internal/printer" - "github.com/ryanfowler/fetch/internal/vars" ) type ContentType int @@ -35,20 +33,11 @@ const ( TypeXML ) -type Format int - -const ( - FormatUnknown Format = iota - FormatAuto - FormatOff - FormatOn -) - type Request struct { DNSServer string DryRun bool Edit bool - Format Format + Format core.Format HTTP client.HTTPVersion IgnoreStatus bool Insecure bool @@ -57,17 +46,17 @@ type Request struct { Output string PrinterHandle *printer.Handle TLS uint16 - Verbosity Verbosity + Verbosity core.Verbosity Method string URL *url.URL Body io.Reader - Form []vars.KeyVal + Form []core.KeyVal Multipart *multipart.Multipart - Headers []vars.KeyVal - QueryParams []vars.KeyVal + Headers []core.KeyVal + QueryParams []core.KeyVal AWSSigv4 *aws.Config - Basic *vars.KeyVal + Basic *core.KeyVal Bearer string JSON bool XML bool @@ -149,7 +138,7 @@ func fetch(ctx context.Context, r *Request) (int, error) { } } - if r.Verbosity >= VExtraVerbose || r.DryRun { + if r.Verbosity >= core.VExtraVerbose || r.DryRun { errPrinter := r.PrinterHandle.Stderr() printRequestMetadata(errPrinter, req) @@ -172,7 +161,7 @@ func fetch(ctx context.Context, r *Request) (int, error) { if r.Timeout > 0 { var cancel context.CancelFunc - cause := vars.ErrRequestTimedOut{Timeout: r.Timeout} + cause := core.ErrRequestTimedOut{Timeout: r.Timeout} ctx, cancel = context.WithTimeoutCause(req.Context(), r.Timeout, cause) defer cancel() req = req.WithContext(ctx) @@ -193,7 +182,7 @@ func makeRequest(r *Request, c *client.Client, req *http.Request) (int, error) { exitCode = getExitCodeForStatus(resp.StatusCode) } - if r.Verbosity >= VNormal { + if r.Verbosity >= core.VNormal { p := r.PrinterHandle.Stderr() printResponseMetadata(p, r.Verbosity, resp) p.Flush() @@ -228,7 +217,7 @@ func formatResponse(r *Request, resp *http.Response, p *printer.Printer) (io.Rea return nil, err } - if r.Format == FormatOff || (!vars.IsStdoutTerm && r.Format != FormatOn) { + if r.Format == core.FormatOff || (!core.IsStdoutTerm && r.Format != core.FormatOn) { return resp.Body, nil } @@ -266,117 +255,6 @@ func formatResponse(r *Request, resp *http.Response, p *printer.Printer) (io.Rea return bytes.NewReader(buf), nil } -func printRequestMetadata(p *printer.Printer, req *http.Request) { - p.Set(printer.Bold) - p.Set(printer.Yellow) - p.WriteString(req.Method) - p.Reset() - - path := req.URL.Path - if path == "" { - path = "/" - } - - p.WriteString(" ") - p.Set(printer.Bold) - p.Set(printer.Cyan) - p.WriteString(path) - p.Reset() - - q := req.URL.RawQuery - if req.URL.ForceQuery || q != "" { - p.Set(printer.Italic) - p.Set(printer.Cyan) - p.WriteString("?") - p.WriteString(q) - p.Reset() - } - - p.WriteString(" ") - p.Set(printer.Dim) - p.WriteString(req.Proto) - p.Reset() - - p.WriteString("\n") - - headers := getHeaders(req.Header) - if req.Header.Get("Host") == "" { - headers = addHeader(headers, vars.KeyVal{Key: "host", Val: req.URL.Host}) - } - - for _, h := range headers { - p.Set(printer.Bold) - p.Set(printer.Blue) - p.WriteString(h.Key) - p.Reset() - p.WriteString(": ") - p.WriteString(h.Val) - p.WriteString("\n") - } -} - -func printResponseMetadata(p *printer.Printer, v Verbosity, resp *http.Response) { - p.Set(printer.Dim) - p.WriteString(resp.Proto) - p.Reset() - p.WriteString(" ") - - statusColor := colorForStatus(resp.StatusCode) - p.Set(statusColor) - p.Set(printer.Bold) - p.WriteString(strconv.Itoa(resp.StatusCode)) - - text := http.StatusText(resp.StatusCode) - if text != "" { - p.Reset() - p.WriteString(" ") - p.Set(statusColor) - p.WriteString(text) - } - - p.Reset() - p.WriteString("\n") - - if v > VNormal { - printResponseHeaders(p, resp) - } - - p.WriteString("\n") -} - -func printResponseHeaders(p *printer.Printer, resp *http.Response) { - headers := getHeaders(resp.Header) - if resp.ContentLength >= 0 && resp.Header.Get("Content-Length") == "" { - val := strconv.FormatInt(resp.ContentLength, 10) - headers = addHeader(headers, vars.KeyVal{Key: "content-length", Val: val}) - } - if len(resp.TransferEncoding) > 0 && resp.Header.Get("Transfer-Encoding") == "" { - val := strings.Join(resp.TransferEncoding, ",") - headers = addHeader(headers, vars.KeyVal{Key: "transfer-encoding", Val: val}) - } - - for _, h := range headers { - p.Set(printer.Bold) - p.Set(printer.Cyan) - p.WriteString(h.Key) - p.Reset() - p.WriteString(": ") - p.WriteString(h.Val) - p.WriteString("\n") - } -} - -func colorForStatus(code int) printer.Sequence { - switch { - case code >= 200 && code < 300: - return printer.Green - case code >= 300 && code < 400: - return printer.Yellow - default: - return printer.Red - } -} - func getContentType(headers http.Header) ContentType { contentType := headers.Get("Content-Type") if contentType == "" { @@ -420,7 +298,7 @@ func getContentType(headers http.Header) ContentType { func streamToStdout(r io.Reader, p *printer.Printer, forceOutput, noPager bool) error { // Check output to see if it's likely safe to print to stdout. - if vars.IsStdoutTerm && !forceOutput { + if core.IsStdoutTerm && !forceOutput { var ok bool var err error ok, r, err = isPrintable(r) @@ -434,7 +312,7 @@ func streamToStdout(r io.Reader, p *printer.Printer, forceOutput, noPager bool) } // Optionally stream output to a pager. - if !noPager && vars.IsStdoutTerm { + if !noPager && core.IsStdoutTerm { path, err := exec.LookPath("less") if err == nil { return streamToPager(r, path) @@ -466,50 +344,21 @@ func getExitCodeForStatus(status int) int { } } -// isPrintable returns true if the data in the provided io.Reader is likely -// okay to print to a terminal. -func isPrintable(r io.Reader) (bool, io.Reader, error) { - buf := make([]byte, 1024) - n, err := io.ReadFull(r, buf) - switch { - case err == io.EOF || err == io.ErrUnexpectedEOF: - buf = buf[:n] - r = bytes.NewReader(buf) - case err != nil: - return false, nil, err - default: - r = io.MultiReader(bytes.NewReader(buf), r) - } - - if bytes.ContainsRune(buf, '\x00') { - return false, r, nil - } - - var safe, total int - for len(buf) > 0 { - c, size := utf8.DecodeRune(buf) - buf = buf[size:] - if c == utf8.RuneError && len(buf) < 4 { - break - } - total++ - if unicode.IsPrint(c) || unicode.IsSpace(c) || c == '\x1b' { - safe++ - } +func getHeaders(headers http.Header) []core.KeyVal { + out := make([]core.KeyVal, 0, len(headers)) + for k, v := range headers { + k = strings.ToLower(k) + out = append(out, core.KeyVal{Key: k, Val: strings.Join(v, ",")}) } - - if total == 0 { - return true, r, nil - } - return float64(safe)/float64(total) >= 0.9, r, nil + slices.SortFunc(out, func(a, b core.KeyVal) int { + return strings.Compare(a.Key, b.Key) + }) + return out } -func printBinaryWarning(p *printer.Printer) { - p.Set(printer.Bold) - p.Set(printer.Yellow) - p.WriteString("warning") - p.Reset() - p.WriteString(": the response body appears to be binary\n\n") - p.WriteString("To output to the terminal anyway, use '--output -'\n") - p.Flush() +func addHeader(headers []core.KeyVal, h core.KeyVal) []core.KeyVal { + i, _ := slices.BinarySearchFunc(headers, h, func(a, b core.KeyVal) int { + return strings.Compare(a.Key, b.Key) + }) + return slices.Insert(headers, i, h) } diff --git a/internal/fetch/http.go b/internal/fetch/http.go deleted file mode 100644 index fd74b3c..0000000 --- a/internal/fetch/http.go +++ /dev/null @@ -1,28 +0,0 @@ -package fetch - -import ( - "net/http" - "slices" - "strings" - - "github.com/ryanfowler/fetch/internal/vars" -) - -func getHeaders(headers http.Header) []vars.KeyVal { - out := make([]vars.KeyVal, 0, len(headers)) - for k, v := range headers { - k = strings.ToLower(k) - out = append(out, vars.KeyVal{Key: k, Val: strings.Join(v, ",")}) - } - slices.SortFunc(out, func(a, b vars.KeyVal) int { - return strings.Compare(a.Key, b.Key) - }) - return out -} - -func addHeader(headers []vars.KeyVal, h vars.KeyVal) []vars.KeyVal { - i, _ := slices.BinarySearchFunc(headers, h, func(a, b vars.KeyVal) int { - return strings.Compare(a.Key, b.Key) - }) - return slices.Insert(headers, i, h) -} diff --git a/internal/fetch/print.go b/internal/fetch/print.go new file mode 100644 index 0000000..ebbde48 --- /dev/null +++ b/internal/fetch/print.go @@ -0,0 +1,173 @@ +package fetch + +import ( + "bytes" + "io" + "net/http" + "strconv" + "strings" + "unicode" + "unicode/utf8" + + "github.com/ryanfowler/fetch/internal/core" + "github.com/ryanfowler/fetch/internal/printer" +) + +func printRequestMetadata(p *printer.Printer, req *http.Request) { + p.Set(printer.Bold) + p.Set(printer.Yellow) + p.WriteString(req.Method) + p.Reset() + + path := req.URL.Path + if path == "" { + path = "/" + } + + p.WriteString(" ") + p.Set(printer.Bold) + p.Set(printer.Cyan) + p.WriteString(path) + p.Reset() + + q := req.URL.RawQuery + if req.URL.ForceQuery || q != "" { + p.Set(printer.Italic) + p.Set(printer.Cyan) + p.WriteString("?") + p.WriteString(q) + p.Reset() + } + + p.WriteString(" ") + p.Set(printer.Dim) + p.WriteString(req.Proto) + p.Reset() + + p.WriteString("\n") + + headers := getHeaders(req.Header) + if req.Header.Get("Host") == "" { + headers = addHeader(headers, core.KeyVal{Key: "host", Val: req.URL.Host}) + } + + for _, h := range headers { + p.Set(printer.Bold) + p.Set(printer.Blue) + p.WriteString(h.Key) + p.Reset() + p.WriteString(": ") + p.WriteString(h.Val) + p.WriteString("\n") + } +} + +func printResponseMetadata(p *printer.Printer, v core.Verbosity, resp *http.Response) { + p.Set(printer.Dim) + p.WriteString(resp.Proto) + p.Reset() + p.WriteString(" ") + + statusColor := colorForStatus(resp.StatusCode) + p.Set(statusColor) + p.Set(printer.Bold) + p.WriteString(strconv.Itoa(resp.StatusCode)) + + text := http.StatusText(resp.StatusCode) + if text != "" { + p.Reset() + p.WriteString(" ") + p.Set(statusColor) + p.WriteString(text) + } + + p.Reset() + p.WriteString("\n") + + if v > core.VNormal { + printResponseHeaders(p, resp) + } + + p.WriteString("\n") +} + +func printResponseHeaders(p *printer.Printer, resp *http.Response) { + headers := getHeaders(resp.Header) + if resp.ContentLength >= 0 && resp.Header.Get("Content-Length") == "" { + val := strconv.FormatInt(resp.ContentLength, 10) + headers = addHeader(headers, core.KeyVal{Key: "content-length", Val: val}) + } + if len(resp.TransferEncoding) > 0 && resp.Header.Get("Transfer-Encoding") == "" { + val := strings.Join(resp.TransferEncoding, ",") + headers = addHeader(headers, core.KeyVal{Key: "transfer-encoding", Val: val}) + } + + for _, h := range headers { + p.Set(printer.Bold) + p.Set(printer.Cyan) + p.WriteString(h.Key) + p.Reset() + p.WriteString(": ") + p.WriteString(h.Val) + p.WriteString("\n") + } +} + +func printBinaryWarning(p *printer.Printer) { + p.Set(printer.Bold) + p.Set(printer.Yellow) + p.WriteString("warning") + p.Reset() + p.WriteString(": the response body appears to be binary\n\n") + p.WriteString("To output to the terminal anyway, use '--output -'\n") + p.Flush() +} + +func colorForStatus(code int) printer.Sequence { + switch { + case code >= 200 && code < 300: + return printer.Green + case code >= 300 && code < 400: + return printer.Yellow + default: + return printer.Red + } +} + +// isPrintable returns true if the data in the provided io.Reader is likely +// okay to print to a terminal. +func isPrintable(r io.Reader) (bool, io.Reader, error) { + buf := make([]byte, 1024) + n, err := io.ReadFull(r, buf) + switch { + case err == io.EOF || err == io.ErrUnexpectedEOF: + buf = buf[:n] + r = bytes.NewReader(buf) + case err != nil: + return false, nil, err + default: + r = io.MultiReader(bytes.NewReader(buf), r) + } + + if bytes.ContainsRune(buf, '\x00') { + return false, r, nil + } + + var safe, total int + for len(buf) > 0 { + c, size := utf8.DecodeRune(buf) + buf = buf[size:] + if c == utf8.RuneError && len(buf) < 4 { + break + } + total++ + if unicode.IsPrint(c) || unicode.IsSpace(c) || c == '\x1b' { + safe++ + } + } + + if total == 0 { + return true, r, nil + } + return float64(safe)/float64(total) >= 0.9, r, nil +} diff --git a/internal/fetch/verbosity.go b/internal/fetch/verbosity.go deleted file mode 100644 index a32ca76..0000000 --- a/internal/fetch/verbosity.go +++ /dev/null @@ -1,10 +0,0 @@ -package fetch - -type Verbosity int - -const ( - VSilent Verbosity = iota - VNormal - VVerbose - VExtraVerbose -) diff --git a/internal/multipart/multipart.go b/internal/multipart/multipart.go index b972d13..4883646 100644 --- a/internal/multipart/multipart.go +++ b/internal/multipart/multipart.go @@ -6,7 +6,7 @@ import ( "os" "strings" - "github.com/ryanfowler/fetch/internal/vars" + "github.com/ryanfowler/fetch/internal/core" ) type Multipart struct { @@ -14,7 +14,7 @@ type Multipart struct { contentType string } -func NewMultipart(kvs []vars.KeyVal) *Multipart { +func NewMultipart(kvs []core.KeyVal) *Multipart { if len(kvs) == 0 { return nil } diff --git a/internal/printer/printer.go b/internal/printer/printer.go index e0692e4..eb884f0 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -5,37 +5,29 @@ import ( "io" "os" - "github.com/ryanfowler/fetch/internal/vars" + "github.com/ryanfowler/fetch/internal/core" ) -const escape = "\x1b" - type Sequence string const ( - Bold = "1m" - Dim = "2m" - Italic = "3m" - Underline = "4m" - - Black = "30m" - Red = "31m" - Green = "32m" - Yellow = "33m" - Blue = "34m" - Magenta = "35m" - Cyan = "36m" - White = "37m" - Default = "39m" -) - -type Color int - -const ( - ColorUnknown Color = iota - ColorAuto - ColorOn - ColorOff + escape = "\x1b" + reset = "0" + + Bold Sequence = "1" + Dim Sequence = "2" + Italic Sequence = "3" + Underline Sequence = "4" + + Black Sequence = "30" + Red Sequence = "31" + Green Sequence = "32" + Yellow Sequence = "33" + Blue Sequence = "34" + Magenta Sequence = "35" + Cyan Sequence = "36" + White Sequence = "37" + Default Sequence = "39" ) type PrinterTo interface { @@ -47,10 +39,10 @@ type Handle struct { stdout *Printer } -func NewHandle(c Color) *Handle { +func NewHandle(c core.Color) *Handle { return &Handle{ - stderr: newPrinter(os.Stderr, vars.IsStderrTerm, c), - stdout: newPrinter(os.Stdout, vars.IsStdoutTerm, c), + stderr: newPrinter(os.Stderr, core.IsStderrTerm, c), + stdout: newPrinter(os.Stdout, core.IsStdoutTerm, c), } } @@ -68,12 +60,12 @@ type Printer struct { useColor bool } -func newPrinter(file *os.File, isTerm bool, c Color) *Printer { +func newPrinter(file *os.File, isTerm bool, c core.Color) *Printer { var useColor bool switch c { - case ColorOn: + case core.ColorOn: useColor = true - case ColorOff: + case core.ColorOff: useColor = false default: useColor = isTerm @@ -86,14 +78,12 @@ func (p *Printer) Set(s Sequence) { p.buf.WriteString(escape) p.buf.WriteByte('[') p.buf.WriteString(string(s)) + p.buf.WriteByte('m') } } func (p *Printer) Reset() { - if p.useColor { - p.buf.WriteString(escape) - p.buf.WriteString("[0m") - } + p.Set(reset) } func (p *Printer) Flush() error { diff --git a/internal/update/update.go b/internal/update/update.go index 4b68e6e..21ac344 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -15,8 +15,8 @@ import ( "time" "github.com/ryanfowler/fetch/internal/client" + "github.com/ryanfowler/fetch/internal/core" "github.com/ryanfowler/fetch/internal/printer" - "github.com/ryanfowler/fetch/internal/vars" ) func Update(ctx context.Context, p *printer.Printer, timeout time.Duration, silent bool) bool { @@ -44,7 +44,7 @@ func update(ctx context.Context, p *printer.Printer, timeout time.Duration, sile if timeout > 0 { var cancel context.CancelFunc - cause := vars.ErrRequestTimedOut{Timeout: timeout} + cause := core.ErrRequestTimedOut{Timeout: timeout} ctx, cancel = context.WithTimeoutCause(ctx, timeout, cause) defer cancel() } @@ -55,8 +55,8 @@ func update(ctx context.Context, p *printer.Printer, timeout time.Duration, sile return fmt.Errorf("fetching latest release: %w", err) } - if latest.TagName == vars.Version { - writeInfo(p, silent, fmt.Sprintf("currently using the latest version (%s)", vars.Version)) + if latest.TagName == core.Version { + writeInfo(p, silent, fmt.Sprintf("currently using the latest version (%s)", core.Version)) return nil } @@ -94,7 +94,7 @@ func update(ctx context.Context, p *printer.Printer, timeout time.Duration, sile return err } - msg := fmt.Sprintf("fetch successfully updated (%s -> %s)", vars.Version, latest.TagName) + msg := fmt.Sprintf("fetch successfully updated (%s -> %s)", core.Version, latest.TagName) writeInfo(p, silent, msg) return nil } diff --git a/main.go b/main.go index 461fe23..48c3461 100644 --- a/main.go +++ b/main.go @@ -9,12 +9,12 @@ import ( "syscall" "github.com/ryanfowler/fetch/internal/cli" + "github.com/ryanfowler/fetch/internal/core" "github.com/ryanfowler/fetch/internal/fetch" "github.com/ryanfowler/fetch/internal/format" "github.com/ryanfowler/fetch/internal/multipart" "github.com/ryanfowler/fetch/internal/printer" "github.com/ryanfowler/fetch/internal/update" - "github.com/ryanfowler/fetch/internal/vars" ) func main() { @@ -23,7 +23,7 @@ func main() { signal.Notify(chSig, syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM) go func() { sig := <-chSig - cancel(vars.SignalError(sig.String())) + cancel(core.SignalError(sig.String())) }() app, err := cli.Parse(os.Args[1:]) @@ -43,18 +43,18 @@ func main() { os.Exit(0) } if app.Version { - fmt.Fprintln(os.Stdout, "fetch", vars.Version) + fmt.Fprintln(os.Stdout, "fetch", core.Version) os.Exit(0) } if app.Versions { p := printerHandle.Stdout() - format.FormatJSON(vars.GetVersions(), p) + format.FormatJSON(core.GetVersions(), p) p.Flush() os.Exit(0) } if app.Update { p := printerHandle.Stderr() - ok := update.Update(ctx, p, app.Timeout, verbosity == fetch.VSilent) + ok := update.Update(ctx, p, app.Timeout, verbosity == core.VSilent) if ok { os.Exit(0) } @@ -101,17 +101,17 @@ func main() { os.Exit(status) } -func getVerbosity(app *cli.App) fetch.Verbosity { +func getVerbosity(app *cli.App) core.Verbosity { if app.Silent { - return fetch.VSilent + return core.VSilent } switch app.Verbose { case 0: - return fetch.VNormal + return core.VNormal case 1: - return fetch.VVerbose + return core.VVerbose default: - return fetch.VExtraVerbose + return core.VExtraVerbose } }