Repository: charmbracelet/bubbletea
Pull request #922 (2024-02-09 17:11:40)

chore(deps): bump golangci/golangci-lint-action from 3 to 4
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3 to 4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/golangci/golangci-lint-action/releases">golangci/golangci-lint-action's releases</a>.</em></p>
<blockquote>
<h2>v3.7.0</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3>Changes</h3>
<ul>
<li>feat: working-directory with only-new-issues by <a href="https://github.com/CfirTsabari"><code>@​CfirTsabari</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/795">golangci/golangci-lint-action#795</a></li>
</ul>
<h3>Documentation</h3>
<ul>
<li>docs: improve <code>How to use</code> section in README.md by <a href="https://github.com/tungbq"><code>@​tungbq</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/780">golangci/golangci-lint-action#780</a></li>
</ul>
<h3>Dependencies</h3>
<ul>
<li>build(deps): bump <code>@​types/node</code> from 20.2.5 to 20.3.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/771">golangci/golangci-lint-action#771</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from 5.59.8 to 5.59.9 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/772">golangci/golangci-lint-action#772</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 5.59.8 to 5.59.9 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/773">golangci/golangci-lint-action#773</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from 5.59.9 to 5.59.11 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/776">golangci/golangci-lint-action#776</a></li>
<li>build(deps-dev): bump eslint from 8.42.0 to 8.43.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/775">golangci/golangci-lint-action#775</a></li>
<li>build(deps): bump <code>@​types/node</code> from 20.3.0 to 20.3.1 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/777">golangci/golangci-lint-action#777</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 5.59.9 to 5.59.11 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/778">golangci/golangci-lint-action#778</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 5.59.11 to 5.60.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/782">golangci/golangci-lint-action#782</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from 5.59.11 to 5.60.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/783">golangci/golangci-lint-action#783</a></li>
<li>build(deps): bump <code>@​types/node</code> from 20.3.1 to 20.3.3 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/784">golangci/golangci-lint-action#784</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from 5.60.0 to 5.60.1 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/785">golangci/golangci-lint-action#785</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 5.60.0 to 5.60.1 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/786">golangci/golangci-lint-action#786</a></li>
<li>build(deps-dev): bump typescript from 5.1.3 to 5.1.6 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/787">golangci/golangci-lint-action#787</a></li>
<li>build(deps-dev): bump eslint from 8.43.0 to 8.44.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/788">golangci/golangci-lint-action#788</a></li>
<li>build(deps): bump tough-cookie and <code>@​azure/ms-rest-js</code> by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/790">golangci/golangci-lint-action#790</a></li>
<li>build(deps): bump <code>@​types/node</code> from 20.3.3 to 20.4.1 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/791">golangci/golangci-lint-action#791</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from 5.60.1 to 5.61.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/792">golangci/golangci-lint-action#792</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 5.60.1 to 5.61.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/793">golangci/golangci-lint-action#793</a></li>
<li>build(deps): bump semver from 6.3.0 to 6.3.1 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/796">golangci/golangci-lint-action#796</a></li>
<li>build(deps-dev): bump prettier from 2.8.8 to 3.0.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/794">golangci/golangci-lint-action#794</a></li>
<li>build(deps): bump <code>@​types/node</code> from 20.4.1 to 20.4.2 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/798">golangci/golangci-lint-action#798</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from 5.61.0 to 5.62.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/799">golangci/golangci-lint-action#799</a></li>
<li>build(deps-dev): bump eslint from 8.44.0 to 8.45.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/801">golangci/golangci-lint-action#801</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 5.61.0 to 6.0.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/800">golangci/golangci-lint-action#800</a></li>
<li>build(deps): bump <code>@​types/node</code> from 20.4.2 to 20.4.4 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/803">golangci/golangci-lint-action#803</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 6.0.0 to 6.1.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/804">golangci/golangci-lint-action#804</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from 6.0.0 to 6.1.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/805">golangci/golangci-lint-action#805</a></li>
<li>build(deps-dev): bump eslint-plugin-import from 2.27.5 to 2.28.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/808">golangci/golangci-lint-action#808</a></li>
<li>build(deps): bump <code>@​types/node</code> from 20.4.4 to 20.4.5 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/809">golangci/golangci-lint-action#809</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 6.1.0 to 6.2.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/810">golangci/golangci-lint-action#810</a></li>
<li>build(deps-dev): bump eslint-config-prettier from 8.8.0 to 8.9.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/811">golangci/golangci-lint-action#811</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from 6.1.0 to 6.2.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/812">golangci/golangci-lint-action#812</a></li>
<li>build(deps): bump <code>@​types/node</code> from 20.4.5 to 20.4.8 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/815">golangci/golangci-lint-action#815</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 6.2.0 to 6.2.1 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/816">golangci/golangci-lint-action#816</a></li>
<li>build(deps-dev): bump prettier from 3.0.0 to 3.0.1 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/817">golangci/golangci-lint-action#817</a></li>
<li>build(deps-dev): bump eslint-config-prettier from 8.9.0 to 9.0.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/818">golangci/golangci-lint-action#818</a></li>
<li>build(deps): bump <code>@​actions/http-client</code> from 2.1.0 to 2.1.1 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/819">golangci/golangci-lint-action#819</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from 6.2.0 to 6.3.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/821">golangci/golangci-lint-action#821</a></li>
<li>build(deps-dev): bump eslint from 8.45.0 to 8.47.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/823">golangci/golangci-lint-action#823</a></li>
<li>build(deps): bump <code>@​types/node</code> from 20.4.8 to 20.5.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/824">golangci/golangci-lint-action#824</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 6.2.1 to 6.3.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/golangci/golangci-lint-action/pull/822">golangci/golangci-lint-action#822</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/golangci/golangci-lint-action/commit/3cfe3a4abbb849e10058ce4af15d205b6da42804"><code>3cfe3a4</code></a> build(deps): bump <code>@​actions/cache</code> from 3.2.3 to 3.2.4 (<a href="https://redirect.github.com/golangci/golangci-lint-action/issues/963">#963</a>)</li>
<li><a href="https://github.com/golangci/golangci-lint-action/commit/cbc59cf0d18a8aa6b4806cab8d5a6ad3622f57fe"><code>cbc59cf</code></a> build(deps-dev): bump prettier from 3.2.4 to 3.2.5 (<a href="https://redirect.github.com/golangci/golangci-lint-action/issues/960">#960</a>)</li>
<li><a href="https://github.com/golangci/golangci-lint-action/commit/459a04b021b544b77372de9a6e429e0d3c136fac"><code>459a04b</code></a> build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 6.19.1 to 6.20.0 ...</li>
<li><a href="https://github.com/golangci/golangci-lint-action/commit/e2315b67db0fd76598007338449373a42c572b25"><code>e2315b6</code></a> build(deps-dev): bump <code>@​typescript-eslint/parser</code> from 6.19.1 to 6.20.0 (<a href="https://redirect.github.com/golangci/golangci-lint-action/issues/961">#961</a>)</li>
<li><a href="https://github.com/golangci/golangci-lint-action/commit/d6173a45d0441c7db097b6014862dde0ddc9b579"><code>d6173a4</code></a> build(deps): bump <code>@​types/node</code> from 20.11.10 to 20.11.16 (<a href="https://redirect.github.com/golangci/golangci-lint-action/issues/962">#962</a>)</li>
<li><a href="https://github.com/golangci/golangci-lint-action/commit/0e8f5bf773dd8000d6aeb01936f3090976982f33"><code>0e8f5bf</code></a> build(deps): bump <code>@​types/node</code> from 20.11.5 to 20.11.10 (<a href="https://redirect.github.com/golangci/golangci-lint-action/issues/958">#958</a>)</li>
<li><a href="https://github.com/golangci/golangci-lint-action/commit/349d20632dbaed38f0a492cc991152e3d351e854"><code>349d206</code></a> build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 6.19.0 to 6.19.1 ...</li>
<li><a href="https://github.com/golangci/golangci-lint-action/commit/2221aee28499deb9551e403a0d876df4b0e70067"><code>2221aee</code></a> build(deps-dev): bump <code>@​typescript-eslint/parser</code> from 6.18.1 to 6.19.1 (<a href="https://redirect.github.com/golangci/golangci-lint-action/issues/954">#954</a>)</li>
<li><a href="https://github.com/golangci/golangci-lint-action/commit/3b44ae5b24a084588d4051d37ccb95e8a709a9a3"><code>3b44ae5</code></a> build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 6.18.1 to 6.19.0 ...</li>
<li><a href="https://github.com/golangci/golangci-lint-action/commit/323b871bbc74bdc20959d979cb52c68910b77f37"><code>323b871</code></a> build(deps-dev): bump prettier from 3.2.2 to 3.2.4 (<a href="https://redirect.github.com/golangci/golangci-lint-action/issues/950">#950</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/golangci/golangci-lint-action/compare/v3...v4">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golangci/golangci-lint-action&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..d834ea6
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,1 @@
+*  @meowgorithm @muesli
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 067d42c..d197e45 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -24,5 +24,5 @@ jobs:
           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
+          go install github.com/mattn/goveralls@latest
+          goveralls -coverprofile=profile.cov -service=github
diff --git a/.github/workflows/lint-soft.yml b/.github/workflows/lint-soft.yml
index 4f3fbc0..a1eafa0 100644
--- a/.github/workflows/lint-soft.yml
+++ b/.github/workflows/lint-soft.yml
@@ -20,7 +20,7 @@ jobs:
 
       - uses: actions/checkout@v4
       - name: golangci-lint
-        uses: golangci/golangci-lint-action@v3
+        uses: golangci/golangci-lint-action@v4
         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 10df8cc..881d0d3 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -20,7 +20,7 @@ jobs:
 
       - uses: actions/checkout@v4
       - name: golangci-lint
-        uses: golangci/golangci-lint-action@v3
+        uses: golangci/golangci-lint-action@v4
         with:
           # Optional: golangci-lint command line arguments.
           #args:
diff --git a/README.md b/README.md
index c5eefd6..91100cf 100644
--- a/README.md
+++ b/README.md
@@ -316,6 +316,7 @@ your program in another window.
 
 For some Bubble Tea programs in production, see:
 
+* [ASCII Movie](https://github.com/gabe565/ascii-movie): Star Wars ASCII art movie player
 * [AT CLI](https://github.com/daskycodes/at_cli): execute AT Commands via serial port connections
 * [Aztify](https://github.com/Azure/aztfy): bring Microsoft Azure resources under Terraform
 * [brows](https://github.com/rubysolo/brows): a GitHub release browser
@@ -363,6 +364,7 @@ For some Bubble Tea programs in production, see:
 * [nom](https://github.com/guyfedwards/nom): an RSS reader and manager
 * [pathos](https://github.com/chip/pathos): a PATH environment variable editor
 * [portal](https://github.com/ZinoKader/portal): secure transfers between computers
+* [puffin](https://github.com/siddhantac/puffin): a TUI for hledger to manage your finances
 * [redis-viewer](https://github.com/SaltFishPr/redis-viewer): a Redis database browser
 * [redis_tui](https://github.com/mat2cc/redis_tui): a Redis database browser
 * [scrabbler](https://github.com/wI2L/scrabbler): an automatic draw TUI for your duplicate Scrabble games
diff --git a/commands.go b/commands.go
index 7b139b8..164dad2 100644
--- a/commands.go
+++ b/commands.go
@@ -20,11 +20,15 @@ func Batch(cmds ...Cmd) Cmd {
 		}
 		validCmds = append(validCmds, c)
 	}
-	if len(validCmds) == 0 {
+	switch len(validCmds) {
+	case 0:
 		return nil
-	}
-	return func() Msg {
-		return BatchMsg(validCmds)
+	case 1:
+		return validCmds[0]
+	default:
+		return func() Msg {
+			return BatchMsg(validCmds)
+		}
 	}
 }
 
diff --git a/examples/file-picker/main.go b/examples/file-picker/main.go
index f0470cd..c67bcc8 100644
--- a/examples/file-picker/main.go
+++ b/examples/file-picker/main.go
@@ -88,7 +88,7 @@ func main() {
 	m := model{
 		filepicker: fp,
 	}
-	tm, _ := tea.NewProgram(&m, tea.WithOutput(os.Stderr)).Run()
+	tm, _ := tea.NewProgram(&m).Run()
 	mm := tm.(model)
 	fmt.Println("\n  You selected: " + m.filepicker.Styles.Selected.Render(mm.selectedFile) + "\n")
 }
diff --git a/examples/go.mod b/examples/go.mod
index 155537a..ee3928c 100644
--- a/examples/go.mod
+++ b/examples/go.mod
@@ -8,7 +8,7 @@ require (
 	github.com/charmbracelet/glamour v0.6.0
 	github.com/charmbracelet/harmonica v0.2.0
 	github.com/charmbracelet/lipgloss v0.9.1
-	github.com/charmbracelet/x/exp/teatest v0.0.0-20230508155401-2bd6fa14c46a
+	github.com/charmbracelet/x/exp/teatest v0.0.0-20240229115032-4b79243a3516
 	github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776
 	github.com/lucasb-eyer/go-colorful v1.2.0
 	github.com/mattn/go-isatty v0.0.20
@@ -20,16 +20,17 @@ require (
 	github.com/alecthomas/chroma v0.10.0 // indirect
 	github.com/atotto/clipboard v0.1.4 // indirect
 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
-	github.com/aymanbagabas/go-udiff v0.1.0 // indirect
+	github.com/aymanbagabas/go-udiff v0.2.0 // indirect
 	github.com/aymerick/douceur v0.2.0 // indirect
-	github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
+	github.com/charmbracelet/x/exp/golden v0.0.0-20240222125807-0344fda748f8 // indirect
 	github.com/dlclark/regexp2 v1.4.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.0 // indirect
 	github.com/mattn/go-localereader v0.0.1 // indirect
 	github.com/mattn/go-runewidth v0.0.15 // indirect
 	github.com/microcosm-cc/bluemonday v1.0.21 // indirect
-	github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
+	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
 	github.com/muesli/cancelreader v0.2.2 // indirect
 	github.com/olekukonko/tablewriter v0.0.5 // indirect
 	github.com/rivo/uniseg v0.4.6 // indirect
@@ -37,9 +38,9 @@ require (
 	github.com/yuin/goldmark v1.5.2 // indirect
 	github.com/yuin/goldmark-emoji v1.0.1 // indirect
 	golang.org/x/net v0.17.0 // indirect
-	golang.org/x/sync v0.5.0 // indirect
-	golang.org/x/sys v0.16.0 // indirect
-	golang.org/x/term v0.16.0 // indirect
+	golang.org/x/sync v0.6.0 // indirect
+	golang.org/x/sys v0.17.0 // indirect
+	golang.org/x/term v0.17.0 // indirect
 	golang.org/x/text v0.13.0 // indirect
 )
 
diff --git a/examples/go.sum b/examples/go.sum
index 38003dc..ac642c5 100644
--- a/examples/go.sum
+++ b/examples/go.sum
@@ -5,8 +5,8 @@ github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn
 github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
 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/aymanbagabas/go-udiff v0.1.0 h1:9Dpklm2oBBhMxIFbMffmPvDaF7vOYfv9B5HXVr42KMU=
-github.com/aymanbagabas/go-udiff v0.1.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
+github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
+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 v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
@@ -17,10 +17,10 @@ github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG
 github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
 github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
 github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
-github.com/charmbracelet/x/exp/teatest v0.0.0-20230508155401-2bd6fa14c46a h1:GpNt24LKE8sH5G0SZUpu4Tg15sP5XSt1mnfIqE7fW34=
-github.com/charmbracelet/x/exp/teatest v0.0.0-20230508155401-2bd6fa14c46a/go.mod h1:dpMo6PfGlhavn+ofggWhfFQmK9sZB2Ewljiz9bZtKVI=
-github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
-github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
+github.com/charmbracelet/x/exp/golden v0.0.0-20240222125807-0344fda748f8 h1:kyT+aGp1z5jwlus3OY0cP6FuT05jYeeExx/4TYxnyrs=
+github.com/charmbracelet/x/exp/golden v0.0.0-20240222125807-0344fda748f8/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
+github.com/charmbracelet/x/exp/teatest v0.0.0-20240229115032-4b79243a3516 h1:7IZFEUZpEgjlTSd7P1MRRhGXs7t4F6mENeMw17TxnQs=
+github.com/charmbracelet/x/exp/teatest v0.0.0-20240229115032-4b79243a3516/go.mod h1:SG24wGkG/mix5V2dZLXfQ6Bod43HGvk9CkTDxATwKN4=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -28,6 +28,8 @@ github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E
 github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
 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.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
@@ -47,8 +49,8 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ
 github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
 github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
-github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
-github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
 github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
 github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
@@ -77,18 +79,18 @@ github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGj
 golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
 golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
-golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
-golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
-golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
+golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
diff --git a/examples/list-fancy/main.go b/examples/list-fancy/main.go
index 3646ceb..dfa580a 100644
--- a/examples/list-fancy/main.go
+++ b/examples/list-fancy/main.go
@@ -118,7 +118,7 @@ func newModel() model {
 }
 
 func (m model) Init() tea.Cmd {
-	return tea.EnterAltScreen
+	return nil
 }
 
 func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -183,7 +183,7 @@ func (m model) View() string {
 func main() {
 	rand.Seed(time.Now().UTC().UnixNano())
 
-	if _, err := tea.NewProgram(newModel()).Run(); err != nil {
+	if _, err := tea.NewProgram(newModel(), tea.WithAltScreen()).Run(); err != nil {
 		fmt.Println("Error running program:", err)
 		os.Exit(1)
 	}
diff --git a/go.mod b/go.mod
index 04ee57b..53eb8ae 100644
--- a/go.mod
+++ b/go.mod
@@ -3,22 +3,22 @@ module github.com/charmbracelet/bubbletea
 go 1.18
 
 require (
-	github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81
-	github.com/mattn/go-isatty v0.0.20
+	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f
 	github.com/mattn/go-localereader v0.0.1
-	github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b
+	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
 	github.com/muesli/cancelreader v0.2.2
 	github.com/muesli/reflow v0.3.0
 	github.com/muesli/termenv v0.15.2
-	golang.org/x/sync v0.5.0
-	golang.org/x/term v0.16.0
+	golang.org/x/sync v0.6.0
+	golang.org/x/sys v0.17.0
+	golang.org/x/term v0.17.0
 )
 
 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.18 // indirect
 	github.com/mattn/go-runewidth v0.0.15 // indirect
 	github.com/rivo/uniseg v0.4.6 // indirect
-	golang.org/x/sys v0.16.0 // indirect
 	golang.org/x/text v0.3.8 // indirect
 )
diff --git a/go.sum b/go.sum
index 13bd01e..39b77dd 100644
--- a/go.sum
+++ b/go.sum
@@ -1,18 +1,18 @@
 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/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
-github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
+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/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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
+github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
 github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
 github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
-github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
 github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
 github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
@@ -23,13 +23,13 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
 github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
-golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
-golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
+golang.org/x/sync v0.6.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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
-golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
+golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
 golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
diff --git a/inputreader_other.go b/inputreader_other.go
new file mode 100644
index 0000000..8e63a87
--- /dev/null
+++ b/inputreader_other.go
@@ -0,0 +1,14 @@
+//go:build !windows
+// +build !windows
+
+package tea
+
+import (
+	"io"
+
+	"github.com/muesli/cancelreader"
+)
+
+func newInputReader(r io.Reader) (cancelreader.CancelReader, error) {
+	return cancelreader.NewReader(r)
+}
diff --git a/inputreader_windows.go b/inputreader_windows.go
new file mode 100644
index 0000000..8daa6e6
--- /dev/null
+++ b/inputreader_windows.go
@@ -0,0 +1,154 @@
+//go:build windows
+// +build windows
+
+package tea
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"sync"
+
+	"github.com/erikgeiser/coninput"
+	"github.com/muesli/cancelreader"
+	"golang.org/x/sys/windows"
+)
+
+type conInputReader struct {
+	cancelMixin
+
+	conin       windows.Handle
+	cancelEvent windows.Handle
+
+	originalMode uint32
+
+	// inputEvent holds the input event that was read in order to avoid
+	// unneccessary allocations. This re-use is possible because
+	// InputRecord.Unwarp which is called inparseInputMsgFromInputRecord
+	// returns an data structure that is independent of the passed InputRecord.
+	inputEvent []coninput.InputRecord
+}
+
+var _ cancelreader.CancelReader = &conInputReader{}
+
+func newInputReader(r io.Reader) (cancelreader.CancelReader, error) {
+	fallback := func(io.Reader) (cancelreader.CancelReader, error) {
+		return cancelreader.NewReader(r)
+	}
+	if f, ok := r.(*os.File); !ok || f.Fd() != os.Stdin.Fd() {
+		return fallback(r)
+	}
+
+	conin, err := coninput.NewStdinHandle()
+	if err != nil {
+		return fallback(r)
+	}
+
+	originalMode, err := prepareConsole(conin,
+		windows.ENABLE_MOUSE_INPUT,
+		windows.ENABLE_WINDOW_INPUT,
+		windows.ENABLE_PROCESSED_INPUT,
+		windows.ENABLE_EXTENDED_FLAGS,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("failed to prepare console input: %w", err)
+	}
+
+	cancelEvent, err := windows.CreateEvent(nil, 0, 0, nil)
+	if err != nil {
+		return nil, fmt.Errorf("create stop event: %w", err)
+	}
+
+	return &conInputReader{
+		conin:        conin,
+		cancelEvent:  cancelEvent,
+		originalMode: originalMode,
+	}, nil
+}
+
+// Cancel implements cancelreader.CancelReader.
+func (r *conInputReader) Cancel() bool {
+	r.setCanceled()
+
+	err := windows.SetEvent(r.cancelEvent)
+	if err != nil {
+		return false
+	}
+
+	return true
+}
+
+// Close implements cancelreader.CancelReader.
+func (r *conInputReader) Close() error {
+	err := windows.CloseHandle(r.cancelEvent)
+	if err != nil {
+		return fmt.Errorf("closing cancel event handle: %w", err)
+	}
+
+	if r.originalMode != 0 {
+		err := windows.SetConsoleMode(r.conin, r.originalMode)
+		if err != nil {
+			return fmt.Errorf("reset console mode: %w", err)
+		}
+	}
+
+	return nil
+}
+
+// Read implements cancelreader.CancelReader.
+func (*conInputReader) Read(_ []byte) (n int, err error) {
+	return 0, nil
+}
+
+func prepareConsole(input windows.Handle, modes ...uint32) (originalMode uint32, err error) {
+	err = windows.GetConsoleMode(input, &originalMode)
+	if err != nil {
+		return 0, fmt.Errorf("get console mode: %w", err)
+	}
+
+	newMode := coninput.AddInputModes(0, modes...)
+
+	err = windows.SetConsoleMode(input, newMode)
+	if err != nil {
+		return 0, fmt.Errorf("set console mode: %w", err)
+	}
+
+	return originalMode, nil
+}
+
+func waitForInput(conin, cancel windows.Handle) error {
+	event, err := windows.WaitForMultipleObjects([]windows.Handle{conin, cancel}, false, windows.INFINITE)
+	switch {
+	case windows.WAIT_OBJECT_0 <= event && event < windows.WAIT_OBJECT_0+2:
+		if event == windows.WAIT_OBJECT_0+1 {
+			return cancelreader.ErrCanceled
+		}
+
+		if event == windows.WAIT_OBJECT_0 {
+			return nil
+		}
+
+		return fmt.Errorf("unexpected wait object is ready: %d", event-windows.WAIT_OBJECT_0)
+	case windows.WAIT_ABANDONED <= event && event < windows.WAIT_ABANDONED+2:
+		return fmt.Errorf("abandoned")
+	case event == uint32(windows.WAIT_TIMEOUT):
+		return fmt.Errorf("timeout")
+	case event == windows.WAIT_FAILED:
+		return fmt.Errorf("failed")
+	default:
+		return fmt.Errorf("unexpected error: %w", err)
+	}
+}
+
+// cancelMixin represents a goroutine-safe cancelation status.
+type cancelMixin struct {
+	unsafeCanceled bool
+	lock           sync.Mutex
+}
+
+func (c *cancelMixin) setCanceled() {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+
+	c.unsafeCanceled = true
+}
diff --git a/key.go b/key.go
index 05d0440..f659a06 100644
--- a/key.go
+++ b/key.go
@@ -553,9 +553,9 @@ func (u unknownCSISequenceMsg) String() string {
 
 var spaceRunes = []rune{' '}
 
-// readInputs reads keypress and mouse inputs from a TTY and produces messages
+// readAnsiInputs reads keypress and mouse inputs from a TTY and produces messages
 // containing information about the key or mouse events accordingly.
-func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error {
+func readAnsiInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error {
 	var buf [256]byte
 
 	var leftOverFromPrevIteration []byte
diff --git a/key_other.go b/key_other.go
new file mode 100644
index 0000000..b8c4608
--- /dev/null
+++ b/key_other.go
@@ -0,0 +1,13 @@
+//go:build !windows
+// +build !windows
+
+package tea
+
+import (
+	"context"
+	"io"
+)
+
+func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error {
+	return readAnsiInputs(ctx, msgs, input)
+}
diff --git a/key_windows.go b/key_windows.go
new file mode 100644
index 0000000..c4b8aa7
--- /dev/null
+++ b/key_windows.go
@@ -0,0 +1,277 @@
+//go:build windows
+// +build windows
+
+package tea
+
+import (
+	"context"
+	"fmt"
+	"io"
+
+	"github.com/erikgeiser/coninput"
+	localereader "github.com/mattn/go-localereader"
+	"golang.org/x/sys/windows"
+)
+
+func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error {
+	if coninReader, ok := input.(*conInputReader); ok {
+		return readConInputs(ctx, msgs, coninReader.conin)
+	}
+
+	return readAnsiInputs(ctx, msgs, localereader.NewReader(input))
+}
+
+func readConInputs(ctx context.Context, msgsch chan<- Msg, con windows.Handle) error {
+	var ps coninput.ButtonState // keep track of previous mouse state
+	for {
+		events, err := coninput.ReadNConsoleInputs(con, 16)
+		if err != nil {
+			return fmt.Errorf("read coninput events: %w", err)
+		}
+
+		for _, event := range events {
+			var msgs []Msg
+			switch e := event.Unwrap().(type) {
+			case coninput.KeyEventRecord:
+				if !e.KeyDown || e.VirtualKeyCode == coninput.VK_SHIFT {
+					continue
+				}
+
+				for i := 0; i < int(e.RepeatCount); i++ {
+					msgs = append(msgs, KeyMsg{
+						Type:  keyType(e),
+						Runes: []rune{e.Char},
+						Alt:   e.ControlKeyState.Contains(coninput.LEFT_ALT_PRESSED | coninput.RIGHT_ALT_PRESSED),
+					})
+				}
+			case coninput.WindowBufferSizeEventRecord:
+				msgs = append(msgs, WindowSizeMsg{
+					Width:  int(e.Size.X),
+					Height: int(e.Size.Y),
+				})
+			case coninput.MouseEventRecord:
+				event := mouseEvent(ps, e)
+				if event.Type != MouseUnknown {
+					msgs = append(msgs, event)
+				}
+				ps = e.ButtonState
+			case coninput.FocusEventRecord, coninput.MenuEventRecord:
+				// ignore
+			default: // unknown event
+				continue
+			}
+
+			// Send all messages to the channel
+			for _, msg := range msgs {
+				select {
+				case msgsch <- msg:
+				case <-ctx.Done():
+					err := ctx.Err()
+					if err != nil {
+						return fmt.Errorf("coninput context error: %w", err)
+					}
+					return err
+				}
+			}
+		}
+	}
+}
+
+func mouseEventButton(p, s coninput.ButtonState) (button MouseButton, action MouseAction) {
+	btn := p ^ s
+	action = MouseActionPress
+	if btn&s == 0 {
+		action = MouseActionRelease
+	}
+
+	if btn == 0 {
+		switch {
+		case s&coninput.FROM_LEFT_1ST_BUTTON_PRESSED > 0:
+			button = MouseButtonLeft
+		case s&coninput.FROM_LEFT_2ND_BUTTON_PRESSED > 0:
+			button = MouseButtonMiddle
+		case s&coninput.RIGHTMOST_BUTTON_PRESSED > 0:
+			button = MouseButtonRight
+		case s&coninput.FROM_LEFT_3RD_BUTTON_PRESSED > 0:
+			button = MouseButtonBackward
+		case s&coninput.FROM_LEFT_4TH_BUTTON_PRESSED > 0:
+			button = MouseButtonForward
+		}
+		return
+	}
+
+	switch {
+	case btn == coninput.FROM_LEFT_1ST_BUTTON_PRESSED: // left button
+		button = MouseButtonLeft
+	case btn == coninput.RIGHTMOST_BUTTON_PRESSED: // right button
+		button = MouseButtonRight
+	case btn == coninput.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button
+		button = MouseButtonMiddle
+	case btn == coninput.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward)
+		button = MouseButtonBackward
+	case btn == coninput.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward)
+		button = MouseButtonForward
+	}
+
+	return button, action
+}
+
+func mouseEvent(p coninput.ButtonState, e coninput.MouseEventRecord) MouseMsg {
+	ev := MouseMsg{
+		X:     int(e.MousePositon.X),
+		Y:     int(e.MousePositon.Y),
+		Alt:   e.ControlKeyState.Contains(coninput.LEFT_ALT_PRESSED | coninput.RIGHT_ALT_PRESSED),
+		Ctrl:  e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED | coninput.RIGHT_CTRL_PRESSED),
+		Shift: e.ControlKeyState.Contains(coninput.SHIFT_PRESSED),
+	}
+	switch e.EventFlags {
+	case coninput.CLICK, coninput.DOUBLE_CLICK:
+		ev.Button, ev.Action = mouseEventButton(p, e.ButtonState)
+		if ev.Action == MouseActionRelease {
+			ev.Type = MouseRelease
+		}
+		switch ev.Button {
+		case MouseButtonLeft:
+			ev.Type = MouseLeft
+		case MouseButtonMiddle:
+			ev.Type = MouseMiddle
+		case MouseButtonRight:
+			ev.Type = MouseRight
+		case MouseButtonBackward:
+			ev.Type = MouseBackward
+		case MouseButtonForward:
+			ev.Type = MouseForward
+		}
+	case coninput.MOUSE_WHEELED:
+		if e.WheelDirection > 0 {
+			ev.Button = MouseButtonWheelUp
+			ev.Type = MouseWheelUp
+		} else {
+			ev.Button = MouseButtonWheelDown
+			ev.Type = MouseWheelDown
+		}
+	case coninput.MOUSE_HWHEELED:
+		if e.WheelDirection > 0 {
+			ev.Button = MouseButtonWheelRight
+			ev.Type = MouseWheelRight
+		} else {
+			ev.Button = MouseButtonWheelLeft
+			ev.Type = MouseWheelLeft
+		}
+	case coninput.MOUSE_MOVED:
+		ev.Button, _ = mouseEventButton(p, e.ButtonState)
+		ev.Action = MouseActionMotion
+		ev.Type = MouseMotion
+	}
+
+	return ev
+}
+
+func keyType(e coninput.KeyEventRecord) KeyType {
+	code := e.VirtualKeyCode
+
+	switch code {
+	case coninput.VK_RETURN:
+		return KeyEnter
+	case coninput.VK_BACK:
+		return KeyBackspace
+	case coninput.VK_TAB:
+		return KeyTab
+	case coninput.VK_SPACE:
+		return KeyRunes // this could be KeySpace but on unix space also produces KeyRunes
+	case coninput.VK_ESCAPE:
+		return KeyEscape
+	case coninput.VK_UP:
+		return KeyUp
+	case coninput.VK_DOWN:
+		return KeyDown
+	case coninput.VK_RIGHT:
+		return KeyRight
+	case coninput.VK_LEFT:
+		return KeyLeft
+	case coninput.VK_HOME:
+		return KeyHome
+	case coninput.VK_END:
+		return KeyEnd
+	case coninput.VK_PRIOR:
+		return KeyPgUp
+	case coninput.VK_NEXT:
+		return KeyPgDown
+	case coninput.VK_DELETE:
+		return KeyDelete
+	default:
+		if e.ControlKeyState&(coninput.LEFT_CTRL_PRESSED|coninput.RIGHT_CTRL_PRESSED) == 0 {
+			return KeyRunes
+		}
+
+		switch e.Char {
+		case '@':
+			return KeyCtrlAt
+		case '\x01':
+			return KeyCtrlA
+		case '\x02':
+			return KeyCtrlB
+		case '\x03':
+			return KeyCtrlC
+		case '\x04':
+			return KeyCtrlD
+		case '\x05':
+			return KeyCtrlE
+		case '\x06':
+			return KeyCtrlF
+		case '\a':
+			return KeyCtrlG
+		case '\b':
+			return KeyCtrlH
+		case '\t':
+			return KeyCtrlI
+		case '\n':
+			return KeyCtrlJ
+		case '\v':
+			return KeyCtrlK
+		case '\f':
+			return KeyCtrlL
+		case '\r':
+			return KeyCtrlM
+		case '\x0e':
+			return KeyCtrlN
+		case '\x0f':
+			return KeyCtrlO
+		case '\x10':
+			return KeyCtrlP
+		case '\x11':
+			return KeyCtrlQ
+		case '\x12':
+			return KeyCtrlR
+		case '\x13':
+			return KeyCtrlS
+		case '\x14':
+			return KeyCtrlT
+		case '\x15':
+			return KeyCtrlU
+		case '\x16':
+			return KeyCtrlV
+		case '\x17':
+			return KeyCtrlW
+		case '\x18':
+			return KeyCtrlX
+		case '\x19':
+			return KeyCtrlY
+		case '\x1a':
+			return KeyCtrlZ
+		case '\x1b':
+			return KeyCtrlCloseBracket
+		case '\x1c':
+			return KeyCtrlBackslash
+		case '\x1f':
+			return KeyCtrlUnderscore
+		}
+
+		switch code {
+		case coninput.VK_OEM_4:
+			return KeyCtrlOpenBracket
+		}
+
+		return KeyRunes
+	}
+}
diff --git a/standard_renderer.go b/standard_renderer.go
index 4fc3698..4e63fd6 100644
--- a/standard_renderer.go
+++ b/standard_renderer.go
@@ -173,19 +173,18 @@ func (r *standardRenderer) flush() {
 	skipLines := make(map[int]struct{})
 	flushQueuedMessages := len(r.queuedMessageLines) > 0 && !r.altScreenActive
 
-	// Add any queued messages to this render
-	if flushQueuedMessages {
-		newLines = append(r.queuedMessageLines, newLines...)
-		r.queuedMessageLines = []string{}
-	}
-
 	// Clear any lines we painted in the last render.
 	if r.linesRendered > 0 {
 		for i := r.linesRendered - 1; i > 0; i-- {
-			// If the number of lines we want to render hasn't increased and
-			// new line is the same as the old line we can skip rendering for
-			// this line as a performance optimization.
-			if (len(newLines) <= len(oldLines)) && (len(newLines) > i && len(oldLines) > i) && (newLines[i] == oldLines[i]) {
+			// if we are clearing queued messages, we want to clear all lines, since
+			// printing messages allows for native terminal word-wrap, we
+			// don't have control over the queued lines
+			if flushQueuedMessages {
+				out.ClearLine()
+			} else if (len(newLines) <= len(oldLines)) && (len(newLines) > i && len(oldLines) > i) && (newLines[i] == oldLines[i]) {
+				// If the number of lines we want to render hasn't increased and
+				// new line is the same as the old line we can skip rendering for
+				// this line as a performance optimization.
 				skipLines[i] = struct{}{}
 			} else if _, exists := r.ignoreLines[i]; !exists {
 				out.ClearLine()
@@ -215,6 +214,16 @@ func (r *standardRenderer) flush() {
 		skipLines[k] = v
 	}
 
+	if flushQueuedMessages {
+		// Dump the lines we've queued up for printing
+		for _, line := range r.queuedMessageLines {
+			_, _ = out.WriteString(line)
+			_, _ = out.WriteString("\r\n")
+		}
+		// clear the queued message lines
+		r.queuedMessageLines = []string{}
+	}
+
 	// Paint new lines
 	for i := 0; i < len(newLines); i++ {
 		if _, skip := skipLines[i]; skip {
diff --git a/tea.go b/tea.go
index b6a92b0..ee3d9cd 100644
--- a/tea.go
+++ b/tea.go
@@ -21,11 +21,10 @@ import (
 	"sync/atomic"
 	"syscall"
 
-	"github.com/containerd/console"
-	isatty "github.com/mattn/go-isatty"
 	"github.com/muesli/cancelreader"
 	"github.com/muesli/termenv"
 	"golang.org/x/sync/errgroup"
+	"golang.org/x/term"
 )
 
 // ErrProgramKilled is returned by [Program.Run] when the program got killed.
@@ -101,18 +100,19 @@ const (
 	withoutBracketedPaste
 )
 
-// handlers manages series of channels returned by various processes. It allows
-// us to wait for those processes to terminate before exiting the program.
-type handlers []chan struct{}
+// channelHandlers manages the series of channels returned by various processes.
+// It allows us to wait for those processes to terminate before exiting the
+// program.
+type channelHandlers []chan struct{}
 
 // Adds a channel to the list of handlers. We wait for all handlers to terminate
 // gracefully on shutdown.
-func (h *handlers) add(ch chan struct{}) {
+func (h *channelHandlers) add(ch chan struct{}) {
 	*h = append(*h, ch)
 }
 
 // shutdown waits for all handlers to terminate.
-func (h handlers) shutdown() {
+func (h channelHandlers) shutdown() {
 	var wg sync.WaitGroup
 	for _, ch := range h {
 		wg.Add(1)
@@ -147,10 +147,12 @@ type Program struct {
 	renderer      renderer
 
 	// where to read inputs from, this will usually be os.Stdin.
-	input        io.Reader
-	cancelReader cancelreader.CancelReader
-	readLoopDone chan struct{}
-	console      console.Console
+	input io.Reader
+	// tty is null if input is not a TTY.
+	tty              *os.File
+	previousTtyState *term.State
+	cancelReader     cancelreader.CancelReader
+	readLoopDone     chan struct{}
 
 	// was the altscreen active before releasing the terminal?
 	altScreenWasActive bool
@@ -158,15 +160,6 @@ type Program struct {
 
 	bpWasActive bool // was the bracketed paste mode active before releasing the terminal?
 
-	// Stores the original reference to stdin for cases where input is not a
-	// TTY on windows and we've automatically opened CONIN$ to receive input.
-	// When the program exits this will be restored.
-	//
-	// Lint ignore note: the linter will find false positive on unix systems
-	// as this value only comes into play on Windows, hence the ignore comment
-	// below.
-	windowsStdin *os.File //nolint:golint,structcheck,unused
-
 	filter func(Model, Msg) Msg
 
 	// fps is the frames per second we should set on the renderer, if
@@ -256,7 +249,7 @@ func (p *Program) handleSignals() chan struct{} {
 func (p *Program) handleResize() chan struct{} {
 	ch := make(chan struct{})
 
-	if f, ok := p.output.TTY().(*os.File); ok && isatty.IsTerminal(f.Fd()) {
+	if f, ok := p.output.TTY().(*os.File); ok && term.IsTerminal(int(f.Fd())) {
 		// Get the initial terminal size and send it to the program.
 		go p.checkResize()
 
@@ -427,7 +420,7 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
 // terminated by either [Program.Quit], [Program.Kill], or its signal handler.
 // Returns the final model.
 func (p *Program) Run() (Model, error) {
-	handlers := handlers{}
+	handlers := channelHandlers{}
 	cmds := make(chan Cmd)
 	p.errs = make(chan error)
 	p.finished = make(chan struct{}, 1)
@@ -448,7 +441,7 @@ func (p *Program) Run() (Model, error) {
 		if !isFile {
 			break
 		}
-		if isatty.IsTerminal(f.Fd()) {
+		if term.IsTerminal(int(f.Fd())) {
 			break
 		}
 
diff --git a/tty.go b/tty.go
index 015720d..e7b602c 100644
--- a/tty.go
+++ b/tty.go
@@ -7,25 +7,15 @@ import (
 	"os"
 	"time"
 
-	isatty "github.com/mattn/go-isatty"
-	localereader "github.com/mattn/go-localereader"
 	"github.com/muesli/cancelreader"
 	"golang.org/x/term"
 )
 
 func (p *Program) initTerminal() error {
-	err := p.initInput()
-	if err != nil {
+	if err := p.initInput(); err != nil {
 		return err
 	}
 
-	if p.console != nil {
-		err = p.console.SetRaw()
-		if err != nil {
-			return fmt.Errorf("error entering raw mode: %w", err)
-		}
-	}
-
 	p.renderer.hideCursor()
 	return nil
 }
@@ -46,20 +36,23 @@ func (p *Program) restoreTerminalState() error {
 		}
 	}
 
-	if p.console != nil {
-		err := p.console.Reset()
-		if err != nil {
-			return fmt.Errorf("error restoring terminal state: %w", err)
+	return p.restoreInput()
+}
+
+// restoreInput restores the tty input to its original state.
+func (p *Program) restoreInput() error {
+	if p.tty != nil && p.previousTtyState != nil {
+		if err := term.Restore(int(p.tty.Fd()), p.previousTtyState); err != nil {
+			return fmt.Errorf("error restoring console: %w", err)
 		}
 	}
-
-	return p.restoreInput()
+	return nil
 }
 
 // initCancelReader (re)commences reading inputs.
 func (p *Program) initCancelReader() error {
 	var err error
-	p.cancelReader, err = cancelreader.NewReader(p.input)
+	p.cancelReader, err = newInputReader(p.input)
 	if err != nil {
 		return fmt.Errorf("error creating cancelreader: %w", err)
 	}
@@ -73,8 +66,7 @@ func (p *Program) initCancelReader() error {
 func (p *Program) readLoop() {
 	defer close(p.readLoopDone)
 
-	input := localereader.NewReader(p.cancelReader)
-	err := readInputs(p.ctx, p.msgs, input)
+	err := readInputs(p.ctx, p.msgs, p.cancelReader)
 	if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) {
 		select {
 		case <-p.ctx.Done():
@@ -98,7 +90,7 @@ func (p *Program) waitForReadLoop() {
 // via a WindowSizeMsg.
 func (p *Program) checkResize() {
 	f, ok := p.output.TTY().(*os.File)
-	if !ok || !isatty.IsTerminal(f.Fd()) {
+	if !ok || !term.IsTerminal(int(f.Fd())) {
 		// can't query window size
 		return
 	}
diff --git a/tty_unix.go b/tty_unix.go
index d5436af..a93444d 100644
--- a/tty_unix.go
+++ b/tty_unix.go
@@ -7,35 +7,22 @@ import (
 	"fmt"
 	"os"
 
-	"github.com/containerd/console"
+	"golang.org/x/term"
 )
 
-func (p *Program) initInput() error {
-	// If input's a file, use console to manage it
-	if f, ok := p.input.(*os.File); ok {
-		c, err := console.ConsoleFromFile(f)
+func (p *Program) initInput() (err error) {
+	// Check if input is a terminal
+	if f, ok := p.input.(*os.File); ok && term.IsTerminal(int(f.Fd())) {
+		p.tty = f
+		p.previousTtyState, err = term.MakeRaw(int(p.tty.Fd()))
 		if err != nil {
-			return nil //nolint:nilerr // ignore error, this was just a test
+			return fmt.Errorf("error entering raw mode: %w", err)
 		}
-		p.console = c
 	}
 
 	return nil
 }
 
-// On unix systems, RestoreInput closes any TTYs we opened for input. Note that
-// we don't do this on Windows as it causes the prompt to not be drawn until
-// the terminal receives a keypress rather than appearing promptly after the
-// program exits.
-func (p *Program) restoreInput() error {
-	if p.console != nil {
-		if err := p.console.Reset(); err != nil {
-			return fmt.Errorf("error restoring console: %w", err)
-		}
-	}
-	return nil
-}
-
 func openInputTTY() (*os.File, error) {
 	f, err := os.Open("/dev/tty")
 	if err != nil {
diff --git a/tty_windows.go b/tty_windows.go
index be415ae..10b6108 100644
--- a/tty_windows.go
+++ b/tty_windows.go
@@ -4,37 +4,36 @@
 package tea
 
 import (
+	"fmt"
 	"os"
 
-	"github.com/containerd/console"
+	"golang.org/x/sys/windows"
+	"golang.org/x/term"
 )
 
-func (p *Program) initInput() error {
-	// If input's a file, use console to manage it
-	if f, ok := p.input.(*os.File); ok {
-		// Save a reference to the current stdin then replace stdin with our
-		// input. We do this so we can hand input off to containerd/console to
-		// set raw mode, and do it in this fashion because the method
-		// console.ConsoleFromFile isn't supported on Windows.
-		p.windowsStdin = os.Stdin
-		os.Stdin = f
-
-		// Note: this will panic if it fails.
-		c := console.Current()
-		p.console = c
+func (p *Program) initInput() (err error) {
+	// Save stdin state and enable VT input
+	// We enable VT processing using Termenv, but we also need to enable VT
+	// input here.
+	if f, ok := p.input.(*os.File); ok && term.IsTerminal(int(f.Fd())) {
+		p.tty = f
+		p.previousTtyState, err = term.MakeRaw(int(p.tty.Fd()))
+		if err != nil {
+			return err
+		}
+
+		// Enable VT input
+		var mode uint32
+		if err := windows.GetConsoleMode(windows.Handle(p.tty.Fd()), &mode); err != nil {
+			return fmt.Errorf("error getting console mode: %w", err)
+		}
+
+		if err := windows.SetConsoleMode(windows.Handle(p.tty.Fd()), mode|windows.ENABLE_VIRTUAL_TERMINAL_INPUT); err != nil {
+			return fmt.Errorf("error setting console mode: %w", err)
+		}
 	}
 
-	return nil
-}
-
-// restoreInput restores stdout in the event that we placed it aside to handle
-// input with CONIN$, above.
-func (p *Program) restoreInput() error {
-	if p.windowsStdin != nil {
-		os.Stdin = p.windowsStdin
-	}
-
-	return nil
+	return
 }
 
 // Open the Windows equivalent of a TTY.
diff --git a/tutorials/go.mod b/tutorials/go.mod
index 546f70d..80bfc8b 100644
--- a/tutorials/go.mod
+++ b/tutorials/go.mod
@@ -6,19 +6,19 @@ require github.com/charmbracelet/bubbletea v0.25.0
 
 require (
 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
-	github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
+	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-localereader v0.0.1 // indirect
 	github.com/mattn/go-runewidth v0.0.15 // indirect
-	github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
+	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
 	github.com/muesli/cancelreader v0.2.2 // indirect
 	github.com/muesli/reflow v0.3.0 // indirect
 	github.com/muesli/termenv v0.15.2 // indirect
 	github.com/rivo/uniseg v0.4.6 // indirect
-	golang.org/x/sync v0.5.0 // indirect
-	golang.org/x/sys v0.16.0 // indirect
-	golang.org/x/term v0.16.0 // indirect
+	golang.org/x/sync v0.6.0 // indirect
+	golang.org/x/sys v0.17.0 // indirect
+	golang.org/x/term v0.17.0 // indirect
 	golang.org/x/text v0.13.0 // indirect
 )
 
diff --git a/tutorials/go.sum b/tutorials/go.sum
index fb8aedf..08417d0 100644
--- a/tutorials/go.sum
+++ b/tutorials/go.sum
@@ -1,7 +1,7 @@
 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/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
-github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
+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/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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -11,8 +11,8 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei
 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
 github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
-github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
 github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
 github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
@@ -23,13 +23,13 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
 github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
-golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
-golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
+golang.org/x/sync v0.6.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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
-golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
+golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=


Test patch:
diff --git a/commands_test.go b/commands_test.go
index 0ec1419..feb1ee8 100644
--- a/commands_test.go
+++ b/commands_test.go
@@ -94,8 +94,8 @@ func TestBatch(t *testing.T) {
 	})
 	t.Run("single cmd", func(t *testing.T) {
 		b := Batch(Quit)()
-		if l := len(b.(BatchMsg)); l != 1 {
-			t.Fatalf("expected a []Cmd with len 1, got %d", l)
+		if _, ok := b.(QuitMsg); !ok {
+			t.Fatalf("expected a QuitMsg, got %T", b)
 		}
 	})
 	t.Run("mixed nil cmds", func(t *testing.T) {
diff --git a/key_test.go b/key_test.go
index 830b8cf..578faef 100644
--- a/key_test.go
+++ b/key_test.go
@@ -528,7 +528,7 @@ func testReadInputs(t *testing.T, input io.Reader) []Msg {
 	wg.Add(1)
 	go func() {
 		defer wg.Done()
-		inputErr = readInputs(ctx, msgsC, input)
+		inputErr = readAnsiInputs(ctx, msgsC, input)
 		msgsC <- nil
 	}()