Skip to content

Commit 4a0af78

Browse files
committed
feat(progress): support multiple stops and improved blend algorithm
Signed-off-by: Liam Stanley <liam@liam.sh>
1 parent c690ea5 commit 4a0af78

13 files changed

+162
-108
lines changed

go.mod

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,34 @@
11
module github.com/charmbracelet/bubbles/v2
22

3-
go 1.23.0
3+
go 1.24.2
44

55
require (
66
github.com/MakeNowJust/heredoc v1.0.0
77
github.com/atotto/clipboard v0.1.4
8-
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1
8+
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4
99
github.com/charmbracelet/harmonica v0.2.0
10-
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1
11-
github.com/charmbracelet/x/ansi v0.8.0
10+
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250904155632-71dd8ee66ac1
11+
github.com/charmbracelet/x/ansi v0.9.3
1212
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a
1313
github.com/dustin/go-humanize v1.0.1
14-
github.com/lucasb-eyer/go-colorful v1.2.0
1514
github.com/mattn/go-runewidth v0.0.16
1615
github.com/rivo/uniseg v0.4.7
1716
github.com/sahilm/fuzzy v0.1.1
1817
)
1918

2019
require (
2120
github.com/aymanbagabas/go-udiff v0.2.0 // indirect
22-
github.com/charmbracelet/colorprofile v0.3.0 // indirect
23-
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
24-
github.com/charmbracelet/x/input v0.3.4 // indirect
21+
github.com/charmbracelet/colorprofile v0.3.1 // indirect
22+
github.com/charmbracelet/ultraviolet v0.0.0-20250721205647-f6ac6eda5d42 // indirect
23+
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250505150409-97991a1f17d1 // indirect
24+
github.com/charmbracelet/x/input v0.3.7 // indirect
2525
github.com/charmbracelet/x/term v0.2.1 // indirect
26-
github.com/charmbracelet/x/windows v0.2.0 // indirect
26+
github.com/charmbracelet/x/termios v0.1.1 // indirect
27+
github.com/charmbracelet/x/windows v0.2.1 // indirect
2728
github.com/kylelemons/godebug v1.1.0 // indirect
29+
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
2830
github.com/muesli/cancelreader v0.2.2 // indirect
2931
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
30-
golang.org/x/sync v0.12.0 // indirect
31-
golang.org/x/sys v0.31.0 // indirect
32+
golang.org/x/sync v0.16.0 // indirect
33+
golang.org/x/sys v0.34.0 // indirect
3234
)

go.sum

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,30 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
44
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
55
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
66
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
7-
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1 h1:RvpXiXuPAuaKCHPCsE/lK5+zztnNDTSCa0CpeeIKdDU=
8-
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1/go.mod h1:qbcZLI5z8R49v9xBdU5V5Dh5D2uccx8wSwBqxQyErqc=
9-
github.com/charmbracelet/colorprofile v0.3.0 h1:KtLh9uuu1RCt+Hml4s6Hz+kB1PfV3wi++1h5ia65yKQ=
10-
github.com/charmbracelet/colorprofile v0.3.0/go.mod h1:oHJ340RS2nmG1zRGPmhJKJ/jf4FPNNk0P39/wBPA1G0=
7+
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4 h1:UgUuKKvBwgqm2ZEL+sKv/OLeavrUb4gfHgdxe6oIOno=
8+
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4/go.mod h1:0wWFRpsgF7vHsCukVZ5LAhZkiR4j875H6KEM2/tFQmA=
9+
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
10+
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
1111
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
1212
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
13-
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 h1:D9AJJuYTN5pvz6mpIGO1ijLKpfTYSHOtKGgwoTQ4Gog=
14-
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc=
15-
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
16-
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
17-
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
18-
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
13+
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250904155632-71dd8ee66ac1 h1:6SJhC6FgW6k3eHeFNqCj+2CGjM/VuHjpqC6AvAZaVD4=
14+
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250904155632-71dd8ee66ac1/go.mod h1:XIuqKpZTUXtVyeyiN1k9Tc/U7EzfaDnVc34feFHfBws=
15+
github.com/charmbracelet/ultraviolet v0.0.0-20250721205647-f6ac6eda5d42 h1:Zqw2oP9Wo8VzMijVJbtIJcAaZviYyU07stvmCFCfn0Y=
16+
github.com/charmbracelet/ultraviolet v0.0.0-20250721205647-f6ac6eda5d42/go.mod h1:XrrgNFfXLrFAyd9DUmrqVc3yQFVv8Uk+okj4PsNNzpc=
17+
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
18+
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
19+
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250505150409-97991a1f17d1 h1:MTSs/nsZNfZPbYk/r9hluK2BtwoqvEYruAujNVwgDv0=
20+
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250505150409-97991a1f17d1/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk=
1921
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw=
2022
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
21-
github.com/charmbracelet/x/input v0.3.4 h1:Mujmnv/4DaitU0p+kIsrlfZl/UlmeLKw1wAP3e1fMN0=
22-
github.com/charmbracelet/x/input v0.3.4/go.mod h1:JI8RcvdZWQIhn09VzeK3hdp4lTz7+yhiEdpEQtZN+2c=
23+
github.com/charmbracelet/x/input v0.3.7 h1:UzVbkt1vgM9dBQ+K+uRolBlN6IF2oLchmPKKo/aucXo=
24+
github.com/charmbracelet/x/input v0.3.7/go.mod h1:ZSS9Cia6Cycf2T6ToKIOxeTBTDwl25AGwArJuGaOBH8=
2325
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
2426
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
25-
github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw=
26-
github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s=
27+
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
28+
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
29+
github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I=
30+
github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M=
2731
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
2832
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
2933
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
@@ -43,7 +47,7 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
4347
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
4448
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
4549
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
46-
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
47-
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
48-
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
49-
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
50+
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
51+
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
52+
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
53+
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=

progress/progress.go

Lines changed: 89 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"github.com/charmbracelet/harmonica"
1414
"github.com/charmbracelet/lipgloss/v2"
1515
"github.com/charmbracelet/x/ansi"
16-
"github.com/lucasb-eyer/go-colorful"
1716
)
1817

1918
// Internal ID management. Used during animating to assure that frame messages
@@ -34,35 +33,94 @@ const (
3433
// Option is used to set options in New. For example:
3534
//
3635
// progress := New(
37-
// WithRamp("#ff0000", "#0000ff"),
36+
// WithBlend(
37+
// lipgloss.Color("#ff0000"),
38+
// lipgloss.Color("#0000ff"),
39+
// ),
3840
// WithoutPercentage(),
3941
// )
4042
type Option func(*Model)
4143

44+
// WithDefaultBlend sets a default blend of colors.
45+
func WithDefaultBlend() Option {
46+
return WithBlend(
47+
lipgloss.Color("#5A56E0"),
48+
lipgloss.Color("#EE6FF8"),
49+
)
50+
}
51+
4252
// WithDefaultGradient sets a gradient fill with default colors.
53+
//
54+
// Deprecated: Use [WithDefaultBlend] instead.
4355
func WithDefaultGradient() Option {
44-
return WithGradient("#5A56E0", "#EE6FF8")
56+
return WithDefaultBlend()
4557
}
4658

47-
// WithGradient sets a gradient fill blending between two colors.
48-
func WithGradient(colorA, colorB string) Option {
59+
// WithBlend uses a blend of multiple color stops to fill the progress bar. If the
60+
// blend has only 1 color, it will be treated as a solid fill. If the blend has 0
61+
// colors, it will be treated as a default blend.
62+
func WithBlend(blend ...color.Color) Option {
63+
if len(blend) == 1 {
64+
return WithSolidFill(blend[0])
65+
}
66+
if len(blend) == 0 {
67+
return WithDefaultBlend()
68+
}
4969
return func(m *Model) {
50-
m.setRamp(colorA, colorB, false)
70+
m.setRamp(blend, false)
5171
}
5272
}
5373

74+
// WithGradient sets a gradient fill blending between two colors.
75+
//
76+
// Deprecated: Use [WithBlend] instead.
77+
func WithGradient(colorA, colorB string) Option {
78+
return WithBlend(
79+
lipgloss.Color(colorA),
80+
lipgloss.Color(colorB),
81+
)
82+
}
83+
5484
// WithDefaultScaledGradient sets a gradient with default colors, and scales the
5585
// gradient to fit the filled portion of the ramp.
5686
func WithDefaultScaledGradient() Option {
57-
return WithScaledGradient("#5A56E0", "#EE6FF8")
87+
return WithDefaultScaledBlend()
88+
}
89+
90+
// WithDefaultScaledBlend sets a default blend of colors, and scales the blend
91+
// to fit the width of the filled portion of the progress bar.
92+
func WithDefaultScaledBlend() Option {
93+
return WithScaledBlend(
94+
lipgloss.Color("#5A56E0"),
95+
lipgloss.Color("#EE6FF8"),
96+
)
97+
}
98+
99+
// WithScaledBlend scales the blend of colors to fit the width of the filled portion
100+
// of the progress bar. If the blend has only 1 color, it will be treated as a
101+
// solid fill. If the blend has 0 colors, it will be treated as a default scaled
102+
// blend.
103+
func WithScaledBlend(blend ...color.Color) Option {
104+
if len(blend) == 1 {
105+
return WithSolidFill(blend[0])
106+
}
107+
if len(blend) == 0 {
108+
return WithDefaultScaledBlend()
109+
}
110+
return func(m *Model) {
111+
m.setRamp(blend, true)
112+
}
58113
}
59114

60115
// WithScaledGradient scales the gradient to fit the width of the filled portion of
61116
// the progress bar.
117+
//
118+
// Deprecated: Use [WithScaledBlend] instead.
62119
func WithScaledGradient(colorA, colorB string) Option {
63-
return func(m *Model) {
64-
m.setRamp(colorA, colorB, true)
65-
}
120+
return WithScaledBlend(
121+
lipgloss.Color(colorA),
122+
lipgloss.Color(colorB),
123+
)
66124
}
67125

68126
// WithSolidFill sets the progress to use a solid fill with the given color.
@@ -73,7 +131,8 @@ func WithSolidFill(color color.Color) Option {
73131
}
74132
}
75133

76-
// WithFillCharacters sets the characters used to construct the full and empty components of the progress bar.
134+
// WithFillCharacters sets the characters used to construct the full and empty
135+
// components of the progress bar.
77136
func WithFillCharacters(full rune, empty rune) Option {
78137
return func(m *Model) {
79138
m.Full = full
@@ -93,7 +152,7 @@ func WithoutPercentage() Option {
93152
// waiting for a tea.WindowSizeMsg.
94153
func WithWidth(w int) Option {
95154
return func(m *Model) {
96-
m.width = w
155+
m.SetWidth(w)
97156
}
98157
}
99158

@@ -148,9 +207,8 @@ type Model struct {
148207
velocity float64
149208

150209
// Gradient settings
151-
useRamp bool
152-
rampColorA colorful.Color
153-
rampColorB colorful.Color
210+
useRamp bool
211+
blend []color.Color
154212

155213
// When true, we scale the gradient to fit the width of the filled section
156214
// of the progress bar. When false, the width of the gradient will be set
@@ -288,35 +346,31 @@ func (m Model) barView(b *strings.Builder, percent float64, textWidth int) {
288346
var (
289347
tw = max(0, m.width-textWidth) // total width
290348
fw = int(math.Round((float64(tw) * percent))) // filled width
291-
p float64
292349
)
293350

294351
fw = max(0, min(tw, fw))
295352

296353
if m.useRamp {
297-
// Gradient fill
354+
var blend []color.Color
355+
if m.scaleRamp {
356+
blend = lipgloss.Blend1D(fw, m.blend...)
357+
} else {
358+
blend = lipgloss.Blend1D(tw, m.blend...)
359+
}
360+
// Blend fill.
298361
for i := range fw {
299-
if fw == 1 {
300-
// this is up for debate: in a gradient of width=1, should the
301-
// single character rendered be the first color, the last color
302-
// or exactly 50% in between? I opted for 50%
303-
p = 0.5
304-
} else if m.scaleRamp {
305-
p = float64(i) / float64(fw-1)
306-
} else {
307-
p = float64(i) / float64(tw-1)
308-
}
309-
c := m.rampColorA.BlendLuv(m.rampColorB, p)
310-
b.WriteString(lipgloss.NewStyle().Foreground(c).Render(string(m.Full)))
362+
b.WriteString(lipgloss.NewStyle().
363+
Foreground(blend[i]).
364+
Render(string(m.Full)))
311365
}
312366
} else {
313-
// Solid fill
367+
// Solid fill.
314368
b.WriteString(lipgloss.NewStyle().
315369
Foreground(m.FullColor).
316370
Render(strings.Repeat(string(m.Full), fw)))
317371
}
318372

319-
// Empty fill
373+
// Empty fill.
320374
n := max(0, tw-fw)
321375
b.WriteString(lipgloss.NewStyle().
322376
Foreground(m.EmptyColor).
@@ -333,20 +387,14 @@ func (m Model) percentageView(percent float64) string {
333387
return percentage
334388
}
335389

336-
func (m *Model) setRamp(colorA, colorB string, scaled bool) {
337-
// In the event of an error colors here will default to black. For
338-
// usability's sake, and because such an error is only cosmetic, we're
339-
// ignoring the error.
340-
a, _ := colorful.Hex(colorA)
341-
b, _ := colorful.Hex(colorB)
342-
390+
func (m *Model) setRamp(blend []color.Color, scaled bool) {
343391
m.useRamp = true
344392
m.scaleRamp = scaled
345-
m.rampColorA = a
346-
m.rampColorB = b
393+
m.blend = blend
347394
}
348395

349-
// IsAnimating returns false if the progress bar reached equilibrium and is no longer animating.
396+
// IsAnimating returns false if the progress bar reached equilibrium and is no
397+
// longer animating.
350398
func (m *Model) IsAnimating() bool {
351399
dist := math.Abs(m.percentShown - m.targetPercent)
352400
return !(dist < 0.001 && m.velocity < 0.01)

progress/progress_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ const (
1111
AnsiReset = "\x1b[m"
1212
)
1313

14-
func TestGradient(t *testing.T) {
15-
colA := "#FF0000"
16-
colB := "#00FF00"
14+
func TestBlend(t *testing.T) {
15+
colA := lipgloss.Color("#FF0000")
16+
colB := lipgloss.Color("#00FF00")
1717

1818
var p Model
1919
var descr string
@@ -23,22 +23,22 @@ func TestGradient(t *testing.T) {
2323
WithoutPercentage(),
2424
}
2525
if scale {
26-
descr = "progress bar with scaled gradient"
27-
opts = append(opts, WithScaledGradient(colA, colB))
26+
descr = "progress bar with scaled blend"
27+
opts = append(opts, WithScaledBlend(colA, colB))
2828
} else {
2929
descr = "progress bar with gradient"
30-
opts = append(opts, WithGradient(colA, colB))
30+
opts = append(opts, WithBlend(colA, colB))
3131
}
3232

3333
t.Run(descr, func(t *testing.T) {
3434
p = New(opts...)
3535

3636
// build the expected colors by colorizing an empty string and then cutting off the following reset sequence
3737
sb := strings.Builder{}
38-
sb.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color(colA)).String())
38+
sb.WriteString(lipgloss.NewStyle().Foreground(colA).String())
3939
expFirst := strings.Split(sb.String(), AnsiReset)[0]
4040
sb.Reset()
41-
sb.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color(colB)).String())
41+
sb.WriteString(lipgloss.NewStyle().Foreground(colB).String())
4242
expLast := strings.Split(sb.String(), AnsiReset)[0]
4343

4444
for _, width := range []int{3, 5, 50} {
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11

22

3-
  Name     Country of Orig…    Dunk-able   
3+
Name Country of Orig… Dunk-able
44

55

66

77

8-
  Chocolate Digestives     UK     Yes   
8+
Chocolate Digestives UK Yes
99

1010

1111

1212

13-
  Tim Tams     Australia     No   
13+
Tim Tams Australia No
1414

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
 Name   Country of Orig…  Dunk-able  
2-
 Chocolate Digestives   UK   Yes  
3-
 Tim Tams   Australia   No  
4-
 Hobnobs   UK   Yes  
1+
Name Country of Orig… Dunk-able
2+
Chocolate Digestives UK Yes
3+
Tim Tams Australia No
4+
Hobnobs UK Yes
55

66

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
 Name   Country of Orig…  Dunk-able  
2-
 Chocolate Digestives   UK   Yes  
1+
Name Country of Orig… Dunk-able
2+
Chocolate Digestives UK Yes

0 commit comments

Comments
 (0)