diff --git a/examples/go.mod b/examples/go.mod index 2cc9e899fd..dffef7889d 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( github.com/charmbracelet/bubbles v0.18.0 - github.com/charmbracelet/bubbletea v0.25.0 + github.com/charmbracelet/bubbletea v0.26.2 github.com/charmbracelet/glamour v0.7.0 github.com/charmbracelet/harmonica v0.2.0 github.com/charmbracelet/lipgloss v0.10.0 @@ -20,7 +20,11 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-udiff v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect - github.com/charmbracelet/x/exp/golden v0.0.0-20240222125807-0344fda748f8 // indirect + github.com/charmbracelet/x/ansi v0.1.1 // indirect + github.com/charmbracelet/x/exp/golden v0.0.0-20240521172236-71f88323a7ca // indirect + github.com/charmbracelet/x/input v0.1.0 // indirect + github.com/charmbracelet/x/term v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.1.0 // 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 @@ -35,12 +39,12 @@ require ( github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.5.4 // indirect github.com/yuin/goldmark-emoji v1.0.2 // indirect golang.org/x/net v0.23.0 // indirect - golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.14.0 // indirect ) diff --git a/examples/go.sum b/examples/go.sum index a986f59a31..6d152cf217 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -19,10 +19,18 @@ 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.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= -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/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk= +github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/golden v0.0.0-20240521172236-71f88323a7ca h1:Cw9p8EJdhDGIWICF34TIxTcQrAdzBdgkvaLA4AmqDVk= +github.com/charmbracelet/x/exp/golden v0.0.0-20240521172236-71f88323a7ca/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/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ= +github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28= +github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= +github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= +github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4= +github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= 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= @@ -63,20 +71,21 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +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= github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -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/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/examples/simple/testdata/TestApp.golden b/examples/simple/testdata/TestApp.golden index dbb51f684c..5aeba096c4 100644 --- a/examples/simple/testdata/TestApp.golden +++ b/examples/simple/testdata/TestApp.golden @@ -1,3 +1,3 @@ -[?25l[?2004h Hi. This program will exit in 10 seconds. To quit sooner press any key -Hi. This program will exit in 9 seconds. To quit sooner press any key. +[?25l[?2004h Hi. This program will exit in 10 seconds. To quit sooner press any key +Hi. This program will exit in 9 seconds. To quit sooner press any key. [?2004l[?25h[?1002l[?1003l[?1006l \ No newline at end of file diff --git a/exec.go b/exec.go index fb6d91ed15..7a14d2a778 100644 --- a/exec.go +++ b/exec.go @@ -109,7 +109,7 @@ func (p *Program) exec(c ExecCommand, fn ExecCallback) { } c.SetStdin(p.input) - c.SetStdout(p.output.TTY()) + c.SetStdout(p.output) c.SetStderr(os.Stderr) // Execute system command. diff --git a/go.mod b/go.mod index 40b1383211..0e2ae893cd 100644 --- a/go.mod +++ b/go.mod @@ -3,22 +3,21 @@ module github.com/charmbracelet/bubbletea go 1.18 require ( + github.com/charmbracelet/x/ansi v0.1.1 + github.com/charmbracelet/x/term v0.1.1 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f github.com/mattn/go-localereader v0.0.1 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.7.0 golang.org/x/sys v0.20.0 - golang.org/x/term v0.20.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/charmbracelet/x/input v0.1.0 // indirect + github.com/charmbracelet/x/windows v0.1.0 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/rivo/uniseg v0.4.6 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index 1f5eb9be44..9830ed772c 100644 --- a/go.sum +++ b/go.sum @@ -1,35 +1,31 @@ -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/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk= +github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ= +github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28= +github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= +github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= +github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4= +github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= 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.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-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= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.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.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 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_windows.go b/inputreader_windows.go index 643529fc81..449df4790c 100644 --- a/inputreader_windows.go +++ b/inputreader_windows.go @@ -9,6 +9,7 @@ import ( "os" "sync" + "github.com/charmbracelet/x/term" "github.com/erikgeiser/coninput" "github.com/muesli/cancelreader" "golang.org/x/sys/windows" @@ -28,7 +29,7 @@ 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() { + if f, ok := r.(term.File); !ok || f.Fd() != os.Stdin.Fd() { return fallback(r) } diff --git a/nil_renderer.go b/nil_renderer.go index b9edeac134..f4a83b6bc4 100644 --- a/nil_renderer.go +++ b/nil_renderer.go @@ -22,3 +22,4 @@ func (n nilRenderer) disableBracketedPaste() {} func (n nilRenderer) enableMouseSGRMode() {} func (n nilRenderer) disableMouseSGRMode() {} func (n nilRenderer) bracketedPasteActive() bool { return false } +func (n nilRenderer) setWindowTitle(_ string) {} diff --git a/options.go b/options.go index e78a048740..8aee6da6be 100644 --- a/options.go +++ b/options.go @@ -4,8 +4,6 @@ import ( "context" "io" "sync/atomic" - - "github.com/muesli/termenv" ) // ProgramOption is used to set options when initializing a Program. Program can @@ -29,11 +27,7 @@ func WithContext(ctx context.Context) ProgramOption { // won't need to use this. func WithOutput(output io.Writer) ProgramOption { return func(p *Program) { - if o, ok := output.(*termenv.Output); ok { - p.output = o - } else { - p.output = termenv.NewOutput(output, termenv.WithColorCache(true)) - } + p.output = output } } diff --git a/options_test.go b/options_test.go index dd51982cdf..ce8d41795a 100644 --- a/options_test.go +++ b/options_test.go @@ -2,6 +2,7 @@ package tea import ( "bytes" + "os" "sync/atomic" "testing" ) @@ -10,8 +11,8 @@ func TestOptions(t *testing.T) { t.Run("output", func(t *testing.T) { var b bytes.Buffer p := NewProgram(nil, WithOutput(&b)) - if p.output.TTY() != nil { - t.Errorf("expected output to custom, got %v", p.output.TTY().Fd()) + if f, ok := p.output.(*os.File); ok { + t.Errorf("expected output to custom, got %v", f.Fd()) } }) @@ -66,7 +67,6 @@ func TestOptions(t *testing.T) { var b bytes.Buffer exercise(t, WithInput(&b), customInput) }) - }) t.Run("startup options", func(t *testing.T) { diff --git a/renderer.go b/renderer.go index 65c2ae6dfa..de3936e73b 100644 --- a/renderer.go +++ b/renderer.go @@ -67,6 +67,9 @@ type renderer interface { // bracketedPasteActive reports whether bracketed paste mode is // currently enabled. bracketedPasteActive() bool + + // setWindowTitle sets the terminal window title. + setWindowTitle(string) } // repaintMsg forces a full repaint. diff --git a/screen.go b/screen.go index b34af56d7b..c17e5b6c69 100644 --- a/screen.go +++ b/screen.go @@ -198,5 +198,5 @@ func (p *Program) DisableMouseAllMotion() { // SetWindowTitle sets the terminal window title. func (p *Program) SetWindowTitle(title string) { - p.output.SetWindowTitle(title) + p.renderer.setWindowTitle(title) } diff --git a/screen_test.go b/screen_test.go index 552b6f5c2b..906c582735 100644 --- a/screen_test.go +++ b/screen_test.go @@ -14,47 +14,47 @@ func TestClearMsg(t *testing.T) { { name: "clear_screen", cmds: []Cmd{ClearScreen}, - expected: "\x1b[?25l\x1b[?2004h\x1b[2J\x1b[1;1H\x1b[1;1H\rsuccess\r\n\x1b[0D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l", + expected: "\x1b[?25l\x1b[?2004h\x1b[2J\x1b[1;1H\rsuccess\r\n\x1b[D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l", }, { name: "altscreen", cmds: []Cmd{EnterAltScreen, ExitAltScreen}, - expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1H\x1b[?25l\x1b[?1049l\x1b[?25l\rsuccess\r\n\x1b[0D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l", + expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[?25l\x1b[?1049l\x1b[?25l\rsuccess\r\n\x1b[D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l", }, { name: "altscreen_autoexit", cmds: []Cmd{EnterAltScreen}, - expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1H\x1b[?25l\rsuccess\r\n\x1b[2;0H\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1049l\x1b[?25h", + expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[?25l\rsuccess\r\n\x1b[2;0H\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1049l\x1b[?25h", }, { name: "mouse_cellmotion", cmds: []Cmd{EnableMouseCellMotion}, - expected: "\x1b[?25l\x1b[?2004h\x1b[?1002h\x1b[?1006h\rsuccess\r\n\x1b[0D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l", + expected: "\x1b[?25l\x1b[?2004h\x1b[?1002h\x1b[?1006h\rsuccess\r\n\x1b[D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l", }, { name: "mouse_allmotion", cmds: []Cmd{EnableMouseAllMotion}, - expected: "\x1b[?25l\x1b[?2004h\x1b[?1003h\x1b[?1006h\rsuccess\r\n\x1b[0D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l", + expected: "\x1b[?25l\x1b[?2004h\x1b[?1003h\x1b[?1006h\rsuccess\r\n\x1b[D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l", }, { name: "mouse_disable", cmds: []Cmd{EnableMouseAllMotion, DisableMouse}, - expected: "\x1b[?25l\x1b[?2004h\x1b[?1003h\x1b[?1006h\x1b[?1002l\x1b[?1003l\x1b[?1006l\rsuccess\r\n\x1b[0D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l", + expected: "\x1b[?25l\x1b[?2004h\x1b[?1003h\x1b[?1006h\x1b[?1002l\x1b[?1003l\x1b[?1006l\rsuccess\r\n\x1b[D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l", }, { name: "cursor_hide", cmds: []Cmd{HideCursor}, - expected: "\x1b[?25l\x1b[?2004h\x1b[?25l\rsuccess\r\n\x1b[0D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l", + expected: "\x1b[?25l\x1b[?2004h\x1b[?25l\rsuccess\r\n\x1b[D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l", }, { name: "cursor_hideshow", cmds: []Cmd{HideCursor, ShowCursor}, - expected: "\x1b[?25l\x1b[?2004h\x1b[?25l\x1b[?25h\rsuccess\r\n\x1b[0D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l", + expected: "\x1b[?25l\x1b[?2004h\x1b[?25l\x1b[?25h\rsuccess\r\n\x1b[D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l", }, { name: "bp_stop_start", cmds: []Cmd{DisableBracketedPaste, EnableBracketedPaste}, - expected: "\x1b[?25l\x1b[?2004h\x1b[?2004l\x1b[?2004h\rsuccess\r\n\x1b[0D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l", + expected: "\x1b[?25l\x1b[?2004h\x1b[?2004l\x1b[?2004h\rsuccess\r\n\x1b[D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l", }, } diff --git a/standard_renderer.go b/standard_renderer.go index b2c2348328..1b7f4b9bdc 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -8,9 +8,8 @@ import ( "sync" "time" + "github.com/charmbracelet/x/ansi" "github.com/muesli/ansi/compressor" - "github.com/muesli/reflow/truncate" - "github.com/muesli/termenv" ) const ( @@ -27,7 +26,7 @@ const ( // to exclude ranges of lines, allowing them to be written to directly. type standardRenderer struct { mtx *sync.Mutex - out *termenv.Output + out io.Writer buf bytes.Buffer queuedMessageLines []string @@ -58,7 +57,7 @@ type standardRenderer struct { // newRenderer creates a new renderer. Normally you'll want to initialize it // with os.Stdout as the first argument. -func newRenderer(out *termenv.Output, useANSICompressor bool, fps int) renderer { +func newRenderer(out io.Writer, useANSICompressor bool, fps int) renderer { if fps < 1 { fps = defaultFPS } else if fps > maxFPS { @@ -73,7 +72,7 @@ func newRenderer(out *termenv.Output, useANSICompressor bool, fps int) renderer queuedMessageLines: []string{}, } if r.useANSICompressor { - r.out = termenv.NewOutput(&compressor.Writer{Forward: out}) + r.out = &compressor.Writer{Forward: out} } return r } @@ -108,15 +107,20 @@ func (r *standardRenderer) stop() { r.mtx.Lock() defer r.mtx.Unlock() - r.out.ClearLine() + r.execute(ansi.EraseEntireLine) if r.useANSICompressor { - if w, ok := r.out.TTY().(io.WriteCloser); ok { + if w, ok := r.out.(io.WriteCloser); ok { _ = w.Close() } } } +// execute writes a sequence to the terminal. +func (r *standardRenderer) execute(seq string) { + _, _ = io.WriteString(r.out, seq) +} + // kill halts the renderer. The final frame will not be rendered. func (r *standardRenderer) kill() { // Stop the renderer before acquiring the mutex to avoid a deadlock. @@ -127,7 +131,7 @@ func (r *standardRenderer) kill() { r.mtx.Lock() defer r.mtx.Unlock() - r.out.ClearLine() + r.execute(ansi.EraseEntireLine) } // listen waits for ticks on the ticker, or a signal to stop the renderer. @@ -156,7 +160,6 @@ func (r *standardRenderer) flush() { // Output buffer buf := &bytes.Buffer{} - out := termenv.NewOutput(buf) newLines := strings.Split(r.buf.String(), "\n") @@ -180,17 +183,17 @@ func (r *standardRenderer) flush() { // printing messages allows for native terminal word-wrap, we // don't have control over the queued lines if flushQueuedMessages { - out.ClearLine() + buf.WriteString(ansi.EraseEntireLine) } 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() + buf.WriteString(ansi.EraseEntireLine) } - out.CursorUp(1) + buf.WriteString(ansi.CursorUp1) } if _, exists := r.ignoreLines[0]; !exists { @@ -203,8 +206,8 @@ func (r *standardRenderer) flush() { // standard (whereas others are proprietary to, say, VT100/VT52). // If cursor previous line (ESC[ + + F) were better supported // we could use that above to eliminate this step. - out.CursorBack(r.width) - out.ClearLine() + buf.WriteString(ansi.CursorLeft(r.width)) + buf.WriteString(ansi.EraseEntireLine) } } @@ -217,8 +220,8 @@ func (r *standardRenderer) flush() { if flushQueuedMessages { // Dump the lines we've queued up for printing for _, line := range r.queuedMessageLines { - _, _ = out.WriteString(line) - _, _ = out.WriteString("\r\n") + _, _ = buf.WriteString(line) + _, _ = buf.WriteString("\r\n") } // clear the queued message lines r.queuedMessageLines = []string{} @@ -229,7 +232,7 @@ func (r *standardRenderer) flush() { if _, skip := skipLines[i]; skip { // Unless this is the last line, move the cursor down. if i < len(newLines)-1 { - out.CursorDown(1) + buf.WriteString(ansi.CursorDown1) } } else { if i == 0 && r.lastRender == "" { @@ -248,13 +251,13 @@ func (r *standardRenderer) flush() { // program initialization, so after a resize this won't perform // correctly (signal SIGWINCH is not supported on Windows). if r.width > 0 { - line = truncate.String(line, uint(r.width)) + line = ansi.Truncate(line, r.width, "") } - _, _ = out.WriteString(line) + _, _ = buf.WriteString(line) if i < len(newLines)-1 { - _, _ = out.WriteString("\r\n") + _, _ = buf.WriteString("\r\n") } } } @@ -266,9 +269,9 @@ func (r *standardRenderer) flush() { // This case fixes a bug in macOS terminal. In other terminals the // other case seems to do the job regardless of whether or not we're // using the full terminal window. - out.MoveCursor(r.linesRendered, 0) + buf.WriteString(ansi.MoveCursor(r.linesRendered, 0)) } else { - out.CursorBack(r.width) + buf.WriteString(ansi.CursorLeft(r.width)) } _, _ = r.out.Write(buf.Bytes()) @@ -302,8 +305,8 @@ func (r *standardRenderer) clearScreen() { r.mtx.Lock() defer r.mtx.Unlock() - r.out.ClearScreen() - r.out.MoveCursor(1, 1) + r.execute(ansi.EraseEntireDisplay) + r.execute(ansi.MoveCursorOrigin) r.repaint() } @@ -324,7 +327,7 @@ func (r *standardRenderer) enterAltScreen() { } r.altScreenActive = true - r.out.AltScreen() + r.execute(ansi.EnableAltScreenBuffer) // Ensure that the terminal is cleared, even when it doesn't support // alt screen (or alt screen support is disabled, like GNU screen by @@ -332,16 +335,16 @@ func (r *standardRenderer) enterAltScreen() { // // Note: we can't use r.clearScreen() here because the mutex is already // locked. - r.out.ClearScreen() - r.out.MoveCursor(1, 1) + r.execute(ansi.EraseEntireDisplay) + r.execute(ansi.MoveCursorOrigin) // 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 enter AltScreen. if r.cursorHidden { - r.out.HideCursor() + r.execute(ansi.HideCursor) } else { - r.out.ShowCursor() + r.execute(ansi.ShowCursor) } r.repaint() @@ -356,15 +359,15 @@ func (r *standardRenderer) exitAltScreen() { } r.altScreenActive = false - r.out.ExitAltScreen() + r.execute(ansi.DisableAltScreenBuffer) // 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. if r.cursorHidden { - r.out.HideCursor() + r.execute(ansi.HideCursor) } else { - r.out.ShowCursor() + r.execute(ansi.ShowCursor) } r.repaint() @@ -375,7 +378,7 @@ func (r *standardRenderer) showCursor() { defer r.mtx.Unlock() r.cursorHidden = false - r.out.ShowCursor() + r.execute(ansi.ShowCursor) } func (r *standardRenderer) hideCursor() { @@ -383,56 +386,56 @@ func (r *standardRenderer) hideCursor() { defer r.mtx.Unlock() r.cursorHidden = true - r.out.HideCursor() + r.execute(ansi.HideCursor) } func (r *standardRenderer) enableMouseCellMotion() { r.mtx.Lock() defer r.mtx.Unlock() - r.out.EnableMouseCellMotion() + r.execute(ansi.EnableMouseCellMotion) } func (r *standardRenderer) disableMouseCellMotion() { r.mtx.Lock() defer r.mtx.Unlock() - r.out.DisableMouseCellMotion() + r.execute(ansi.DisableMouseCellMotion) } func (r *standardRenderer) enableMouseAllMotion() { r.mtx.Lock() defer r.mtx.Unlock() - r.out.EnableMouseAllMotion() + r.execute(ansi.EnableMouseAllMotion) } func (r *standardRenderer) disableMouseAllMotion() { r.mtx.Lock() defer r.mtx.Unlock() - r.out.DisableMouseAllMotion() + r.execute(ansi.DisableMouseAllMotion) } func (r *standardRenderer) enableMouseSGRMode() { r.mtx.Lock() defer r.mtx.Unlock() - r.out.EnableMouseExtendedMode() + r.execute(ansi.EnableMouseSgrExt) } func (r *standardRenderer) disableMouseSGRMode() { r.mtx.Lock() defer r.mtx.Unlock() - r.out.DisableMouseExtendedMode() + r.execute(ansi.DisableMouseSgrExt) } func (r *standardRenderer) enableBracketedPaste() { r.mtx.Lock() defer r.mtx.Unlock() - r.out.EnableBracketedPaste() + r.execute(ansi.EnableBracketedPaste) r.bpActive = true } @@ -440,7 +443,7 @@ func (r *standardRenderer) disableBracketedPaste() { r.mtx.Lock() defer r.mtx.Unlock() - r.out.DisableBracketedPaste() + r.execute(ansi.DisableBracketedPaste) r.bpActive = false } @@ -451,6 +454,11 @@ func (r *standardRenderer) bracketedPasteActive() bool { return r.bpActive } +// setWindowTitle sets the terminal window title. +func (r *standardRenderer) setWindowTitle(title string) { + r.execute(ansi.SetWindowTitle(title)) +} + // setIgnoredLines specifies lines not to be touched by the standard Bubble Tea // renderer. func (r *standardRenderer) setIgnoredLines(from int, to int) { @@ -471,15 +479,14 @@ func (r *standardRenderer) setIgnoredLines(from int, to int) { // Erase ignored lines if r.linesRendered > 0 { buf := &bytes.Buffer{} - out := termenv.NewOutput(buf) for i := r.linesRendered - 1; i >= 0; i-- { if _, exists := r.ignoreLines[i]; exists { - out.ClearLine() + buf.WriteString(ansi.EraseEntireLine) } - out.CursorUp(1) + buf.WriteString(ansi.CursorUp1) } - out.MoveCursor(r.linesRendered, 0) // put cursor back + buf.WriteString(ansi.MoveCursor(r.linesRendered, 0)) // put cursor back _, _ = r.out.Write(buf.Bytes()) } } @@ -514,16 +521,15 @@ func (r *standardRenderer) insertTop(lines []string, topBoundary, bottomBoundary defer r.mtx.Unlock() buf := &bytes.Buffer{} - out := termenv.NewOutput(buf) - out.ChangeScrollingRegion(topBoundary, bottomBoundary) - out.MoveCursor(topBoundary, 0) - out.InsertLines(len(lines)) - _, _ = out.WriteString(strings.Join(lines, "\r\n")) - out.ChangeScrollingRegion(0, r.height) + buf.WriteString(ansi.SetScrollingRegion(topBoundary, bottomBoundary)) + buf.WriteString(ansi.MoveCursor(topBoundary, 0)) + buf.WriteString(ansi.InsertLine(len(lines))) + _, _ = buf.WriteString(strings.Join(lines, "\r\n")) + buf.WriteString(ansi.SetScrollingRegion(0, r.height)) // Move cursor back to where the main rendering routine expects it to be - out.MoveCursor(r.linesRendered, 0) + buf.WriteString(ansi.MoveCursor(r.linesRendered, 0)) _, _ = r.out.Write(buf.Bytes()) } @@ -542,15 +548,14 @@ func (r *standardRenderer) insertBottom(lines []string, topBoundary, bottomBound defer r.mtx.Unlock() buf := &bytes.Buffer{} - out := termenv.NewOutput(buf) - out.ChangeScrollingRegion(topBoundary, bottomBoundary) - out.MoveCursor(bottomBoundary, 0) - _, _ = out.WriteString("\r\n" + strings.Join(lines, "\r\n")) - out.ChangeScrollingRegion(0, r.height) + buf.WriteString(ansi.SetScrollingRegion(topBoundary, bottomBoundary)) + buf.WriteString(ansi.MoveCursor(bottomBoundary, 0)) + _, _ = buf.WriteString("\r\n" + strings.Join(lines, "\r\n")) + buf.WriteString(ansi.SetScrollingRegion(0, r.height)) // Move cursor back to where the main rendering routine expects it to be - out.MoveCursor(r.linesRendered, 0) + buf.WriteString(ansi.MoveCursor(r.linesRendered, 0)) _, _ = r.out.Write(buf.Bytes()) } diff --git a/tea.go b/tea.go index 7225d4a506..0915d1adbd 100644 --- a/tea.go +++ b/tea.go @@ -21,10 +21,9 @@ import ( "sync/atomic" "syscall" + "github.com/charmbracelet/x/term" "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. @@ -142,17 +141,19 @@ type Program struct { finished chan struct{} // where to send output, this will usually be os.Stdout. - output *termenv.Output - restoreOutput func() error - renderer renderer + output io.Writer + // ttyOutput is null if output is not a TTY. + ttyOutput term.File + previousOutputState *term.State + renderer renderer // where to read inputs from, this will usually be os.Stdin. input io.Reader - // tty is null if input is not a TTY. - tty *os.File - previousTtyState *term.State - cancelReader cancelreader.CancelReader - readLoopDone chan struct{} + // ttyInput is null if input is not a TTY. + ttyInput term.File + previousTtyInputState *term.State + cancelReader cancelreader.CancelReader + readLoopDone chan struct{} // was the altscreen active before releasing the terminal? altScreenWasActive bool @@ -198,14 +199,9 @@ func NewProgram(model Model, opts ...ProgramOption) *Program { // if no output was set, set it to stdout if p.output == nil { - p.output = termenv.DefaultOutput() - - // cache detected color values - termenv.WithColorCache(true)(p.output) + p.output = os.Stdout } - p.restoreOutput, _ = termenv.EnableVirtualTerminalProcessing(p.output) - return p } @@ -249,7 +245,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 && term.IsTerminal(int(f.Fd())) { + if p.ttyOutput != nil { // Get the initial terminal size and send it to the program. go p.checkResize() @@ -437,11 +433,11 @@ func (p *Program) Run() (Model, error) { // piped in or redirected to the application. // // To disable input entirely pass nil to the [WithInput] program option. - f, isFile := p.input.(*os.File) + f, isFile := p.input.(term.File) if !isFile { break } - if term.IsTerminal(int(f.Fd())) { + if term.IsTerminal(f.Fd()) { break } @@ -642,9 +638,6 @@ func (p *Program) shutdown(kill bool) { } _ = p.restoreTerminalState() - if p.restoreOutput != nil { - _ = p.restoreOutput() - } p.finished <- struct{}{} } diff --git a/tty.go b/tty.go index e7b602cf56..2fdd36b4e4 100644 --- a/tty.go +++ b/tty.go @@ -4,11 +4,10 @@ import ( "errors" "fmt" "io" - "os" "time" + "github.com/charmbracelet/x/term" "github.com/muesli/cancelreader" - "golang.org/x/term" ) func (p *Program) initTerminal() error { @@ -41,8 +40,13 @@ func (p *Program) restoreTerminalState() error { // 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 { + if p.ttyInput != nil && p.previousTtyInputState != nil { + if err := term.Restore(p.ttyInput.Fd(), p.previousTtyInputState); err != nil { + return fmt.Errorf("error restoring console: %w", err) + } + } + if p.ttyOutput != nil && p.previousOutputState != nil { + if err := term.Restore(p.ttyOutput.Fd(), p.previousOutputState); err != nil { return fmt.Errorf("error restoring console: %w", err) } } @@ -89,13 +93,12 @@ func (p *Program) waitForReadLoop() { // checkResize detects the current size of the output and informs the program // via a WindowSizeMsg. func (p *Program) checkResize() { - f, ok := p.output.TTY().(*os.File) - if !ok || !term.IsTerminal(int(f.Fd())) { + if p.ttyOutput == nil { // can't query window size return } - w, h, err := term.GetSize(int(f.Fd())) + w, h, err := term.GetSize(p.ttyOutput.Fd()) if err != nil { select { case <-p.ctx.Done(): diff --git a/tty_unix.go b/tty_unix.go index a93444d05c..362e585095 100644 --- a/tty_unix.go +++ b/tty_unix.go @@ -7,19 +7,23 @@ import ( "fmt" "os" - "golang.org/x/term" + "github.com/charmbracelet/x/term" ) 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 f, ok := p.input.(term.File); ok && term.IsTerminal(f.Fd()) { + p.ttyInput = f + p.previousTtyInputState, err = term.MakeRaw(p.ttyInput.Fd()) if err != nil { return fmt.Errorf("error entering raw mode: %w", err) } } + if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) { + p.ttyOutput = f + } + return nil } diff --git a/tty_windows.go b/tty_windows.go index 10b6108766..de6a82f9c4 100644 --- a/tty_windows.go +++ b/tty_windows.go @@ -7,28 +7,46 @@ import ( "fmt" "os" + "github.com/charmbracelet/x/term" "golang.org/x/sys/windows" - "golang.org/x/term" ) 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 + // 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 f, ok := p.input.(term.File); ok && term.IsTerminal(f.Fd()) { + p.ttyInput = f + p.previousTtyInputState, err = term.MakeRaw(p.ttyInput.Fd()) if err != nil { return err } // Enable VT input var mode uint32 - if err := windows.GetConsoleMode(windows.Handle(p.tty.Fd()), &mode); err != nil { + if err := windows.GetConsoleMode(windows.Handle(p.ttyInput.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 { + if err := windows.SetConsoleMode(windows.Handle(p.ttyInput.Fd()), mode|windows.ENABLE_VIRTUAL_TERMINAL_INPUT); err != nil { + return fmt.Errorf("error setting console mode: %w", err) + } + } + + // Save output screen buffer state and enable VT processing. + if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) { + p.ttyOutput = f + p.previousOutputState, err = term.GetState(f.Fd()) + if err != nil { + return err + } + + var mode uint32 + if err := windows.GetConsoleMode(windows.Handle(p.ttyOutput.Fd()), &mode); err != nil { + return fmt.Errorf("error getting console mode: %w", err) + } + + if err := windows.SetConsoleMode(windows.Handle(p.ttyOutput.Fd()), mode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err != nil { return fmt.Errorf("error setting console mode: %w", err) } } @@ -38,7 +56,7 @@ func (p *Program) initInput() (err error) { // Open the Windows equivalent of a TTY. func openInputTTY() (*os.File, error) { - f, err := os.OpenFile("CONIN$", os.O_RDWR, 0644) + f, err := os.OpenFile("CONIN$", os.O_RDWR, 0o644) if err != nil { return nil, err } diff --git a/tutorials/go.mod b/tutorials/go.mod index 80bfc8b032..2b31c4f4f7 100644 --- a/tutorials/go.mod +++ b/tutorials/go.mod @@ -5,20 +5,19 @@ go 1.18 require github.com/charmbracelet/bubbletea v0.25.0 require ( - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/x/ansi v0.1.1 // indirect + github.com/charmbracelet/x/input v0.1.0 // indirect + github.com/charmbracelet/x/term v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.1.0 // 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-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.6.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/term v0.17.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.13.0 // indirect ) diff --git a/tutorials/go.sum b/tutorials/go.sum index 08417d04ef..1c500b5377 100644 --- a/tutorials/go.sum +++ b/tutorials/go.sum @@ -1,35 +1,31 @@ -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/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk= +github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ= +github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28= +github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= +github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= +github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4= +github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= 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-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-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= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.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.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/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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=