diff --git a/cmd/cli/args.go b/cmd/cli/args.go index df04d78..b30e2c2 100644 --- a/cmd/cli/args.go +++ b/cmd/cli/args.go @@ -4,12 +4,14 @@ import ( "errors" "flag" "fmt" + "os" "strings" "github.com/agrski/greg/pkg/auth" fetchTypes "github.com/agrski/greg/pkg/fetch/types" "github.com/agrski/greg/pkg/match" "github.com/agrski/greg/pkg/types" + "github.com/mattn/go-isatty" "golang.org/x/oauth2" ) @@ -39,8 +41,10 @@ type rawArgs struct { accessToken string accessTokenFile string // Presentation/display behaviour - quiet bool - verbose bool + quiet bool + verbose bool + colour bool + noColour bool } type Args struct { @@ -49,6 +53,7 @@ type Args struct { filetypes []types.FileExtension tokenSource oauth2.TokenSource verbosity VerbosityLevel + enableColour bool } func GetArgs() (*Args, error) { @@ -81,12 +86,15 @@ func GetArgs() (*Args, error) { verbosity := getVerbosity(raw.quiet, raw.verbose) + enableColour := getColourEnabled(raw.colour, raw.noColour) + return &Args{ location: location, searchPattern: pattern, filetypes: filetypes, tokenSource: tokenSource, verbosity: verbosity, + enableColour: enableColour, }, nil } @@ -112,6 +120,8 @@ func parseArguments() (*rawArgs, error) { ) flag.BoolVar(&args.quiet, "quiet", false, "disable logging; overrides verbose mode") flag.BoolVar(&args.verbose, "verbose", false, "increase logging; overridden by quiet mode") + flag.BoolVar(&args.colour, "colour", false, "force coloured outputs; overridden by no-colour") + flag.BoolVar(&args.noColour, "no-colour", false, "force uncoloured outputs; overrides colour") flag.Parse() if 1 != flag.NArg() { @@ -247,3 +257,14 @@ func getVerbosity(quiet bool, verbose bool) VerbosityLevel { return VerbosityNormal } } + +func getColourEnabled(forceColour bool, forceNoColour bool) bool { + if forceNoColour { + return false + } else if forceColour { + return true + } else { + fd := os.Stdout.Fd() + return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd) + } +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 13e57f4..220edfd 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -7,19 +7,23 @@ import ( "strings" "time" + "github.com/rs/zerolog" + "github.com/agrski/greg/pkg/fetch" fetchTypes "github.com/agrski/greg/pkg/fetch/types" - "github.com/rs/zerolog" + "github.com/agrski/greg/pkg/match" + "github.com/agrski/greg/pkg/present/console" ) func main() { - logger := makeLogger(zerolog.InfoLevel) - args, err := GetArgs() if err != nil { + logger := makeLogger(zerolog.InfoLevel, false) logger.Fatal().Err(err).Send() } + logger := makeLogger(zerolog.InfoLevel, args.enableColour) + switch args.verbosity { case VerbosityQuiet: logger = logger.Level(zerolog.Disabled) @@ -29,6 +33,10 @@ func main() { // Already at normal verbosity } + console := console.New(os.Stdout, args.enableColour) + + matcher := match.New(logger, args.filetypes) + fetcher := fetch.New(logger, args.location, args.tokenSource) uri := makeURI(args.location) @@ -41,12 +49,14 @@ func main() { fetcher.Start() next, ok := fetcher.Next() if ok { - fmt.Println(next) + if m, ok := matcher.Match(args.searchPattern, next); ok { + console.Write(next, m) + } } fetcher.Stop() } -func makeLogger(level zerolog.Level) zerolog.Logger { +func makeLogger(level zerolog.Level, enableColour bool) zerolog.Logger { fieldKeyFormatter := func(v interface{}) string { return strings.ToUpper( fmt.Sprintf("%s=", v), @@ -54,6 +64,7 @@ func makeLogger(level zerolog.Level) zerolog.Logger { } logWriter := zerolog.ConsoleWriter{ Out: os.Stderr, + NoColor: !enableColour, TimeFormat: time.RFC3339, FormatLevel: func(v interface{}) string { l, ok := v.(string) diff --git a/go.mod b/go.mod index 6810758..ade8cd4 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/hasura/go-graphql-client v0.6.3 + github.com/mattn/go-isatty v0.0.12 github.com/rs/zerolog v1.26.1 github.com/stretchr/testify v1.4.0 golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b diff --git a/pkg/match/exact.go b/pkg/match/exact.go index 568a1b5..84c4d7b 100644 --- a/pkg/match/exact.go +++ b/pkg/match/exact.go @@ -33,16 +33,19 @@ func (em *exactMatcher) Match(pattern string, next *types.FileInfo) (*Match, boo lineReader := bufio.NewScanner( strings.NewReader(next.Text), ) - row := uint(0) - for lineReader.Scan() { - row++ - - matchColumns := em.matchLine(pattern, lineReader.Text()) + for row := 0; lineReader.Scan(); row++ { + line := lineReader.Text() + matchColumns := em.matchLine(pattern, line) for _, column := range matchColumns { match.Positions = append( match.Positions, - &FilePosition{Line: row, Column: column}, + &FilePosition{ + Line: uint(row), + ColumnStart: column, + ColumnEnd: column + uint(len(pattern)), + Text: line, + }, ) } } @@ -68,7 +71,7 @@ func (em *exactMatcher) matchLine(pattern string, line string) []uint { break } else { column += offset - matchColumns = append(matchColumns, uint(1+column)) + matchColumns = append(matchColumns, uint(column)) column += len(pattern) line = line[offset+len(pattern):] diff --git a/pkg/match/exact_test.go b/pkg/match/exact_test.go index 73355d1..2744491 100644 --- a/pkg/match/exact_test.go +++ b/pkg/match/exact_test.go @@ -59,8 +59,10 @@ func TestMatch(t *testing.T) { expected: &Match{ Positions: []*FilePosition{ { - Line: 1, - Column: 5, + Line: 0, + ColumnStart: 4, + ColumnEnd: 7, + Text: "foo bar baz", }, }, }, @@ -79,8 +81,10 @@ foo expected: &Match{ Positions: []*FilePosition{ { - Line: 5, - Column: 1, + Line: 4, + ColumnStart: 0, + ColumnEnd: 3, + Text: "foo", }, }, }, @@ -99,12 +103,16 @@ foo fifth expected: &Match{ Positions: []*FilePosition{ { - Line: 2, - Column: 8, + Line: 1, + ColumnStart: 7, + ColumnEnd: 10, + Text: "second foo", }, { - Line: 5, - Column: 1, + Line: 4, + ColumnStart: 0, + ColumnEnd: 3, + Text: "foo fifth", }, }, }, @@ -118,12 +126,16 @@ foo fifth expected: &Match{ Positions: []*FilePosition{ { - Line: 1, - Column: 1, + Line: 0, + ColumnStart: 0, + ColumnEnd: 3, + Text: "foo bar foo", }, { - Line: 1, - Column: 9, + Line: 0, + ColumnStart: 8, + ColumnEnd: 11, + Text: "foo bar foo", }, }, }, diff --git a/pkg/match/match.go b/pkg/match/match.go index 79de0c4..34dc1bc 100644 --- a/pkg/match/match.go +++ b/pkg/match/match.go @@ -14,8 +14,10 @@ type Match struct { } type FilePosition struct { - Line uint - Column uint + Line uint + ColumnStart uint + ColumnEnd uint + Text string } type filteringMatcher struct { diff --git a/pkg/present/console/colour.go b/pkg/present/console/colour.go new file mode 100644 index 0000000..ebf77c8 --- /dev/null +++ b/pkg/present/console/colour.go @@ -0,0 +1,61 @@ +package console + +// ANSI colour codes + +type ansiCode string + +const ( + escape ansiCode = "\u001b" + csi ansiCode = "[" + codePrefix = escape + csi + codeSuffix = "m" +) + +const ( + reset ansiCode = codePrefix + "0" + codeSuffix + + bold ansiCode = codePrefix + "1" + codeSuffix + faint ansiCode = codePrefix + "2" + codeSuffix + italic ansiCode = codePrefix + "3" + codeSuffix + underline ansiCode = codePrefix + "4" + codeSuffix + invert ansiCode = codePrefix + "7" + codeSuffix + conceal ansiCode = codePrefix + "8" + codeSuffix + strikethrough ansiCode = codePrefix + "9" + codeSuffix + + fgBlack ansiCode = codePrefix + "30" + codeSuffix + fgRed ansiCode = codePrefix + "31" + codeSuffix + fgGreen ansiCode = codePrefix + "32" + codeSuffix + fgYellow ansiCode = codePrefix + "33" + codeSuffix + fgBlue ansiCode = codePrefix + "34" + codeSuffix + fgMagenta ansiCode = codePrefix + "35" + codeSuffix + fgCyan ansiCode = codePrefix + "36" + codeSuffix + fgWhite ansiCode = codePrefix + "37" + codeSuffix + fgDefault ansiCode = codePrefix + "39" + codeSuffix + + bgBlack ansiCode = codePrefix + "40" + codeSuffix + bgRed ansiCode = codePrefix + "41" + codeSuffix + bgGreen ansiCode = codePrefix + "42" + codeSuffix + bgYellow ansiCode = codePrefix + "43" + codeSuffix + bgBlue ansiCode = codePrefix + "44" + codeSuffix + bgMagenta ansiCode = codePrefix + "45" + codeSuffix + bgCyan ansiCode = codePrefix + "46" + codeSuffix + bgWhite ansiCode = codePrefix + "47" + codeSuffix + + fgIntenseBlack ansiCode = codePrefix + "90" + codeSuffix + fgIntenseRed ansiCode = codePrefix + "91" + codeSuffix + fgIntenseGreen ansiCode = codePrefix + "92" + codeSuffix + fgIntenseYellow ansiCode = codePrefix + "93" + codeSuffix + fgIntenseBlue ansiCode = codePrefix + "94" + codeSuffix + fgIntenseMagenta ansiCode = codePrefix + "95" + codeSuffix + fgIntenseCyan ansiCode = codePrefix + "96" + codeSuffix + fgIntenseWhite ansiCode = codePrefix + "97" + codeSuffix + + bgIntenseBlack ansiCode = codePrefix + "100" + codeSuffix + bgIntenseRed ansiCode = codePrefix + "101" + codeSuffix + bgIntenseGreen ansiCode = codePrefix + "102" + codeSuffix + bgIntenseYellow ansiCode = codePrefix + "103" + codeSuffix + bgIntenseBlue ansiCode = codePrefix + "104" + codeSuffix + bgIntenseMagenta ansiCode = codePrefix + "105" + codeSuffix + bgIntenseCyan ansiCode = codePrefix + "106" + codeSuffix + bgIntenseWhite ansiCode = codePrefix + "107" + codeSuffix +) diff --git a/pkg/present/console/console.go b/pkg/present/console/console.go new file mode 100644 index 0000000..ee44dce --- /dev/null +++ b/pkg/present/console/console.go @@ -0,0 +1,74 @@ +package console + +import ( + "io" + "strconv" + "strings" + + "github.com/agrski/greg/pkg/match" + "github.com/agrski/greg/pkg/types" +) + +type Console struct { + enableColour bool + out io.StringWriter +} + +func New(out io.StringWriter, enableColour bool) *Console { + return &Console{ + enableColour: enableColour, + out: out, + } +} + +func (c *Console) Write(fileInfo *types.FileInfo, match *match.Match) { + sb := strings.Builder{} + + if c.enableColour { + sb.WriteString(string(fgBlue)) + sb.WriteString(fileInfo.Path) + sb.WriteString(string(reset)) + } else { + sb.WriteString(fileInfo.Path) + } + sb.WriteString("\n") + _, err := c.out.WriteString(sb.String()) + if err != nil { + return + } + + for _, p := range match.Positions { + sb := strings.Builder{} + + line := strconv.Itoa(int(p.Line + 1)) + + if c.enableColour { + // Line number + sb.WriteString(string(fgMagenta)) + sb.WriteString(line) + sb.WriteString(string(reset)) + sb.WriteByte(':') + // Text + sb.WriteString(p.Text[:p.ColumnStart]) + sb.WriteString(string(fgRed)) + sb.WriteString(p.Text[p.ColumnStart:p.ColumnEnd]) + sb.WriteString(string(reset)) + sb.WriteString(p.Text[p.ColumnEnd:]) + } else { + // Line number + sb.WriteString(line) + sb.WriteByte(':') + // Text + sb.WriteString(p.Text) + } + + sb.WriteString("\n") + + _, err := c.out.WriteString(sb.String()) + if err != nil { + return + } + } + + _, _ = c.out.WriteString("\n") +}