Skip to content

Commit

Permalink
Pager friendly terminal formatting (#1006)
Browse files Browse the repository at this point in the history
Let's say a pager, like [moar](https://github.com/walles/moar) (uses
Chroma for syntax highlighting) or `less`, shows a line in the middle of
a file.

Unless that line starts with the correct formatting for the line, the
pager would have to scan the whole file from the start to get the
coloring of this single line right.

Before this PR, lines were not guaranteed to start with formatting, but
could sometimes rely on formatting from the preceding lines.

With this change in place, lines can now stand by themselves, and paging
will work better on Chroma's output.

Missing formatting at the start of the line would happen when a token
had a linefeed in the middle.
  • Loading branch information
walles authored Oct 2, 2024
1 parent a56e228 commit 876fb61
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 29 deletions.
9 changes: 1 addition & 8 deletions formatters/tty_indexed.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package formatters

import (
"fmt"
"io"
"math"

Expand Down Expand Up @@ -257,13 +256,7 @@ func (c *indexedTTYFormatter) Format(w io.Writer, style *chroma.Style, it chroma
}
}

if clr != "" {
fmt.Fprint(w, clr)
}
fmt.Fprint(w, token.Value)
if clr != "" {
fmt.Fprintf(w, "\033[0m")
}
writeToken(w, clr, token.Value)
}
return nil
}
Expand Down
76 changes: 55 additions & 21 deletions formatters/tty_truecolour.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,74 @@ package formatters
import (
"fmt"
"io"
"regexp"

"github.com/alecthomas/chroma/v2"
)

// TTY16m is a true-colour terminal formatter.
var TTY16m = Register("terminal16m", chroma.FormatterFunc(trueColourFormatter))

var crOrCrLf = regexp.MustCompile(`\r?\n`)

// Print the text with the given formatting, resetting the formatting at the end
// of each line and resuming it on the next line.
//
// This way, a pager (like https://github.com/walles/moar for example) can show
// any line in the output by itself, and it will get the right formatting.
func writeToken(w io.Writer, formatting string, text string) {
if formatting == "" {
fmt.Fprint(w, text)
return
}

newlineIndices := crOrCrLf.FindAllStringIndex(text, -1)

afterLastNewline := 0
for _, indices := range newlineIndices {
newlineStart, afterNewline := indices[0], indices[1]
fmt.Fprint(w, formatting)
fmt.Fprint(w, text[afterLastNewline:newlineStart])
fmt.Fprint(w, "\033[0m")
fmt.Fprint(w, text[newlineStart:afterNewline])
afterLastNewline = afterNewline
}

if afterLastNewline < len(text) {
// Print whatever is left after the last newline
fmt.Fprint(w, formatting)
fmt.Fprint(w, text[afterLastNewline:])
fmt.Fprint(w, "\033[0m")
}
}

func trueColourFormatter(w io.Writer, style *chroma.Style, it chroma.Iterator) error {
style = clearBackground(style)
for token := it(); token != chroma.EOF; token = it() {
entry := style.Get(token.Type)
if !entry.IsZero() {
out := ""
if entry.Bold == chroma.Yes {
out += "\033[1m"
}
if entry.Underline == chroma.Yes {
out += "\033[4m"
}
if entry.Italic == chroma.Yes {
out += "\033[3m"
}
if entry.Colour.IsSet() {
out += fmt.Sprintf("\033[38;2;%d;%d;%dm", entry.Colour.Red(), entry.Colour.Green(), entry.Colour.Blue())
}
if entry.Background.IsSet() {
out += fmt.Sprintf("\033[48;2;%d;%d;%dm", entry.Background.Red(), entry.Background.Green(), entry.Background.Blue())
}
fmt.Fprint(w, out)
if entry.IsZero() {
fmt.Fprint(w, token.Value)
continue
}

formatting := ""
if entry.Bold == chroma.Yes {
formatting += "\033[1m"
}
if entry.Underline == chroma.Yes {
formatting += "\033[4m"
}
fmt.Fprint(w, token.Value)
if !entry.IsZero() {
fmt.Fprint(w, "\033[0m")
if entry.Italic == chroma.Yes {
formatting += "\033[3m"
}
if entry.Colour.IsSet() {
formatting += fmt.Sprintf("\033[38;2;%d;%d;%dm", entry.Colour.Red(), entry.Colour.Green(), entry.Colour.Blue())
}
if entry.Background.IsSet() {
formatting += fmt.Sprintf("\033[48;2;%d;%d;%dm", entry.Background.Red(), entry.Background.Green(), entry.Background.Blue())
}

writeToken(w, formatting, token.Value)
}
return nil
}

0 comments on commit 876fb61

Please sign in to comment.