Repository: charmbracelet/bubbletea
Pull request #1246 (2024-11-21 17:24:08)

(v2) x/ansi and x/cellbuf changes


Patch:
diff --git a/da1.go b/da1.go
index ff550e8..08944d2 100644
--- a/da1.go
+++ b/da1.go
@@ -9,11 +9,10 @@ type PrimaryDeviceAttributesMsg []int
 func parsePrimaryDevAttrs(csi *ansi.CsiSequence) Msg {
 	// Primary Device Attributes
 	da1 := make(PrimaryDeviceAttributesMsg, len(csi.Params))
-	csi.Range(func(i int, p int, hasMore bool) bool {
-		if !hasMore {
-			da1[i] = p
+	for i, p := range csi.Params {
+		if !p.HasMore() {
+			da1[i] = p.Param(0)
 		}
-		return true
-	})
+	}
 	return da1
 }
diff --git a/examples/go.mod b/examples/go.mod
index 2c637f7..ba648dd 100644
--- a/examples/go.mod
+++ b/examples/go.mod
@@ -3,13 +3,13 @@ module examples
 go 1.23.1
 
 require (
-	github.com/charmbracelet/bubbles/v2 v2.0.0-alpha.2
-	github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2
-	github.com/charmbracelet/colorprofile v0.1.6
+	github.com/charmbracelet/bubbles/v2 v2.0.0-alpha.2.0.20241121172047-bd415b4ebae8
+	github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20241121171714-fbd5423ea935
+	github.com/charmbracelet/colorprofile v0.1.8
 	github.com/charmbracelet/glamour v0.8.0
 	github.com/charmbracelet/harmonica v0.2.0
-	github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2
-	github.com/charmbracelet/x/ansi v0.4.3
+	github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20241121164047-8448a9be4804
+	github.com/charmbracelet/x/ansi v0.5.1
 	github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20241016014612-3b4d04043233
 	github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776
 	github.com/lucasb-eyer/go-colorful v1.2.0
@@ -23,15 +23,14 @@ require (
 	github.com/aymanbagabas/go-udiff v0.2.0 // indirect
 	github.com/aymerick/douceur v0.2.0 // indirect
 	github.com/charmbracelet/lipgloss v0.13.0 // indirect
-	github.com/charmbracelet/x/cellbuf v0.0.3 // indirect
+	github.com/charmbracelet/x/cellbuf v0.0.6 // indirect
 	github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 // indirect
-	github.com/charmbracelet/x/input v0.2.0 // indirect
-	github.com/charmbracelet/x/term v0.2.0 // indirect
-	github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91 // indirect
+	github.com/charmbracelet/x/term v0.2.1 // indirect
+	github.com/charmbracelet/x/vt v0.0.0-20241121165045-a3720547cbb4 // indirect
+	github.com/charmbracelet/x/wcwidth v0.0.0-20241113152101-0af7d04e9f32 // indirect
 	github.com/charmbracelet/x/windows v0.2.0 // indirect
 	github.com/dlclark/regexp2 v1.11.0 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
-	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
 	github.com/gorilla/css v1.0.1 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/microcosm-cc/bluemonday v1.0.27 // indirect
@@ -47,7 +46,7 @@ require (
 	golang.org/x/sync v0.9.0 // indirect
 	golang.org/x/sys v0.27.0 // indirect
 	golang.org/x/term v0.22.0 // indirect
-	golang.org/x/text v0.19.0 // indirect
+	golang.org/x/text v0.20.0 // indirect
 )
 
 replace github.com/charmbracelet/bubbletea/v2 => ../
diff --git a/examples/go.sum b/examples/go.sum
index 99538f1..1271155 100644
--- a/examples/go.sum
+++ b/examples/go.sum
@@ -14,44 +14,38 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp
 github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
-github.com/charmbracelet/bubbles/v2 v2.0.0-alpha.1.0.20241031195835-cc9a7270283d h1:iXR9lgmZhwy9yrrCmmDv++akjSbZJs4Owzlbfoc9OrQ=
-github.com/charmbracelet/bubbles/v2 v2.0.0-alpha.1.0.20241031195835-cc9a7270283d/go.mod h1:m1OsgLhZIiKx3O0R51XwnXwwm9vP/LFa8IBUMQiiZmw=
-github.com/charmbracelet/bubbles/v2 v2.0.0-alpha.2 h1:Oevn3XNNcccbI8m6cOI6rAMsY1niKsDMv55qtejWRXE=
-github.com/charmbracelet/bubbles/v2 v2.0.0-alpha.2/go.mod h1:BWGE1i9NQA60C720gn2FYOyRyJp2BVtQNVfai7wcMoM=
-github.com/charmbracelet/colorprofile v0.1.6 h1:nMMqCns0c0DfCwNGdagBh6SxutFqkltSxxKk5S9kt+Y=
-github.com/charmbracelet/colorprofile v0.1.6/go.mod h1:3EMXDxwRDJl0c17eJ1jX99MhtlP9OxE/9Qw0C5lvyUg=
+github.com/charmbracelet/bubbles/v2 v2.0.0-alpha.2.0.20241121172047-bd415b4ebae8 h1:CRhvWh0cIainbY47znHAxzohyXDNmcmrp9ggjvn1cJk=
+github.com/charmbracelet/bubbles/v2 v2.0.0-alpha.2.0.20241121172047-bd415b4ebae8/go.mod h1:fKcC1zxdgRjgg21XbRIf/bkSELpd9D9XKlHRCrhR1Tk=
+github.com/charmbracelet/colorprofile v0.1.8 h1:PywDeXsiAzlPtkiiKgMEVLvb6nlEuKrMj9+FJBtj4jU=
+github.com/charmbracelet/colorprofile v0.1.8/go.mod h1:+jpmObxZl1Dab3H3IMVIPSZTsKcFpjJUv97G0dLqM60=
 github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs=
 github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw=
 github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
 github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
 github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
 github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
-github.com/charmbracelet/lipgloss/v2 v2.0.0-20241029194924-049a2d260c67 h1:t/4KovO/31pdh9e74EVGVSbIlcY3bG4lmtrL3pD4fgU=
-github.com/charmbracelet/lipgloss/v2 v2.0.0-20241029194924-049a2d260c67/go.mod h1:EcAf9+4/UeilG4rNQmiRzrou258LAVjVK4YUh+BvfqQ=
-github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2 h1:Gp+S9hMymU6HmxD1dihbnoMOGwt6wDMMvf0jyw3gEc0=
-github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2/go.mod h1:72/7KVsLdRldv/CeBjZx6igXIZ9CFtBzQUmDEbhXZ3w=
-github.com/charmbracelet/x/ansi v0.4.3 h1:wcdDrW0ejaaZGJxCyxVNzzmctqV+oARIudaFGQvsRkA=
-github.com/charmbracelet/x/ansi v0.4.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
-github.com/charmbracelet/x/cellbuf v0.0.3 h1:HapUUjlo0pZ7iGijrTer1f4X8Uvq17l0zR+80Oh+iJg=
-github.com/charmbracelet/x/cellbuf v0.0.3/go.mod h1:SF8R3AqchNzYKKJCFT7co8wt1HgQDfAitQ+SBoxWLNc=
+github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20241121164047-8448a9be4804 h1:7CYjb9YMZA4kMhLgGdtlXvq+nu1oyENpMyMQlTvqSFw=
+github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20241121164047-8448a9be4804/go.mod h1:F/6E/LGdH3eHCJf2rG8/O3CjlW8cZFL5YJCknJs1GkI=
+github.com/charmbracelet/x/ansi v0.5.1 h1:+mg6abP9skvsu/JQZrIJ9Z/4O1YDnLVkpfutar3dUnc=
+github.com/charmbracelet/x/ansi v0.5.1/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
+github.com/charmbracelet/x/cellbuf v0.0.6 h1:pJUWN/G1jbt1Nj/+ILfC2/ABQoZzWu1vG73yHQEYELI=
+github.com/charmbracelet/x/cellbuf v0.0.6/go.mod h1:d72o71glp8flkCz54PHLe3+nuw5u2v3UxmKqruUERWQ=
 github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
 github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
 github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20241016014612-3b4d04043233 h1:2bTR/MtnJuq9RrCZSPwCOO34YSDByKL6nzXQMnsKK6U=
 github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20241016014612-3b4d04043233/go.mod h1:cw9df32BXdkcd0LzAHsFMmvXOsrrlDKazIW8PCq0cPM=
-github.com/charmbracelet/x/input v0.2.0 h1:1Sv+y/flcqUfUH2PXNIDKDIdT2G8smOnGOgawqhwy8A=
-github.com/charmbracelet/x/input v0.2.0/go.mod h1:KUSFIS6uQymtnr5lHVSOK9j8RvwTD4YHnWnzJUYnd/M=
-github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
-github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
-github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91 h1:D5OO0lVavz7A+Swdhp62F9gbkibxmz9B2hZ/jVdMPf0=
-github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91/go.mod h1:Ey8PFmYwH+/td9bpiEx07Fdx9ZVkxfIjWXxBluxF4Nw=
+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
+github.com/charmbracelet/x/vt v0.0.0-20241121165045-a3720547cbb4 h1:EacjHxcQEEgOZ7TbkAU3b84hd1Bn5NwA8YV5uyJ9EI4=
+github.com/charmbracelet/x/vt v0.0.0-20241121165045-a3720547cbb4/go.mod h1:1/jFoHl7/I4br0StC9OXXEondkK9qi3nUtKoqI35HcI=
+github.com/charmbracelet/x/wcwidth v0.0.0-20241113152101-0af7d04e9f32 h1:14czE6R5CgOlvONsJYa2B1uTyLvXzGXpBqw2AyZeTh4=
+github.com/charmbracelet/x/wcwidth v0.0.0-20241113152101-0af7d04e9f32/go.mod h1:hyua5CY63kyl7IfyIxv1SjVEqoKze/XmDkEglItuVjA=
 github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw=
 github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s=
 github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
 github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
-github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
-github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
 github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776 h1:VRIbnDWRmAh5yBdz+J6yFMF5vso1It6vn+WmM/5l7MA=
 github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776/go.mod h1:9wvnDu3YOfxzWM9Cst40msBF1C2UdQgDv962oTxSuMs=
 github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
@@ -94,11 +88,10 @@ golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
 golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
 golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
 golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
 golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
 golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
-golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
-golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
+golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
diff --git a/examples/list-fancy/main.go b/examples/list-fancy/main.go
index 94127bf..96f7fb3 100644
--- a/examples/list-fancy/main.go
+++ b/examples/list-fancy/main.go
@@ -28,7 +28,7 @@ func newStyles(darkBG bool) styles {
 			Background(lipgloss.Color("#25A065")).
 			Padding(0, 1),
 		statusMessage: lipgloss.NewStyle().
-			Foreground(lightDark("#04B575", "#04B575")),
+			Foreground(lightDark(lipgloss.Color("#04B575"), lipgloss.Color("#04B575"))),
 	}
 }
 
diff --git a/examples/paginator/main.go b/examples/paginator/main.go
index 8db3c9a..83c234e 100644
--- a/examples/paginator/main.go
+++ b/examples/paginator/main.go
@@ -22,8 +22,8 @@ type styles struct {
 func newStyles(bgIsDark bool) (s styles) {
 	lightDark := lipgloss.LightDark(bgIsDark)
 
-	s.activeDot = lipgloss.NewStyle().Foreground(lightDark("235", "252")).SetString("•")
-	s.inactiveDot = s.activeDot.Foreground(lightDark("250", "238")).SetString("•")
+	s.activeDot = lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("235"), lipgloss.Color("252"))).SetString("•")
+	s.inactiveDot = s.activeDot.Foreground(lightDark(lipgloss.Color("250"), lipgloss.Color("238"))).SetString("•")
 	return s
 }
 
diff --git a/examples/tabs/main.go b/examples/tabs/main.go
index 0010ca4..f159cf8 100644
--- a/examples/tabs/main.go
+++ b/examples/tabs/main.go
@@ -22,7 +22,7 @@ func newStyles(bgIsDark bool) *styles {
 
 	inactiveTabBorder := tabBorderWithBottom("┴", "─", "┴")
 	activeTabBorder := tabBorderWithBottom("┘", " ", "└")
-	highlightColor := lightDark("#874BFD", "#7D56F4")
+	highlightColor := lightDark(lipgloss.Color("#874BFD"), lipgloss.Color("#7D56F4"))
 
 	s := new(styles)
 	s.doc = lipgloss.NewStyle().
diff --git a/ferocious_renderer.go b/ferocious_renderer.go
index 3dded25..9bdc1e6 100644
--- a/ferocious_renderer.go
+++ b/ferocious_renderer.go
@@ -10,6 +10,7 @@ import (
 	"github.com/charmbracelet/colorprofile"
 	"github.com/charmbracelet/x/ansi"
 	"github.com/charmbracelet/x/cellbuf"
+	"github.com/charmbracelet/x/vt"
 )
 
 var undefPoint = image.Pt(-1, -1)
@@ -22,10 +23,10 @@ type cursor struct {
 
 // screen represents a terminal screen.
 type screen struct {
-	dirty          map[int]int // keeps track of dirty cells
-	linew          []int       // keeps track of the width of each line
-	cellbuf.Buffer             // the cell buffer
-	cur            cursor      // cursor state
+	dirty      map[int]int // keeps track of dirty cells
+	linew      []int       // keeps track of the width of each line
+	cur        cursor      // cursor state
+	*vt.Buffer             // the cell buffer
 }
 
 // isDirty returns true if the cell at the given position is dirty.
@@ -37,19 +38,15 @@ func (s *screen) isDirty(x, y int) bool {
 
 // reset resets the screen to its initial state.
 func (s *screen) reset() {
-	s.Buffer = cellbuf.Buffer{}
+	s.Buffer = vt.NewBuffer(0, 0)
 	s.dirty = make(map[int]int)
 	s.cur = cursor{}
 	s.linew = make([]int, 0)
 }
 
 // Set implements [cellbuf.Grid] and marks changed cells as dirty.
-func (s *screen) SetCell(x, y int, cell cellbuf.Cell) (v bool) {
-	c, ok := s.Cell(x, y)
-	if !ok {
-		return
-	}
-
+func (s *screen) SetCell(x, y int, cell *cellbuf.Cell) (v bool) {
+	c := s.Cell(x, y)
 	if c.Equal(cell) {
 		// Cells are the same, no need to update.
 		return
@@ -128,7 +125,11 @@ func (c *ferociousRenderer) close() error {
 		seq += "\r"
 		c.scr.cur.X = 0
 	}
-	if _, line := cellbuf.RenderLineWithProfile(c.scr, y, c.profile); line != "" {
+
+	if _, line := cellbuf.RenderLine(
+		c.scr, y,
+		cellbuf.WithRenderProfile(c.profile),
+	); line != "" {
 		// OPTIM: We only clear the line if there's content on it.
 		seq += ansi.EraseEntireLine
 	}
@@ -226,7 +227,7 @@ func (c *ferociousRenderer) render(s string) {
 	// Ensure the buffer is at least the height of the new frame.
 	height := cellbuf.Height(s)
 	c.scr.Resize(c.scr.Width(), height)
-	linew := cellbuf.SetContent(c.scr, c.method, s)
+	linew := cellbuf.Paint(c.scr, c.method, s, nil)
 	c.scr.linew = linew
 }
 
@@ -308,8 +309,8 @@ func (c *ferociousRenderer) update(msg Msg) {
 		}
 
 	case enableModeMsg:
-		switch string(msg) {
-		case ansi.AltScreenBufferMode.String():
+		switch ansi.DECMode(msg) {
+		case ansi.AltScreenSaveCursorMode:
 			if c.altScreen {
 				return
 			}
@@ -326,7 +327,7 @@ func (c *ferociousRenderer) update(msg Msg) {
 			// whenever we enter or leave AltScreen.
 			c.updateCursorVisibility()
 
-		case ansi.CursorEnableMode.String():
+		case ansi.TextCursorEnableMode:
 			if !c.cursorHidden {
 				return
 			}
@@ -335,8 +336,8 @@ func (c *ferociousRenderer) update(msg Msg) {
 		}
 
 	case disableModeMsg:
-		switch string(msg) {
-		case ansi.AltScreenBufferMode.String():
+		switch ansi.DECMode(msg) {
+		case ansi.AltScreenSaveCursorMode:
 			if !c.altScreen {
 				return
 			}
@@ -349,7 +350,7 @@ func (c *ferociousRenderer) update(msg Msg) {
 			// whenever we enter or leave AltScreen.
 			c.updateCursorVisibility()
 
-		case ansi.CursorEnableMode.String():
+		case ansi.TextCursorEnableMode:
 			if c.cursorHidden {
 				return
 			}
@@ -362,7 +363,7 @@ func (c *ferociousRenderer) update(msg Msg) {
 	}
 }
 
-var spaceCell = cellbuf.Cell{Content: " ", Width: 1}
+var spaceCell = &cellbuf.Cell{Content: " ", Width: 1}
 
 // changes commits the changes from the cell buffer using the dirty cells map
 // and writes them to the internal buffer.
@@ -379,7 +380,7 @@ func (c *ferociousRenderer) changes() {
 		var x int
 		for y := 0; y < height; y++ {
 			var line string
-			x, line = cellbuf.RenderLineWithProfile(c.scr, y, c.profile)
+			x, line = cellbuf.RenderLine(c.scr, y, cellbuf.WithRenderProfile(c.profile))
 			c.buf.WriteString(line)
 			if y < height-1 {
 				x = 0
@@ -399,14 +400,15 @@ func (c *ferociousRenderer) changes() {
 		var segX int    // The start position of the current segment.
 		var eraser bool // Whether we're erasing using spaces and no styles or links.
 		for x := 0; x < width; x++ {
-			cell, ok := c.scr.Cell(x, y)
-			if !ok || cell.Width == 0 {
+			cell := c.scr.Cell(x, y)
+			if cell.Width == 0 {
 				continue
 			}
 
 			// Convert the cell to respect the current color profile.
-			cell.Style = cell.Style.Convert(c.profile)
-			cell.Link = cell.Link.Convert(c.profile)
+			// TODO: ??
+			// cell.Style = cell.Style.Convert(c.profile)
+			// cell.Link = cell.Link.Convert(c.profile)
 
 			if !c.scr.isDirty(x, y) {
 				if seg != nil {
@@ -559,9 +561,8 @@ func (c *ferociousRenderer) moveCursor(x, y int) {
 				// OPTIM: We write the cell content under the cursor if it's the same
 				// style and link. This is more efficient than moving the cursor which
 				// costs at least 3 bytes [ansi.CursorRight].
-				cell, ok := c.scr.Cell(c.scr.cur.X, c.scr.cur.Y)
-				if ok &&
-					(cell.Style.Equal(c.pen) && cell.Link == c.link) {
+				cell := c.scr.Cell(c.scr.cur.X, c.scr.cur.Y)
+				if cell.Style.Equal(c.pen) && cell.Link == c.link {
 					c.buf.WriteString(cell.Content)
 					break
 				}
diff --git a/go.mod b/go.mod
index 8233aff..00c240b 100644
--- a/go.mod
+++ b/go.mod
@@ -3,20 +3,21 @@ module github.com/charmbracelet/bubbletea/v2
 go 1.18
 
 require (
-	github.com/charmbracelet/colorprofile v0.1.6
-	github.com/charmbracelet/x/ansi v0.4.3
-	github.com/charmbracelet/x/cellbuf v0.0.3
-	github.com/charmbracelet/x/term v0.2.0
+	github.com/charmbracelet/colorprofile v0.1.8
+	github.com/charmbracelet/x/ansi v0.5.1
+	github.com/charmbracelet/x/cellbuf v0.0.6
+	github.com/charmbracelet/x/term v0.2.1
+	github.com/charmbracelet/x/vt v0.0.0-20241121165045-a3720547cbb4
 	github.com/charmbracelet/x/windows v0.2.0
 	github.com/muesli/cancelreader v0.2.2
 	github.com/rivo/uniseg v0.4.7
 	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e
-	golang.org/x/sync v0.8.0
-	golang.org/x/sys v0.26.0
+	golang.org/x/sync v0.9.0
+	golang.org/x/sys v0.27.0
 )
 
 require (
-	github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91 // indirect
+	github.com/charmbracelet/x/wcwidth v0.0.0-20241113152101-0af7d04e9f32 // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
-	golang.org/x/text v0.19.0 // indirect
+	golang.org/x/text v0.20.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 9ea1e3e..a766c97 100644
--- a/go.sum
+++ b/go.sum
@@ -1,13 +1,15 @@
-github.com/charmbracelet/colorprofile v0.1.6 h1:nMMqCns0c0DfCwNGdagBh6SxutFqkltSxxKk5S9kt+Y=
-github.com/charmbracelet/colorprofile v0.1.6/go.mod h1:3EMXDxwRDJl0c17eJ1jX99MhtlP9OxE/9Qw0C5lvyUg=
-github.com/charmbracelet/x/ansi v0.4.3 h1:wcdDrW0ejaaZGJxCyxVNzzmctqV+oARIudaFGQvsRkA=
-github.com/charmbracelet/x/ansi v0.4.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
-github.com/charmbracelet/x/cellbuf v0.0.3 h1:HapUUjlo0pZ7iGijrTer1f4X8Uvq17l0zR+80Oh+iJg=
-github.com/charmbracelet/x/cellbuf v0.0.3/go.mod h1:SF8R3AqchNzYKKJCFT7co8wt1HgQDfAitQ+SBoxWLNc=
-github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
-github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
-github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91 h1:D5OO0lVavz7A+Swdhp62F9gbkibxmz9B2hZ/jVdMPf0=
-github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91/go.mod h1:Ey8PFmYwH+/td9bpiEx07Fdx9ZVkxfIjWXxBluxF4Nw=
+github.com/charmbracelet/colorprofile v0.1.8 h1:PywDeXsiAzlPtkiiKgMEVLvb6nlEuKrMj9+FJBtj4jU=
+github.com/charmbracelet/colorprofile v0.1.8/go.mod h1:+jpmObxZl1Dab3H3IMVIPSZTsKcFpjJUv97G0dLqM60=
+github.com/charmbracelet/x/ansi v0.5.1 h1:+mg6abP9skvsu/JQZrIJ9Z/4O1YDnLVkpfutar3dUnc=
+github.com/charmbracelet/x/ansi v0.5.1/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
+github.com/charmbracelet/x/cellbuf v0.0.6 h1:pJUWN/G1jbt1Nj/+ILfC2/ABQoZzWu1vG73yHQEYELI=
+github.com/charmbracelet/x/cellbuf v0.0.6/go.mod h1:d72o71glp8flkCz54PHLe3+nuw5u2v3UxmKqruUERWQ=
+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
+github.com/charmbracelet/x/vt v0.0.0-20241121165045-a3720547cbb4 h1:EacjHxcQEEgOZ7TbkAU3b84hd1Bn5NwA8YV5uyJ9EI4=
+github.com/charmbracelet/x/vt v0.0.0-20241121165045-a3720547cbb4/go.mod h1:1/jFoHl7/I4br0StC9OXXEondkK9qi3nUtKoqI35HcI=
+github.com/charmbracelet/x/wcwidth v0.0.0-20241113152101-0af7d04e9f32 h1:14czE6R5CgOlvONsJYa2B1uTyLvXzGXpBqw2AyZeTh4=
+github.com/charmbracelet/x/wcwidth v0.0.0-20241113152101-0af7d04e9f32/go.mod h1:hyua5CY63kyl7IfyIxv1SjVEqoKze/XmDkEglItuVjA=
 github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw=
 github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s=
 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
@@ -19,9 +21,9 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
 golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
-golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
-golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
-golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
-golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
+golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
+golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
+golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
diff --git a/kitty.go b/kitty.go
index 988fac5..e35aaea 100644
--- a/kitty.go
+++ b/kitty.go
@@ -201,72 +201,85 @@ func parseKittyKeyboard(csi *ansi.CsiSequence) (msg Msg) {
 	var isRelease bool
 	var key Key
 
-	if params := csi.Subparams(0); len(params) > 0 {
-		var foundKey bool
-		code := params[0]
-		key, foundKey = kittyKeyMap[code]
-		if !foundKey {
-			r := rune(code)
-			if !utf8.ValidRune(r) {
-				r = utf8.RuneError
-			}
+	// The index of parameters separated by semicolons ';'. Sub parameters are
+	// separated by colons ':'.
+	var paramIdx int
+	var sudIdx int // The sub parameter index
+	for _, p := range csi.Params {
+		// Kitty Keyboard Protocol has 3 optional components.
+		switch paramIdx {
+		case 0:
+			switch sudIdx {
+			case 0:
+				var foundKey bool
+				code := p.Param(1) // CSI u has a default value of 1
+				key, foundKey = kittyKeyMap[code]
+				if !foundKey {
+					r := rune(code)
+					if !utf8.ValidRune(r) {
+						r = utf8.RuneError
+					}
 
-			key.Code = r
-		}
+					key.Code = r
+				}
 
-		// alternate key reporting
-		switch len(params) {
-		case 3:
-			// shifted key + base key
-			if b := rune(params[2]); unicode.IsPrint(b) {
-				// XXX: When alternate key reporting is enabled, the protocol
-				// can return 3 things, the unicode codepoint of the key,
-				// the shifted codepoint of the key, and the standard
-				// PC-101 key layout codepoint.
-				// This is useful to create an unambiguous mapping of keys
-				// when using a different language layout.
-				key.BaseCode = b
-			}
-			fallthrough
-		case 2:
-			// shifted key
-			if s := rune(params[1]); unicode.IsPrint(s) {
-				// XXX: We swap keys here because we want the shifted key
-				// to be the Rune that is returned by the event.
-				// For example, shift+a should produce "A" not "a".
-				// In such a case, we set AltRune to the original key "a"
-				// and Rune to "A".
-				key.ShiftedCode = s
-			}
-		}
-	}
+			case 2:
+				// shifted key + base key
+				if b := rune(p.Param(1)); unicode.IsPrint(b) {
+					// XXX: When alternate key reporting is enabled, the protocol
+					// can return 3 things, the unicode codepoint of the key,
+					// the shifted codepoint of the key, and the standard
+					// PC-101 key layout codepoint.
+					// This is useful to create an unambiguous mapping of keys
+					// when using a different language layout.
+					key.BaseCode = b
+				}
+				fallthrough
 
-	if params := csi.Subparams(1); len(params) > 0 {
-		mod := params[0]
-		if mod > 1 {
-			key.Mod = fromKittyMod(mod - 1)
-			if key.Mod > ModShift {
-				// XXX: We need to clear the text if we have a modifier key
-				// other than a [ModShift] key.
-				key.Text = ""
+			case 1:
+				// shifted key
+				if s := rune(p.Param(1)); unicode.IsPrint(s) {
+					// XXX: We swap keys here because we want the shifted key
+					// to be the Rune that is returned by the event.
+					// For example, shift+a should produce "A" not "a".
+					// In such a case, we set AltRune to the original key "a"
+					// and Rune to "A".
+					key.ShiftedCode = s
+				}
 			}
-		}
-		if len(params) > 1 {
-			switch params[1] {
+		case 1:
+			switch sudIdx {
+			case 0:
+				mod := p.Param(1)
+				if mod > 1 {
+					key.Mod = fromKittyMod(mod - 1)
+					if key.Mod > ModShift {
+						// XXX: We need to clear the text if we have a modifier key
+						// other than a [ModShift] key.
+						key.Text = ""
+					}
+				}
+
+			case 1:
+				switch p.Param(1) {
+				case 2:
+					key.IsRepeat = true
+				case 3:
+					isRelease = true
+				}
 			case 2:
-				key.IsRepeat = true
-			case 3:
-				isRelease = true
 			}
-		}
-	}
-
-	if params := csi.Subparams(2); len(params) > 0 {
-		for _, code := range params {
-			if code != 0 {
+		case 2:
+			if code := p.Param(0); code != 0 {
 				key.Text += string(rune(code))
 			}
 		}
+
+		sudIdx++
+		if !p.HasMore() {
+			paramIdx++
+			sudIdx = 0
+		}
 	}
 
 	if len(key.Text) == 0 && unicode.IsPrint(key.Code) &&
@@ -298,9 +311,10 @@ func parseKittyKeyboard(csi *ansi.CsiSequence) (msg Msg) {
 // and CSI ~.
 func parseKittyKeyboardExt(csi *ansi.CsiSequence, k KeyPressMsg) Msg {
 	// Handle Kitty keyboard protocol
-	if csi.HasMore(1) {
-		switch csi.Param(2) {
-		case 1:
+	if len(csi.Params) > 2 && // We have at least 3 parameters
+		csi.Params[0].Param(1) == 1 && // The first parameter is 1 (defaults to 1)
+		csi.Params[1].HasMore() { // The second parameter is a subparameter (separated by a ":")
+		switch csi.Params[2].Param(1) { // The third parameter is the event type (defaults to 1)
 		case 2:
 			k.IsRepeat = true
 		case 3:
diff --git a/mouse.go b/mouse.go
index 54f5c37..12cfed0 100644
--- a/mouse.go
+++ b/mouse.go
@@ -194,10 +194,17 @@ func (e MouseMotionMsg) Mouse() Mouse {
 //
 // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
 func parseSGRMouseEvent(csi *ansi.CsiSequence) Msg {
-	x := csi.Param(1)
-	y := csi.Param(2)
+	x, ok := csi.Param(1, 1)
+	if !ok {
+		x = 1
+	}
+	y, ok := csi.Param(2, 1)
+	if !ok {
+		y = 1
+	}
 	release := csi.Command() == 'm'
-	mod, btn, _, isMotion := parseMouseButton(csi.Param(0))
+	b, _ := csi.Param(0, 0)
+	mod, btn, _, isMotion := parseMouseButton(b)
 
 	// (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
 	x--
diff --git a/parse.go b/parse.go
index bbd73b2..9b5d686 100644
--- a/parse.go
+++ b/parse.go
@@ -162,7 +162,7 @@ func (p *inputParser) parseCsi(b []byte) (int, Msg) {
 	}
 
 	var csi ansi.CsiSequence
-	var params [parser.MaxParamsSize]int
+	var params [parser.MaxParamsSize]ansi.Parameter
 	var paramsLen int
 
 	var i int
@@ -175,7 +175,7 @@ func (p *inputParser) parseCsi(b []byte) (int, Msg) {
 
 	// Initial CSI byte
 	if i < len(b) && b[i] >= '<' && b[i] <= '?' {
-		csi.Cmd |= int(b[i]) << parser.MarkerShift
+		csi.Cmd |= ansi.Command(b[i]) << parser.MarkerShift
 	}
 
 	// Scan parameter bytes in the range 0x30-0x3F
@@ -186,7 +186,7 @@ func (p *inputParser) parseCsi(b []byte) (int, Msg) {
 				params[paramsLen] = 0
 			}
 			params[paramsLen] *= 10
-			params[paramsLen] += int(b[i]) - '0'
+			params[paramsLen] += ansi.Parameter(b[i]) - '0'
 		}
 		if b[i] == ':' {
 			params[paramsLen] |= parser.HasMoreFlag
@@ -212,7 +212,7 @@ func (p *inputParser) parseCsi(b []byte) (int, Msg) {
 	}
 
 	// Set the intermediate byte
-	csi.Cmd |= int(intermed) << parser.IntermedShift
+	csi.Cmd |= ansi.Command(intermed) << parser.IntermedShift
 
 	// Scan final byte in the range 0x40-0x7E
 	if i >= len(b) || b[i] < 0x40 || b[i] > 0x7E {
@@ -230,30 +230,44 @@ func (p *inputParser) parseCsi(b []byte) (int, Msg) {
 	}
 
 	// Add the final byte
-	csi.Cmd |= int(b[i])
+	csi.Cmd |= ansi.Command(b[i])
 	i++
 
 	csi.Params = params[:paramsLen]
 	switch cmd := csi.Cmd; cmd {
 	case 'y' | '?'<<parser.MarkerShift | '$'<<parser.IntermedShift:
 		// Report Mode (DECRPM)
-		if paramsLen == 2 && csi.Param(0) != -1 && csi.Param(1) != -1 {
-			return i, modeReportMsg{Mode: csi.Param(0), Value: csi.Param(1)}
+		mode, ok := csi.Param(0, -1)
+		if !ok || mode == -1 {
+			break
+		}
+		value, ok := csi.Param(1, -1)
+		if !ok || value == -1 {
+			break
 		}
+		return i, modeReportMsg{Mode: mode, Value: value}
 	case 'c' | '?'<<parser.MarkerShift:
 		// Primary Device Attributes
 		return i, parsePrimaryDevAttrs(&csi)
 	case 'u' | '?'<<parser.MarkerShift:
 		// Kitty keyboard flags
-		if param := csi.Param(0); param != -1 {
-			return i, KeyboardEnhancementsMsg{kittyFlags: param}
+		flags, ok := csi.Param(0, -1)
+		if !ok || flags == -1 {
+			break
 		}
+		return i, KeyboardEnhancementsMsg{kittyFlags: flags}
 	case 'R' | '?'<<parser.MarkerShift:
 		// This report may return a third parameter representing the page
 		// number, but we don't really need it.
-		if paramsLen >= 2 && csi.Param(0) != -1 && csi.Param(1) != -1 {
-			return i, CursorPositionMsg{Y: csi.Param(0) - 1, X: csi.Param(1) - 1}
+		row, ok := csi.Param(0, 1)
+		if !ok {
+			break
 		}
+		col, ok := csi.Param(1, 1)
+		if !ok {
+			break
+		}
+		return i, CursorPositionMsg{Y: row - 1, X: col - 1}
 	case 'm' | '<'<<parser.MarkerShift, 'M' | '<'<<parser.MarkerShift:
 		// Handle SGR mouse
 		if paramsLen == 3 {
@@ -261,25 +275,33 @@ func (p *inputParser) parseCsi(b []byte) (int, Msg) {
 		}
 	case 'm' | '>'<<parser.MarkerShift:
 		// XTerm modifyOtherKeys
-		if paramsLen == 2 && csi.Param(0) == 4 && csi.Param(1) != -1 {
-			return i, KeyboardEnhancementsMsg{modifyOtherKeys: csi.Param(1)}
+		mok, ok := csi.Param(0, 0)
+		if !ok || mok != 4 {
+			break
 		}
+		val, ok := csi.Param(1, -1)
+		if !ok || val == -1 {
+			break
+		}
+		return i, KeyboardEnhancementsMsg{modifyOtherKeys: val}
 	case 'I':
 		return i, FocusMsg{}
 	case 'O':
 		return i, BlurMsg{}
 	case 'R':
 		// Cursor position report OR modified F3
-		if paramsLen == 2 && csi.Param(0) != -1 && csi.Param(1) != -1 {
-			m := CursorPositionMsg{Y: csi.Param(0) - 1, X: csi.Param(1) - 1}
-			if csi.Param(0) == 1 && csi.Param(1)-1 <= int(ModMeta|ModShift|ModAlt|ModCtrl) {
+		row, rok := csi.Param(0, 1)
+		col, cok := csi.Param(1, 1)
+		if paramsLen == 2 && rok && cok {
+			m := CursorPositionMsg{Y: row - 1, X: col - 1}
+			if row == 1 && col-1 <= int(ModMeta|ModShift|ModAlt|ModCtrl) {
 				// XXX: We cannot differentiate between cursor position report and
 				// CSI 1 ; <mod> R (which is modified F3) when the cursor is at the
 				// row 1. In this case, we report both messages.
 				//
 				// For a non ambiguous cursor position report, use
 				// [ansi.RequestExtendedCursorPosition] (DECXCPR) instead.
-				return i, multiMsg{KeyPressMsg{Code: KeyF3, Mod: KeyMod(csi.Param(1) - 1)}, m}
+				return i, multiMsg{KeyPressMsg{Code: KeyF3, Mod: KeyMod(col - 1)}, m}
 			}
 
 			return i, m
@@ -309,9 +331,17 @@ func (p *inputParser) parseCsi(b []byte) (int, Msg) {
 		case 'Z':
 			k = KeyPressMsg{Code: KeyTab, Mod: ModShift}
 		}
-		if paramsLen > 1 && csi.Param(0) == 1 && csi.Param(1) != -1 {
+		id, _ := csi.Param(0, 1)
+		if id == 0 {
+			id = 1
+		}
+		mod, _ := csi.Param(1, 1)
+		if mod == 0 {
+			mod = 1
+		}
+		if paramsLen > 1 && id == 1 && mod != -1 {
 			// CSI 1 ; <modifiers> A
-			k.Mod |= KeyMod(csi.Param(1) - 1)
+			k.Mod |= KeyMod(mod - 1)
 		}
 		// Don't forget to handle Kitty keyboard protocol
 		return i, parseKittyKeyboardExt(&csi, k)
@@ -323,10 +353,15 @@ func (p *inputParser) parseCsi(b []byte) (int, Msg) {
 		return i + 3, parseX10MouseEvent(append(b[:i], b[i:i+3]...))
 	case 'y':
 		// Report Mode (DECRPM)
-		if paramsLen != 2 && csi.Param(0) != -1 && csi.Param(0) != -1 {
-			return i, UnknownMsg(b[:i])
+		mode, ok := csi.Param(0, -1)
+		if !ok || mode == -1 {
+			break
+		}
+		val, ok := csi.Param(1, -1)
+		if !ok || val == -1 {
+			break
 		}
-		return i, modeReportMsg{Mode: csi.Param(0), Value: csi.Param(1)}
+		return i, modeReportMsg{Mode: mode, Value: val}
 	case 'u':
 		// Kitty keyboard protocol & CSI u (fixterms)
 		if paramsLen == 0 {
@@ -339,19 +374,25 @@ func (p *inputParser) parseCsi(b []byte) (int, Msg) {
 			return i, UnknownMsg(b[:i])
 		}
 
-		rc := uint16(csi.Param(5)) //nolint:gosec
+		vrc, _ := csi.Param(5, 0)
+		rc := uint16(vrc) //nolint:gosec
 		if rc == 0 {
 			rc = 1
 		}
 
+		vk, _ := csi.Param(0, 0)
+		sc, _ := csi.Param(1, 0)
+		uc, _ := csi.Param(2, 0)
+		kd, _ := csi.Param(3, 0)
+		cs, _ := csi.Param(4, 0)
 		event := p.parseWin32InputKeyEvent(
 			nil,
-			uint16(csi.Param(0)), //nolint:gosec // Vk wVirtualKeyCode
-			uint16(csi.Param(1)), //nolint:gosec // Sc wVirtualScanCode
-			rune(csi.Param(2)),   // Uc UnicodeChar
-			csi.Param(3) == 1,    // Kd bKeyDown
-			uint32(csi.Param(4)), //nolint:gosec // Cs dwControlKeyState
-			rc,                   // Rc wRepeatCount
+			uint16(vk), //nolint:gosec // Vk wVirtualKeyCode
+			uint16(sc), //nolint:gosec // Sc wVirtualScanCode
+			rune(uc),   // Uc UnicodeChar
+			kd == 1,    // Kd bKeyDown
+			uint32(cs), //nolint:gosec // Cs dwControlKeyState
+			rc,         // Rc wRepeatCount
 		)
 
 		if event == nil {
@@ -364,7 +405,7 @@ func (p *inputParser) parseCsi(b []byte) (int, Msg) {
 			return i, UnknownMsg(b[:i])
 		}
 
-		param := csi.Param(0)
+		param, _ := csi.Param(0, 0)
 		switch cmd {
 		case '~':
 			switch param {
@@ -428,8 +469,9 @@ func (p *inputParser) parseCsi(b []byte) (int, Msg) {
 			}
 
 			// modifiers
-			if paramsLen > 1 && csi.Param(1) != -1 {
-				k.Mod |= KeyMod(csi.Param(1) - 1)
+			mod, _ := csi.Param(1, -1)
+			if paramsLen > 1 && mod != -1 {
+				k.Mod |= KeyMod(mod - 1)
 			}
 
 			// Handle URxvt weird keys
@@ -646,7 +688,7 @@ func (p *inputParser) parseDcs(b []byte) (int, Msg) {
 		return 2, KeyPressMsg{Code: rune(b[1]), Mod: ModAlt}
 	}
 
-	var params [16]int
+	var params [16]ansi.Parameter
 	var paramsLen int
 	var dcs ansi.DcsSequence
 
@@ -661,7 +703,7 @@ func (p *inputParser) parseDcs(b []byte) (int, Msg) {
 
 	// initial DCS byte
 	if i < len(b) && b[i] >= '<' && b[i] <= '?' {
-		dcs.Cmd |= int(b[i]) << parser.MarkerShift
+		dcs.Cmd |= ansi.Command(b[i]) << parser.MarkerShift
 	}
 
 	// Scan parameter bytes in the range 0x30-0x3F
@@ -672,7 +714,7 @@ func (p *inputParser) parseDcs(b []byte) (int, Msg) {
 				params[paramsLen] = 0
 			}
 			params[paramsLen] *= 10
-			params[paramsLen] += int(b[i]) - '0'
+			params[paramsLen] += ansi.Parameter(b[i]) - '0'
 		}
 		if b[i] == ':' {
 			params[paramsLen] |= parser.HasMoreFlag
@@ -698,7 +740,7 @@ func (p *inputParser) parseDcs(b []byte) (int, Msg) {
 	}
 
 	// set intermediate byte
-	dcs.Cmd |= int(intermed) << parser.IntermedShift
+	dcs.Cmd |= ansi.Command(intermed) << parser.IntermedShift
 
 	// Scan final byte in the range 0x40-0x7E
 	if i >= len(b) || b[i] < 0x40 || b[i] > 0x7E {
@@ -706,7 +748,7 @@ func (p *inputParser) parseDcs(b []byte) (int, Msg) {
 	}
 
 	// Add the final byte
-	dcs.Cmd |= int(b[i])
+	dcs.Cmd |= ansi.Command(b[i])
 	i++
 
 	start := i // start of the sequence data
@@ -732,7 +774,8 @@ func (p *inputParser) parseDcs(b []byte) (int, Msg) {
 	switch cmd := dcs.Cmd; cmd {
 	case 'r' | '+'<<parser.IntermedShift:
 		// XTGETTCAP responses
-		switch param := dcs.Param(0); param {
+		param, _ := dcs.Param(0, 0)
+		switch param {
 		case 1: // 1 means valid response, 0 means invalid response
 			tc := parseTermcap(b[start:end])
 			// XXX: some terminals like KiTTY report invalid responses with
diff --git a/screen.go b/screen.go
index 280c343..8cb193c 100644
--- a/screen.go
+++ b/screen.go
@@ -32,7 +32,7 @@ type clearScreenMsg struct{}
 // model's Init function. To initialize your program with the altscreen enabled
 // use the WithAltScreen ProgramOption instead.
 func EnterAltScreen() Msg {
-	return enableMode(ansi.AltScreenBufferMode.String())
+	return enableMode(ansi.AltScreenSaveCursorMode)
 }
 
 // ExitAltScreen is a special command that tells the Bubble Tea program to exit
@@ -42,7 +42,7 @@ func EnterAltScreen() Msg {
 // Note that the alternate screen buffer will be automatically exited when the
 // program quits.
 func ExitAltScreen() Msg {
-	return disableMode(ansi.AltScreenBufferMode.String())
+	return disableMode(ansi.AltScreenSaveCursorMode)
 }
 
 // EnableMouseCellMotion is a special command that enables mouse click,
@@ -53,8 +53,8 @@ func ExitAltScreen() Msg {
 // model's Init function. Use the WithMouseCellMotion ProgramOption instead.
 func EnableMouseCellMotion() Msg {
 	return sequenceMsg{
-		func() Msg { return enableMode(ansi.MouseCellMotionMode.String()) },
-		func() Msg { return enableMode(ansi.MouseSgrExtMode.String()) },
+		func() Msg { return enableMode(ansi.ButtonEventMouseMode) },
+		func() Msg { return enableMode(ansi.SgrExtMouseMode) },
 	}
 }
 
@@ -69,17 +69,17 @@ func EnableMouseCellMotion() Msg {
 // model's Init function. Use the WithMouseAllMotion ProgramOption instead.
 func EnableMouseAllMotion() Msg {
 	return sequenceMsg{
-		func() Msg { return enableMode(ansi.MouseAllMotionMode.String()) },
-		func() Msg { return enableMode(ansi.MouseSgrExtMode.String()) },
+		func() Msg { return enableMode(ansi.AnyEventMouseMode) },
+		func() Msg { return enableMode(ansi.SgrExtMouseMode) },
 	}
 }
 
 // DisableMouse is a special command that stops listening for mouse events.
 func DisableMouse() Msg {
 	return sequenceMsg{
-		func() Msg { return disableMode(ansi.MouseCellMotionMode.String()) },
-		func() Msg { return disableMode(ansi.MouseAllMotionMode.String()) },
-		func() Msg { return disableMode(ansi.MouseSgrExtMode.String()) },
+		func() Msg { return disableMode(ansi.ButtonEventMouseMode) },
+		func() Msg { return disableMode(ansi.AnyEventMouseMode) },
+		func() Msg { return disableMode(ansi.SgrExtMouseMode) },
 	}
 }
 
@@ -88,13 +88,13 @@ func DisableMouse() Msg {
 // to show the cursor, which is normally hidden for the duration of a Bubble
 // Tea program's lifetime. You will most likely not need to use this command.
 func HideCursor() Msg {
-	return disableMode(ansi.CursorEnableMode.String())
+	return disableMode(ansi.TextCursorEnableMode)
 }
 
 // ShowCursor is a special command for manually instructing Bubble Tea to show
 // the cursor.
 func ShowCursor() Msg {
-	return enableMode(ansi.CursorEnableMode.String())
+	return enableMode(ansi.TextCursorEnableMode)
 }
 
 // EnableBracketedPaste is a special command that tells the Bubble Tea program
@@ -103,7 +103,7 @@ func ShowCursor() Msg {
 // Note that bracketed paste will be automatically disabled when the
 // program quits.
 func EnableBracketedPaste() Msg {
-	return enableMode(ansi.BracketedPasteMode.String())
+	return enableMode(ansi.BracketedPasteMode)
 }
 
 // DisableBracketedPaste is a special command that tells the Bubble Tea program
@@ -112,42 +112,42 @@ func EnableBracketedPaste() Msg {
 // Note that bracketed paste will be automatically disabled when the
 // program quits.
 func DisableBracketedPaste() Msg {
-	return disableMode(ansi.BracketedPasteMode.String())
+	return disableMode(ansi.BracketedPasteMode)
 }
 
 // EnableGraphemeClustering is a special command that tells the Bubble Tea
 // program to enable grapheme clustering. This is enabled by default.
 func EnableGraphemeClustering() Msg {
-	return enableMode(ansi.GraphemeClusteringMode.String())
+	return enableMode(ansi.GraphemeClusteringMode)
 }
 
 // DisableGraphemeClustering is a special command that tells the Bubble Tea
 // program to disable grapheme clustering. This mode will be disabled
 // automatically when the program quits.
 func DisableGraphemeClustering() Msg {
-	return disableMode(ansi.GraphemeClusteringMode.String())
+	return disableMode(ansi.GraphemeClusteringMode)
 }
 
 // EnabledReportFocus is a special command that tells the Bubble Tea program
 // to enable focus reporting.
-func EnabledReportFocus() Msg { return enableMode(ansi.ReportFocusMode.String()) }
+func EnabledReportFocus() Msg { return enableMode(ansi.FocusEventMode) }
 
 // DisabledReportFocus is a special command that tells the Bubble Tea program
 // to disable focus reporting.
-func DisabledReportFocus() Msg { return disableMode(ansi.ReportFocusMode.String()) }
+func DisabledReportFocus() Msg { return disableMode(ansi.FocusEventMode) }
 
 // enableModeMsg is an internal message that signals to set a terminal mode.
-type enableModeMsg string
+type enableModeMsg ansi.DECMode
 
 // enableMode is an internal command that signals to set a terminal mode.
-func enableMode(mode string) Msg {
+func enableMode(mode ansi.DECMode) Msg {
 	return enableModeMsg(mode)
 }
 
 // disableModeMsg is an internal message that signals to unset a terminal mode.
-type disableModeMsg string
+type disableModeMsg ansi.DECMode
 
 // disableMode is an internal command that signals to unset a terminal mode.
-func disableMode(mode string) Msg {
+func disableMode(mode ansi.DECMode) Msg {
 	return disableModeMsg(mode)
 }
diff --git a/standard_renderer.go b/standard_renderer.go
index db5e20c..fe2fb32 100644
--- a/standard_renderer.go
+++ b/standard_renderer.go
@@ -276,8 +276,8 @@ func (r *standardRenderer) update(msg Msg) {
 		r.profile = msg.Profile
 
 	case enableModeMsg:
-		switch string(msg) {
-		case ansi.AltScreenBufferMode.String():
+		switch ansi.DECMode(msg) {
+		case ansi.AltScreenSaveCursorMode:
 			if r.altScreenActive {
 				return
 			}
@@ -285,7 +285,7 @@ func (r *standardRenderer) update(msg Msg) {
 			r.setAltScreenBuffer(true)
 			r.altScreenActive = true
 			r.repaint()
-		case ansi.CursorEnableMode.String():
+		case ansi.TextCursorEnableMode:
 			if !r.cursorHidden {
 				return
 			}
@@ -294,8 +294,8 @@ func (r *standardRenderer) update(msg Msg) {
 		}
 
 	case disableModeMsg:
-		switch string(msg) {
-		case ansi.AltScreenBufferMode.String():
+		switch ansi.DECMode(msg) {
+		case ansi.AltScreenSaveCursorMode:
 			if !r.altScreenActive {
 				return
 			}
@@ -303,7 +303,7 @@ func (r *standardRenderer) update(msg Msg) {
 			r.setAltScreenBuffer(false)
 			r.altScreenActive = false
 			r.repaint()
-		case ansi.CursorEnableMode.String():
+		case ansi.TextCursorEnableMode:
 			if r.cursorHidden {
 				return
 			}
diff --git a/tea.go b/tea.go
index 0333218..cc0406c 100644
--- a/tea.go
+++ b/tea.go
@@ -196,7 +196,7 @@ type Program struct {
 	readLoopDone          chan struct{}
 
 	// modes keeps track of terminal modes that have been enabled or disabled.
-	modes         map[string]bool
+	modes         map[ansi.DECMode]bool
 	ignoreSignals uint32
 
 	filter func(Model, Msg) Msg
@@ -256,7 +256,7 @@ func NewProgram(model Model, opts ...ProgramOption) *Program {
 		initialModel: model,
 		msgs:         make(chan Msg),
 		rendererDone: make(chan struct{}),
-		modes:        make(map[string]bool),
+		modes:        make(map[ansi.DECMode]bool),
 		exp:          experimentalOptions{},
 	}
 
@@ -451,30 +451,32 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
 				switch msg.Mode {
 				case int(ansi.GraphemeClusteringMode):
 					// 1 means mode is set (see DECRPM).
-					p.modes[ansi.GraphemeClusteringMode.String()] = msg.Value == 1 || msg.Value == 3
+					p.modes[ansi.GraphemeClusteringMode] = msg.Value == 1 || msg.Value == 3
 				}
 
 			case enableModeMsg:
-				if on, ok := p.modes[string(msg)]; ok && on {
+				mode := ansi.DECMode(msg)
+				if on, ok := p.modes[mode]; ok && on {
 					break
 				}
 
-				p.execute(fmt.Sprintf("\x1b[%sh", string(msg)))
-				p.modes[string(msg)] = true
-				switch string(msg) {
-				case ansi.GraphemeClusteringMode.String():
+				p.execute(fmt.Sprintf("\x1b[?%dh", mode.Mode()))
+				p.modes[mode] = true
+				switch mode {
+				case ansi.GraphemeClusteringMode:
 					// We store the state of grapheme clustering after we enable it
 					// and get a response in the eventLoop.
-					p.execute(ansi.RequestGraphemeClustering)
+					p.execute(ansi.RequestGraphemeClusteringMode)
 				}
 
 			case disableModeMsg:
-				if on, ok := p.modes[string(msg)]; ok && !on {
+				mode := ansi.DECMode(msg)
+				if on, ok := p.modes[mode]; ok && !on {
 					break
 				}
 
-				p.execute(fmt.Sprintf("\x1b[%sl", string(msg)))
-				p.modes[string(msg)] = false
+				p.execute(fmt.Sprintf("\x1b[?%dl", mode))
+				p.modes[mode] = false
 
 			case readClipboardMsg:
 				p.execute(ansi.RequestSystemClipboard)
@@ -737,44 +739,44 @@ func (p *Program) Run() (Model, error) {
 	}
 
 	// Hide the cursor before starting the renderer.
-	p.modes[ansi.CursorEnableMode.String()] = false
+	p.modes[ansi.TextCursorEnableMode] = false
 	p.execute(ansi.HideCursor)
-	p.renderer.update(disableMode(ansi.CursorEnableMode.String()))
+	p.renderer.update(disableMode(ansi.TextCursorEnableMode))
 
 	// Honor program startup options.
 	if p.startupTitle != "" {
 		p.execute(ansi.SetWindowTitle(p.startupTitle))
 	}
 	if p.startupOptions&withAltScreen != 0 {
-		p.execute(ansi.EnableAltScreenBuffer)
-		p.modes[ansi.AltScreenBufferMode.String()] = true
-		p.renderer.update(enableMode(ansi.AltScreenBufferMode.String()))
+		p.execute(ansi.SetAltScreenSaveCursorMode)
+		p.modes[ansi.AltScreenSaveCursorMode] = true
+		p.renderer.update(enableMode(ansi.AltScreenSaveCursorMode))
 	}
 	if p.startupOptions&withoutBracketedPaste == 0 {
-		p.execute(ansi.EnableBracketedPaste)
-		p.modes[ansi.BracketedPasteMode.String()] = true
+		p.execute(ansi.SetBracketedPasteMode)
+		p.modes[ansi.BracketedPasteMode] = true
 	}
 	if p.startupOptions&withGraphemeClustering != 0 {
-		p.execute(ansi.EnableGraphemeClustering)
-		p.execute(ansi.RequestGraphemeClustering)
+		p.execute(ansi.SetGraphemeClusteringMode)
+		p.execute(ansi.RequestGraphemeClusteringMode)
 		// We store the state of grapheme clustering after we query it and get
 		// a response in the eventLoop.
 	}
 	if p.startupOptions&withMouseCellMotion != 0 {
-		p.execute(ansi.EnableMouseCellMotion)
-		p.execute(ansi.EnableMouseSgrExt)
-		p.modes[ansi.MouseCellMotionMode.String()] = true
-		p.modes[ansi.MouseSgrExtMode.String()] = true
+		p.execute(ansi.SetButtonEventMouseMode)
+		p.execute(ansi.SetSgrExtMouseMode)
+		p.modes[ansi.ButtonEventMouseMode] = true
+		p.modes[ansi.SgrExtMouseMode] = true
 	} else if p.startupOptions&withMouseAllMotion != 0 {
-		p.execute(ansi.EnableMouseAllMotion)
-		p.execute(ansi.EnableMouseSgrExt)
-		p.modes[ansi.MouseAllMotionMode.String()] = true
-		p.modes[ansi.MouseSgrExtMode.String()] = true
+		p.execute(ansi.SetAnyEventMouseMode)
+		p.execute(ansi.SetSgrExtMouseMode)
+		p.modes[ansi.AnyEventMouseMode] = true
+		p.modes[ansi.SgrExtMouseMode] = true
 	}
 
 	if p.startupOptions&withReportFocus != 0 {
-		p.execute(ansi.EnableReportFocus)
-		p.modes[ansi.ReportFocusMode.String()] = true
+		p.execute(ansi.SetFocusEventMode)
+		p.modes[ansi.FocusEventMode] = true
 	}
 	if p.startupOptions&withKeyboardEnhancements != 0 && runtime.GOOS != "windows" {
 		// We use the Windows Console API which supports keyboard
@@ -947,21 +949,21 @@ func (p *Program) RestoreTerminal() error {
 	if err := p.initInputReader(); err != nil {
 		return err
 	}
-	if p.modes[ansi.AltScreenBufferMode.String()] {
-		p.execute(ansi.EnableAltScreenBuffer)
+	if p.modes[ansi.AltScreenSaveCursorMode] {
+		p.execute(ansi.SetAltScreenSaveCursorMode)
 	} else {
 		// entering alt screen already causes a repaint.
 		go p.Send(repaintMsg{})
 	}
 
 	p.startRenderer()
-	if !p.modes[ansi.CursorEnableMode.String()] {
+	if !p.modes[ansi.TextCursorEnableMode] {
 		p.execute(ansi.HideCursor)
 	} else {
 		p.execute(ansi.ShowCursor)
 	}
-	if p.modes[ansi.BracketedPasteMode.String()] {
-		p.execute(ansi.EnableBracketedPaste)
+	if p.modes[ansi.BracketedPasteMode] {
+		p.execute(ansi.SetBracketedPasteMode)
 	}
 	if p.keyboard.modifyOtherKeys != 0 {
 		p.execute(ansi.ModifyOtherKeys(p.keyboard.modifyOtherKeys))
@@ -969,20 +971,20 @@ func (p *Program) RestoreTerminal() error {
 	if p.keyboard.kittyFlags != 0 {
 		p.execute(ansi.PushKittyKeyboard(p.keyboard.kittyFlags))
 	}
-	if p.modes[ansi.ReportFocusMode.String()] {
-		p.execute(ansi.EnableReportFocus)
+	if p.modes[ansi.FocusEventMode] {
+		p.execute(ansi.SetFocusEventMode)
 	}
-	if p.modes[ansi.MouseCellMotionMode.String()] || p.modes[ansi.MouseAllMotionMode.String()] {
+	if p.modes[ansi.ButtonEventMouseMode] || p.modes[ansi.AnyEventMouseMode] {
 		if p.startupOptions&withMouseCellMotion != 0 {
-			p.execute(ansi.EnableMouseCellMotion)
-			p.execute(ansi.EnableMouseSgrExt)
+			p.execute(ansi.SetButtonEventMouseMode)
+			p.execute(ansi.SetSgrExtMouseMode)
 		} else if p.startupOptions&withMouseAllMotion != 0 {
-			p.execute(ansi.EnableMouseAllMotion)
-			p.execute(ansi.EnableMouseSgrExt)
+			p.execute(ansi.SetAnyEventMouseMode)
+			p.execute(ansi.SetSgrExtMouseMode)
 		}
 	}
-	if p.modes[ansi.GraphemeClusteringMode.String()] {
-		p.execute(ansi.EnableGraphemeClustering)
+	if p.modes[ansi.GraphemeClusteringMode] {
+		p.execute(ansi.SetGraphemeClusteringMode)
 	}
 
 	// Restore terminal colors.
diff --git a/tty.go b/tty.go
index 662b0ee..d570e0f 100644
--- a/tty.go
+++ b/tty.go
@@ -36,16 +36,16 @@ func (p *Program) initTerminal() error {
 // restoreTerminalState restores the terminal to the state prior to running the
 // Bubble Tea program.
 func (p *Program) restoreTerminalState() error {
-	if p.modes[ansi.BracketedPasteMode.String()] {
-		p.execute(ansi.DisableBracketedPaste)
+	if p.modes[ansi.BracketedPasteMode] {
+		p.execute(ansi.ResetBracketedPasteMode)
 	}
-	if !p.modes[ansi.CursorEnableMode.String()] {
+	if !p.modes[ansi.TextCursorEnableMode] {
 		p.execute(ansi.ShowCursor)
 	}
-	if p.modes[ansi.MouseCellMotionMode.String()] || p.modes[ansi.MouseAllMotionMode.String()] {
-		p.execute(ansi.DisableMouseCellMotion)
-		p.execute(ansi.DisableMouseAllMotion)
-		p.execute(ansi.DisableMouseSgrExt)
+	if p.modes[ansi.ButtonEventMouseMode] || p.modes[ansi.AnyEventMouseMode] {
+		p.execute(ansi.ResetButtonEventMouseMode)
+		p.execute(ansi.ResetAnyEventMouseMode)
+		p.execute(ansi.ResetSgrExtMouseMode)
 	}
 	if p.keyboard.modifyOtherKeys != 0 {
 		p.execute(ansi.DisableModifyOtherKeys)
@@ -53,14 +53,14 @@ func (p *Program) restoreTerminalState() error {
 	if p.keyboard.kittyFlags != 0 {
 		p.execute(ansi.DisableKittyKeyboard)
 	}
-	if p.modes[ansi.ReportFocusMode.String()] {
-		p.execute(ansi.DisableReportFocus)
+	if p.modes[ansi.FocusEventMode] {
+		p.execute(ansi.ResetFocusEventMode)
 	}
-	if p.modes[ansi.GraphemeClusteringMode.String()] {
-		p.execute(ansi.DisableGraphemeClustering)
+	if p.modes[ansi.GraphemeClusteringMode] {
+		p.execute(ansi.ResetGraphemeClusteringMode)
 	}
-	if p.modes[ansi.AltScreenBufferMode.String()] {
-		p.execute(ansi.DisableAltScreenBuffer)
+	if p.modes[ansi.AltScreenSaveCursorMode] {
+		p.execute(ansi.ResetAltScreenSaveCursorMode)
 		// cmd.exe and other terminals keep separate cursor states for the AltScreen
 		// and the main buffer. We have to explicitly reset the cursor visibility
 		// whenever we exit AltScreen.
diff --git a/tutorials/go.mod b/tutorials/go.mod
index 9c66981..48577f2 100644
--- a/tutorials/go.mod
+++ b/tutorials/go.mod
@@ -5,19 +5,20 @@ go 1.18
 require github.com/charmbracelet/bubbletea/v2 v2.0.0-20240918180721-14cb6b5de1d2
 
 require (
-	github.com/charmbracelet/colorprofile v0.1.6 // indirect
-	github.com/charmbracelet/x/ansi v0.4.3 // indirect
-	github.com/charmbracelet/x/cellbuf v0.0.3 // indirect
-	github.com/charmbracelet/x/term v0.2.0 // indirect
-	github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91 // indirect
+	github.com/charmbracelet/colorprofile v0.1.8-0.20241114170416-4ca4b7121c58 // indirect
+	github.com/charmbracelet/x/ansi v0.4.6-0.20241114164159-aea15a2cc929 // indirect
+	github.com/charmbracelet/x/cellbuf v0.0.6-0.20241114164159-aea15a2cc929 // indirect
+	github.com/charmbracelet/x/term v0.2.1 // indirect
+	github.com/charmbracelet/x/vt v0.0.0-20241114164159-aea15a2cc929 // indirect
+	github.com/charmbracelet/x/wcwidth v0.0.0-20241113152101-0af7d04e9f32 // indirect
 	github.com/charmbracelet/x/windows v0.2.0 // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 	github.com/muesli/cancelreader v0.2.2 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
-	golang.org/x/sync v0.8.0 // indirect
-	golang.org/x/sys v0.26.0 // indirect
-	golang.org/x/text v0.19.0 // indirect
+	golang.org/x/sync v0.9.0 // indirect
+	golang.org/x/sys v0.27.0 // indirect
+	golang.org/x/text v0.20.0 // indirect
 )
 
 replace github.com/charmbracelet/bubbletea/v2 => ../
diff --git a/tutorials/go.sum b/tutorials/go.sum
index 9ea1e3e..61d4dc8 100644
--- a/tutorials/go.sum
+++ b/tutorials/go.sum
@@ -1,13 +1,15 @@
-github.com/charmbracelet/colorprofile v0.1.6 h1:nMMqCns0c0DfCwNGdagBh6SxutFqkltSxxKk5S9kt+Y=
-github.com/charmbracelet/colorprofile v0.1.6/go.mod h1:3EMXDxwRDJl0c17eJ1jX99MhtlP9OxE/9Qw0C5lvyUg=
-github.com/charmbracelet/x/ansi v0.4.3 h1:wcdDrW0ejaaZGJxCyxVNzzmctqV+oARIudaFGQvsRkA=
-github.com/charmbracelet/x/ansi v0.4.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
-github.com/charmbracelet/x/cellbuf v0.0.3 h1:HapUUjlo0pZ7iGijrTer1f4X8Uvq17l0zR+80Oh+iJg=
-github.com/charmbracelet/x/cellbuf v0.0.3/go.mod h1:SF8R3AqchNzYKKJCFT7co8wt1HgQDfAitQ+SBoxWLNc=
-github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
-github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
-github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91 h1:D5OO0lVavz7A+Swdhp62F9gbkibxmz9B2hZ/jVdMPf0=
-github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91/go.mod h1:Ey8PFmYwH+/td9bpiEx07Fdx9ZVkxfIjWXxBluxF4Nw=
+github.com/charmbracelet/colorprofile v0.1.8-0.20241114170416-4ca4b7121c58 h1:O7ZVm7uxDSU06e5+Ps5c3bC5zWkqfRUKsH4M3/taYq8=
+github.com/charmbracelet/colorprofile v0.1.8-0.20241114170416-4ca4b7121c58/go.mod h1:Fenu2dAsg1m0qWUHcsOwjr12BuDyMaUP6UCQjAUFVrg=
+github.com/charmbracelet/x/ansi v0.4.6-0.20241114164159-aea15a2cc929 h1:CxDzlAZwEEcq5DNjRlx+RQ0acLKBg/J5ZmvW+db9kAc=
+github.com/charmbracelet/x/ansi v0.4.6-0.20241114164159-aea15a2cc929/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
+github.com/charmbracelet/x/cellbuf v0.0.6-0.20241114164159-aea15a2cc929 h1:3WICEOCsGzB3TAj9nYj64ojaWl8HpUGxagmVKiiBDXA=
+github.com/charmbracelet/x/cellbuf v0.0.6-0.20241114164159-aea15a2cc929/go.mod h1:OJj3QVur0XOJQgNCsE1Q4xdFLgXeQhWkchE8zYzlbMs=
+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
+github.com/charmbracelet/x/vt v0.0.0-20241114164159-aea15a2cc929 h1:/xi/eowrQ14ihrKyr9mBdOuKK02QfgPAo0vrKK9OIJc=
+github.com/charmbracelet/x/vt v0.0.0-20241114164159-aea15a2cc929/go.mod h1:mMsiDODOSTc241mLfeVdqqHYhuzjSP5uEPcJDGScsRg=
+github.com/charmbracelet/x/wcwidth v0.0.0-20241113152101-0af7d04e9f32 h1:14czE6R5CgOlvONsJYa2B1uTyLvXzGXpBqw2AyZeTh4=
+github.com/charmbracelet/x/wcwidth v0.0.0-20241113152101-0af7d04e9f32/go.mod h1:hyua5CY63kyl7IfyIxv1SjVEqoKze/XmDkEglItuVjA=
 github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw=
 github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s=
 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
@@ -19,9 +21,9 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
 golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
-golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
-golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
-golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
-golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
+golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
+golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
+golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
diff --git a/xterm.go b/xterm.go
index b6140b5..04fc8ab 100644
--- a/xterm.go
+++ b/xterm.go
@@ -6,8 +6,10 @@ import (
 
 func parseXTermModifyOtherKeys(csi *ansi.CsiSequence) Msg {
 	// XTerm modify other keys starts with ESC [ 27 ; <modifier> ; <code> ~
-	mod := KeyMod(csi.Param(1) - 1)
-	r := rune(csi.Param(2))
+	xmod, _ := csi.Param(1, 1)
+	xrune, _ := csi.Param(2, 1)
+	mod := KeyMod(xmod - 1)
+	r := rune(xrune)
 
 	switch r {
 	case ansi.BS:


Test patch:
diff --git a/mouse_test.go b/mouse_test.go
index 69b2730..52625ed 100644
--- a/mouse_test.go
+++ b/mouse_test.go
@@ -312,8 +312,12 @@ func TestParseSGRMouseEvent(t *testing.T) {
 			re = 'm'
 		}
 		return &ansi.CsiSequence{
-			Params: []int{b, x + 1, y + 1},
-			Cmd:    int(re) | ('<' << parser.MarkerShift),
+			Params: []ansi.Parameter{
+				ansi.Parameter(b),
+				ansi.Parameter(x + 1),
+				ansi.Parameter(y + 1),
+			},
+			Cmd: ansi.Command(re) | ('<' << parser.MarkerShift),
 		}
 	}
 
diff --git a/screen_test.go b/screen_test.go
index 3b2333e..1524661 100644
--- a/screen_test.go
+++ b/screen_test.go
@@ -29,7 +29,7 @@ func TestClearMsg(t *testing.T) {
 		{
 			name:     "altscreen_autoexit",
 			cmds:     []Cmd{EnterAltScreen},
-			expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[H\x1b[?25l\rsuccess\r\n\x1b[2;0H\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1049l\x1b[?25h",
+			expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[H\x1b[?25l\rsuccess\r\n\x1b[2;H\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1049l\x1b[?25h",
 		},
 		{
 			name:     "mouse_cellmotion",