Skip to content

Commit

Permalink
Add option to highlight specific lines
Browse files Browse the repository at this point in the history
The highlighting is done by applying a grey background with 50% opacity
to all selected lines.

The parsing of the option is as follows:
    - Each input (lines or line ranges) is separated by ;
    - To provide a range, "-" separates the start and the end of the range.
    - In a range, both start and end are required, and end must be greater than start.
  • Loading branch information
Hazegard committed Dec 15, 2024
1 parent f6b970f commit 4f19f05
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 3 deletions.
58 changes: 58 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package main
import (
"embed"
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"time"

"github.com/adrg/xdg"
Expand Down Expand Up @@ -47,9 +50,64 @@ type Config struct {
// Line
LineHeight float64 `json:"line_height" help:"Line height relative to font size." group:"Line" placeholder:"1.2"`
Lines []int `json:"-" help:"Lines to capture (start,end)." group:"Line" placeholder:"0,-1" value:"0,-1"`
HighlightLines string `json:"-" help:"Lines to highlight (range: \"start-end\", separator:\";\")." group:"Line" placeholder:"0,10" value:""`
ShowLineNumbers bool `json:"show_line_numbers" help:"" group:"Line" placeholder:"false"`
}

// ComputeHighlightedLines parse the config.HighlightLines option
// And return a map of Line numbers where the highlight should be applied
func (cfg Config) ComputeHighlightedLines() map[int]bool {
uniqueNumbers := make(map[int]bool) // Use a map to ensure uniqueness
if cfg.HighlightLines == "" {
return uniqueNumbers
}
// Split the input by ';'
parts := strings.Split(cfg.HighlightLines, ";")

for _, part := range parts {
// Check if the part contains a dash '-'
if strings.Contains(part, "-") {
// Split the part by '-' to get start and end of the range
rangeParts := strings.Split(part, "-")
if len(rangeParts) == 2 {
start, err1 := strconv.Atoi(rangeParts[0])
end, err2 := strconv.Atoi(rangeParts[1])

if end <= start {
err := fmt.Errorf("end of range lower (%d) than start of range (%d): %s", end, start, part)
printErrorFatal("error while parsing highlight lines range", err)
}
// If parsing is successful and start <= end
if err1 == nil && err2 == nil && start <= end {
// Add all numbers in the range to the map
for i := start; i <= end; i++ {
uniqueNumbers[i] = true
}
} else if err1 != nil {
err := fmt.Errorf("unable to parse the first part of the range: %s", rangeParts[0])
printErrorFatal("error while parsing highlight lines range", err)
} else if err2 != nil {
err := fmt.Errorf("unable to parse the second part of the range: %s", rangeParts[1])
printErrorFatal("error while parsing highlight lines range", err)
}
} else {
err := fmt.Errorf("a range should contains exactly two part: %s", part)
printErrorFatal("error while parsing highlight lines range", err)
}
} else {
// If no dash, just convert the number and add it to the map
num, err := strconv.Atoi(part)
if err != nil {
err := fmt.Errorf("unable to parse to integer: %s", part)
printErrorFatal("error while parsing highlight lines", err)
}
uniqueNumbers[num] = true
}
}

return uniqueNumbers
}

// Shadow is the configuration options for a drop shadow.
type Shadow struct {
Blur float64 `json:"blur" help:"Shadow Gaussian Blur." placeholder:"0"`
Expand Down
54 changes: 51 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"bytes"
"errors"
"fmt"
"math"
"os"
"path/filepath"
"runtime/debug"
"strconv"
"strings"

"github.com/alecthomas/chroma/v2"
Expand Down Expand Up @@ -171,6 +173,7 @@ func main() {
for i := range config.Lines {
config.Lines[i]--
}
highlightedLines := config.ComputeHighlightedLines()

var strippedInput string = ansi.Strip(input)
isAnsi := strings.ToLower(config.Language) == "ansi" || strippedInput != input
Expand Down Expand Up @@ -344,10 +347,29 @@ func main() {
}
// Offset the text by padding...
// (x, y) -> (x+p, y+p)

var bg *etree.Element
doHighlightLine := highlightedLines[i+1+offsetLine-softWrapOffset]
if config.SoftWrap {
// If the current line is soft-wrapped, we need to find the previous real line
if !isRealLine[i] {
j := i
// iterate previous lines until we find the previous real line
for ; !isRealLine[j]; j-- {
}
// we apply to the current line the highlight status of the found real line
doHighlightLine = highlightedLines[j+1+offsetLine-softWrapOffset]
}
}

if config.ShowLineNumbers {
ln := etree.NewElement("tspan")
ln.CreateAttr("xml:space", "preserve")
ln.CreateAttr("fill", s.Get(chroma.LineNumbers).Colour.String())
if doHighlightLine {
ln.CreateAttr("fill", s.Get(chroma.LineHighlight).Colour.String())
} else {
ln.CreateAttr("fill", s.Get(chroma.LineNumbers).Colour.String())
}
if config.SoftWrap {
if (isAnsi && strippedIsRealLine[i]) || (!isAnsi && isRealLine[i]) {
ln.SetText(fmt.Sprintf("%3d ", i+1+offsetLine-softWrapOffset))
Expand All @@ -357,14 +379,33 @@ func main() {
} else {
ln.SetText(fmt.Sprintf("%3d ", i+1+offsetLine))
}
ln.CreateAttr("height", strconv.Itoa(int(math.Round(config.Font.Size*config.LineHeight))))
line.InsertChildAt(0, ln)
}
if config.SoftWrap && !((isAnsi && strippedIsRealLine[i]) || (!isAnsi && isRealLine[i])) {
softWrapOffset++
}
x := float64(config.Padding[left] + config.Margin[left])
y := (float64(i+1))*(config.Font.Size*config.LineHeight) + float64(config.Padding[top]) + float64(config.Margin[top])

// Rounding required to ensure that each line have the same height
y := (float64(i+1))*math.Round(config.Font.Size*config.LineHeight) + float64(config.Padding[top]) + float64(config.Margin[top])
if doHighlightLine {
// Create a background element, with grey color and 50% opacity
bg = etree.NewElement("rect")
bg.CreateAttr("fill", "grey")
bg.CreateAttr("fill-opacity", "0.5")
// This lineWidth is not accurate when the width is dynamic, it will be computed later
lineWidth := imageWidth + config.Margin[left] + config.Padding[left] + config.Margin[right] + config.Padding[right]
bg.CreateAttr("width", strconv.Itoa(int(lineWidth)))
// We round to ensure that two highlighted consecutive lines do not leave a one pixel line between
bg.CreateAttr("height", strconv.Itoa(int(math.Round(config.Font.Size*config.LineHeight))))
line.Parent().InsertChildAt(0, bg)

yRect := float64(i)*math.Round(config.Font.Size*config.LineHeight) +
config.Padding[top] +
config.Margin[top] + math.Round(config.LineHeight*config.Font.Size)/4
// We round to ensure that two highlighted consecutive lines do not leave a one pixel line between
svg.Move(bg, 0, math.Round(yRect))
}
svg.Move(line, x, y)

// We are passed visible lines, remove the rest.
Expand Down Expand Up @@ -402,6 +443,13 @@ func main() {
}
}

// Adjust the highlighted rect width with the accurate computed width
if len(highlightedLines) != 0 {
for _, elem := range textGroup.SelectElements("rect") {
elem.CreateAttr("width", strconv.Itoa(int(imageWidth)))
}
}

if !autoHeight || !autoWidth {
svg.AddClipPath(image, "terminalMask",
config.Margin[left], config.Margin[top],
Expand Down

0 comments on commit 4f19f05

Please sign in to comment.