New Upstream Release - golang-github-charmbracelet-lipgloss

Ready changes

Summary

Merged new upstream version: 0.7.1 (was: 0.6.0).

Diff

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..a5d9663
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,29 @@
+version: 2
+updates:
+  - package-ecosystem: "gomod"
+    directory: "/"
+    schedule:
+      interval: "daily"
+    labels:
+      - "dependencies"
+    commit-message:
+      prefix: "feat"
+      include: "scope"
+  - package-ecosystem: "gomod"
+    directory: "/example"
+    schedule:
+      interval: "daily"
+    labels:
+      - "dependencies"
+    commit-message:
+      prefix: "chore"
+      include: "scope"
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "daily"
+    labels:
+      - "dependencies"
+    commit-message:
+      prefix: "chore"
+      include: "scope"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b0c9f68..56fe761 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -4,7 +4,7 @@ jobs:
   test:
     strategy:
       matrix:
-        go-version: [~1.13, ^1]
+        go-version: [~1.17, ^1]
         os: [ubuntu-latest, macos-latest, windows-latest]
     runs-on: ${{ matrix.os }}
     env:
@@ -16,7 +16,7 @@ jobs:
           go-version: ${{ matrix.go-version }}
 
       - name: Checkout code
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3.3.0
 
       - name: Download Go modules
         run: go mod download
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
new file mode 100644
index 0000000..b226612
--- /dev/null
+++ b/.github/workflows/coverage.yml
@@ -0,0 +1,28 @@
+name: coverage
+on: [push, pull_request]
+
+jobs:
+  coverage:
+    strategy:
+      matrix:
+        go-version: [^1]
+        os: [ubuntu-latest]
+    runs-on: ${{ matrix.os }}
+    env:
+      GO111MODULE: "on"
+    steps:
+      - name: Install Go
+        uses: actions/setup-go@v3
+        with:
+          go-version: ${{ matrix.go-version }}
+
+      - name: Checkout code
+        uses: actions/checkout@v3.3.0
+
+      - name: Coverage
+        env:
+          COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          go test -race -covermode atomic -coverprofile=profile.cov ./...
+          GO111MODULE=off go get github.com/mattn/goveralls
+          $(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github
diff --git a/.github/workflows/lint-soft.yml b/.github/workflows/lint-soft.yml
index b6c06e6..b91419d 100644
--- a/.github/workflows/lint-soft.yml
+++ b/.github/workflows/lint-soft.yml
@@ -13,9 +13,14 @@ jobs:
     name: lint-soft
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
+      - name: Install Go
+        uses: actions/setup-go@v3
+        with:
+          go-version: ^1
+
+      - uses: actions/checkout@v3.3.0
       - name: golangci-lint
-        uses: golangci/golangci-lint-action@v2
+        uses: golangci/golangci-lint-action@v3
         with:
           # Optional: golangci-lint command line arguments.
           args: --config .golangci-soft.yml --issues-exit-code=0
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 74f4c5c..5b6cedc 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -13,9 +13,14 @@ jobs:
     name: lint
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
+      - name: Install Go
+        uses: actions/setup-go@v3
+        with:
+          go-version: ^1
+
+      - uses: actions/checkout@v3.3.0
       - name: golangci-lint
-        uses: golangci/golangci-lint-action@v2
+        uses: golangci/golangci-lint-action@v3
         with:
           # Optional: golangci-lint command line arguments.
           #args:
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a170af0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+ssh_example_ed25519*
\ No newline at end of file
diff --git a/README.md b/README.md
index 039e28c..a071564 100644
--- a/README.md
+++ b/README.md
@@ -27,10 +27,9 @@ var style = lipgloss.NewStyle().
     PaddingLeft(4).
     Width(22)
 
-fmt.Println(style.Render("Hello, kitty."))
+fmt.Println(style.Render("Hello, kitty"))
 ```
 
-
 ## Colors
 
 Lip Gloss supports the following color profiles:
@@ -59,7 +58,7 @@ lipgloss.Color("#04B575") // a green
 lipgloss.Color("#3C3C3C") // a dark gray
 ```
 
-...as well as a 1-bit Ascii profile, which is black and white only.
+...as well as a 1-bit ASCII profile, which is black and white only.
 
 The terminal's color profile will be automatically detected, and colors outside
 the gamut of the current palette will be automatically coerced to their closest
@@ -77,6 +76,29 @@ lipgloss.AdaptiveColor{Light: "236", Dark: "248"}
 The terminal's background color will automatically be detected and the
 appropriate color will be chosen at runtime.
 
+### Complete Colors
+
+CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color
+profiles.
+
+```go
+lipgloss.CompleteColor{True: "#0000FF", ANSI256: "86", ANSI: "5"}
+```
+
+Automatic color degradation will not be performed in this case and it will be
+based on the color specified.
+
+### Complete Adaptive Colors
+
+You can use CompleteColor with AdaptiveColor to specify the exact values for
+light and dark backgrounds without automatic color degradation.
+
+```go
+lipgloss.CompleteAdaptiveColor{
+    Light: CompleteColor{TrueColor: "#d7ffae", ANSI256: "193", ANSI: "11"},
+    Dark:  CompleteColor{TrueColor: "#d75fee", ANSI256: "163", ANSI: "5"},
+}
+```
 
 ## Inline Formatting
 
@@ -151,11 +173,11 @@ var style = lipgloss.NewStyle().
 Setting a minimum width and height is simple and straightforward.
 
 ```go
-var str = lipgloss.NewStyle().
+var style = lipgloss.NewStyle().
+    SetString("What’s for lunch?").
     Width(24).
     Height(32).
-    Foreground(lipgloss.Color("63")).
-    Render("What’s for lunch?")
+    Foreground(lipgloss.Color("63"))
 ```
 
 
@@ -218,7 +240,7 @@ var wildStyle = style.Copy().Blink(true)
 ```
 
 `Copy()` performs a copy on the underlying data structure ensuring that you get
-a true, dereferenced copy of a style. Without copying it's possible to mutate
+a true, dereferenced copy of a style. Without copying, it's possible to mutate
 styles.
 
 
@@ -274,20 +296,43 @@ someStyle.MaxWidth(5).MaxHeight(5).Render("yadda yadda")
 
 ## Rendering
 
-Generally, you just call the `Render(string)` method on a `lipgloss.Style`:
+Generally, you just call the `Render(string...)` method on a `lipgloss.Style`:
 
 ```go
-fmt.Println(lipgloss.NewStyle().Bold(true).Render("Hello, kitty."))
+style := lipgloss.NewStyle().Bold(true).SetString("Hello,")
+fmt.Println(style.Render("kitty.")) // Hello, kitty.
+fmt.Println(style.Render("puppy.")) // Hello, puppy.
 ```
 
 But you could also use the Stringer interface:
 
 ```go
 var style = lipgloss.NewStyle().SetString("你好,猫咪。").Bold(true)
+fmt.Println(style) // 你好,猫咪。
+```
+
+### Custom Renderers
+
+Custom renderers allow you to render to a specific outputs. This is
+particularly important when you want to render to different outputs and
+correctly detect the color profile and dark background status for each, such as
+in a server-client situation.
+
+```go
+func myLittleHandler(sess ssh.Session) {
+    // Create a renderer for the client.
+    renderer := lipgloss.NewRenderer(sess)
+
+    // Create a new style on the renderer.
+    style := renderer.NewStyle().Background(lipgloss.AdaptiveColor{Light: "63", Dark: "228"})
 
-fmt.Printf("%s\n", style)
+    // Render. The color profile and dark background state will be correctly detected.
+    io.WriteString(sess, style.Render("Heyyyyyyy"))
+}
 ```
 
+For an example on using a custom renderer over SSH with [Wish][wish] see the
+[SSH example][ssh-example].
 
 ## Utilities
 
@@ -318,10 +363,11 @@ Sometimes you’ll want to know the width and height of text blocks when buildin
 your layouts.
 
 ```go
-var block string = lipgloss.NewStyle().
+// Render a block of text.
+var style = lipgloss.NewStyle().
     Width(40).
-    Padding(2).
-    Render(someLongString)
+    Padding(2)
+var block string = style.Render(someLongString)
 
 // Get the actual, physical dimensions of the text block.
 width := lipgloss.Width(block)
@@ -386,18 +432,27 @@ the stylesheet-based Markdown renderer.
 [glamour]: https://github.com/charmbracelet/glamour
 
 
+## Feedback
+
+We’d love to hear your thoughts on this project. Feel free to drop us a note!
+
+* [Twitter](https://twitter.com/charmcli)
+* [The Fediverse](https://mastodon.social/@charmcli)
+* [Discord](https://charm.sh/chat)
+
 ## License
 
 [MIT](https://github.com/charmbracelet/lipgloss/raw/master/LICENSE)
 
-
 ***
 
 Part of [Charm](https://charm.sh).
 
-<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge-unrounded.jpg" width="400"></a>
+<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
 
 Charm热爱开源 • Charm loves open source
 
 
 [docs]: https://pkg.go.dev/github.com/charmbracelet/lipgloss?tab=doc
+[wish]: https://github.com/charmbracelet/wish
+[ssh-example]: examples/ssh
diff --git a/align.go b/align.go
index 03e7889..c399703 100644
--- a/align.go
+++ b/align.go
@@ -10,7 +10,7 @@ import (
 // Perform text alignment. If the string is multi-lined, we also make all lines
 // the same width by padding them with spaces. If a termenv style is passed,
 // use that to style the spaces added.
-func alignText(str string, pos Position, width int, style *termenv.Style) string {
+func alignTextHorizontal(str string, pos Position, width int, style *termenv.Style) string {
 	lines, widestLine := getLines(str)
 	var b strings.Builder
 
@@ -57,3 +57,26 @@ func alignText(str string, pos Position, width int, style *termenv.Style) string
 
 	return b.String()
 }
+
+func alignTextVertical(str string, pos Position, height int, _ *termenv.Style) string {
+	strHeight := strings.Count(str, "\n") + 1
+	if height < strHeight {
+		return str
+	}
+
+	switch pos {
+	case Top:
+		return str + strings.Repeat("\n", height-strHeight)
+	case Center:
+		var topPadding, bottomPadding = (height - strHeight) / 2, (height - strHeight) / 2
+		if strHeight+topPadding+bottomPadding > height {
+			topPadding--
+		} else if strHeight+topPadding+bottomPadding < height {
+			bottomPadding++
+		}
+		return strings.Repeat("\n", topPadding) + str + strings.Repeat("\n", bottomPadding)
+	case Bottom:
+		return strings.Repeat("\n", height-strHeight) + str
+	}
+	return str
+}
diff --git a/align_test.go b/align_test.go
new file mode 100644
index 0000000..dd5addb
--- /dev/null
+++ b/align_test.go
@@ -0,0 +1,41 @@
+package lipgloss
+
+import "testing"
+
+func TestAlignTextVertical(t *testing.T) {
+	tests := []struct {
+		str    string
+		pos    Position
+		height int
+		want   string
+	}{
+		{str: "Foo", pos: Top, height: 2, want: "Foo\n"},
+		{str: "Foo", pos: Center, height: 5, want: "\n\nFoo\n\n"},
+		{str: "Foo", pos: Bottom, height: 5, want: "\n\n\n\nFoo"},
+
+		{str: "Foo\nBar", pos: Bottom, height: 5, want: "\n\n\nFoo\nBar"},
+		{str: "Foo\nBar", pos: Center, height: 5, want: "\nFoo\nBar\n\n"},
+		{str: "Foo\nBar", pos: Top, height: 5, want: "Foo\nBar\n\n\n"},
+
+		{str: "Foo\nBar\nBaz", pos: Bottom, height: 5, want: "\n\nFoo\nBar\nBaz"},
+		{str: "Foo\nBar\nBaz", pos: Center, height: 5, want: "\nFoo\nBar\nBaz\n"},
+
+		{str: "Foo\nBar\nBaz", pos: Bottom, height: 3, want: "Foo\nBar\nBaz"},
+		{str: "Foo\nBar\nBaz", pos: Center, height: 3, want: "Foo\nBar\nBaz"},
+		{str: "Foo\nBar\nBaz", pos: Top, height: 3, want: "Foo\nBar\nBaz"},
+
+		{str: "Foo\n\n\n\nBar", pos: Bottom, height: 5, want: "Foo\n\n\n\nBar"},
+		{str: "Foo\n\n\n\nBar", pos: Center, height: 5, want: "Foo\n\n\n\nBar"},
+		{str: "Foo\n\n\n\nBar", pos: Top, height: 5, want: "Foo\n\n\n\nBar"},
+
+		{str: "Foo\nBar\nBaz", pos: Center, height: 9, want: "\n\n\nFoo\nBar\nBaz\n\n\n"},
+		{str: "Foo\nBar\nBaz", pos: Center, height: 10, want: "\n\n\nFoo\nBar\nBaz\n\n\n\n"},
+	}
+
+	for _, test := range tests {
+		got := alignTextVertical(test.str, test.pos, test.height, nil)
+		if got != test.want {
+			t.Errorf("alignTextVertical(%q, %v, %d) = %q, want %q", test.str, test.pos, test.height, got, test.want)
+		}
+	}
+}
diff --git a/borders.go b/borders.go
index a3284ac..1896422 100644
--- a/borders.go
+++ b/borders.go
@@ -84,6 +84,39 @@ var (
 		BottomRight: "╯",
 	}
 
+	blockBorder = Border{
+		Top:         "█",
+		Bottom:      "█",
+		Left:        "█",
+		Right:       "█",
+		TopLeft:     "█",
+		TopRight:    "█",
+		BottomLeft:  "█",
+		BottomRight: "█",
+	}
+
+	outerHalfBlockBorder = Border{
+		Top:         "▀",
+		Bottom:      "▄",
+		Left:        "▌",
+		Right:       "▐",
+		TopLeft:     "▛",
+		TopRight:    "▜",
+		BottomLeft:  "▙",
+		BottomRight: "▟",
+	}
+
+	innerHalfBlockBorder = Border{
+		Top:         "▄",
+		Bottom:      "▀",
+		Left:        "▐",
+		Right:       "▌",
+		TopLeft:     "▗",
+		TopRight:    "▖",
+		BottomLeft:  "▝",
+		BottomRight: "▘",
+	}
+
 	thickBorder = Border{
 		Top:         "━",
 		Bottom:      "━",
@@ -129,6 +162,21 @@ func RoundedBorder() Border {
 	return roundedBorder
 }
 
+// BlockBorder returns a border that takes the whole block.
+func BlockBorder() Border {
+	return blockBorder
+}
+
+// OuterHalfBlockBorder returns a half-block border that sits outside the frame.
+func OuterHalfBlockBorder() Border {
+	return outerHalfBlockBorder
+}
+
+// InnerHalfBlockBorder returns a half-block border that sits inside the frame.
+func InnerHalfBlockBorder() Border {
+	return innerHalfBlockBorder
+}
+
 // ThickBorder returns a border that's thicker than the one returned by
 // NormalBorder.
 func ThickBorder() Border {
@@ -199,7 +247,7 @@ func (s Style) applyBorder(str string) string {
 		border.Right = " "
 	}
 
-	// If corners should be render but are set with the empty string, fill them
+	// If corners should be rendered but are set with the empty string, fill them
 	// with a single space.
 	if hasTop && hasLeft && border.TopLeft == "" {
 		border.TopLeft = " "
@@ -250,7 +298,7 @@ func (s Style) applyBorder(str string) string {
 	// Render top
 	if hasTop {
 		top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width)
-		top = styleBorder(top, topFG, topBG)
+		top = s.styleBorder(top, topFG, topBG)
 		out.WriteString(top)
 		out.WriteRune('\n')
 	}
@@ -269,7 +317,7 @@ func (s Style) applyBorder(str string) string {
 			if leftIndex >= len(leftRunes) {
 				leftIndex = 0
 			}
-			out.WriteString(styleBorder(r, leftFG, leftBG))
+			out.WriteString(s.styleBorder(r, leftFG, leftBG))
 		}
 		out.WriteString(l)
 		if hasRight {
@@ -278,7 +326,7 @@ func (s Style) applyBorder(str string) string {
 			if rightIndex >= len(rightRunes) {
 				rightIndex = 0
 			}
-			out.WriteString(styleBorder(r, rightFG, rightBG))
+			out.WriteString(s.styleBorder(r, rightFG, rightBG))
 		}
 		if i < len(lines)-1 {
 			out.WriteRune('\n')
@@ -288,7 +336,7 @@ func (s Style) applyBorder(str string) string {
 	// Render bottom
 	if hasBottom {
 		bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width)
-		bottom = styleBorder(bottom, bottomFG, bottomBG)
+		bottom = s.styleBorder(bottom, bottomFG, bottomBG)
 		out.WriteRune('\n')
 		out.WriteString(bottom)
 	}
@@ -328,7 +376,7 @@ func renderHorizontalEdge(left, middle, right string, width int) string {
 }
 
 // Apply foreground and background styling to a border.
-func styleBorder(border string, fg, bg TerminalColor) string {
+func (s Style) styleBorder(border string, fg, bg TerminalColor) string {
 	if fg == noColor && bg == noColor {
 		return border
 	}
@@ -336,10 +384,10 @@ func styleBorder(border string, fg, bg TerminalColor) string {
 	var style = termenv.Style{}
 
 	if fg != noColor {
-		style = style.Foreground(ColorProfile().Color(fg.value()))
+		style = style.Foreground(fg.color(s.r))
 	}
 	if bg != noColor {
-		style = style.Background(ColorProfile().Color(bg.value()))
+		style = style.Background(bg.color(s.r))
 	}
 
 	return style.Styled(border)
diff --git a/color.go b/color.go
index 8a09d1b..ef7fd27 100644
--- a/color.go
+++ b/color.go
@@ -1,152 +1,86 @@
 package lipgloss
 
 import (
-	"sync"
+	"strconv"
 
-	"github.com/lucasb-eyer/go-colorful"
 	"github.com/muesli/termenv"
 )
 
-var (
-	colorProfile         termenv.Profile
-	getColorProfile      sync.Once
-	explicitColorProfile bool
-
-	hasDarkBackground       bool
-	getBackgroundColor      sync.Once
-	explicitBackgroundColor bool
-
-	colorProfileMtx sync.Mutex
-)
-
-// ColorProfile returns the detected termenv color profile. It will perform the
-// actual check only once.
-func ColorProfile() termenv.Profile {
-	if !explicitColorProfile {
-		getColorProfile.Do(func() {
-			colorProfile = termenv.EnvColorProfile()
-		})
-	}
-	return colorProfile
-}
-
-// SetColorProfile sets the color profile on a package-wide context. This
-// function exists mostly for testing purposes so that you can assure you're
-// testing against a specific profile.
-//
-// Outside of testing you likely won't want to use this function as
-// ColorProfile() will detect and cache the terminal's color capabilities
-// and choose the best available profile.
-//
-// Available color profiles are:
-//
-// termenv.Ascii (no color, 1-bit)
-// termenv.ANSI (16 colors, 4-bit)
-// termenv.ANSI256 (256 colors, 8-bit)
-// termenv.TrueColor (16,777,216 colors, 24-bit)
-//
-// This function is thread-safe.
-func SetColorProfile(p termenv.Profile) {
-	colorProfileMtx.Lock()
-	defer colorProfileMtx.Unlock()
-	colorProfile = p
-	explicitColorProfile = true
-}
-
-// HasDarkBackground returns whether or not the terminal has a dark background.
-func HasDarkBackground() bool {
-	if !explicitBackgroundColor {
-		getBackgroundColor.Do(func() {
-			hasDarkBackground = termenv.HasDarkBackground()
-		})
-	}
-
-	return hasDarkBackground
-}
-
-// SetHasDarkBackground sets the value of the background color detection on a
-// package-wide context. This function exists mostly for testing purposes so
-// that you can assure you're testing against a specific background color
-// setting.
-//
-// Outside of testing you likely won't want to use this function as
-// HasDarkBackground() will detect and cache the terminal's current background
-// color setting.
-//
-// This function is thread-safe.
-func SetHasDarkBackground(b bool) {
-	colorProfileMtx.Lock()
-	defer colorProfileMtx.Unlock()
-	hasDarkBackground = b
-	explicitBackgroundColor = true
-}
-
-// TerminalColor is a color intended to be rendered in the terminal. It
-// satisfies the Go color.Color interface.
+// TerminalColor is a color intended to be rendered in the terminal.
 type TerminalColor interface {
-	value() string
-	color() termenv.Color
+	color(*Renderer) termenv.Color
 	RGBA() (r, g, b, a uint32)
 }
 
+var noColor = NoColor{}
+
 // NoColor is used to specify the absence of color styling. When this is active
 // foreground colors will be rendered with the terminal's default text color,
 // and background colors will not be drawn at all.
 //
 // Example usage:
 //
-//     var style = someStyle.Copy().Background(lipgloss.NoColor{})
-//
+//	var style = someStyle.Copy().Background(lipgloss.NoColor{})
 type NoColor struct{}
 
-func (n NoColor) value() string {
-	return ""
-}
-
-func (n NoColor) color() termenv.Color {
-	return ColorProfile().Color("")
+func (NoColor) color(*Renderer) termenv.Color {
+	return termenv.NoColor{}
 }
 
 // RGBA returns the RGBA value of this color. Because we have to return
 // something, despite this color being the absence of color, we're returning
-// the same value that go-colorful returns on error:
+// black with 100% opacity.
 //
 // Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
+//
+// Deprecated.
 func (n NoColor) RGBA() (r, g, b, a uint32) {
 	return 0x0, 0x0, 0x0, 0xFFFF
 }
 
-var noColor = NoColor{}
-
 // Color specifies a color by hex or ANSI value. For example:
 //
-//     ansiColor := lipgloss.Color("21")
-//     hexColor := lipgloss.Color("#0000ff")
-//
+//	ansiColor := lipgloss.Color("21")
+//	hexColor := lipgloss.Color("#0000ff")
 type Color string
 
-func (c Color) value() string {
-	return string(c)
+func (c Color) color(r *Renderer) termenv.Color {
+	return r.ColorProfile().Color(string(c))
 }
 
-func (c Color) color() termenv.Color {
-	return ColorProfile().Color(string(c))
+// RGBA returns the RGBA value of this color. This satisfies the Go Color
+// interface. Note that on error we return black with 100% opacity, or:
+//
+// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
+//
+// Deprecated.
+func (c Color) RGBA() (r, g, b, a uint32) {
+	return termenv.ConvertToRGB(c.color(renderer)).RGBA()
+}
+
+// ANSIColor is a color specified by an ANSI color value. It's merely syntactic
+// sugar for the more general Color function. Invalid colors will render as
+// black.
+//
+// Example usage:
+//
+//	// These two statements are equivalent.
+//	colorA := lipgloss.ANSIColor(21)
+//	colorB := lipgloss.Color("21")
+type ANSIColor uint
+
+func (ac ANSIColor) color(r *Renderer) termenv.Color {
+	return Color(strconv.FormatUint(uint64(ac), 10)).color(r)
 }
 
 // RGBA returns the RGBA value of this color. This satisfies the Go Color
 // interface. Note that on error we return black with 100% opacity, or:
 //
-// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF
+// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
 //
-// This is inline with go-colorful's default behavior.
-func (c Color) RGBA() (r, g, b, a uint32) {
-	cf, err := colorful.Hex(c.value())
-	if err != nil {
-		// If we ignore the return behavior and simply return what go-colorful
-		// give us for the color value we'd be returning exactly this, however
-		// we're being explicit here for the sake of clarity.
-		return colorful.Color{}.RGBA()
-	}
+// Deprecated.
+func (ac ANSIColor) RGBA() (r, g, b, a uint32) {
+	cf := Color(strconv.FormatUint(uint64(ac), 10))
 	return cf.RGBA()
 }
 
@@ -156,34 +90,83 @@ func (c Color) RGBA() (r, g, b, a uint32) {
 //
 // Example usage:
 //
-//     color := lipgloss.AdaptiveColor{Light: "#0000ff", Dark: "#000099"}
-//
+//	color := lipgloss.AdaptiveColor{Light: "#0000ff", Dark: "#000099"}
 type AdaptiveColor struct {
 	Light string
 	Dark  string
 }
 
-func (ac AdaptiveColor) value() string {
-	if HasDarkBackground() {
-		return ac.Dark
+func (ac AdaptiveColor) color(r *Renderer) termenv.Color {
+	if r.HasDarkBackground() {
+		return Color(ac.Dark).color(r)
 	}
-	return ac.Light
+	return Color(ac.Light).color(r)
+}
+
+// RGBA returns the RGBA value of this color. This satisfies the Go Color
+// interface. Note that on error we return black with 100% opacity, or:
+//
+// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
+//
+// Deprecated.
+func (ac AdaptiveColor) RGBA() (r, g, b, a uint32) {
+	return termenv.ConvertToRGB(ac.color(renderer)).RGBA()
 }
 
-func (ac AdaptiveColor) color() termenv.Color {
-	return ColorProfile().Color(ac.value())
+// CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color
+// profiles. Automatic color degradation will not be performed.
+type CompleteColor struct {
+	TrueColor string
+	ANSI256   string
+	ANSI      string
+}
+
+func (c CompleteColor) color(r *Renderer) termenv.Color {
+	p := r.ColorProfile()
+	switch p {
+	case termenv.TrueColor:
+		return p.Color(c.TrueColor)
+	case termenv.ANSI256:
+		return p.Color(c.ANSI256)
+	case termenv.ANSI:
+		return p.Color(c.ANSI)
+	default:
+		return termenv.NoColor{}
+	}
 }
 
 // RGBA returns the RGBA value of this color. This satisfies the Go Color
 // interface. Note that on error we return black with 100% opacity, or:
 //
-// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF
+// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
+// CompleteAdaptiveColor specifies exact values for truecolor, ANSI256, and ANSI color
 //
-// This is inline with go-colorful's default behavior.
-func (ac AdaptiveColor) RGBA() (r, g, b, a uint32) {
-	cf, err := colorful.Hex(ac.value())
-	if err != nil {
-		return colorful.Color{}.RGBA()
+// Deprecated.
+func (c CompleteColor) RGBA() (r, g, b, a uint32) {
+	return termenv.ConvertToRGB(c.color(renderer)).RGBA()
+}
+
+// CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color
+// profiles, with separate options for light and dark backgrounds. Automatic
+// color degradation will not be performed.
+type CompleteAdaptiveColor struct {
+	Light CompleteColor
+	Dark  CompleteColor
+}
+
+func (cac CompleteAdaptiveColor) color(r *Renderer) termenv.Color {
+	if r.HasDarkBackground() {
+		return cac.Dark.color(r)
 	}
-	return cf.RGBA()
+	return cac.Light.color(r)
+}
+
+// RGBA returns the RGBA value of this color. This satisfies the Go Color
+// interface. Note that on error we return black with 100% opacity, or:
+//
+// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
+//
+// Deprecated.
+func (cac CompleteAdaptiveColor) RGBA() (r, g, b, a uint32) {
+	return termenv.ConvertToRGB(cac.color(renderer)).RGBA()
 }
diff --git a/color_test.go b/color_test.go
index ff522e2..0881076 100644
--- a/color_test.go
+++ b/color_test.go
@@ -1,53 +1,282 @@
 package lipgloss
 
 import (
+	"image/color"
 	"testing"
 
 	"github.com/muesli/termenv"
 )
 
 func TestSetColorProfile(t *testing.T) {
-	t.Parallel()
+	r := renderer
+	input := "hello"
 
 	tt := []struct {
+		name     string
 		profile  termenv.Profile
-		input    string
-		style    Style
 		expected string
 	}{
 		{
+			"ascii",
 			termenv.Ascii,
 			"hello",
-			NewStyle().Foreground(Color("#5A56E0")),
-			"hello",
 		},
 		{
+			"ansi",
 			termenv.ANSI,
-			"hello",
-			NewStyle().Foreground(Color("#5A56E0")),
 			"\x1b[94mhello\x1b[0m",
 		},
 		{
+			"ansi256",
 			termenv.ANSI256,
-			"hello",
-			NewStyle().Foreground(Color("#5A56E0")),
 			"\x1b[38;5;62mhello\x1b[0m",
 		},
 		{
+			"truecolor",
 			termenv.TrueColor,
-			"hello",
-			NewStyle().Foreground(Color("#5A56E0")),
 			"\x1b[38;2;89;86;224mhello\x1b[0m",
 		},
 	}
 
+	for _, tc := range tt {
+		t.Run(tc.name, func(t *testing.T) {
+			r.SetColorProfile(tc.profile)
+			style := NewStyle().Foreground(Color("#5A56E0"))
+			res := style.Render(input)
+
+			if res != tc.expected {
+				t.Errorf("Expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n",
+					tc.expected, formatEscapes(tc.expected),
+					res, formatEscapes(res))
+			}
+		})
+	}
+}
+
+func TestHexToColor(t *testing.T) {
+	t.Parallel()
+
+	tt := []struct {
+		input    string
+		expected uint
+	}{
+		{
+			"#FF0000",
+			0xFF0000,
+		},
+		{
+			"#00F",
+			0x0000FF,
+		},
+		{
+			"#6B50FF",
+			0x6B50FF,
+		},
+		{
+			"invalid color",
+			0x0,
+		},
+	}
+
 	for i, tc := range tt {
-		SetColorProfile(tc.profile)
-		res := tc.style.Render(tc.input)
-		if res != tc.expected {
-			t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n",
-				i, tc.expected, formatEscapes(tc.expected),
-				res, formatEscapes(res))
+		h := hexToColor(tc.input)
+		o := uint(h.R)<<16 + uint(h.G)<<8 + uint(h.B)
+		if o != tc.expected {
+			t.Errorf("expected %X, got %X (test #%d)", tc.expected, o, i+1)
 		}
 	}
 }
+
+func TestRGBA(t *testing.T) {
+	tt := []struct {
+		profile  termenv.Profile
+		darkBg   bool
+		input    TerminalColor
+		expected uint
+	}{
+		// lipgloss.Color
+		{
+			termenv.TrueColor,
+			true,
+			Color("#FF0000"),
+			0xFF0000,
+		},
+		{
+			termenv.TrueColor,
+			true,
+			Color("9"),
+			0xFF0000,
+		},
+		{
+			termenv.TrueColor,
+			true,
+			Color("21"),
+			0x0000FF,
+		},
+		// lipgloss.AdaptiveColor
+		{
+			termenv.TrueColor,
+			true,
+			AdaptiveColor{Light: "#0000FF", Dark: "#FF0000"},
+			0xFF0000,
+		},
+		{
+			termenv.TrueColor,
+			false,
+			AdaptiveColor{Light: "#0000FF", Dark: "#FF0000"},
+			0x0000FF,
+		},
+		{
+			termenv.TrueColor,
+			true,
+			AdaptiveColor{Light: "21", Dark: "9"},
+			0xFF0000,
+		},
+		{
+			termenv.TrueColor,
+			false,
+			AdaptiveColor{Light: "21", Dark: "9"},
+			0x0000FF,
+		},
+		// lipgloss.CompleteColor
+		{
+			termenv.TrueColor,
+			true,
+			CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
+			0xFF0000,
+		},
+		{
+			termenv.ANSI256,
+			true,
+			CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
+			0xFFFFFF,
+		},
+		{
+			termenv.ANSI,
+			true,
+			CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
+			0x0000FF,
+		},
+		{
+			termenv.TrueColor,
+			true,
+			CompleteColor{TrueColor: "", ANSI256: "231", ANSI: "12"},
+			0x000000,
+		},
+		// lipgloss.CompleteAdaptiveColor
+		// dark
+		{
+			termenv.TrueColor,
+			true,
+			CompleteAdaptiveColor{
+				Light: CompleteColor{TrueColor: "#0000FF", ANSI256: "231", ANSI: "12"},
+				Dark:  CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
+			},
+			0xFF0000,
+		},
+		{
+			termenv.ANSI256,
+			true,
+			CompleteAdaptiveColor{
+				Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "21", ANSI: "12"},
+				Dark:  CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
+			},
+			0xFFFFFF,
+		},
+		{
+			termenv.ANSI,
+			true,
+			CompleteAdaptiveColor{
+				Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "9"},
+				Dark:  CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
+			},
+			0x0000FF,
+		},
+		// light
+		{
+			termenv.TrueColor,
+			false,
+			CompleteAdaptiveColor{
+				Light: CompleteColor{TrueColor: "#0000FF", ANSI256: "231", ANSI: "12"},
+				Dark:  CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
+			},
+			0x0000FF,
+		},
+		{
+			termenv.ANSI256,
+			false,
+			CompleteAdaptiveColor{
+				Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "21", ANSI: "12"},
+				Dark:  CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
+			},
+			0x0000FF,
+		},
+		{
+			termenv.ANSI,
+			false,
+			CompleteAdaptiveColor{
+				Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "9"},
+				Dark:  CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
+			},
+			0xFF0000,
+		},
+	}
+
+	r := DefaultRenderer()
+	for i, tc := range tt {
+		r.SetColorProfile(tc.profile)
+		r.SetHasDarkBackground(tc.darkBg)
+
+		r, g, b, _ := tc.input.RGBA()
+		o := uint(r/256)<<16 + uint(g/256)<<8 + uint(b/256)
+
+		if o != tc.expected {
+			t.Errorf("expected %X, got %X (test #%d)", tc.expected, o, i+1)
+		}
+	}
+}
+
+// hexToColor translates a hex color string (#RRGGBB or #RGB) into a color.RGB,
+// which satisfies the color.Color interface. If an invalid string is passed
+// black with 100% opacity will be returned: or, in hex format, 0x000000FF.
+func hexToColor(hex string) (c color.RGBA) {
+	c.A = 0xFF
+
+	if hex == "" || hex[0] != '#' {
+		return c
+	}
+
+	const (
+		fullFormat  = 7 // #RRGGBB
+		shortFormat = 4 // #RGB
+	)
+
+	switch len(hex) {
+	case fullFormat:
+		const offset = 4
+		c.R = hexToByte(hex[1])<<offset + hexToByte(hex[2])
+		c.G = hexToByte(hex[3])<<offset + hexToByte(hex[4])
+		c.B = hexToByte(hex[5])<<offset + hexToByte(hex[6])
+	case shortFormat:
+		const offset = 0x11
+		c.R = hexToByte(hex[1]) * offset
+		c.G = hexToByte(hex[2]) * offset
+		c.B = hexToByte(hex[3]) * offset
+	}
+
+	return c
+}
+
+func hexToByte(b byte) byte {
+	const offset = 10
+	switch {
+	case b >= '0' && b <= '9':
+		return b - '0'
+	case b >= 'a' && b <= 'f':
+		return b - 'a' + offset
+	case b >= 'A' && b <= 'F':
+		return b - 'A' + offset
+	}
+	// Invalid, but just return 0.
+	return 0
+}
diff --git a/debian/changelog b/debian/changelog
index 7fecc13..4556ffe 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+golang-github-charmbracelet-lipgloss (0.7.1-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Tue, 20 Jun 2023 02:42:38 -0000
+
 golang-github-charmbracelet-lipgloss (0.5.0-1) unstable; urgency=medium
 
   * New upstream version 0.5.0
diff --git a/example/go.mod b/example/go.mod
deleted file mode 100644
index 3ff788f..0000000
--- a/example/go.mod
+++ /dev/null
@@ -1,11 +0,0 @@
-module example
-
-go 1.16
-
-require (
-	github.com/charmbracelet/lipgloss v0.4.0
-	github.com/lucasb-eyer/go-colorful v1.2.0
-	golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
-)
-
-replace github.com/charmbracelet/lipgloss => ../
diff --git a/example/go.sum b/example/go.sum
deleted file mode 100644
index 9ef83e7..0000000
--- a/example/go.sum
+++ /dev/null
@@ -1,19 +0,0 @@
-github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
-github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
-github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
-github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
-github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9FnmSjdbWUlLGvcxrY0Rw3ATltrxOhk=
-github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
-github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 h1:STjmj0uFfRryL9fzRA/OupNppeAID6QJYPMavTL7jtY=
-github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
-github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
-github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
-golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
diff --git a/examples/go.mod b/examples/go.mod
new file mode 100644
index 0000000..d613d63
--- /dev/null
+++ b/examples/go.mod
@@ -0,0 +1,29 @@
+module examples
+
+go 1.17
+
+replace github.com/charmbracelet/lipgloss => ../
+
+require (
+	github.com/charmbracelet/lipgloss v0.4.0
+	github.com/charmbracelet/wish v0.5.0
+	github.com/gliderlabs/ssh v0.3.4
+	github.com/kr/pty v1.1.1
+	github.com/lucasb-eyer/go-colorful v1.2.0
+	github.com/muesli/termenv v0.15.0
+	golang.org/x/term v0.0.0-20210422114643-f5beecf764ed
+)
+
+require (
+	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
+	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+	github.com/caarlos0/sshmarshal v0.1.0 // indirect
+	github.com/charmbracelet/keygen v0.3.0 // indirect
+	github.com/mattn/go-isatty v0.0.17 // indirect
+	github.com/mattn/go-runewidth v0.0.14 // indirect
+	github.com/mitchellh/go-homedir v1.1.0 // indirect
+	github.com/muesli/reflow v0.3.0 // indirect
+	github.com/rivo/uniseg v0.2.0 // indirect
+	golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 // indirect
+	golang.org/x/sys v0.6.0 // indirect
+)
diff --git a/examples/go.sum b/examples/go.sum
new file mode 100644
index 0000000..0651f5b
--- /dev/null
+++ b/examples/go.sum
@@ -0,0 +1,123 @@
+github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
+github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
+github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
+github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA=
+github.com/caarlos0/sshmarshal v0.1.0 h1:zTCZrDORFfWh526Tsb7vCm3+Yg/SfW/Ub8aQDeosk0I=
+github.com/caarlos0/sshmarshal v0.1.0/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA=
+github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM=
+github.com/charmbracelet/keygen v0.3.0 h1:mXpsQcH7DDlST5TddmXNXjS0L7ECk4/kLQYyBcsan2Y=
+github.com/charmbracelet/keygen v0.3.0/go.mod h1:1ukgO8806O25lUZ5s0IrNur+RlwTBERlezdgW71F5rM=
+github.com/charmbracelet/wish v0.5.0 h1:FkkdNBFqrLABR1ciNrAL2KCxoyWfKhXnIGZw6GfAtPg=
+github.com/charmbracelet/wish v0.5.0/go.mod h1:5GAn5SrDSZ7cgKjnC+3kDmiIo7I6k4/AYiRzC4+tpCk=
+github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/gliderlabs/ssh v0.3.4 h1:+AXBtim7MTKaLVPgvE+3mhewYRawNLTd+jEEz/wExZw=
+github.com/gliderlabs/ssh v0.3.4/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
+github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
+github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
+github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
+github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
+github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
+github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
+github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
+github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
+github.com/muesli/termenv v0.15.0 h1:ZYfCF4CZGhAA4meilZ5pd7tfUX4QLH4zB7OBie4RMS8=
+github.com/muesli/termenv v0.15.0/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
+golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 h1:syTAU9FwmvzEoIYMqcPHOcVm4H3U5u90WsvuYgwpETU=
+golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0=
+golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/example/main.go b/examples/layout/main.go
similarity index 99%
rename from example/main.go
rename to examples/layout/main.go
index 8ec832b..e68cf77 100644
--- a/example/main.go
+++ b/examples/layout/main.go
@@ -1,5 +1,7 @@
 package main
 
+// This example demonstrates various Lip Gloss style and layout features.
+
 import (
 	"fmt"
 	"os"
diff --git a/examples/ssh/main.go b/examples/ssh/main.go
new file mode 100644
index 0000000..cd23b05
--- /dev/null
+++ b/examples/ssh/main.go
@@ -0,0 +1,196 @@
+package main
+
+// This example demonstrates how to use a custom Lip Gloss renderer with Wish,
+// a package for building custom SSH servers.
+//
+// The big advantage to using custom renderers here is that we can accurately
+// detect the background color and color profile for each client and render
+// against that accordingly.
+//
+// For details on wish see: https://github.com/charmbracelet/wish/
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"strings"
+
+	"github.com/charmbracelet/lipgloss"
+	"github.com/charmbracelet/wish"
+	lm "github.com/charmbracelet/wish/logging"
+	"github.com/gliderlabs/ssh"
+	"github.com/kr/pty"
+	"github.com/muesli/termenv"
+)
+
+// Available styles.
+type styles struct {
+	bold          lipgloss.Style
+	faint         lipgloss.Style
+	italic        lipgloss.Style
+	underline     lipgloss.Style
+	strikethrough lipgloss.Style
+	red           lipgloss.Style
+	green         lipgloss.Style
+	yellow        lipgloss.Style
+	blue          lipgloss.Style
+	magenta       lipgloss.Style
+	cyan          lipgloss.Style
+	gray          lipgloss.Style
+}
+
+// Create new styles against a given renderer.
+func makeStyles(r *lipgloss.Renderer) styles {
+	return styles{
+		bold:          r.NewStyle().SetString("bold").Bold(true),
+		faint:         r.NewStyle().SetString("faint").Faint(true),
+		italic:        r.NewStyle().SetString("italic").Italic(true),
+		underline:     r.NewStyle().SetString("underline").Underline(true),
+		strikethrough: r.NewStyle().SetString("strikethrough").Strikethrough(true),
+		red:           r.NewStyle().SetString("red").Foreground(lipgloss.Color("#E88388")),
+		green:         r.NewStyle().SetString("green").Foreground(lipgloss.Color("#A8CC8C")),
+		yellow:        r.NewStyle().SetString("yellow").Foreground(lipgloss.Color("#DBAB79")),
+		blue:          r.NewStyle().SetString("blue").Foreground(lipgloss.Color("#71BEF2")),
+		magenta:       r.NewStyle().SetString("magenta").Foreground(lipgloss.Color("#D290E4")),
+		cyan:          r.NewStyle().SetString("cyan").Foreground(lipgloss.Color("#66C2CD")),
+		gray:          r.NewStyle().SetString("gray").Foreground(lipgloss.Color("#B9BFCA")),
+	}
+}
+
+// Bridge Wish and Termenv so we can query for a user's terminal capabilities.
+type sshOutput struct {
+	ssh.Session
+	tty *os.File
+}
+
+func (s *sshOutput) Write(p []byte) (int, error) {
+	return s.Session.Write(p)
+}
+
+func (s *sshOutput) Read(p []byte) (int, error) {
+	return s.Session.Read(p)
+}
+
+func (s *sshOutput) Fd() uintptr {
+	return s.tty.Fd()
+}
+
+type sshEnviron struct {
+	environ []string
+}
+
+func (s *sshEnviron) Getenv(key string) string {
+	for _, v := range s.environ {
+		if strings.HasPrefix(v, key+"=") {
+			return v[len(key)+1:]
+		}
+	}
+	return ""
+}
+
+func (s *sshEnviron) Environ() []string {
+	return s.environ
+}
+
+// Create a termenv.Output from the session.
+func outputFromSession(sess ssh.Session) *termenv.Output {
+	sshPty, _, _ := sess.Pty()
+	_, tty, err := pty.Open()
+	if err != nil {
+		log.Fatal(err)
+	}
+	o := &sshOutput{
+		Session: sess,
+		tty:     tty,
+	}
+	environ := sess.Environ()
+	environ = append(environ, fmt.Sprintf("TERM=%s", sshPty.Term))
+	e := &sshEnviron{environ: environ}
+	// We need to use unsafe mode here because the ssh session is not running
+	// locally and we already know that the session is a TTY.
+	return termenv.NewOutput(o, termenv.WithUnsafe(), termenv.WithEnvironment(e))
+}
+
+// Handle SSH requests.
+func handler(next ssh.Handler) ssh.Handler {
+	return func(sess ssh.Session) {
+		// Get client's output.
+		clientOutput := outputFromSession(sess)
+
+		pty, _, active := sess.Pty()
+		if !active {
+			next(sess)
+			return
+		}
+		width := pty.Window.Width
+
+		// Initialize new renderer for the client.
+		renderer := lipgloss.NewRenderer(sess)
+		renderer.SetOutput(clientOutput)
+
+		// Initialize new styles against the renderer.
+		styles := makeStyles(renderer)
+
+		str := strings.Builder{}
+
+		fmt.Fprintf(&str, "\n\n%s %s %s %s %s",
+			styles.bold,
+			styles.faint,
+			styles.italic,
+			styles.underline,
+			styles.strikethrough,
+		)
+
+		fmt.Fprintf(&str, "\n%s %s %s %s %s %s %s",
+			styles.red,
+			styles.green,
+			styles.yellow,
+			styles.blue,
+			styles.magenta,
+			styles.cyan,
+			styles.gray,
+		)
+
+		fmt.Fprintf(&str, "\n%s %s %s %s %s %s %s\n\n",
+			styles.red,
+			styles.green,
+			styles.yellow,
+			styles.blue,
+			styles.magenta,
+			styles.cyan,
+			styles.gray,
+		)
+
+		fmt.Fprintf(&str, "%s %t %s\n\n", styles.bold.Copy().UnsetString().Render("Has dark background?"),
+			renderer.HasDarkBackground(),
+			renderer.Output().BackgroundColor())
+
+		block := renderer.Place(width,
+			lipgloss.Height(str.String()), lipgloss.Center, lipgloss.Center, str.String(),
+			lipgloss.WithWhitespaceChars("/"),
+			lipgloss.WithWhitespaceForeground(lipgloss.AdaptiveColor{Light: "250", Dark: "236"}),
+		)
+
+		// Render to client.
+		wish.WriteString(sess, block)
+
+		next(sess)
+	}
+}
+
+func main() {
+	port := 3456
+	s, err := wish.NewServer(
+		wish.WithAddress(fmt.Sprintf(":%d", port)),
+		wish.WithHostKeyPath("ssh_example"),
+		wish.WithMiddleware(handler, lm.Middleware()),
+	)
+	if err != nil {
+		log.Fatal(err)
+	}
+	log.Printf("SSH server listening on port %d", port)
+	log.Printf("To connect from your local machine run: ssh localhost -p %d", port)
+	if err := s.ListenAndServe(); err != nil {
+		log.Fatal(err)
+	}
+}
diff --git a/get.go b/get.go
index 8790622..eb24a4e 100644
--- a/get.go
+++ b/get.go
@@ -6,42 +6,42 @@ import (
 	"github.com/muesli/reflow/ansi"
 )
 
-// GetBold returns the style's bold value It no value is set false is returned.
+// GetBold returns the style's bold value. If no value is set false is returned.
 func (s Style) GetBold() bool {
 	return s.getAsBool(boldKey, false)
 }
 
-// GetItalic returns the style's italic value. It no value is set false is
+// GetItalic returns the style's italic value. If no value is set false is
 // returned.
 func (s Style) GetItalic() bool {
 	return s.getAsBool(italicKey, false)
 }
 
-// GetUnderline returns the style's underline value. It no value is set false is
+// GetUnderline returns the style's underline value. If no value is set false is
 // returned.
 func (s Style) GetUnderline() bool {
 	return s.getAsBool(underlineKey, false)
 }
 
-// GetStrikethrough returns the style's strikethrough value. It no value is set false
+// GetStrikethrough returns the style's strikethrough value. If no value is set false
 // is returned.
 func (s Style) GetStrikethrough() bool {
 	return s.getAsBool(strikethroughKey, false)
 }
 
-// GetReverse returns the style's reverse value. It no value is set false is
+// GetReverse returns the style's reverse value. If no value is set false is
 // returned.
 func (s Style) GetReverse() bool {
 	return s.getAsBool(reverseKey, false)
 }
 
-// GetBlink returns the style's blink value. It no value is set false is
+// GetBlink returns the style's blink value. If no value is set false is
 // returned.
 func (s Style) GetBlink() bool {
 	return s.getAsBool(blinkKey, false)
 }
 
-// GetFaint returns the style's faint value. It no value is set false is
+// GetFaint returns the style's faint value. If no value is set false is
 // returned.
 func (s Style) GetFaint() bool {
 	return s.getAsBool(faintKey, false)
@@ -71,16 +71,36 @@ func (s Style) GetHeight() int {
 	return s.getAsInt(heightKey)
 }
 
-// GetAlign returns the style's implicit alignment setting. If no alignment is
-// set Position.AlignLeft is returned.
+// GetAlign returns the style's implicit horizontal alignment setting.
+// If no alignment is set Position.Left is returned.
 func (s Style) GetAlign() Position {
-	v := s.getAsPosition(alignKey)
+	v := s.getAsPosition(alignHorizontalKey)
 	if v == Position(0) {
 		return Left
 	}
 	return v
 }
 
+// GetAlignHorizontal returns the style's implicit horizontal alignment setting.
+// If no alignment is set Position.Left is returned.
+func (s Style) GetAlignHorizontal() Position {
+	v := s.getAsPosition(alignHorizontalKey)
+	if v == Position(0) {
+		return Left
+	}
+	return v
+}
+
+// GetAlignVertical returns the style's implicit vertical alignment setting.
+// If no alignment is set Position.Top is returned.
+func (s Style) GetAlignVertical() Position {
+	v := s.getAsPosition(alignVerticalKey)
+	if v == Position(0) {
+		return Top
+	}
+	return v
+}
+
 // GetPadding returns the style's top, right, bottom, and left padding values,
 // in that order. 0 is returned for unset values.
 func (s Style) GetPadding() (top, right, bottom, left int) {
@@ -180,7 +200,7 @@ func (s Style) GetVerticalMargins() int {
 // GetBorder returns the style's border style (type Border) and value for the
 // top, right, bottom, and left in that order. If no value is set for the
 // border style, Border{} is returned. For all other unset values false is
-// returend.
+// returned.
 func (s Style) GetBorder() (b Border, top, right, bottom, left bool) {
 	return s.getBorderStyle(),
 		s.getAsBool(borderTopKey, false),
@@ -270,7 +290,16 @@ func (s Style) GetBorderLeftBackground() TerminalColor {
 // GetBorderTopWidth returns the width of the top border. If borders contain
 // runes of varying widths, the widest rune is returned. If no border exists on
 // the top edge, 0 is returned.
+//
+// Deprecated: This function simply calls Style.GetBorderTopSize.
 func (s Style) GetBorderTopWidth() int {
+	return s.GetBorderTopSize()
+}
+
+// GetBorderTopSize returns the width of the top border. If borders contain
+// runes of varying widths, the widest rune is returned. If no border exists on
+// the top edge, 0 is returned.
+func (s Style) GetBorderTopSize() int {
 	if !s.getAsBool(borderTopKey, false) {
 		return 0
 	}
@@ -395,12 +424,12 @@ func (s Style) getAsBool(k propKey, defaultVal bool) bool {
 func (s Style) getAsColor(k propKey) TerminalColor {
 	v, ok := s.rules[k]
 	if !ok {
-		return NoColor{}
+		return noColor
 	}
 	if c, ok := v.(TerminalColor); ok {
 		return c
 	}
-	return NoColor{}
+	return noColor
 }
 
 func (s Style) getAsInt(k propKey) int {
diff --git a/go.mod b/go.mod
index c511c03..a1376ba 100644
--- a/go.mod
+++ b/go.mod
@@ -1,11 +1,17 @@
 module github.com/charmbracelet/lipgloss
 
-go 1.15
+go 1.17
 
 require (
-	github.com/lucasb-eyer/go-colorful v1.2.0
-	github.com/mattn/go-runewidth v0.0.13
-	github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68
-	github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0
-	golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
+	github.com/mattn/go-runewidth v0.0.14
+	github.com/muesli/reflow v0.3.0
+	github.com/muesli/termenv v0.15.1
+)
+
+require (
+	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+	github.com/mattn/go-isatty v0.0.17 // indirect
+	github.com/rivo/uniseg v0.2.0 // indirect
+	golang.org/x/sys v0.6.0 // indirect
 )
diff --git a/go.sum b/go.sum
index ba0a481..2c273fd 100644
--- a/go.sum
+++ b/go.sum
@@ -1,16 +1,19 @@
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
-github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
-github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
-github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9FnmSjdbWUlLGvcxrY0Rw3ATltrxOhk=
-github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
-github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 h1:STjmj0uFfRryL9fzRA/OupNppeAID6QJYPMavTL7jtY=
-github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
+github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
+github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
+github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/join.go b/join.go
index 69ffdc9..cc16600 100644
--- a/join.go
+++ b/join.go
@@ -17,15 +17,14 @@ import (
 //
 // Example:
 //
-//     blockB := "...\n...\n..."
-//     blockA := "...\n...\n...\n...\n..."
+//	blockB := "...\n...\n..."
+//	blockA := "...\n...\n...\n...\n..."
 //
-//     // Join 20% from the top
-//     str := lipgloss.JoinHorizontal(0.2, blockA, blockB)
-//
-//     // Join on the top edge
-//     str := lipgloss.JoinHorizontal(lipgloss.Top, blockA, blockB)
+//	// Join 20% from the top
+//	str := lipgloss.JoinHorizontal(0.2, blockA, blockB)
 //
+//	// Join on the top edge
+//	str := lipgloss.JoinHorizontal(lipgloss.Top, blockA, blockB)
 func JoinHorizontal(pos Position, strs ...string) string {
 	if len(strs) == 0 {
 		return ""
@@ -106,15 +105,14 @@ func JoinHorizontal(pos Position, strs ...string) string {
 //
 // Example:
 //
-//     blockB := "...\n...\n..."
-//     blockA := "...\n...\n...\n...\n..."
-//
-//     // Join 20% from the top
-//     str := lipgloss.JoinVertical(0.2, blockA, blockB)
+//	blockB := "...\n...\n..."
+//	blockA := "...\n...\n...\n...\n..."
 //
-//     // Join on the right edge
-//     str := lipgloss.JoinVertical(lipgloss.Right, blockA, blockB)
+//	// Join 20% from the top
+//	str := lipgloss.JoinVertical(0.2, blockA, blockB)
 //
+//	// Join on the right edge
+//	str := lipgloss.JoinVertical(lipgloss.Right, blockA, blockB)
 func JoinVertical(pos Position, strs ...string) string {
 	if len(strs) == 0 {
 		return ""
diff --git a/join_test.go b/join_test.go
index 813280d..9dcf513 100644
--- a/join_test.go
+++ b/join_test.go
@@ -4,18 +4,21 @@ import "testing"
 
 func TestJoinVertical(t *testing.T) {
 	type test struct {
+		name     string
 		result   string
 		expected string
 	}
 	tests := []test{
-		{JoinVertical(0, "A", "BBBB"), "A   \nBBBB"},
-		{JoinVertical(1, "A", "BBBB"), "   A\nBBBB"},
-		{JoinVertical(0.25, "A", "BBBB"), " A  \nBBBB"},
+		{"por0", JoinVertical(0, "A", "BBBB"), "A   \nBBBB"},
+		{"pos1", JoinVertical(1, "A", "BBBB"), "   A\nBBBB"},
+		{"pos0.25", JoinVertical(0.25, "A", "BBBB"), " A  \nBBBB"},
 	}
 
 	for _, test := range tests {
-		if test.result != test.expected {
-			t.Errorf("Got \n%s\n, expected \n%s\n", test.result, test.expected)
-		}
+		t.Run(test.name, func(t *testing.T) {
+			if test.result != test.expected {
+				t.Errorf("Got \n%s\n, expected \n%s\n", test.result, test.expected)
+			}
+		})
 	}
 }
diff --git a/position.go b/position.go
index 2ecb897..28f5ccb 100644
--- a/position.go
+++ b/position.go
@@ -34,13 +34,26 @@ const (
 // Place places a string or text block vertically in an unstyled box of a given
 // width or height.
 func Place(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption) string {
-	return PlaceVertical(height, vPos, PlaceHorizontal(width, hPos, str, opts...), opts...)
+	return renderer.Place(width, height, hPos, vPos, str, opts...)
+}
+
+// Place places a string or text block vertically in an unstyled box of a given
+// width or height.
+func (r *Renderer) Place(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption) string {
+	return r.PlaceVertical(height, vPos, r.PlaceHorizontal(width, hPos, str, opts...), opts...)
 }
 
 // PlaceHorizontal places a string or text block horizontally in an unstyled
 // block of a given width. If the given width is shorter than the max width of
-// the string (measured by it's longest line) this will be a noöp.
+// the string (measured by its longest line) this will be a noop.
 func PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOption) string {
+	return renderer.PlaceHorizontal(width, pos, str, opts...)
+}
+
+// PlaceHorizontal places a string or text block horizontally in an unstyled
+// block of a given width. If the given width is shorter than the max width of
+// the string (measured by it's longest line) this will be a noöp.
+func (r *Renderer) PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOption) string {
 	lines, contentWidth := getLines(str)
 	gap := width - contentWidth
 
@@ -48,10 +61,7 @@ func PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOpti
 		return str
 	}
 
-	ws := &whitespace{}
-	for _, opt := range opts {
-		opt(ws)
-	}
+	ws := newWhitespace(r, opts...)
 
 	var b strings.Builder
 	for i, l := range lines {
@@ -89,8 +99,15 @@ func PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOpti
 
 // PlaceVertical places a string or text block vertically in an unstyled block
 // of a given height. If the given height is shorter than the height of the
-// string (measured by it's newlines) then this will be a noöp.
+// string (measured by its newlines) then this will be a noop.
 func PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOption) string {
+	return renderer.PlaceVertical(height, pos, str, opts...)
+}
+
+// PlaceVertical places a string or text block vertically in an unstyled block
+// of a given height. If the given height is shorter than the height of the
+// string (measured by it's newlines) then this will be a noöp.
+func (r *Renderer) PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOption) string {
 	contentHeight := strings.Count(str, "\n") + 1
 	gap := height - contentHeight
 
@@ -98,10 +115,7 @@ func PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOptio
 		return str
 	}
 
-	ws := &whitespace{}
-	for _, opt := range opts {
-		opt(ws)
-	}
+	ws := newWhitespace(r, opts...)
 
 	_, width := getLines(str)
 	emptyLine := ws.render(width)
diff --git a/renderer.go b/renderer.go
new file mode 100644
index 0000000..4bea837
--- /dev/null
+++ b/renderer.go
@@ -0,0 +1,143 @@
+package lipgloss
+
+import (
+	"io"
+
+	"github.com/muesli/termenv"
+)
+
+// We're manually creating the struct here to avoid initializing the output and
+// query the terminal multiple times.
+var renderer = &Renderer{
+	output: termenv.DefaultOutput(),
+}
+
+// Renderer is a lipgloss terminal renderer.
+type Renderer struct {
+	output            *termenv.Output
+	hasDarkBackground *bool
+}
+
+// RendererOption is a function that can be used to configure a [Renderer].
+type RendererOption func(r *Renderer)
+
+// DefaultRenderer returns the default renderer.
+func DefaultRenderer() *Renderer {
+	return renderer
+}
+
+// SetDefaultRenderer sets the default global renderer.
+func SetDefaultRenderer(r *Renderer) {
+	renderer = r
+}
+
+// NewRenderer creates a new Renderer.
+//
+// w will be used to determine the terminal's color capabilities.
+func NewRenderer(w io.Writer, opts ...termenv.OutputOption) *Renderer {
+	r := &Renderer{
+		output: termenv.NewOutput(w, opts...),
+	}
+	return r
+}
+
+// Output returns the termenv output.
+func (r *Renderer) Output() *termenv.Output {
+	return r.output
+}
+
+// SetOutput sets the termenv output.
+func (r *Renderer) SetOutput(o *termenv.Output) {
+	r.output = o
+}
+
+// ColorProfile returns the detected termenv color profile.
+func (r *Renderer) ColorProfile() termenv.Profile {
+	return r.output.Profile
+}
+
+// ColorProfile returns the detected termenv color profile.
+func ColorProfile() termenv.Profile {
+	return renderer.ColorProfile()
+}
+
+// SetColorProfile sets the color profile on the renderer. This function exists
+// mostly for testing purposes so that you can assure you're testing against
+// a specific profile.
+//
+// Outside of testing you likely won't want to use this function as the color
+// profile will detect and cache the terminal's color capabilities and choose
+// the best available profile.
+//
+// Available color profiles are:
+//
+//	termenv.Ascii     // no color, 1-bit
+//	termenv.ANSI      //16 colors, 4-bit
+//	termenv.ANSI256   // 256 colors, 8-bit
+//	termenv.TrueColor // 16,777,216 colors, 24-bit
+//
+// This function is thread-safe.
+func (r *Renderer) SetColorProfile(p termenv.Profile) {
+	r.output.Profile = p
+}
+
+// SetColorProfile sets the color profile on the default renderer. This
+// function exists mostly for testing purposes so that you can assure you're
+// testing against a specific profile.
+//
+// Outside of testing you likely won't want to use this function as the color
+// profile will detect and cache the terminal's color capabilities and choose
+// the best available profile.
+//
+// Available color profiles are:
+//
+//	termenv.Ascii     // no color, 1-bit
+//	termenv.ANSI      //16 colors, 4-bit
+//	termenv.ANSI256   // 256 colors, 8-bit
+//	termenv.TrueColor // 16,777,216 colors, 24-bit
+//
+// This function is thread-safe.
+func SetColorProfile(p termenv.Profile) {
+	renderer.SetColorProfile(p)
+}
+
+// HasDarkBackground returns whether or not the terminal has a dark background.
+func HasDarkBackground() bool {
+	return renderer.HasDarkBackground()
+}
+
+// HasDarkBackground returns whether or not the renderer will render to a dark
+// background. A dark background can either be auto-detected, or set explicitly
+// on the renderer.
+func (r *Renderer) HasDarkBackground() bool {
+	if r.hasDarkBackground != nil {
+		return *r.hasDarkBackground
+	}
+	return r.output.HasDarkBackground()
+}
+
+// SetHasDarkBackground sets the background color detection value for the
+// default renderer. This function exists mostly for testing purposes so that
+// you can assure you're testing against a specific background color setting.
+//
+// Outside of testing you likely won't want to use this function as the
+// backgrounds value will be automatically detected and cached against the
+// terminal's current background color setting.
+//
+// This function is thread-safe.
+func SetHasDarkBackground(b bool) {
+	renderer.SetHasDarkBackground(b)
+}
+
+// SetHasDarkBackground sets the background color detection value on the
+// renderer. This function exists mostly for testing purposes so that you can
+// assure you're testing against a specific background color setting.
+//
+// Outside of testing you likely won't want to use this function as the
+// backgrounds value will be automatically detected and cached against the
+// terminal's current background color setting.
+//
+// This function is thread-safe.
+func (r *Renderer) SetHasDarkBackground(b bool) {
+	r.hasDarkBackground = &b
+}
diff --git a/renderer_test.go b/renderer_test.go
new file mode 100644
index 0000000..7f05acd
--- /dev/null
+++ b/renderer_test.go
@@ -0,0 +1,35 @@
+package lipgloss
+
+import (
+	"os"
+	"testing"
+
+	"github.com/muesli/termenv"
+)
+
+func TestRendererHasDarkBackground(t *testing.T) {
+	r1 := NewRenderer(os.Stdout)
+	r1.SetHasDarkBackground(false)
+	if r1.HasDarkBackground() {
+		t.Error("Expected renderer to have light background")
+	}
+	r2 := NewRenderer(os.Stdout)
+	r2.SetHasDarkBackground(true)
+	if !r2.HasDarkBackground() {
+		t.Error("Expected renderer to have dark background")
+	}
+}
+
+func TestRendererWithOutput(t *testing.T) {
+	f, err := os.Create(t.Name())
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer f.Close()
+	defer os.Remove(f.Name())
+	r := NewRenderer(f)
+	r.SetColorProfile(termenv.TrueColor)
+	if r.output.Profile != termenv.TrueColor {
+		t.Error("Expected renderer to use true color")
+	}
+}
diff --git a/runes.go b/runes.go
index 723f6db..7a49e32 100644
--- a/runes.go
+++ b/runes.go
@@ -4,7 +4,7 @@ import (
 	"strings"
 )
 
-// StyleRunes applys a given style to runes at the given indicesin the string.
+// StyleRunes apply a given style to runes at the given indices in the string.
 // Note that you must provide styling options for both matched and unmatched
 // runes. Indices out of bounds will be ignored.
 func StyleRunes(str string, indices []int, matched, unmatched Style) string {
diff --git a/runes_test.go b/runes_test.go
index 3e27658..44f3963 100644
--- a/runes_test.go
+++ b/runes_test.go
@@ -6,37 +6,41 @@ import (
 )
 
 func TestStyleRunes(t *testing.T) {
-	t.Parallel()
-
 	matchedStyle := NewStyle().Reverse(true)
 	unmatchedStyle := NewStyle()
 
 	tt := []struct {
+		name     string
 		input    string
 		indices  []int
 		expected string
 	}{
 		{
+			"hello 0",
 			"hello",
 			[]int{0},
 			"\x1b[7mh\x1b[0mello",
 		},
 		{
+			"你好 1",
 			"你好",
 			[]int{1},
 			"你\x1b[7m好\x1b[0m",
 		},
 		{
+			"hello 你好 6,7",
 			"hello 你好",
 			[]int{6, 7},
 			"hello \x1b[7m你好\x1b[0m",
 		},
 		{
+			"hello 1,3",
 			"hello",
 			[]int{1, 3},
 			"h\x1b[7me\x1b[0ml\x1b[7ml\x1b[0mo",
 		},
 		{
+			"你好 0,1",
 			"你好",
 			[]int{0, 1},
 			"\x1b[7m你好\x1b[0m",
@@ -47,13 +51,15 @@ func TestStyleRunes(t *testing.T) {
 		return StyleRunes(str, indices, matchedStyle, unmatchedStyle)
 	}
 
-	for i, tc := range tt {
-		res := fn(tc.input, tc.indices)
-		if fn(tc.input, tc.indices) != tc.expected {
-			t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual Output:\n\n`%s`\n`%s`\n\n",
-				i, tc.expected, formatEscapes(tc.expected),
-				res, formatEscapes(res))
-		}
+	for _, tc := range tt {
+		t.Run(tc.name, func(t *testing.T) {
+			res := fn(tc.input, tc.indices)
+			if res != tc.expected {
+				t.Errorf("Expected:\n\n`%s`\n`%s`\n\nActual Output:\n\n`%s`\n`%s`\n\n",
+					tc.expected, formatEscapes(tc.expected),
+					res, formatEscapes(res))
+			}
+		})
 	}
 }
 
diff --git a/set.go b/set.go
index 2e7dfc8..f8bf9a2 100644
--- a/set.go
+++ b/set.go
@@ -1,7 +1,7 @@
 package lipgloss
 
 // This could (should) probably just be moved into NewStyle(). We've broken it
-// out so we can call it in a lazy way.
+// out, so we can call it in a lazy way.
 func (s *Style) init() {
 	if s.rules == nil {
 		s.rules = make(rules)
@@ -16,7 +16,7 @@ func (s *Style) set(key propKey, value interface{}) {
 	case int:
 		// We don't allow negative integers on any of our values, so just keep
 		// them at zero or above. We could use uints instead, but the
-		// conversions are a little tedious so we're sticking with ints for
+		// conversions are a little tedious, so we're sticking with ints for
 		// sake of usability.
 		s.rules[key] = max(0, v)
 	default:
@@ -39,7 +39,7 @@ func (s Style) Italic(v bool) Style {
 
 // Underline sets an underline rule. By default, underlines will not be drawn on
 // whitespace like margins and padding. To change this behavior set
-// renderUnderlinesOnSpaces.
+// UnderlineSpaces.
 func (s Style) Underline(v bool) Style {
 	s.set(underlineKey, v)
 	return s
@@ -47,7 +47,7 @@ func (s Style) Underline(v bool) Style {
 
 // Strikethrough sets a strikethrough rule. By default, strikes will not be
 // drawn on whitespace like margins and padding. To change this behavior set
-// renderStrikethroughOnSpaces.
+// StrikethroughSpaces.
 func (s Style) Strikethrough(v bool) Style {
 	s.set(strikethroughKey, v)
 	return s
@@ -73,12 +73,11 @@ func (s Style) Faint(v bool) Style {
 
 // Foreground sets a foreground color.
 //
-//     // Sets the foreground to blue
-//     s := lipgloss.NewStyle().Foreground(lipgloss.Color("#0000ff"))
-//
-//     // Removes the foreground color
-//     s.Foreground(lipgloss.NoColor)
+//	// Sets the foreground to blue
+//	s := lipgloss.NewStyle().Foreground(lipgloss.Color("#0000ff"))
 //
+//	// Removes the foreground color
+//	s.Foreground(lipgloss.NoColor)
 func (s Style) Foreground(c TerminalColor) Style {
 	s.set(foregroundKey, c)
 	return s
@@ -97,7 +96,7 @@ func (s Style) Width(i int) Style {
 	return s
 }
 
-// Height sets the width of the block before applying margins. If the height of
+// Height sets the height of the block before applying margins. If the height of
 // the text block is less than this value after applying padding (or not), the
 // block will be set to this height.
 func (s Style) Height(i int) Style {
@@ -105,9 +104,31 @@ func (s Style) Height(i int) Style {
 	return s
 }
 
-// Align sets a text alignment rule.
-func (s Style) Align(p Position) Style {
-	s.set(alignKey, p)
+// Align is a shorthand method for setting horizontal and vertical alignment.
+//
+// With one argument, the position value is applied to the horizontal alignment.
+//
+// With two arguments, the value is applied to the vertical and horizontal
+// alignments, in that order.
+func (s Style) Align(p ...Position) Style {
+	if len(p) > 0 {
+		s.set(alignHorizontalKey, p[0])
+	}
+	if len(p) > 1 {
+		s.set(alignVerticalKey, p[1])
+	}
+	return s
+}
+
+// AlignHorizontal sets a horizontal text alignment rule.
+func (s Style) AlignHorizontal(p Position) Style {
+	s.set(alignHorizontalKey, p)
+	return s
+}
+
+// AlignVertical sets a text alignment rule.
+func (s Style) AlignVertical(p Position) Style {
+	s.set(alignVerticalKey, p)
 	return s
 }
 
@@ -230,7 +251,7 @@ func (s Style) MarginBackground(c TerminalColor) Style {
 	return s
 }
 
-// Border is shorthand for setting a the border style and which sides should
+// Border is shorthand for setting the border style and which sides should
 // have a border at once. The variadic argument sides works as follows:
 //
 // With one value, the value is applied to all sides.
@@ -248,12 +269,11 @@ func (s Style) MarginBackground(c TerminalColor) Style {
 //
 // Examples:
 //
-//     // Applies borders to the top and bottom only
-//     lipgloss.NewStyle().Border(lipgloss.NormalBorder(), true, false)
-//
-//     // Applies rounded borders to the right and bottom only
-//     lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), false, true, true, false)
+//	// Applies borders to the top and bottom only
+//	lipgloss.NewStyle().Border(lipgloss.NormalBorder(), true, false)
 //
+//	// Applies rounded borders to the right and bottom only
+//	lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), false, true, true, false)
 func (s Style) Border(b Border, sides ...bool) Style {
 	s.set(borderStyleKey, b)
 
@@ -280,13 +300,13 @@ func (s Style) Border(b Border, sides ...bool) Style {
 // the border style, the border will be enabled for all sides during rendering.
 //
 // You can define border characters as you'd like, though several default
-// styles are included: NormalBorder(), RoundedBorder(), ThickBorder(), and
-// DoubleBorder().
+// styles are included: NormalBorder(), RoundedBorder(), BlockBorder(),
+// OuterHalfBlockBorder(), InnerHalfBlockBorder(), ThickBorder(),
+// and DoubleBorder().
 //
 // Example:
 //
-//     lipgloss.NewStyle().BorderStyle(lipgloss.ThickBorder())
-//
+//	lipgloss.NewStyle().BorderStyle(lipgloss.ThickBorder())
 func (s Style) BorderStyle(b Border) Style {
 	s.set(borderStyleKey, b)
 	return s
@@ -445,10 +465,9 @@ func (s Style) BorderLeftBackground(c TerminalColor) Style {
 //
 // Example:
 //
-//     var userInput string = "..."
-//     var userStyle = text.Style{ /* ... */ }
-//     fmt.Println(userStyle.Inline(true).Render(userInput))
-//
+//	var userInput string = "..."
+//	var userStyle = text.Style{ /* ... */ }
+//	fmt.Println(userStyle.Inline(true).Render(userInput))
 func (s Style) Inline(v bool) Style {
 	o := s.Copy()
 	o.set(inlineKey, v)
@@ -464,18 +483,17 @@ func (s Style) Inline(v bool) Style {
 //
 // Example:
 //
-//     var userInput string = "..."
-//     var userStyle = text.Style{ /* ... */ }
-//     fmt.Println(userStyle.MaxWidth(16).Render(userInput))
-//
+//	var userInput string = "..."
+//	var userStyle = text.Style{ /* ... */ }
+//	fmt.Println(userStyle.MaxWidth(16).Render(userInput))
 func (s Style) MaxWidth(n int) Style {
 	o := s.Copy()
 	o.set(maxWidthKey, n)
 	return o
 }
 
-// MaxHeight applies a max width to a given style. This is useful in enforcing
-// a certain width at render time, particularly with arbitrary strings and
+// MaxHeight applies a max height to a given style. This is useful in enforcing
+// a certain height at render time, particularly with arbitrary strings and
 // styles.
 //
 // Because this in intended to be used at the time of render, this method will
@@ -487,7 +505,7 @@ func (s Style) MaxHeight(n int) Style {
 }
 
 // UnderlineSpaces determines whether to underline spaces between words. By
-// default this is true. Spaces can also be underlined without underlining the
+// default, this is true. Spaces can also be underlined without underlining the
 // text itself.
 func (s Style) UnderlineSpaces(v bool) Style {
 	s.set(underlineSpacesKey, v)
@@ -495,13 +513,20 @@ func (s Style) UnderlineSpaces(v bool) Style {
 }
 
 // StrikethroughSpaces determines whether to apply strikethroughs to spaces
-// between words. By default this is true. Spaces can also be struck without
+// between words. By default, this is true. Spaces can also be struck without
 // underlining the text itself.
 func (s Style) StrikethroughSpaces(v bool) Style {
 	s.set(strikethroughSpacesKey, v)
 	return s
 }
 
+// Renderer sets the renderer for the style. This is useful for changing the
+// renderer for a style that is being used in a different context.
+func (s Style) Renderer(r *Renderer) Style {
+	s.r = r
+	return s
+}
+
 // whichSidesInt is a helper method for setting values on sides of a block based
 // on the number of arguments. It follows the CSS shorthand rules for blocks
 // like margin, padding. and borders. Here are how the rules work:
diff --git a/style.go b/style.go
index a3a5e4a..e94b867 100644
--- a/style.go
+++ b/style.go
@@ -26,7 +26,8 @@ const (
 	backgroundKey
 	widthKey
 	heightKey
-	alignKey
+	alignHorizontalKey
+	alignVerticalKey
 
 	// Padding.
 	paddingTopKey
@@ -74,26 +75,43 @@ const (
 // A set of properties.
 type rules map[propKey]interface{}
 
-// NewStyle returns a new, empty Style.  While it's syntactic sugar for the
+// NewStyle returns a new, empty Style. While it's syntactic sugar for the
 // Style{} primitive, it's recommended to use this function for creating styles
-// incase the underlying implementation changes.
+// in case the underlying implementation changes. It takes an optional string
+// value to be set as the underlying string value for this style.
 func NewStyle() Style {
-	return Style{}
+	return renderer.NewStyle()
+}
+
+// NewStyle returns a new, empty Style. While it's syntactic sugar for the
+// Style{} primitive, it's recommended to use this function for creating styles
+// in case the underlying implementation changes. It takes an optional string
+// value to be set as the underlying string value for this style.
+func (r *Renderer) NewStyle() Style {
+	s := Style{r: r}
+	return s
 }
 
 // Style contains a set of rules that comprise a style as a whole.
 type Style struct {
+	r     *Renderer
 	rules map[propKey]interface{}
 	value string
 }
 
+// joinString joins a list of strings into a single string separated with a
+// space.
+func joinString(strs ...string) string {
+	return strings.Join(strs, " ")
+}
+
 // SetString sets the underlying string value for this style. To render once
 // the underlying string is set, use the Style.String. This method is
 // a convenience for cases when having a stringer implementation is handy, such
 // as when using fmt.Sprintf. You can also simply define a style and render out
 // strings directly with Style.Render.
-func (s Style) SetString(str string) Style {
-	s.value = str
+func (s Style) SetString(strs ...string) Style {
+	s.value = joinString(strs...)
 	return s
 }
 
@@ -106,7 +124,7 @@ func (s Style) Value() string {
 // on the rules in this style. An underlying string value must be set with
 // Style.SetString prior to using this method.
 func (s Style) String() string {
-	return s.Render(s.value)
+	return s.Render()
 }
 
 // Copy returns a copy of this style, including any underlying string values.
@@ -116,13 +134,14 @@ func (s Style) Copy() Style {
 	for k, v := range s.rules {
 		o.rules[k] = v
 	}
+	o.r = s.r
 	o.value = s.value
 	return o
 }
 
-// Inherit takes values from the style in the argument applies them to this
-// style, overwriting existing definitions. Only values explicitly set on the
-// style in argument will be applied.
+// Inherit overlays the style in the argument onto this style by copying each explicitly
+// set value from the argument style onto this style if it is not already explicitly set.
+// Existing set values are kept intact and not overwritten.
 //
 // Margins, padding, and underlying string values are not inherited.
 func (s Style) Inherit(i Style) Style {
@@ -137,8 +156,6 @@ func (s Style) Inherit(i Style) Style {
 			// Padding is not inherited
 			continue
 		case backgroundKey:
-			s.rules[k] = v
-
 			// The margins also inherit the background color
 			if !s.isSet(marginBackgroundKey) && !i.isSet(marginBackgroundKey) {
 				s.rules[marginBackgroundKey] = v
@@ -154,11 +171,20 @@ func (s Style) Inherit(i Style) Style {
 }
 
 // Render applies the defined style formatting to a given string.
-func (s Style) Render(str string) string {
+func (s Style) Render(strs ...string) string {
+	if s.r == nil {
+		s.r = renderer
+	}
+	if s.value != "" {
+		strs = append([]string{s.value}, strs...)
+	}
+
 	var (
-		te           termenv.Style
-		teSpace      termenv.Style
-		teWhitespace termenv.Style
+		str = joinString(strs...)
+
+		te           = s.r.ColorProfile().String()
+		teSpace      = s.r.ColorProfile().String()
+		teWhitespace = s.r.ColorProfile().String()
 
 		bold          = s.getAsBool(boldKey, false)
 		italic        = s.getAsBool(italicKey, false)
@@ -171,9 +197,10 @@ func (s Style) Render(str string) string {
 		fg = s.getAsColor(foregroundKey)
 		bg = s.getAsColor(backgroundKey)
 
-		width  = s.getAsInt(widthKey)
-		height = s.getAsInt(heightKey)
-		align  = s.getAsPosition(alignKey)
+		width           = s.getAsInt(widthKey)
+		height          = s.getAsInt(heightKey)
+		horizontalAlign = s.getAsPosition(alignHorizontalKey)
+		verticalAlign   = s.getAsPosition(alignVerticalKey)
 
 		topPadding    = s.getAsInt(paddingTopKey)
 		rightPadding  = s.getAsInt(paddingRightKey)
@@ -227,24 +254,22 @@ func (s Style) Render(str string) string {
 	}
 
 	if fg != noColor {
-		fgc := fg.color()
-		te = te.Foreground(fgc)
+		te = te.Foreground(fg.color(s.r))
 		if styleWhitespace {
-			teWhitespace = teWhitespace.Foreground(fgc)
+			teWhitespace = teWhitespace.Foreground(fg.color(s.r))
 		}
 		if useSpaceStyler {
-			teSpace = teSpace.Foreground(fgc)
+			teSpace = teSpace.Foreground(fg.color(s.r))
 		}
 	}
 
 	if bg != noColor {
-		bgc := bg.color()
-		te = te.Background(bgc)
+		te = te.Background(bg.color(s.r))
 		if colorWhitespace {
-			teWhitespace = teWhitespace.Background(bgc)
+			teWhitespace = teWhitespace.Background(bg.color(s.r))
 		}
 		if useSpaceStyler {
-			teSpace = teSpace.Background(bgc)
+			teSpace = teSpace.Background(bg.color(s.r))
 		}
 	}
 
@@ -264,7 +289,7 @@ func (s Style) Render(str string) string {
 
 	// Strip newlines in single line mode
 	if inline {
-		str = strings.Replace(str, "\n", "", -1)
+		str = strings.ReplaceAll(str, "\n", "")
 	}
 
 	// Word wrap
@@ -329,10 +354,7 @@ func (s Style) Render(str string) string {
 
 	// Height
 	if height > 0 {
-		h := strings.Count(str, "\n") + 1
-		if height > h {
-			str += strings.Repeat("\n", height-h)
-		}
+		str = alignTextVertical(str, verticalAlign, height, nil)
 	}
 
 	// Set alignment. This will also pad short lines with spaces so that all
@@ -346,7 +368,7 @@ func (s Style) Render(str string) string {
 			if colorWhitespace || styleWhitespace {
 				st = &teWhitespace
 			}
-			str = alignText(str, align, width, st)
+			str = alignTextHorizontal(str, horizontalAlign, width, st)
 		}
 	}
 
@@ -387,7 +409,7 @@ func (s Style) applyMargins(str string, inline bool) string {
 
 	bgc := s.getAsColor(marginBackgroundKey)
 	if bgc != noColor {
-		styler = styler.Background(bgc.color())
+		styler = styler.Background(bgc.color(s.r))
 	}
 
 	// Add left and right margin
@@ -435,7 +457,7 @@ func padLeft(str string, n int, style *termenv.Style) string {
 	return b.String()
 }
 
-// Apply right right padding.
+// Apply right padding.
 func padRight(str string, n int, style *termenv.Style) string {
 	if n == 0 || str == "" {
 		return str
diff --git a/style_test.go b/style_test.go
new file mode 100644
index 0000000..5e1eb73
--- /dev/null
+++ b/style_test.go
@@ -0,0 +1,390 @@
+package lipgloss
+
+import (
+	"io/ioutil"
+	"reflect"
+	"testing"
+
+	"github.com/muesli/termenv"
+)
+
+func TestStyleRender(t *testing.T) {
+	renderer.SetColorProfile(termenv.TrueColor)
+	renderer.SetHasDarkBackground(true)
+	t.Parallel()
+
+	tt := []struct {
+		style    Style
+		expected string
+	}{
+		{
+			NewStyle().Foreground(Color("#5A56E0")),
+			"\x1b[38;2;89;86;224mhello\x1b[0m",
+		},
+		{
+			NewStyle().Foreground(AdaptiveColor{Light: "#fffe12", Dark: "#5A56E0"}),
+			"\x1b[38;2;89;86;224mhello\x1b[0m",
+		},
+		{
+			NewStyle().Bold(true),
+			"\x1b[1mhello\x1b[0m",
+		},
+		{
+			NewStyle().Italic(true),
+			"\x1b[3mhello\x1b[0m",
+		},
+		{
+			NewStyle().Underline(true),
+			"\x1b[4;4mh\x1b[0m\x1b[4;4me\x1b[0m\x1b[4;4ml\x1b[0m\x1b[4;4ml\x1b[0m\x1b[4;4mo\x1b[0m",
+		},
+		{
+			NewStyle().Blink(true),
+			"\x1b[5mhello\x1b[0m",
+		},
+		{
+			NewStyle().Faint(true),
+			"\x1b[2mhello\x1b[0m",
+		},
+	}
+
+	for i, tc := range tt {
+		s := tc.style.Copy().SetString("hello")
+		res := s.Render()
+		if res != tc.expected {
+			t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n",
+				i, tc.expected, formatEscapes(tc.expected),
+				res, formatEscapes(res))
+		}
+	}
+}
+
+func TestStyleCustomRender(t *testing.T) {
+	r := NewRenderer(ioutil.Discard)
+	r.SetHasDarkBackground(false)
+	r.SetColorProfile(termenv.TrueColor)
+	tt := []struct {
+		style    Style
+		expected string
+	}{
+		{
+			r.NewStyle().Foreground(Color("#5A56E0")),
+			"\x1b[38;2;89;86;224mhello\x1b[0m",
+		},
+		{
+			r.NewStyle().Foreground(AdaptiveColor{Light: "#fffe12", Dark: "#5A56E0"}),
+			"\x1b[38;2;255;254;18mhello\x1b[0m",
+		},
+		{
+			r.NewStyle().Bold(true),
+			"\x1b[1mhello\x1b[0m",
+		},
+		{
+			r.NewStyle().Italic(true),
+			"\x1b[3mhello\x1b[0m",
+		},
+		{
+			r.NewStyle().Underline(true),
+			"\x1b[4;4mh\x1b[0m\x1b[4;4me\x1b[0m\x1b[4;4ml\x1b[0m\x1b[4;4ml\x1b[0m\x1b[4;4mo\x1b[0m",
+		},
+		{
+			r.NewStyle().Blink(true),
+			"\x1b[5mhello\x1b[0m",
+		},
+		{
+			r.NewStyle().Faint(true),
+			"\x1b[2mhello\x1b[0m",
+		},
+		{
+			NewStyle().Faint(true).Renderer(r),
+			"\x1b[2mhello\x1b[0m",
+		},
+	}
+
+	for i, tc := range tt {
+		s := tc.style.Copy().SetString("hello")
+		res := s.Render()
+		if res != tc.expected {
+			t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n",
+				i, tc.expected, formatEscapes(tc.expected),
+				res, formatEscapes(res))
+		}
+	}
+}
+
+func TestStyleRenderer(t *testing.T) {
+	r := NewRenderer(ioutil.Discard)
+	s1 := NewStyle().Bold(true)
+	s2 := s1.Renderer(r)
+	if s1.r == s2.r {
+		t.Fatalf("expected different renderers")
+	}
+}
+
+func TestValueCopy(t *testing.T) {
+	t.Parallel()
+
+	s := NewStyle().
+		Bold(true)
+
+	i := s
+	i.Bold(false)
+
+	requireEqual(t, s.GetBold(), i.GetBold())
+}
+
+func TestStyleInherit(t *testing.T) {
+	t.Parallel()
+
+	s := NewStyle().
+		Bold(true).
+		Italic(true).
+		Underline(true).
+		Strikethrough(true).
+		Blink(true).
+		Faint(true).
+		Foreground(Color("#ffffff")).
+		Background(Color("#111111")).
+		Margin(1, 1, 1, 1).
+		Padding(1, 1, 1, 1)
+
+	i := NewStyle().Inherit(s)
+
+	requireEqual(t, s.GetBold(), i.GetBold())
+	requireEqual(t, s.GetItalic(), i.GetItalic())
+	requireEqual(t, s.GetUnderline(), i.GetUnderline())
+	requireEqual(t, s.GetStrikethrough(), i.GetStrikethrough())
+	requireEqual(t, s.GetBlink(), i.GetBlink())
+	requireEqual(t, s.GetFaint(), i.GetFaint())
+	requireEqual(t, s.GetForeground(), i.GetForeground())
+	requireEqual(t, s.GetBackground(), i.GetBackground())
+
+	requireNotEqual(t, s.GetMarginLeft(), i.GetMarginLeft())
+	requireNotEqual(t, s.GetMarginRight(), i.GetMarginRight())
+	requireNotEqual(t, s.GetMarginTop(), i.GetMarginTop())
+	requireNotEqual(t, s.GetMarginBottom(), i.GetMarginBottom())
+	requireNotEqual(t, s.GetPaddingLeft(), i.GetPaddingLeft())
+	requireNotEqual(t, s.GetPaddingRight(), i.GetPaddingRight())
+	requireNotEqual(t, s.GetPaddingTop(), i.GetPaddingTop())
+	requireNotEqual(t, s.GetPaddingBottom(), i.GetPaddingBottom())
+}
+
+func TestStyleCopy(t *testing.T) {
+	t.Parallel()
+
+	s := NewStyle().
+		Bold(true).
+		Italic(true).
+		Underline(true).
+		Strikethrough(true).
+		Blink(true).
+		Faint(true).
+		Foreground(Color("#ffffff")).
+		Background(Color("#111111")).
+		Margin(1, 1, 1, 1).
+		Padding(1, 1, 1, 1)
+
+	i := s.Copy()
+
+	requireEqual(t, s.GetBold(), i.GetBold())
+	requireEqual(t, s.GetItalic(), i.GetItalic())
+	requireEqual(t, s.GetUnderline(), i.GetUnderline())
+	requireEqual(t, s.GetStrikethrough(), i.GetStrikethrough())
+	requireEqual(t, s.GetBlink(), i.GetBlink())
+	requireEqual(t, s.GetFaint(), i.GetFaint())
+	requireEqual(t, s.GetForeground(), i.GetForeground())
+	requireEqual(t, s.GetBackground(), i.GetBackground())
+
+	requireEqual(t, s.GetMarginLeft(), i.GetMarginLeft())
+	requireEqual(t, s.GetMarginRight(), i.GetMarginRight())
+	requireEqual(t, s.GetMarginTop(), i.GetMarginTop())
+	requireEqual(t, s.GetMarginBottom(), i.GetMarginBottom())
+	requireEqual(t, s.GetPaddingLeft(), i.GetPaddingLeft())
+	requireEqual(t, s.GetPaddingRight(), i.GetPaddingRight())
+	requireEqual(t, s.GetPaddingTop(), i.GetPaddingTop())
+	requireEqual(t, s.GetPaddingBottom(), i.GetPaddingBottom())
+}
+
+func TestStyleUnset(t *testing.T) {
+	t.Parallel()
+
+	s := NewStyle().Bold(true)
+	requireTrue(t, s.GetBold())
+	s.UnsetBold()
+	requireFalse(t, s.GetBold())
+
+	s = NewStyle().Italic(true)
+	requireTrue(t, s.GetItalic())
+	s.UnsetItalic()
+	requireFalse(t, s.GetItalic())
+
+	s = NewStyle().Underline(true)
+	requireTrue(t, s.GetUnderline())
+	s.UnsetUnderline()
+	requireFalse(t, s.GetUnderline())
+
+	s = NewStyle().Strikethrough(true)
+	requireTrue(t, s.GetStrikethrough())
+	s.UnsetStrikethrough()
+	requireFalse(t, s.GetStrikethrough())
+
+	s = NewStyle().Reverse(true)
+	requireTrue(t, s.GetReverse())
+	s.UnsetReverse()
+	requireFalse(t, s.GetReverse())
+
+	s = NewStyle().Blink(true)
+	requireTrue(t, s.GetBlink())
+	s.UnsetBlink()
+	requireFalse(t, s.GetBlink())
+
+	s = NewStyle().Faint(true)
+	requireTrue(t, s.GetFaint())
+	s.UnsetFaint()
+	requireFalse(t, s.GetFaint())
+
+	s = NewStyle().Inline(true)
+	requireTrue(t, s.GetInline())
+	s.UnsetInline()
+	requireFalse(t, s.GetInline())
+
+	// colors
+	col := Color("#ffffff")
+	s = NewStyle().Foreground(col)
+	requireEqual(t, col, s.GetForeground())
+	s.UnsetForeground()
+	requireNotEqual(t, col, s.GetForeground())
+
+	s = NewStyle().Background(col)
+	requireEqual(t, col, s.GetBackground())
+	s.UnsetBackground()
+	requireNotEqual(t, col, s.GetBackground())
+
+	// margins
+	s = NewStyle().Margin(1, 2, 3, 4)
+	requireEqual(t, 1, s.GetMarginTop())
+	s.UnsetMarginTop()
+	requireEqual(t, 0, s.GetMarginTop())
+
+	requireEqual(t, 2, s.GetMarginRight())
+	s.UnsetMarginRight()
+	requireEqual(t, 0, s.GetMarginRight())
+
+	requireEqual(t, 3, s.GetMarginBottom())
+	s.UnsetMarginBottom()
+	requireEqual(t, 0, s.GetMarginBottom())
+
+	requireEqual(t, 4, s.GetMarginLeft())
+	s.UnsetMarginLeft()
+	requireEqual(t, 0, s.GetMarginLeft())
+
+	// padding
+	s = NewStyle().Padding(1, 2, 3, 4)
+	requireEqual(t, 1, s.GetPaddingTop())
+	s.UnsetPaddingTop()
+	requireEqual(t, 0, s.GetPaddingTop())
+
+	requireEqual(t, 2, s.GetPaddingRight())
+	s.UnsetPaddingRight()
+	requireEqual(t, 0, s.GetPaddingRight())
+
+	requireEqual(t, 3, s.GetPaddingBottom())
+	s.UnsetPaddingBottom()
+	requireEqual(t, 0, s.GetPaddingBottom())
+
+	requireEqual(t, 4, s.GetPaddingLeft())
+	s.UnsetPaddingLeft()
+	requireEqual(t, 0, s.GetPaddingLeft())
+
+	// border
+	s = NewStyle().Border(normalBorder, true, true, true, true)
+	requireTrue(t, s.GetBorderTop())
+	s.UnsetBorderTop()
+	requireFalse(t, s.GetBorderTop())
+
+	requireTrue(t, s.GetBorderRight())
+	s.UnsetBorderRight()
+	requireFalse(t, s.GetBorderRight())
+
+	requireTrue(t, s.GetBorderBottom())
+	s.UnsetBorderBottom()
+	requireFalse(t, s.GetBorderBottom())
+
+	requireTrue(t, s.GetBorderLeft())
+	s.UnsetBorderLeft()
+	requireFalse(t, s.GetBorderLeft())
+}
+
+func TestStyleValue(t *testing.T) {
+	t.Parallel()
+
+	tt := []struct {
+		name     string
+		style    Style
+		expected string
+	}{
+		{
+			name:     "empty",
+			style:    NewStyle(),
+			expected: "foo",
+		},
+		{
+			name:     "set string",
+			style:    NewStyle().SetString("bar"),
+			expected: "bar foo",
+		},
+		{
+			name:     "set string with bold",
+			style:    NewStyle().SetString("bar").Bold(true),
+			expected: "\x1b[1mbar foo\x1b[0m",
+		},
+		{
+			name:     "new style with string",
+			style:    NewStyle().SetString("bar", "foobar"),
+			expected: "bar foobar foo",
+		},
+	}
+
+	for i, tc := range tt {
+		res := tc.style.Render("foo")
+		if res != tc.expected {
+			t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n",
+				i, tc.expected, formatEscapes(tc.expected),
+				res, formatEscapes(res))
+		}
+	}
+
+}
+
+func BenchmarkStyleRender(b *testing.B) {
+	s := NewStyle().
+		Bold(true).
+		Foreground(Color("#ffffff"))
+
+	for i := 0; i < b.N; i++ {
+		s.Render("Hello world")
+	}
+}
+
+func requireTrue(tb testing.TB, b bool) {
+	requireEqual(tb, true, b)
+}
+
+func requireFalse(tb testing.TB, b bool) {
+	requireEqual(tb, false, b)
+}
+
+func requireEqual(tb testing.TB, a, b interface{}) {
+	tb.Helper()
+	if !reflect.DeepEqual(a, b) {
+		tb.Errorf("%v != %v", a, b)
+		tb.FailNow()
+	}
+}
+
+func requireNotEqual(tb testing.TB, a, b interface{}) {
+	tb.Helper()
+	if reflect.DeepEqual(a, b) {
+		tb.Errorf("%v == %v", a, b)
+		tb.FailNow()
+	}
+}
diff --git a/unset.go b/unset.go
index 0a03720..a836789 100644
--- a/unset.go
+++ b/unset.go
@@ -66,9 +66,22 @@ func (s Style) UnsetHeight() Style {
 	return s
 }
 
-// UnsetAlign removes the text alignment style rule, if set.
+// UnsetAlign removes the horizontal and vertical text alignment style rule, if set.
 func (s Style) UnsetAlign() Style {
-	delete(s.rules, alignKey)
+	delete(s.rules, alignHorizontalKey)
+	delete(s.rules, alignVerticalKey)
+	return s
+}
+
+// UnsetAlignHorizontal removes the horizontal text alignment style rule, if set.
+func (s Style) UnsetAlignHorizontal() Style {
+	delete(s.rules, alignHorizontalKey)
+	return s
+}
+
+// UnsetAlignVertical removes the vertical text alignment style rule, if set.
+func (s Style) UnsetAlignVertical() Style {
+	delete(s.rules, alignVerticalKey)
 	return s
 }
 
diff --git a/whitespace.go b/whitespace.go
index 6c510a7..b043e56 100644
--- a/whitespace.go
+++ b/whitespace.go
@@ -9,10 +9,25 @@ import (
 
 // whitespace is a whitespace renderer.
 type whitespace struct {
+	re    *Renderer
 	style termenv.Style
 	chars string
 }
 
+// newWhitespace creates a new whitespace renderer. The order of the options
+// matters, it you'r using WithWhitespaceRenderer, make sure it comes first as
+// other options might depend on it.
+func newWhitespace(r *Renderer, opts ...WhitespaceOption) *whitespace {
+	w := &whitespace{
+		re:    r,
+		style: r.ColorProfile().String(),
+	}
+	for _, opt := range opts {
+		opt(w)
+	}
+	return w
+}
+
 // Render whitespaces.
 func (w whitespace) render(width int) string {
 	if w.chars == "" {
@@ -49,14 +64,14 @@ type WhitespaceOption func(*whitespace)
 // WithWhitespaceForeground sets the color of the characters in the whitespace.
 func WithWhitespaceForeground(c TerminalColor) WhitespaceOption {
 	return func(w *whitespace) {
-		w.style = w.style.Foreground(c.color())
+		w.style = w.style.Foreground(c.color(w.re))
 	}
 }
 
 // WithWhitespaceBackground sets the background color of the whitespace.
 func WithWhitespaceBackground(c TerminalColor) WhitespaceOption {
 	return func(w *whitespace) {
-		w.style = w.style.Background(c.color())
+		w.style = w.style.Background(c.color(w.re))
 	}
 }
 

More details

Full run details

Historical runs