diff --git a/config.go b/config.go index 68a3554..2ea915b 100644 --- a/config.go +++ b/config.go @@ -31,6 +31,7 @@ type Config struct { Language string `json:"language,omitempty" help:"Language of code file." short:"l" group:"Settings" placeholder:"go"` Theme string `json:"theme" help:"Theme to use for syntax highlighting." short:"t" group:"Settings" placeholder:"charm"` Wrap int `json:"wrap" help:"Wrap lines at a specific width." short:"w" group:"Settings" default:"0" placeholder:"80"` + SoftWrap bool `json:"soft-wrap" help:"Do not count wrapped lines (Lines & LineHeight)." group:"Settings"` Output string `json:"output,omitempty" help:"Output location for {{.svg}}, {{.png}}, or {{.webp}}." short:"o" group:"Settings" default:"" placeholder:"freeze.svg"` Execute string `json:"-" help:"Capture output of command execution." short:"x" group:"Settings" default:""` diff --git a/main.go b/main.go index 3a0919d..673dd6b 100644 --- a/main.go +++ b/main.go @@ -177,7 +177,7 @@ func main() { strippedInput = cut(strippedInput, config.Lines) // wrap to character limit. - if config.Wrap > 0 { + if config.Wrap > 0 && !config.SoftWrap { strippedInput = wordwrap.String(strippedInput, config.Wrap) input = wordwrap.String(input, config.Wrap) } @@ -195,6 +195,23 @@ func main() { } } + isRealLine := []bool{} + strippedIsRealLine := []bool{} + // wrap to character limit. + if config.Wrap > 0 && config.SoftWrap { + isRealLine = SoftWrap(input, config.Wrap) + strippedIsRealLine = SoftWrap(strippedInput, config.Wrap) + strippedInput = wordwrap.String(strippedInput, config.Wrap) + input = wordwrap.String(input, config.Wrap) + } + + if config.Wrap <= 0 { + // If Wrap is disabled, but SoftWrap enabled, we force disable SoftWrap as it does not make sense + // to keep this option enabled. + printError("Wrap option disabled, but SoftWrap option enabled", fmt.Errorf("wrap option disabled")) + config.SoftWrap = false + } + s, ok := styles.Registry[strings.ToLower(config.Theme)] if s == nil || !ok { s = charmStyle @@ -320,6 +337,7 @@ func main() { config.LineHeight *= float64(scale) + softWrapOffset := 0 for i, line := range text { if isAnsi { line.SetText("") @@ -330,9 +348,20 @@ func main() { ln := etree.NewElement("tspan") ln.CreateAttr("xml:space", "preserve") ln.CreateAttr("fill", s.Get(chroma.LineNumbers).Colour.String()) - ln.SetText(fmt.Sprintf("%3d ", i+1+offsetLine)) + if config.SoftWrap { + if (isAnsi && strippedIsRealLine[i]) || (!isAnsi && isRealLine[i]) { + ln.SetText(fmt.Sprintf("%3d ", i+1+offsetLine-softWrapOffset)) + } else { + ln.SetText(" ") + } + } else { + ln.SetText(fmt.Sprintf("%3d ", i+1+offsetLine)) + } 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]) diff --git a/soft_wrap.go b/soft_wrap.go new file mode 100644 index 0000000..0b57e45 --- /dev/null +++ b/soft_wrap.go @@ -0,0 +1,24 @@ +package main + +import ( + "github.com/muesli/reflow/wordwrap" + "strings" +) + +func SoftWrap(input string, wrapLength int) []bool { + var wrap []bool + for _, line := range strings.Split(input, "\n") { + wrappedLine := wordwrap.String(line, wrapLength) + + for i := range strings.Split(wrappedLine, "\n") { + if i == 0 { + // We want line number on the original line + wrap = append(wrap, true) + } else { + // for wrapped line, we do not want line number + wrap = append(wrap, false) + } + } + } + return wrap +} diff --git a/soft_wrap_test.go b/soft_wrap_test.go new file mode 100644 index 0000000..6d6ec11 --- /dev/null +++ b/soft_wrap_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "reflect" + "testing" +) + +// Mock the dependency if needed, assuming wordwrap.String works correctly. +func TestSoftWrap(t *testing.T) { + tests := []struct { + name string + input string + wrapLength int + expected []bool + }{ + { + name: "Single short line, no wrapping", + input: "Hello", + wrapLength: 10, + expected: []bool{true}, + }, + { + name: "Single long line, wrapping", + input: "Hello World, this is a long line", + wrapLength: 10, + expected: []bool{true, false, false, false}, + }, + { + name: "Multiple lines, some wrapped", + input: "Short\nThis is a long line", + wrapLength: 10, + expected: []bool{true, true, false}, + }, + { + name: "Multiple lines, multiple wraps", + input: "This is an long line\nThis is an other long line\nThis is the last long line\nShort line", + wrapLength: 10, + expected: []bool{true, false, true, false, false, true, false, false, true}, + }, + { + name: "Empty input", + input: "", + wrapLength: 10, + expected: []bool{true}, + }, + { + name: "Lines with spaces only", + input: " \n ", + wrapLength: 5, + expected: []bool{true, true}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := SoftWrap(tt.input, tt.wrapLength) + if !reflect.DeepEqual(got, tt.expected) { + t.Errorf("SoftWrap() = %v, expected %v", got, tt.expected) + } + }) + } +}