diff --git a/m/pager_test.go b/m/pager_test.go index a23edac..b9504ee 100644 --- a/m/pager_test.go +++ b/m/pager_test.go @@ -165,7 +165,7 @@ func TestCodeHighlighting(t *testing.T) { panic("Getting current filename failed") } - reader, err := NewReaderFromFilename(filename, *styles.Get("native"), formatters.TTY16m, nil) + reader, err := NewReaderFromFilename(filename, formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) assert.NilError(t, reader._wait()) @@ -191,7 +191,7 @@ func TestCodeHighlighting(t *testing.T) { func TestCodeHighlight_compressed(t *testing.T) { // Same as TestCodeHighlighting but with "compressed-markdown.md.gz" - reader, err := NewReaderFromFilename("../sample-files/compressed-markdown.md.gz", *styles.Get("native"), formatters.TTY16m, nil) + reader, err := NewReaderFromFilename("../sample-files/compressed-markdown.md.gz", formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) assert.NilError(t, reader._wait()) @@ -221,7 +221,7 @@ func TestCodeHighlight_compressed(t *testing.T) { // Sample file sysctl.h from: // https://github.com/fastfetch-cli/fastfetch/blob/f9597eba39d6afd278eeca2f2972f73a7e54f111/src/common/sysctl.h func TestCodeHighlightingIncludes(t *testing.T) { - reader, err := NewReaderFromFilename("../sample-files/sysctl.h", *styles.Get("native"), formatters.TTY16m, nil) + reader, err := NewReaderFromFilename("../sample-files/sysctl.h", formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) assert.NilError(t, reader._wait()) @@ -541,7 +541,7 @@ func TestPageSamples(t *testing.T) { } }() - myReader := NewReaderFromStream(fileName, file, chroma.Style{}, nil, nil) + myReader := NewReaderFromStream(fileName, file, nil, ReaderOptions{Style: &chroma.Style{}}) assert.NilError(t, myReader._wait()) pager := NewPager(myReader) diff --git a/m/reader.go b/m/reader.go index e753eed..54147a2 100644 --- a/m/reader.go +++ b/m/reader.go @@ -26,6 +26,18 @@ import ( //revive:disable-next-line:var-naming const MAX_HIGHLIGHT_SIZE int64 = 1024 * 1024 +type ReaderOptions struct { + // Format JSON input + ShouldFormat bool + + // If this is nil, you must call reader.SetStyleForHighlighting() later if + // you want highlighting. + Style *chroma.Style + + // If this is set, it will be used as the lexer for highlighting + Lexer chroma.Lexer +} + // Reader reads a file into an array of strings. // // It does the reading in the background, and it returns parts of the read data @@ -110,11 +122,13 @@ func (reader *Reader) preAllocLines() { reader.lines = make([]*Line, 0, lineCount) } -func (reader *Reader) readStream(stream io.Reader, formatter chroma.Formatter, lexer chroma.Lexer) { +func (reader *Reader) readStream(stream io.Reader, formatter chroma.Formatter, options ReaderOptions) { reader.consumeLinesFromStream(stream) t0 := time.Now() - highlightFromMemory(reader, <-reader.highlightingStyle, formatter, lexer) + style := <-reader.highlightingStyle + options.Style = &style + highlightFromMemory(reader, formatter, options) log.Debug("highlightFromMemory() took ", time.Since(t0)) reader.done.Store(true) @@ -305,10 +319,17 @@ func (reader *Reader) tailFile() error { } } +// NewReaderFromStream creates a new stream reader +// +// The name can be an empty string (""). +// +// If non-empty, the name will be displayed by the pager in the bottom left +// corner to help the user keep track of what is being paged. +// // Note that you must call reader.SetStyleForHighlighting() after this to get // highlighting. -func NewReaderFromStreamWithoutStyle(name string, reader io.Reader, formatter chroma.Formatter, lexer chroma.Lexer) *Reader { - mReader := newReaderFromStream(reader, nil, formatter, lexer) +func NewReaderFromStream(name string, reader io.Reader, formatter chroma.Formatter, options ReaderOptions) *Reader { + mReader := newReaderFromStream(reader, nil, formatter, options) if len(name) > 0 { mReader.Lock() @@ -316,22 +337,14 @@ func NewReaderFromStreamWithoutStyle(name string, reader io.Reader, formatter ch mReader.Unlock() } - if lexer == nil { + if options.Lexer == nil { mReader.highlightingDone.Store(true) } - return mReader -} + if options.Style != nil { + mReader.SetStyleForHighlighting(*options.Style) + } -// NewReaderFromStream creates a new stream reader -// -// The name can be an empty string (""). -// -// If non-empty, the name will be displayed by the pager in the bottom left -// corner to help the user keep track of what is being paged. -func NewReaderFromStream(name string, reader io.Reader, style chroma.Style, formatter chroma.Formatter, lexer chroma.Lexer) *Reader { - mReader := NewReaderFromStreamWithoutStyle(name, reader, formatter, lexer) - mReader.SetStyleForHighlighting(style) return mReader } @@ -346,7 +359,7 @@ func NewReaderFromStream(name string, reader io.Reader, style chroma.Style, form // // Note that you must call reader.SetStyleForHighlighting() after this to get // highlighting. -func newReaderFromStream(reader io.Reader, originalFileName *string, formatter chroma.Formatter, lexer chroma.Lexer) *Reader { +func newReaderFromStream(reader io.Reader, originalFileName *string, formatter chroma.Formatter, options ReaderOptions) *Reader { done := atomic.Bool{} done.Store(false) highlightingDone := atomic.Bool{} @@ -369,7 +382,7 @@ func newReaderFromStream(reader io.Reader, originalFileName *string, formatter c panicHandler("newReaderFromStream()/readStream()", recover(), debug.Stack()) }() - returnMe.readStream(reader, formatter, lexer) + returnMe.readStream(reader, formatter, options) }() return &returnMe @@ -484,9 +497,17 @@ func countLines(filename string) (uint64, error) { return count, nil } -// Note that you must call reader.SetStyleForHighlighting() after this to get -// highlighting. -func NewReaderFromFilenameWithoutStyle(filename string, formatter chroma.Formatter, lexer chroma.Lexer) (*Reader, error) { +// NewReaderFromFilename creates a new file reader. +// +// If options.Lexer is nil it will be determined from the input file name. +// +// If options.Style is nil, you must call reader.SetStyleForHighlighting() later +// to get highlighting. +// +// The Reader will try to uncompress various compressed file format, and also +// apply highlighting to the file using Chroma: +// https://github.com/alecthomas/chroma +func NewReaderFromFilename(filename string, formatter chroma.Formatter, options ReaderOptions) (*Reader, error) { fileError := tryOpen(filename) if fileError != nil { return nil, fileError @@ -497,42 +518,64 @@ func NewReaderFromFilenameWithoutStyle(filename string, formatter chroma.Formatt return nil, err } - if lexer == nil { - lexer = lexers.Match(highlightingFilename) + if options.Lexer == nil { + options.Lexer = lexers.Match(highlightingFilename) } - returnMe := newReaderFromStream(stream, &filename, formatter, lexer) + returnMe := newReaderFromStream(stream, &filename, formatter, options) returnMe.Lock() returnMe.name = &filename returnMe.fileName = &filename returnMe.Unlock() - if lexer == nil { + if options.Lexer == nil { returnMe.highlightingDone.Store(true) } + if options.Style != nil { + returnMe.SetStyleForHighlighting(*options.Style) + } + return returnMe, nil } -// NewReaderFromFilename creates a new file reader. -// -// If lexer is nil it will be determined from the input file name. -// -// The Reader will try to uncompress various compressed file format, and also -// apply highlighting to the file using Chroma: -// https://github.com/alecthomas/chroma -func NewReaderFromFilename(filename string, style chroma.Style, formatter chroma.Formatter, lexer chroma.Lexer) (*Reader, error) { - mReader, err := NewReaderFromFilenameWithoutStyle(filename, formatter, lexer) +func textAsString(reader *Reader, shouldFormat bool) string { + reader.Lock() + + text := strings.Builder{} + for _, line := range reader.lines { + text.WriteString(line.raw) + text.WriteString("\n") + } + result := text.String() + reader.Unlock() + + if !shouldFormat { + // Formatting disabled, we're done + return result + } + + jsonMap := make(map[string](interface{})) + err := json.Unmarshal([]byte(result), &jsonMap) if err != nil { - return nil, err + // Not JSON, return the text as-is + return result } - mReader.SetStyleForHighlighting(style) - return mReader, nil + + // Pretty print the JSON + prettyJSON, err := json.MarshalIndent(jsonMap, "", " ") + if err != nil { + log.Debug("Failed to pretty print JSON: ", err) + return result + } + + log.Debug("JSON input pretty printed") + return string(prettyJSON) } // We expect this to be executed in a goroutine -func highlightFromMemory(reader *Reader, style chroma.Style, formatter chroma.Formatter, lexer chroma.Lexer) { +func highlightFromMemory(reader *Reader, formatter chroma.Formatter, options ReaderOptions) { defer func() { reader.highlightingDone.Store(true) select { @@ -555,26 +598,24 @@ func highlightFromMemory(reader *Reader, style chroma.Style, formatter chroma.Fo } reader.Unlock() - textBuilder := strings.Builder{} - reader.Lock() - for _, line := range reader.lines { - textBuilder.WriteString(line.raw) - textBuilder.WriteString("\n") - } - reader.Unlock() - text := textBuilder.String() + text := textAsString(reader, options.ShouldFormat) - if lexer == nil && json.Valid([]byte(text)) { + if options.Lexer == nil && json.Valid([]byte(text)) { log.Debug("Buffer is valid JSON, highlighting as JSON") - lexer = lexers.Get("json") + options.Lexer = lexers.Get("json") } - if lexer == nil { + if options.Lexer == nil { log.Debug("No lexer set for highlighting") return } - highlighted, err := highlight(text, style, formatter, lexer) + if options.Style == nil { + log.Debug("No style set for highlighting") + return + } + + highlighted, err := highlight(text, *options.Style, formatter, options.Lexer) if err != nil { log.Warn("Highlighting failed: ", err) return diff --git a/m/reader_test.go b/m/reader_test.go index 7564912..ab66c64 100644 --- a/m/reader_test.go +++ b/m/reader_test.go @@ -143,7 +143,7 @@ func (r *Reader) _wait() error { func TestGetLines(t *testing.T) { for _, file := range getTestFiles(t) { - reader, err := NewReaderFromFilename(file, *styles.Get("native"), formatters.TTY16m, nil) + reader, err := NewReaderFromFilename(file, formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) if err != nil { t.Errorf("Error opening file <%s>: %s", file, err.Error()) continue @@ -197,7 +197,7 @@ func testHighlightingLineCount(t *testing.T, filenameWithPath string) { } // Then load the same file using one of our Readers - reader, err := NewReaderFromFilename(filenameWithPath, *styles.Get("native"), formatters.TTY16m, nil) + reader, err := NewReaderFromFilename(filenameWithPath, formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) err = reader._wait() assert.NilError(t, err) @@ -208,7 +208,7 @@ func testHighlightingLineCount(t *testing.T, filenameWithPath string) { func TestGetLongLine(t *testing.T) { file := "../sample-files/very-long-line.txt" - reader, err := NewReaderFromFilename(file, *styles.Get("native"), formatters.TTY16m, nil) + reader, err := NewReaderFromFilename(file, formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) assert.NilError(t, reader._wait()) @@ -253,7 +253,7 @@ func TestStatusText(t *testing.T) { testStatusText(t, linenumbers.LineNumber{}, linenumbers.LineNumber{}, 1, "1 line 100%") // Test with filename - testMe, err := NewReaderFromFilename(samplesDir+"/empty", *styles.Get("native"), formatters.TTY16m, nil) + testMe, err := NewReaderFromFilename(samplesDir+"/empty", formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) assert.NilError(t, testMe._wait()) @@ -267,7 +267,7 @@ func TestStatusText(t *testing.T) { func testCompressedFile(t *testing.T, filename string) { filenameWithPath := path.Join(samplesDir, filename) - reader, e := NewReaderFromFilename(filenameWithPath, *styles.Get("native"), formatters.TTY16m, nil) + reader, e := NewReaderFromFilename(filenameWithPath, formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) if e != nil { t.Errorf("Error opening file <%s>: %s", filenameWithPath, e.Error()) panic(e) @@ -288,7 +288,7 @@ func TestCompressedFiles(t *testing.T) { func TestReadFileDoneNoHighlighting(t *testing.T) { testMe, err := NewReaderFromFilename(samplesDir+"/empty", - *styles.Get("Native"), formatters.TTY, nil) + formatters.TTY, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) assert.NilError(t, testMe._wait()) @@ -296,14 +296,14 @@ func TestReadFileDoneNoHighlighting(t *testing.T) { func TestReadFileDoneYesHighlighting(t *testing.T) { testMe, err := NewReaderFromFilename("reader_test.go", - *styles.Get("Native"), formatters.TTY, nil) + formatters.TTY, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) assert.NilError(t, testMe._wait()) } func TestReadStreamDoneNoHighlighting(t *testing.T) { - testMe := NewReaderFromStream("", strings.NewReader("Johan"), chroma.Style{}, nil, nil) + testMe := NewReaderFromStream("", strings.NewReader("Johan"), nil, ReaderOptions{Style: &chroma.Style{}}) assert.NilError(t, testMe._wait()) } @@ -311,7 +311,7 @@ func TestReadStreamDoneNoHighlighting(t *testing.T) { func TestReadStreamDoneYesHighlighting(t *testing.T) { testMe := NewReaderFromStream("", strings.NewReader("Johan"), - *styles.Get("Native"), formatters.TTY, lexers.EmacsLisp) + formatters.TTY, ReaderOptions{Lexer: lexers.EmacsLisp, Style: styles.Get("native")}) assert.NilError(t, testMe._wait()) } @@ -322,6 +322,27 @@ func TestReadTextDone(t *testing.T) { assert.NilError(t, testMe._wait()) } +// JSON should be auto detected and formatted +func TestFormatJson(t *testing.T) { + jsonStream := strings.NewReader(`{"key": "value"}`) + testMe := NewReaderFromStream( + "JSON test", + jsonStream, + formatters.TTY, + ReaderOptions{ + Style: styles.Get("native"), + ShouldFormat: true, + }) + + assert.NilError(t, testMe._wait()) + + lines, _ := testMe.GetLines(linenumbers.LineNumber{}, 10) + assert.Equal(t, lines.lines[0].Plain(nil), "{") + assert.Equal(t, lines.lines[1].Plain(nil), ` "key": "value"`) + assert.Equal(t, lines.lines[2].Plain(nil), "}") + assert.Equal(t, len(lines.lines), 3) +} + // If people keep appending to the currently opened file we should display those // changes. func TestReadUpdatingFile(t *testing.T) { @@ -335,7 +356,7 @@ func TestReadUpdatingFile(t *testing.T) { assert.NilError(t, err) // Start a reader on that file - testMe, err := NewReaderFromFilename(file.Name(), *styles.Get("native"), formatters.TTY16m, nil) + testMe, err := NewReaderFromFilename(file.Name(), formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) // Wait for the reader to finish reading @@ -408,7 +429,7 @@ func TestReadUpdatingFile_InitiallyEmpty(t *testing.T) { defer os.Remove(file.Name()) // Start a reader on that file - testMe, err := NewReaderFromFilename(file.Name(), *styles.Get("native"), formatters.TTY16m, nil) + testMe, err := NewReaderFromFilename(file.Name(), formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) // Wait for the reader to finish reading @@ -453,7 +474,7 @@ func TestReadUpdatingFile_HalfLine(t *testing.T) { assert.NilError(t, err) // Start a reader on that file - testMe, err := NewReaderFromFilename(file.Name(), *styles.Get("native"), formatters.TTY16m, nil) + testMe, err := NewReaderFromFilename(file.Name(), formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) // Wait for the reader to finish reading @@ -498,7 +519,7 @@ func TestReadUpdatingFile_HalfUtf8(t *testing.T) { assert.NilError(t, err) // Start a reader on that file - testMe, err := NewReaderFromFilename(file.Name(), *styles.Get("native"), formatters.TTY16m, nil) + testMe, err := NewReaderFromFilename(file.Name(), formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) // Wait for the reader to finish reading @@ -537,7 +558,7 @@ func BenchmarkReaderDone(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { // This is our longest .go file - readMe, err := NewReaderFromFilename(filename, *styles.Get("native"), formatters.TTY16m, nil) + readMe, err := NewReaderFromFilename(filename, formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(b, err) assert.NilError(b, readMe._wait()) @@ -572,7 +593,7 @@ func BenchmarkReadLargeFile(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - readMe, err := NewReaderFromFilename(largeFileName, *styles.Get("native"), formatters.TTY16m, nil) + readMe, err := NewReaderFromFilename(largeFileName, formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(b, err) assert.NilError(b, readMe._wait()) diff --git a/moar.1 b/moar.1 index f7494c8..3e49504 100644 --- a/moar.1 +++ b/moar.1 @@ -62,6 +62,9 @@ Retain screen contents when exiting moar \fB\-\-no\-linenumbers\fR Hide line numbers on startup, press left arrow key to show .TP +\fB\-\-no\-reformat\fR +Moar will implicitly reformat some input (like JSON). This switch disables that reformatting. Even with this switch, highlighting is still done. +.TP \fB\-\-no\-statusbar\fR Hide the status bar, toggle with .B = diff --git a/moar.go b/moar.go index 4719ecf..7cb97b6 100644 --- a/moar.go +++ b/moar.go @@ -331,6 +331,7 @@ func pagerFromArgs( noLineNumbers := flagSet.Bool("no-linenumbers", noLineNumbersDefault(), "Hide line numbers on startup, press left arrow key to show") noStatusBar := flagSet.Bool("no-statusbar", false, "Hide the status bar, toggle with '='") + noReFormat := flagSet.Bool("no-reformat", false, "Never reformat the input (but keep highlighting)") quitIfOneScreen := flagSet.Bool("quit-if-one-screen", false, "Don't page if contents fits on one screen") noClearOnExit := flagSet.Bool("no-clear-on-exit", false, "Retain screen contents when exiting moar") statusBarStyle := flagSetFunc(flagSet, "statusbar", m.STATUSBAR_STYLE_INVERSE, @@ -460,15 +461,16 @@ func pagerFromArgs( } var reader *m.Reader + shouldFormat := !*noReFormat if stdinIsRedirected { // Display input pipe contents - reader = m.NewReaderFromStreamWithoutStyle("", os.Stdin, formatter, *lexer) + reader = m.NewReaderFromStream("", os.Stdin, formatter, m.ReaderOptions{Lexer: *lexer, ShouldFormat: shouldFormat}) } else { // Display the input file contents if len(flagSet.Args()) != 1 { panic("Invariant broken: Expected exactly one filename") } - reader, err = m.NewReaderFromFilenameWithoutStyle(flagSet.Args()[0], formatter, *lexer) + reader, err = m.NewReaderFromFilename(flagSet.Args()[0], formatter, m.ReaderOptions{Lexer: *lexer, ShouldFormat: shouldFormat}) if err != nil { return nil, nil, chroma.Style{}, nil, err }