diffview.go

  1package diffview
  2
  3import (
  4	"fmt"
  5	"image/color"
  6	"os"
  7	"strconv"
  8	"strings"
  9
 10	"github.com/alecthomas/chroma/v2"
 11	"github.com/alecthomas/chroma/v2/lexers"
 12	"github.com/aymanbagabas/go-udiff"
 13	"github.com/aymanbagabas/go-udiff/myers"
 14	"github.com/charmbracelet/lipgloss/v2"
 15	"github.com/charmbracelet/x/ansi"
 16)
 17
 18const (
 19	leadingSymbolsSize = 2
 20	lineNumPadding     = 1
 21)
 22
 23type file struct {
 24	path    string
 25	content string
 26}
 27
 28type layout int
 29
 30const (
 31	layoutUnified layout = iota + 1
 32	layoutSplit
 33)
 34
 35// DiffView represents a view for displaying differences between two files.
 36type DiffView struct {
 37	layout       layout
 38	before       file
 39	after        file
 40	contextLines int
 41	lineNumbers  bool
 42	highlight    bool
 43	height       int
 44	width        int
 45	xOffset      int
 46	yOffset      int
 47	style        Style
 48	tabWidth     int
 49	chromaStyle  *chroma.Style
 50
 51	isComputed bool
 52	err        error
 53	unified    udiff.UnifiedDiff
 54	edits      []udiff.Edit
 55
 56	splitHunks []splitHunk
 57
 58	codeWidth       int
 59	fullCodeWidth   int  // with leading symbols
 60	extraColOnAfter bool // add extra column on after panel
 61	beforeNumDigits int
 62	afterNumDigits  int
 63}
 64
 65// New creates a new DiffView with default settings.
 66func New() *DiffView {
 67	dv := &DiffView{
 68		layout:       layoutUnified,
 69		contextLines: udiff.DefaultContextLines,
 70		lineNumbers:  true,
 71		tabWidth:     8,
 72	}
 73	if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
 74		dv.style = DefaultDarkStyle
 75	} else {
 76		dv.style = DefaultLightStyle
 77	}
 78	return dv
 79}
 80
 81// Unified sets the layout of the DiffView to unified.
 82func (dv *DiffView) Unified() *DiffView {
 83	dv.layout = layoutUnified
 84	return dv
 85}
 86
 87// Split sets the layout of the DiffView to split (side-by-side).
 88func (dv *DiffView) Split() *DiffView {
 89	dv.layout = layoutSplit
 90	return dv
 91}
 92
 93// Before sets the "before" file for the DiffView.
 94func (dv *DiffView) Before(path, content string) *DiffView {
 95	dv.before = file{path: path, content: content}
 96	return dv
 97}
 98
 99// After sets the "after" file for the DiffView.
100func (dv *DiffView) After(path, content string) *DiffView {
101	dv.after = file{path: path, content: content}
102	return dv
103}
104
105// ContextLines sets the number of context lines for the DiffView.
106func (dv *DiffView) ContextLines(contextLines int) *DiffView {
107	dv.contextLines = contextLines
108	return dv
109}
110
111// Style sets the style for the DiffView.
112func (dv *DiffView) Style(style Style) *DiffView {
113	dv.style = style
114	return dv
115}
116
117// LineNumbers sets whether to display line numbers in the DiffView.
118func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
119	dv.lineNumbers = lineNumbers
120	return dv
121}
122
123// SyntaxHightlight sets whether to enable syntax highlighting in the DiffView.
124func (dv *DiffView) SyntaxHightlight(highlight bool) *DiffView {
125	dv.highlight = highlight
126	return dv
127}
128
129// Height sets the height of the DiffView.
130func (dv *DiffView) Height(height int) *DiffView {
131	dv.height = height
132	return dv
133}
134
135// Width sets the width of the DiffView.
136func (dv *DiffView) Width(width int) *DiffView {
137	dv.width = width
138	return dv
139}
140
141// XOffset sets the horizontal offset for the DiffView.
142func (dv *DiffView) XOffset(xOffset int) *DiffView {
143	dv.xOffset = xOffset
144	return dv
145}
146
147// YOffset sets the vertical offset for the DiffView.
148func (dv *DiffView) YOffset(yOffset int) *DiffView {
149	dv.yOffset = yOffset
150	return dv
151}
152
153// TabWidth sets the tab width. Only relevant for code that contains tabs, like
154// Go code.
155func (dv *DiffView) TabWidth(tabWidth int) *DiffView {
156	dv.tabWidth = tabWidth
157	return dv
158}
159
160// ChromaStyle sets the chroma style for syntax highlighting.
161// If nil, no syntax highlighting will be applied.
162func (dv *DiffView) ChromaStyle(style *chroma.Style) *DiffView {
163	dv.chromaStyle = style
164	return dv
165}
166
167// String returns the string representation of the DiffView.
168func (dv *DiffView) String() string {
169	dv.replaceTabs()
170	if err := dv.computeDiff(); err != nil {
171		return err.Error()
172	}
173	dv.convertDiffToSplit()
174	dv.adjustStyles()
175	dv.detectNumDigits()
176
177	if dv.width <= 0 {
178		dv.detectCodeWidth()
179	} else {
180		dv.resizeCodeWidth()
181	}
182
183	style := lipgloss.NewStyle()
184	if dv.width > 0 {
185		style = style.MaxWidth(dv.width)
186	}
187	if dv.height > 0 {
188		style = style.MaxHeight(dv.height)
189	}
190
191	switch dv.layout {
192	case layoutUnified:
193		return style.Render(strings.TrimSuffix(dv.renderUnified(), "\n"))
194	case layoutSplit:
195		return style.Render(strings.TrimSuffix(dv.renderSplit(), "\n"))
196	default:
197		panic("unknown diffview layout")
198	}
199}
200
201// replaceTabs replaces tabs in the before and after file contents with spaces
202// according to the specified tab width.
203func (dv *DiffView) replaceTabs() {
204	spaces := strings.Repeat(" ", dv.tabWidth)
205	dv.before.content = strings.ReplaceAll(dv.before.content, "\t", spaces)
206	dv.after.content = strings.ReplaceAll(dv.after.content, "\t", spaces)
207}
208
209// computeDiff computes the differences between the "before" and "after" files.
210func (dv *DiffView) computeDiff() error {
211	if dv.isComputed {
212		return dv.err
213	}
214	dv.isComputed = true
215	dv.edits = myers.ComputeEdits( //nolint:staticcheck
216		dv.before.content,
217		dv.after.content,
218	)
219	dv.unified, dv.err = udiff.ToUnifiedDiff(
220		dv.before.path,
221		dv.after.path,
222		dv.before.content,
223		dv.edits,
224		dv.contextLines,
225	)
226	return dv.err
227}
228
229// convertDiffToSplit converts the unified diff to a split diff if the layout is
230// set to split.
231func (dv *DiffView) convertDiffToSplit() {
232	if dv.layout != layoutSplit {
233		return
234	}
235
236	dv.splitHunks = make([]splitHunk, len(dv.unified.Hunks))
237	for i, h := range dv.unified.Hunks {
238		dv.splitHunks[i] = hunkToSplit(h)
239	}
240}
241
242// adjustStyles adjusts adds padding and alignment to the styles.
243func (dv *DiffView) adjustStyles() {
244	setPadding := func(s lipgloss.Style) lipgloss.Style {
245		return s.Padding(0, lineNumPadding).Align(lipgloss.Right)
246	}
247	dv.style.MissingLine.LineNumber = setPadding(dv.style.MissingLine.LineNumber)
248	dv.style.DividerLine.LineNumber = setPadding(dv.style.DividerLine.LineNumber)
249	dv.style.EqualLine.LineNumber = setPadding(dv.style.EqualLine.LineNumber)
250	dv.style.InsertLine.LineNumber = setPadding(dv.style.InsertLine.LineNumber)
251	dv.style.DeleteLine.LineNumber = setPadding(dv.style.DeleteLine.LineNumber)
252}
253
254// detectNumDigits calculates the maximum number of digits needed for before and
255// after line numbers.
256func (dv *DiffView) detectNumDigits() {
257	dv.beforeNumDigits = 0
258	dv.afterNumDigits = 0
259
260	for _, h := range dv.unified.Hunks {
261		dv.beforeNumDigits = max(dv.beforeNumDigits, len(strconv.Itoa(h.FromLine+len(h.Lines))))
262		dv.afterNumDigits = max(dv.afterNumDigits, len(strconv.Itoa(h.ToLine+len(h.Lines))))
263	}
264}
265
266// detectCodeWidth calculates the maximum width of code lines in the diff view.
267func (dv *DiffView) detectCodeWidth() {
268	switch dv.layout {
269	case layoutUnified:
270		dv.detectUnifiedCodeWidth()
271	case layoutSplit:
272		dv.detectSplitCodeWidth()
273	}
274	dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
275}
276
277// detectUnifiedCodeWidth calculates the maximum width of code lines in a
278// unified diff.
279func (dv *DiffView) detectUnifiedCodeWidth() {
280	dv.codeWidth = 0
281
282	for _, h := range dv.unified.Hunks {
283		shownLines := ansi.StringWidth(dv.hunkLineFor(h))
284
285		for _, l := range h.Lines {
286			lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n")) + 1
287			dv.codeWidth = max(dv.codeWidth, lineWidth, shownLines)
288		}
289	}
290}
291
292// detectSplitCodeWidth calculates the maximum width of code lines in a
293// split diff.
294func (dv *DiffView) detectSplitCodeWidth() {
295	dv.codeWidth = 0
296
297	for i, h := range dv.splitHunks {
298		shownLines := ansi.StringWidth(dv.hunkLineFor(dv.unified.Hunks[i]))
299
300		for _, l := range h.lines {
301			if l.before != nil {
302				codeWidth := ansi.StringWidth(strings.TrimSuffix(l.before.Content, "\n")) + 1
303				dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
304			}
305			if l.after != nil {
306				codeWidth := ansi.StringWidth(strings.TrimSuffix(l.after.Content, "\n")) + 1
307				dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
308			}
309		}
310	}
311}
312
313// resizeCodeWidth resizes the code width to fit within the specified width.
314func (dv *DiffView) resizeCodeWidth() {
315	fullNumWidth := dv.beforeNumDigits + dv.afterNumDigits
316	fullNumWidth += lineNumPadding * 4 // left and right padding for both line numbers
317
318	switch dv.layout {
319	case layoutUnified:
320		dv.codeWidth = dv.width - fullNumWidth - leadingSymbolsSize
321	case layoutSplit:
322		remainingWidth := dv.width - fullNumWidth - leadingSymbolsSize*2
323		dv.codeWidth = remainingWidth / 2
324		dv.extraColOnAfter = isOdd(remainingWidth)
325	}
326
327	dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
328}
329
330// renderUnified renders the unified diff view as a string.
331func (dv *DiffView) renderUnified() string {
332	var b strings.Builder
333
334	fullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
335	printedLines := -dv.yOffset
336
337	write := func(s string) {
338		if printedLines >= 0 {
339			b.WriteString(s)
340		}
341	}
342
343outer:
344	for i, h := range dv.unified.Hunks {
345		if dv.lineNumbers {
346			write(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
347			write(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
348		}
349		content := ansi.Truncate(dv.hunkLineFor(h), dv.fullCodeWidth, "…")
350		write(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(content))
351		write("\n")
352		printedLines++
353
354		beforeLine := h.FromLine
355		afterLine := h.ToLine
356
357		for j, l := range h.Lines {
358			// print ellipis if we don't have enough space to print the rest of the diff
359			hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
360			isLastHunk := i+1 == len(dv.unified.Hunks)
361			isLastLine := j+1 == len(h.Lines)
362			if hasReachedHeight && (!isLastHunk || !isLastLine) {
363				lineStyle := dv.lineStyleForType(l.Kind)
364				if dv.lineNumbers {
365					write(lineStyle.LineNumber.Render(pad("…", dv.beforeNumDigits)))
366					write(lineStyle.LineNumber.Render(pad("…", dv.afterNumDigits)))
367				}
368				write(fullContentStyle.Render(
369					lineStyle.Code.Width(dv.fullCodeWidth).Render("  …"),
370				))
371				write("\n")
372				break outer
373			}
374
375			getContent := func(ls LineStyle) string {
376				content := strings.TrimSuffix(l.Content, "\n")
377				content = dv.hightlightCode(content, ls.Code.GetBackground())
378				content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
379				content = ansi.Truncate(content, dv.codeWidth, "…")
380				return content
381			}
382
383			leadingEllipsis := dv.xOffset > 0 && strings.TrimSpace(content) != ""
384
385			switch l.Kind {
386			case udiff.Equal:
387				content := getContent(dv.style.EqualLine)
388				if dv.lineNumbers {
389					write(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
390					write(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
391				}
392				write(fullContentStyle.Render(
393					dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", "  ") + content),
394				))
395				beforeLine++
396				afterLine++
397			case udiff.Insert:
398				content := getContent(dv.style.InsertLine)
399				if dv.lineNumbers {
400					write(dv.style.InsertLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
401					write(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
402				}
403				write(fullContentStyle.Render(
404					dv.style.InsertLine.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
405						dv.style.InsertLine.Code.Width(dv.codeWidth).Render(content),
406				))
407				afterLine++
408			case udiff.Delete:
409				content := getContent(dv.style.DeleteLine)
410				if dv.lineNumbers {
411					write(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
412					write(dv.style.DeleteLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
413				}
414				write(fullContentStyle.Render(
415					dv.style.DeleteLine.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
416						dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(content),
417				))
418				beforeLine++
419			}
420			write("\n")
421
422			printedLines++
423		}
424	}
425
426	for printedLines < dv.height {
427		if dv.lineNumbers {
428			write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
429			write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
430		}
431		write(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render("  "))
432		write("\n")
433		printedLines++
434	}
435
436	return b.String()
437}
438
439// renderSplit renders the split (side-by-side) diff view as a string.
440func (dv *DiffView) renderSplit() string {
441	var b strings.Builder
442
443	beforeFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
444	afterFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth + btoi(dv.extraColOnAfter))
445	printedLines := -dv.yOffset
446
447	write := func(s string) {
448		if printedLines >= 0 {
449			b.WriteString(s)
450		}
451	}
452
453outer:
454	for i, h := range dv.splitHunks {
455		if dv.lineNumbers {
456			write(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
457		}
458		content := ansi.Truncate(dv.hunkLineFor(dv.unified.Hunks[i]), dv.fullCodeWidth, "…")
459		write(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(content))
460		if dv.lineNumbers {
461			write(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
462		}
463		write(dv.style.DividerLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
464		write("\n")
465		printedLines++
466
467		beforeLine := h.fromLine
468		afterLine := h.toLine
469
470		for j, l := range h.lines {
471			// print ellipis if we don't have enough space to print the rest of the diff
472			hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
473			isLastHunk := i+1 == len(dv.unified.Hunks)
474			isLastLine := j+1 == len(h.lines)
475			if hasReachedHeight && (!isLastHunk || !isLastLine) {
476				lineStyle := dv.style.MissingLine
477				if l.before != nil {
478					lineStyle = dv.lineStyleForType(l.before.Kind)
479				}
480				if dv.lineNumbers {
481					write(lineStyle.LineNumber.Render(pad("…", dv.beforeNumDigits)))
482				}
483				write(beforeFullContentStyle.Render(
484					lineStyle.Code.Width(dv.fullCodeWidth).Render("  …"),
485				))
486				lineStyle = dv.style.MissingLine
487				if l.after != nil {
488					lineStyle = dv.lineStyleForType(l.after.Kind)
489				}
490				if dv.lineNumbers {
491					write(lineStyle.LineNumber.Render(pad("…", dv.afterNumDigits)))
492				}
493				write(afterFullContentStyle.Render(
494					lineStyle.Code.Width(dv.fullCodeWidth).Render("  …"),
495				))
496				write("\n")
497				break outer
498			}
499
500			getContent := func(content string, ls LineStyle) string {
501				content = strings.TrimSuffix(content, "\n")
502				content = dv.hightlightCode(content, ls.Code.GetBackground())
503				content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
504				content = ansi.Truncate(content, dv.codeWidth, "…")
505				return content
506			}
507			getLeadingEllipsis := func(content string) bool {
508				return dv.xOffset > 0 && strings.TrimSpace(content) != ""
509			}
510
511			switch {
512			case l.before == nil:
513				if dv.lineNumbers {
514					write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
515				}
516				write(beforeFullContentStyle.Render(
517					dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render("  "),
518				))
519			case l.before.Kind == udiff.Equal:
520				content := getContent(l.before.Content, dv.style.EqualLine)
521				leadingEllipsis := getLeadingEllipsis(content)
522				if dv.lineNumbers {
523					write(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
524				}
525				write(beforeFullContentStyle.Render(
526					dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", "  ") + content),
527				))
528				beforeLine++
529			case l.before.Kind == udiff.Delete:
530				content := getContent(l.before.Content, dv.style.DeleteLine)
531				leadingEllipsis := getLeadingEllipsis(content)
532				if dv.lineNumbers {
533					write(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
534				}
535				write(beforeFullContentStyle.Render(
536					dv.style.DeleteLine.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
537						dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(content),
538				))
539				beforeLine++
540			}
541
542			switch {
543			case l.after == nil:
544				if dv.lineNumbers {
545					write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
546				}
547				write(afterFullContentStyle.Render(
548					dv.style.MissingLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render("  "),
549				))
550			case l.after.Kind == udiff.Equal:
551				content := getContent(l.after.Content, dv.style.EqualLine)
552				leadingEllipsis := getLeadingEllipsis(content)
553				if dv.lineNumbers {
554					write(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
555				}
556				write(afterFullContentStyle.Render(
557					dv.style.EqualLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(ternary(leadingEllipsis, " …", "  ") + content),
558				))
559				afterLine++
560			case l.after.Kind == udiff.Insert:
561				content := getContent(l.after.Content, dv.style.InsertLine)
562				leadingEllipsis := getLeadingEllipsis(content)
563				if dv.lineNumbers {
564					write(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
565				}
566				write(afterFullContentStyle.Render(
567					dv.style.InsertLine.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
568						dv.style.InsertLine.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(content),
569				))
570				afterLine++
571			}
572
573			write("\n")
574
575			printedLines++
576		}
577	}
578
579	for printedLines < dv.height {
580		if dv.lineNumbers {
581			write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
582		}
583		write(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
584		if dv.lineNumbers {
585			write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
586		}
587		write(dv.style.MissingLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
588		write("\n")
589		printedLines++
590	}
591
592	return b.String()
593}
594
595// hunkLineFor formats the header line for a hunk in the unified diff view.
596func (dv *DiffView) hunkLineFor(h *udiff.Hunk) string {
597	beforeShownLines, afterShownLines := dv.hunkShownLines(h)
598
599	return fmt.Sprintf(
600		"  @@ -%d,%d +%d,%d @@ ",
601		h.FromLine,
602		beforeShownLines,
603		h.ToLine,
604		afterShownLines,
605	)
606}
607
608// hunkShownLines calculates the number of lines shown in a hunk for both before
609// and after versions.
610func (dv *DiffView) hunkShownLines(h *udiff.Hunk) (before, after int) {
611	for _, l := range h.Lines {
612		switch l.Kind {
613		case udiff.Equal:
614			before++
615			after++
616		case udiff.Insert:
617			after++
618		case udiff.Delete:
619			before++
620		}
621	}
622	return
623}
624
625func (dv *DiffView) lineStyleForType(t udiff.OpKind) LineStyle {
626	switch t {
627	case udiff.Equal:
628		return dv.style.EqualLine
629	case udiff.Insert:
630		return dv.style.InsertLine
631	case udiff.Delete:
632		return dv.style.DeleteLine
633	default:
634		return dv.style.MissingLine
635	}
636}
637
638func (dv *DiffView) hightlightCode(source string, bgColor color.Color) string {
639	if dv.chromaStyle == nil {
640		return source
641	}
642
643	l := dv.getChromaLexer(source)
644	f := dv.getChromaFormatter(bgColor)
645
646	it, err := l.Tokenise(nil, source)
647	if err != nil {
648		return source
649	}
650
651	var b strings.Builder
652	if err := f.Format(&b, dv.chromaStyle, it); err != nil {
653		return source
654	}
655	return b.String()
656}
657
658func (dv *DiffView) getChromaLexer(source string) chroma.Lexer {
659	l := lexers.Match(dv.before.path)
660	if l == nil {
661		l = lexers.Analyse(source)
662	}
663	if l == nil {
664		l = lexers.Fallback
665	}
666	return chroma.Coalesce(l)
667}
668
669func (dv *DiffView) getChromaFormatter(gbColor color.Color) chroma.Formatter {
670	return chromaFormatter{
671		bgColor: gbColor,
672	}
673}