From ed21442977e8042fb1be5277af6c5f15d99d52ce Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Mon, 28 Oct 2024 05:53:49 +0100 Subject: [PATCH 1/7] Extract a function --- m/reader.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/m/reader.go b/m/reader.go index e753eed..0d5d3b4 100644 --- a/m/reader.go +++ b/m/reader.go @@ -531,6 +531,18 @@ func NewReaderFromFilename(filename string, style chroma.Style, formatter chroma return mReader, nil } +func textAsString(reader *Reader) string { + reader.Lock() + defer reader.Unlock() + + text := strings.Builder{} + for _, line := range reader.lines { + text.WriteString(line.raw) + text.WriteString("\n") + } + return text.String() +} + // We expect this to be executed in a goroutine func highlightFromMemory(reader *Reader, style chroma.Style, formatter chroma.Formatter, lexer chroma.Lexer) { defer func() { @@ -555,14 +567,7 @@ 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) if lexer == nil && json.Valid([]byte(text)) { log.Debug("Buffer is valid JSON, highlighting as JSON") From 66414c69d9d8cb209ae64c1a47ea617d557de37f Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Mon, 28 Oct 2024 06:14:13 +0100 Subject: [PATCH 2/7] Auto format JSON --- m/reader.go | 21 +++++++++++++++++++-- m/reader_test.go | 14 ++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/m/reader.go b/m/reader.go index 0d5d3b4..b192b10 100644 --- a/m/reader.go +++ b/m/reader.go @@ -533,14 +533,31 @@ func NewReaderFromFilename(filename string, style chroma.Style, formatter chroma func textAsString(reader *Reader) string { reader.Lock() - defer reader.Unlock() text := strings.Builder{} for _, line := range reader.lines { text.WriteString(line.raw) text.WriteString("\n") } - return text.String() + result := text.String() + reader.Unlock() + + jsonMap := make(map[string](interface{})) + err := json.Unmarshal([]byte(result), &jsonMap) + if err != nil { + // Not JSON, return the text as-is + return result + } + + // 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 diff --git a/m/reader_test.go b/m/reader_test.go index 7564912..6de7035 100644 --- a/m/reader_test.go +++ b/m/reader_test.go @@ -322,6 +322,20 @@ 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, *styles.Get("Native"), formatters.TTY, nil) + + 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) { From f993601b3b2ae7c685887544f3a64f1754394e03 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Fri, 1 Nov 2024 21:49:36 +0100 Subject: [PATCH 3/7] Move Lexer into ReaderOptions This is the first of (currently) three options to go in there. --- m/pager_test.go | 8 ++++---- m/reader.go | 51 +++++++++++++++++++++++++++++++----------------- m/reader_test.go | 32 +++++++++++++++--------------- moar.go | 4 ++-- 4 files changed, 55 insertions(+), 40 deletions(-) diff --git a/m/pager_test.go b/m/pager_test.go index a23edac..ff7cf3f 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, *styles.Get("native"), formatters.TTY16m, ReaderOptions{}) 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", *styles.Get("native"), formatters.TTY16m, ReaderOptions{}) 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", *styles.Get("native"), formatters.TTY16m, ReaderOptions{}) 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, chroma.Style{}, nil, ReaderOptions{}) assert.NilError(t, myReader._wait()) pager := NewPager(myReader) diff --git a/m/reader.go b/m/reader.go index b192b10..095ea58 100644 --- a/m/reader.go +++ b/m/reader.go @@ -26,6 +26,20 @@ import ( //revive:disable-next-line:var-naming const MAX_HIGHLIGHT_SIZE int64 = 1024 * 1024 +type ReaderOptions struct { + //// Format JSON input + //ShouldFormat bool + + FIXME: Use this rather than taking style as a parameter in this file + + //// 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 +124,11 @@ 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) + highlightFromMemory(reader, <-reader.highlightingStyle, formatter, options) log.Debug("highlightFromMemory() took ", time.Since(t0)) reader.done.Store(true) @@ -307,8 +321,8 @@ func (reader *Reader) tailFile() error { // 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 NewReaderFromStreamWithoutStyle(name string, reader io.Reader, formatter chroma.Formatter, options ReaderOptions) *Reader { + mReader := newReaderFromStream(reader, nil, formatter, options) if len(name) > 0 { mReader.Lock() @@ -316,7 +330,7 @@ func NewReaderFromStreamWithoutStyle(name string, reader io.Reader, formatter ch mReader.Unlock() } - if lexer == nil { + if options.Lexer == nil { mReader.highlightingDone.Store(true) } @@ -329,8 +343,8 @@ func NewReaderFromStreamWithoutStyle(name string, reader io.Reader, formatter ch // // 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) +func NewReaderFromStream(name string, reader io.Reader, style chroma.Style, formatter chroma.Formatter, options ReaderOptions) *Reader { + mReader := NewReaderFromStreamWithoutStyle(name, reader, formatter, options) mReader.SetStyleForHighlighting(style) return mReader } @@ -346,7 +360,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 +383,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 @@ -486,7 +500,7 @@ func countLines(filename string) (uint64, error) { // Note that you must call reader.SetStyleForHighlighting() after this to get // highlighting. -func NewReaderFromFilenameWithoutStyle(filename string, formatter chroma.Formatter, lexer chroma.Lexer) (*Reader, error) { +func NewReaderFromFilenameWithoutStyle(filename string, formatter chroma.Formatter, options ReaderOptions) (*Reader, error) { fileError := tryOpen(filename) if fileError != nil { return nil, fileError @@ -497,18 +511,18 @@ 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) } @@ -517,13 +531,13 @@ func NewReaderFromFilenameWithoutStyle(filename string, formatter chroma.Formatt // NewReaderFromFilename creates a new file reader. // -// If lexer is nil it will be determined from the input file name. +// If options.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 NewReaderFromFilename(filename string, style chroma.Style, formatter chroma.Formatter, options ReaderOptions) (*Reader, error) { + mReader, err := NewReaderFromFilenameWithoutStyle(filename, formatter, options) if err != nil { return nil, err } @@ -561,7 +575,7 @@ func textAsString(reader *Reader) string { } // 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, style chroma.Style, formatter chroma.Formatter, options ReaderOptions) { defer func() { reader.highlightingDone.Store(true) select { @@ -586,6 +600,7 @@ func highlightFromMemory(reader *Reader, style chroma.Style, formatter chroma.Fo text := textAsString(reader) + lexer := options.Lexer if lexer == nil && json.Valid([]byte(text)) { log.Debug("Buffer is valid JSON, highlighting as JSON") lexer = lexers.Get("json") diff --git a/m/reader_test.go b/m/reader_test.go index 6de7035..6bbdc64 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, *styles.Get("native"), formatters.TTY16m, ReaderOptions{}) 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, *styles.Get("native"), formatters.TTY16m, ReaderOptions{}) 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, *styles.Get("native"), formatters.TTY16m, ReaderOptions{}) 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", *styles.Get("native"), formatters.TTY16m, ReaderOptions{}) 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, *styles.Get("native"), formatters.TTY16m, ReaderOptions{}) 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) + *styles.Get("Native"), formatters.TTY, ReaderOptions{}) 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) + *styles.Get("Native"), formatters.TTY, ReaderOptions{}) 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"), chroma.Style{}, nil, ReaderOptions{}) 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) + *styles.Get("Native"), formatters.TTY, ReaderOptions{Lexer: lexers.EmacsLisp}) assert.NilError(t, testMe._wait()) } @@ -325,7 +325,7 @@ func TestReadTextDone(t *testing.T) { // JSON should be auto detected and formatted func TestFormatJson(t *testing.T) { jsonStream := strings.NewReader(`{"key": "value"}`) - testMe := NewReaderFromStream("JSON test", jsonStream, *styles.Get("Native"), formatters.TTY, nil) + testMe := NewReaderFromStream("JSON test", jsonStream, *styles.Get("Native"), formatters.TTY, ReaderOptions{}) assert.NilError(t, testMe._wait()) @@ -349,7 +349,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(), *styles.Get("native"), formatters.TTY16m, ReaderOptions{}) assert.NilError(t, err) // Wait for the reader to finish reading @@ -422,7 +422,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(), *styles.Get("native"), formatters.TTY16m, ReaderOptions{}) assert.NilError(t, err) // Wait for the reader to finish reading @@ -467,7 +467,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(), *styles.Get("native"), formatters.TTY16m, ReaderOptions{}) assert.NilError(t, err) // Wait for the reader to finish reading @@ -512,7 +512,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(), *styles.Get("native"), formatters.TTY16m, ReaderOptions{}) assert.NilError(t, err) // Wait for the reader to finish reading @@ -551,7 +551,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, *styles.Get("native"), formatters.TTY16m, ReaderOptions{}) assert.NilError(b, err) assert.NilError(b, readMe._wait()) @@ -586,7 +586,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, *styles.Get("native"), formatters.TTY16m, ReaderOptions{}) assert.NilError(b, err) assert.NilError(b, readMe._wait()) diff --git a/moar.go b/moar.go index 4719ecf..65070f1 100644 --- a/moar.go +++ b/moar.go @@ -462,13 +462,13 @@ func pagerFromArgs( var reader *m.Reader if stdinIsRedirected { // Display input pipe contents - reader = m.NewReaderFromStreamWithoutStyle("", os.Stdin, formatter, *lexer) + reader = m.NewReaderFromStreamWithoutStyle("", os.Stdin, formatter, m.ReaderOptions{Lexer: *lexer}) } 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.NewReaderFromFilenameWithoutStyle(flagSet.Args()[0], formatter, m.ReaderOptions{Lexer: *lexer}) if err != nil { return nil, nil, chroma.Style{}, nil, err } From 12abddaa56bf76b170b6c51c977378fdcc84be56 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Fri, 1 Nov 2024 22:12:41 +0100 Subject: [PATCH 4/7] Use options for passing styles --- m/pager_test.go | 8 ++--- m/reader.go | 88 +++++++++++++++++++++++++----------------------- m/reader_test.go | 32 +++++++++--------- moar.go | 4 +-- 4 files changed, 67 insertions(+), 65 deletions(-) diff --git a/m/pager_test.go b/m/pager_test.go index ff7cf3f..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, ReaderOptions{}) + 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, ReaderOptions{}) + 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, ReaderOptions{}) + 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, ReaderOptions{}) + 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 095ea58..fbf4f39 100644 --- a/m/reader.go +++ b/m/reader.go @@ -27,14 +27,15 @@ import ( const MAX_HIGHLIGHT_SIZE int64 = 1024 * 1024 type ReaderOptions struct { + + FIXME: Implement ShouldFormat and use it from command line parsing + //// Format JSON input //ShouldFormat bool - FIXME: Use this rather than taking style as a parameter in this file - - //// If this is nil, you must call reader.SetStyleForHighlighting() later if - //// you want highlighting. - //Style *chroma.Style + // 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 @@ -128,7 +129,9 @@ func (reader *Reader) readStream(stream io.Reader, formatter chroma.Formatter, o reader.consumeLinesFromStream(stream) t0 := time.Now() - highlightFromMemory(reader, <-reader.highlightingStyle, formatter, options) + style := <-reader.highlightingStyle + options.Style = &style + highlightFromMemory(reader, formatter, options) log.Debug("highlightFromMemory() took ", time.Since(t0)) reader.done.Store(true) @@ -319,9 +322,16 @@ 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, options ReaderOptions) *Reader { +func NewReaderFromStream(name string, reader io.Reader, formatter chroma.Formatter, options ReaderOptions) *Reader { mReader := newReaderFromStream(reader, nil, formatter, options) if len(name) > 0 { @@ -334,18 +344,10 @@ func NewReaderFromStreamWithoutStyle(name string, reader io.Reader, formatter ch 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, options ReaderOptions) *Reader { - mReader := NewReaderFromStreamWithoutStyle(name, reader, formatter, options) - mReader.SetStyleForHighlighting(style) return mReader } @@ -498,9 +500,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, options ReaderOptions) (*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 @@ -526,23 +536,11 @@ func NewReaderFromFilenameWithoutStyle(filename string, formatter chroma.Formatt returnMe.highlightingDone.Store(true) } - return returnMe, nil -} - -// NewReaderFromFilename creates a new file reader. -// -// If options.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, options ReaderOptions) (*Reader, error) { - mReader, err := NewReaderFromFilenameWithoutStyle(filename, formatter, options) - if err != nil { - return nil, err + if options.Style != nil { + returnMe.SetStyleForHighlighting(*options.Style) } - mReader.SetStyleForHighlighting(style) - return mReader, nil + + return returnMe, nil } func textAsString(reader *Reader) string { @@ -575,7 +573,7 @@ func textAsString(reader *Reader) string { } // We expect this to be executed in a goroutine -func highlightFromMemory(reader *Reader, style chroma.Style, formatter chroma.Formatter, options ReaderOptions) { +func highlightFromMemory(reader *Reader, formatter chroma.Formatter, options ReaderOptions) { defer func() { reader.highlightingDone.Store(true) select { @@ -600,18 +598,22 @@ func highlightFromMemory(reader *Reader, style chroma.Style, formatter chroma.Fo text := textAsString(reader) - lexer := options.Lexer - 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 6bbdc64..6bb3505 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, ReaderOptions{}) + 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, ReaderOptions{}) + 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, ReaderOptions{}) + 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, ReaderOptions{}) + 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, ReaderOptions{}) + 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, ReaderOptions{}) + 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, ReaderOptions{}) + 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, ReaderOptions{}) + 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, ReaderOptions{Lexer: lexers.EmacsLisp}) + formatters.TTY, ReaderOptions{Lexer: lexers.EmacsLisp, Style: styles.Get("native")}) assert.NilError(t, testMe._wait()) } @@ -325,7 +325,7 @@ func TestReadTextDone(t *testing.T) { // JSON should be auto detected and formatted func TestFormatJson(t *testing.T) { jsonStream := strings.NewReader(`{"key": "value"}`) - testMe := NewReaderFromStream("JSON test", jsonStream, *styles.Get("Native"), formatters.TTY, ReaderOptions{}) + testMe := NewReaderFromStream("JSON test", jsonStream, formatters.TTY, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, testMe._wait()) @@ -349,7 +349,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, ReaderOptions{}) + testMe, err := NewReaderFromFilename(file.Name(), formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) // Wait for the reader to finish reading @@ -422,7 +422,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, ReaderOptions{}) + testMe, err := NewReaderFromFilename(file.Name(), formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) // Wait for the reader to finish reading @@ -467,7 +467,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, ReaderOptions{}) + testMe, err := NewReaderFromFilename(file.Name(), formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) // Wait for the reader to finish reading @@ -512,7 +512,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, ReaderOptions{}) + testMe, err := NewReaderFromFilename(file.Name(), formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) // Wait for the reader to finish reading @@ -551,7 +551,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, ReaderOptions{}) + readMe, err := NewReaderFromFilename(filename, formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(b, err) assert.NilError(b, readMe._wait()) @@ -586,7 +586,7 @@ func BenchmarkReadLargeFile(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - readMe, err := NewReaderFromFilename(largeFileName, *styles.Get("native"), formatters.TTY16m, ReaderOptions{}) + readMe, err := NewReaderFromFilename(largeFileName, formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(b, err) assert.NilError(b, readMe._wait()) diff --git a/moar.go b/moar.go index 65070f1..c6c671b 100644 --- a/moar.go +++ b/moar.go @@ -462,13 +462,13 @@ func pagerFromArgs( var reader *m.Reader if stdinIsRedirected { // Display input pipe contents - reader = m.NewReaderFromStreamWithoutStyle("", os.Stdin, formatter, m.ReaderOptions{Lexer: *lexer}) + reader = m.NewReaderFromStream("", os.Stdin, formatter, m.ReaderOptions{Lexer: *lexer}) } 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, m.ReaderOptions{Lexer: *lexer}) + reader, err = m.NewReaderFromFilename(flagSet.Args()[0], formatter, m.ReaderOptions{Lexer: *lexer}) if err != nil { return nil, nil, chroma.Style{}, nil, err } From 81ae9356cb87c1576a4e1083092b79fc245999c2 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Fri, 1 Nov 2024 22:19:59 +0100 Subject: [PATCH 5/7] Add command line flag for disabling JSON formatting --- m/reader.go | 16 +++++++++------- moar.go | 6 ++++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/m/reader.go b/m/reader.go index fbf4f39..54147a2 100644 --- a/m/reader.go +++ b/m/reader.go @@ -27,11 +27,8 @@ import ( const MAX_HIGHLIGHT_SIZE int64 = 1024 * 1024 type ReaderOptions struct { - - FIXME: Implement ShouldFormat and use it from command line parsing - - //// Format JSON input - //ShouldFormat bool + // Format JSON input + ShouldFormat bool // If this is nil, you must call reader.SetStyleForHighlighting() later if // you want highlighting. @@ -543,7 +540,7 @@ func NewReaderFromFilename(filename string, formatter chroma.Formatter, options return returnMe, nil } -func textAsString(reader *Reader) string { +func textAsString(reader *Reader, shouldFormat bool) string { reader.Lock() text := strings.Builder{} @@ -554,6 +551,11 @@ func textAsString(reader *Reader) string { 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 { @@ -596,7 +598,7 @@ func highlightFromMemory(reader *Reader, formatter chroma.Formatter, options Rea } reader.Unlock() - text := textAsString(reader) + text := textAsString(reader, options.ShouldFormat) if options.Lexer == nil && json.Valid([]byte(text)) { log.Debug("Buffer is valid JSON, highlighting as JSON") diff --git a/moar.go b/moar.go index c6c671b..c4e004c 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 '='") + noFormat := flagSet.Bool("no-format", false, "Never format the input") 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 := !*noFormat if stdinIsRedirected { // Display input pipe contents - reader = m.NewReaderFromStream("", os.Stdin, formatter, m.ReaderOptions{Lexer: *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.NewReaderFromFilename(flagSet.Args()[0], formatter, m.ReaderOptions{Lexer: *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 } From ed0f9391cbd28e3eb76960761cfce4635566f62f Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Fri, 1 Nov 2024 22:23:09 +0100 Subject: [PATCH 6/7] Fix a test we recently broke --- m/reader_test.go | 9 ++++++++- moar.go | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/m/reader_test.go b/m/reader_test.go index 6bb3505..ab66c64 100644 --- a/m/reader_test.go +++ b/m/reader_test.go @@ -325,7 +325,14 @@ func TestReadTextDone(t *testing.T) { // 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")}) + testMe := NewReaderFromStream( + "JSON test", + jsonStream, + formatters.TTY, + ReaderOptions{ + Style: styles.Get("native"), + ShouldFormat: true, + }) assert.NilError(t, testMe._wait()) diff --git a/moar.go b/moar.go index c4e004c..57f38e9 100644 --- a/moar.go +++ b/moar.go @@ -331,7 +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 '='") - noFormat := flagSet.Bool("no-format", false, "Never format the input") + noFormat := flagSet.Bool("no-format", false, "Never format 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, From 4146c17ea08b9c1ed6af609089114d7973d87af4 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Fri, 1 Nov 2024 22:28:20 +0100 Subject: [PATCH 7/7] Document the --no-reformat command line option --- moar.1 | 3 +++ moar.go | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) 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 57f38e9..7cb97b6 100644 --- a/moar.go +++ b/moar.go @@ -331,7 +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 '='") - noFormat := flagSet.Bool("no-format", false, "Never format the input (but keep highlighting)") + 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, @@ -461,7 +461,7 @@ func pagerFromArgs( } var reader *m.Reader - shouldFormat := !*noFormat + shouldFormat := !*noReFormat if stdinIsRedirected { // Display input pipe contents reader = m.NewReaderFromStream("", os.Stdin, formatter, m.ReaderOptions{Lexer: *lexer, ShouldFormat: shouldFormat})