diff --git a/cmd/root.go b/cmd/root.go index 93e2b42..5844205 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -53,6 +53,9 @@ git config --global pager.diff diffnav # watch mode: auto-refresh a diff command diffnav --watch diffnav --watch --watch-cmd "git diff HEAD" --watch-interval 5s + +# load a PR +diffnav --pr https://github.com/dlvhdr/gh-dash/pull/447 `, } @@ -88,6 +91,7 @@ func init() { BoolP("watch", "w", false, "Watch mode: periodically re-run a diff command and refresh") rootCmd.Flags().String("watch-cmd", "git diff", "Command to run in watch mode") rootCmd.Flags().Duration("watch-interval", 2*time.Second, "Interval between watch refreshes") + rootCmd.Flags().String("pr", "", "load a PR") rootCmd.SetVersionTemplate("\n" + logo + "\n" + `{{printf "version %s\n" .Version}}`) @@ -122,6 +126,10 @@ func init() { if cmd.Flags().Changed("watch-cmd") { watchFlag = true } + prURL, err := cmd.Flags().GetString("pr") + if err != nil { + log.Fatal("Cannot parse the pr flag", err) + } zone.NewGlobal() @@ -171,7 +179,7 @@ func init() { log.Warn("initial watch command failed, starting with empty diff", "err", wErr) } input = output - } else { + } else if prURL == "" { stat, sErr := os.Stdin.Stat() if sErr != nil { panic(sErr) @@ -202,14 +210,6 @@ func init() { fmt.Println("No input provided, exiting") os.Exit(0) } - - if !isUnifiedDiff(input) { - fmt.Print(input) - if !strings.HasSuffix(input, "\n") { - fmt.Println() - } - os.Exit(0) - } } cfg := config.Load() @@ -231,7 +231,10 @@ func init() { if err != nil { log.Fatal(err) } - p := tea.NewProgram(ui.New(input, cfg), tea.WithInput(ttyIn)) + p := tea.NewProgram( + ui.New(ui.ModelOpts{Input: input, PRUrl: prURL}, cfg), + tea.WithInput(ttyIn), + ) if _, err := p.Run(); err != nil { log.Fatal(err) diff --git a/go.mod b/go.mod index cfccdd2..f06280e 100644 --- a/go.mod +++ b/go.mod @@ -12,37 +12,50 @@ require ( github.com/bluekeyes/go-gitdiff v0.8.1 github.com/charmbracelet/colorprofile v0.4.3 github.com/charmbracelet/x/ansi v0.11.7 + github.com/cli/go-gh/v2 v2.13.0 github.com/lrstanley/bubblezone/v2 v2.0.0 github.com/lrstanley/go-nf v0.0.0-20260418212552-215ab243b591 github.com/mattn/go-runewidth v0.0.24 github.com/muesli/reflow v0.3.0 github.com/robinovitch61/viewport v0.14.1-0.20260718213438-0e7e3ee636f2 + github.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed github.com/spf13/cobra v1.10.2 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/ultraviolet v0.0.0-20260615092913-2399af76d5b1 // indirect github.com/charmbracelet/x/exp/charmtone v0.0.0-20260615092313-b57e5e6d29bb // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/cli/safeexec v1.0.0 // indirect + github.com/cli/shurcooL-graphql v0.0.4 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect + github.com/henvic/httpretty v0.0.6 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/mango v0.2.0 // indirect github.com/muesli/mango-cobra v1.3.0 // indirect github.com/muesli/mango-pflag v0.2.0 // indirect github.com/muesli/roff v0.1.0 // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/shurcooL/graphql v0.0.0-20240915155400-7ee5256398cf // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.30.0 // indirect golang.org/x/text v0.38.0 // indirect ) diff --git a/go.sum b/go.sum index 1987bdb..6d182b3 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ charm.land/log/v2 v2.0.0 h1:SY3Cey7ipx86/MBXQHwsguOT6X1exT94mmJRdzTNs+s= charm.land/log/v2 v2.0.0/go.mod h1:c3cZSRqm20qUVVAR1WmS/7ab8bgha3C6G7DjPcaVZz0= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +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.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/bluekeyes/go-gitdiff v0.8.1 h1:lL1GofKMywO17c0lgQmJYcKek5+s8X6tXVNOLxy4smI= @@ -28,11 +30,18 @@ github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8 github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/cli/go-gh/v2 v2.13.0 h1:jEHZu/VPVoIJkciK3pzZd3rbT8J90swsK5Ui4ewH1ys= +github.com/cli/go-gh/v2 v2.13.0/go.mod h1:Us/NbQ8VNM0fdaILgoXSz6PKkV5PWaEzkJdc9vR2geM= +github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= +github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= +github.com/cli/shurcooL-graphql v0.0.4 h1:6MogPnQJLjKkaXPyGqPRXOI2qCsQdqNfUY1QSJu2GuY= +github.com/cli/shurcooL-graphql v0.0.4/go.mod h1:3waN4u02FiZivIV+p1y4d0Jo1jc6BViMA73C+sZo2fk= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= github.com/dlvhdr/bubbles/v2 v2.1.0-rc-dlvhdr h1:y07CsjN8cK5q6lJMmyOvp5bPLHfpiI2GOK0P3E+/SCo= @@ -41,14 +50,24 @@ github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkv github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs= +github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lrstanley/bubblezone/v2 v2.0.0 h1:pMb9fHKs0slJF6OrzQ2hEgWusqyl9VU/S0UZ5hyh7ZA= github.com/lrstanley/bubblezone/v2 v2.0.0/go.mod h1:yV/QTjcm4Zu5cqvGvdHi7xVUfnB36w/SafOuDp57dgY= github.com/lrstanley/go-nf v0.0.0-20260418212552-215ab243b591 h1:WTkxFAeEMXU0lVPiUmUUoZJWCgPquyaxQGUf9e8dBh8= github.com/lrstanley/go-nf v0.0.0-20260418212552-215ab243b591/go.mod h1:MW7vmvMkwop4wD5DgTS5dzykKwN/r+3OWcxfzAANOrw= github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= github.com/lucasb-eyer/go-colorful v1.4.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-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= @@ -64,6 +83,9 @@ 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/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -72,7 +94,13 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robinovitch61/viewport v0.14.1-0.20260718213438-0e7e3ee636f2 h1:w5kS+VBMRZSW7xzhPw6yj3cpSoIvxLc9fLvXe2OzBMo= github.com/robinovitch61/viewport v0.14.1-0.20260718213438-0e7e3ee636f2/go.mod h1:1kKpze6sbzH3QBHNFdMzqfJ8++LcigA01ieq6kIf4ds= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed h1:KT7hI8vYXgU0s2qaMkrfq9tCA1w/iEPgfredVP+4Tzw= +github.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8= +github.com/shurcooL/graphql v0.0.0-20240915155400-7ee5256398cf h1:o1uxfymjZ7jZ4MsgCErcwWGtVKSiNAXtS59Lhs6uI/g= +github.com/shurcooL/graphql v0.0.0-20240915155400-7ee5256398cf/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -80,18 +108,29 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= +github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= 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= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/api.go b/internal/api/api.go new file mode 100644 index 0000000..e65f4b5 --- /dev/null +++ b/internal/api/api.go @@ -0,0 +1,230 @@ +package api + +import ( + "fmt" + "io" + "net/http" + "net/url" + "os" + "regexp" + "time" + + "charm.land/log/v2" + gh "github.com/cli/go-gh/v2/pkg/api" + "github.com/shurcooL/githubv4" +) + +type API struct { + Host string + defaultTransport http.RoundTripper + authToken string +} + +const ( + defaultAPIURL = "https://api.github.com" + defaultServerURL = "https://github.com" +) + +var prURLPattern = regexp.MustCompile( + `^/(?P[^/]+)/(?P[^/]+)/pull/(?P\d+)`, +) + +func New() API { + apiURL := os.Getenv("GITHUB_API_URL") + if apiURL == "" { + apiURL = defaultAPIURL + } + + a := API{ + Host: apiURL, + } + + return a +} + +func (a *API) setDefaultTransport(transport *http.Transport) { + a.defaultTransport = transport +} + +func (a *API) setAuthToken(token string) { + a.authToken = token +} + +func (a *API) getGraphQLClient(opts gh.ClientOptions) (*gh.GraphQLClient, error) { + if a.defaultTransport != nil { + opts.Transport = a.defaultTransport + } + if a.authToken != "" { + opts.AuthToken = a.authToken + } + opts.Host = a.Host + + level := os.Getenv("LOG_LEVEL") + if level == "debug" { + logger := NewHTTPLogger(0) + opts.Log = &logger + opts.LogVerboseHTTP = true + opts.LogColorize = true + } + return gh.NewGraphQLClient(opts) +} + +func (a *API) getRESTClient(opts gh.ClientOptions) (*gh.RESTClient, error) { + if a.defaultTransport != nil { + opts.Transport = a.defaultTransport + } + if a.authToken != "" { + opts.AuthToken = a.authToken + } + + level := os.Getenv("LOG_LEVEL") + if level == "debug" { + logger := NewHTTPLogger(0) + opts.Log = &logger + opts.LogVerboseHTTP = true + opts.LogColorize = true + } + + return gh.NewRESTClient(opts) +} + +func (a *API) getClient(opts gh.ClientOptions) (*http.Client, error) { + if a.defaultTransport != nil { + opts.Transport = a.defaultTransport + } + if a.authToken != "" { + opts.AuthToken = a.authToken + } + + level := os.Getenv("LOG_LEVEL") + if level == "debug" { + logger := NewHTTPLogger(0) + opts.Log = &logger + opts.LogVerboseHTTP = true + opts.LogColorize = true + } + + return gh.NewHTTPClient(opts) +} + +const ( + DiffSideLeft = "LEFT" + DiffSideRight = "RIGHT" +) + +func (a *API) FetchPR(prURL string) (PRQuery, error) { + var err error + var res PRQuery + c, err := a.getGraphQLClient(gh.ClientOptions{}) + if err != nil { + return res, err + } + + parsedPRURL, err := url.Parse(prURL) + if err != nil { + return res, err + } + variables := map[string]any{ + "url": githubv4.URI{URL: parsedPRURL}, + } + + startTime := time.Now() + err = c.Query("FetchPRComments", &res, variables) + if err != nil { + log.Error("error fetching PR", "err", err) + return res, err + } + + log.Debug("FetchPR request completed", "duration", time.Since(startTime)) + return res, nil +} + +type diffResp struct{ Body string } + +func (a *API) FetchPRDiff(prURL string) (string, error) { + c, err := a.getClient(gh.ClientOptions{Headers: map[string]string{ + "Accept": "application/vnd.github.v3.diff", + }}) + if err != nil { + return "", err + } + + parsedPRURL, err := url.Parse(prURL) + if err != nil { + return "", err + } + + // PRURL to diff url + // https://github.com/neovim/neovim/pull/39773 -> + // https://api.github.com/repos/neovim/neovim/pulls/7 + match := prURLPattern.FindStringSubmatch(parsedPRURL.Path) + if match == nil { + return "", fmt.Errorf("failed parsing pr url %s", prURL) + } + + repo := match[prURLPattern.SubexpIndex("owner")] + "/" + + match[prURLPattern.SubexpIndex("repo")] + prNumber := match[prURLPattern.SubexpIndex("number")] + + u := fmt.Sprintf("%s/repos/%s/pulls/%s", a.Host, repo, prNumber) + log.Debug("fetching", "url", u) + resp, err := c.Get(u) + if err != nil { + return "", err + } + + defer resp.Body.Close() + b, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed fetching pr diff: %s", resp.Status) + } + + return string(b), nil +} + +type ReviewThread struct { + Id string + IsResolved bool + IsOutdated bool + Path string + Line int + OriginalLine int + DiffSide string + Comments struct { + Nodes []ReviewThreadComment + } `graphql:"comments(first: 100)"` +} + +type ReviewThreadComment struct { + Id string + Body string + Author struct{ Login string } + CreatedAt time.Time + Url string + ReplyTo struct{ Id string } +} + +type PR struct { + Title string + Number int + Url string + Repository struct { + NameWithOwner string + } + Merged bool + IsDraft bool + Closed bool + HeadRefName string + ReviewThreads struct { + Nodes []ReviewThread + } `graphql:"reviewThreads(first: 10)"` +} + +type PRQuery struct { + Resource struct { + PullRequest PR `graphql:"... on PullRequest"` + } `graphql:"resource(url: $url)"` +} diff --git a/internal/api/api_test.go b/internal/api/api_test.go new file mode 100644 index 0000000..0d0296f --- /dev/null +++ b/internal/api/api_test.go @@ -0,0 +1,133 @@ +package api + +import ( + "io" + "iter" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" +) + +func TestFetchPRComments(t *testing.T) { + transport := localRoundTripper{ + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/graphql" { + t.Fatalf("Incorrect path %s", r.URL.Path) + } + + body := mustRead(t, r.Body) + switch { + case strings.Contains(body, "query FetchPRComments"): + t.Log("matched query FetchPRComments") + d, err := os.ReadFile("./testdata/prComments.json") + if err != nil { + t.Errorf("failed reading mock data file %v", err) + } + mustWrite(t, w, string(d)) + default: + t.Log("unexpected url", r.URL) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + }), + } + api := API{ + Host: "localhost:3000", + defaultTransport: transport, + authToken: "fake-token", + } + + res, err := api.FetchPR("https://github.com/some/repo/pull/12345") + if err != nil { + t.Fatal(err) + } + + if res.Resource.PullRequest.Title != "feat(terminal): replace `libvterm` with `libghostty-vt`" { + t.Fatalf( + `expected a PR with title "%s" but got "%s"`, + "feat(terminal): replace `libvterm` with `libghostty-vt`", + res.Resource.PullRequest.Title, + ) + } + + if len(res.Resource.PullRequest.ReviewThreads.Nodes) != 10 { + t.Fatalf( + `expected a PR with 10 review threads but got %d`, + len(res.Resource.PullRequest.ReviewThreads.Nodes), + ) + } +} + +func TestFetchPRDiff(t *testing.T) { + transport := localRoundTripper{ + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.String() != "localhost:3000/repos/some/repo/pulls/12345" { + t.Fatalf(`Incorrect URL %s`, r.URL) + } + + d, err := os.ReadFile("./testdata/prDiff.diff") + if err != nil { + t.Errorf("failed reading mock pr diff %v", err) + } + val := string(d) + mustWrite(t, w, val) + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/vnd.github.v3.diff") + }), + } + + api := API{ + Host: "localhost:3000", + defaultTransport: transport, + authToken: "fake-token", + } + + res, err := api.FetchPRDiff("https://github.com/some/repo/pull/12345") + if err != nil { + t.Fatal(err) + } + + next, stop := iter.Pull(strings.Lines(res)) + defer stop() + line, _ := next() + if line != "diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml\n" { + t.Fatalf( + `expected a PR diff that starts with "diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml" but got "%s"`, + line, + ) + } +} + +func mustRead(t *testing.T, r io.Reader) string { + t.Helper() + b, err := io.ReadAll(r) + if err != nil { + panic(err) + } + return string(b) +} + +func mustWrite(t *testing.T, w io.Writer, s string) { + t.Helper() + _, err := io.WriteString(w, s) + if err != nil { + panic(err) + } +} + +// localRoundTripper is an http.RoundTripper that executes HTTP transactions +// by using handler directly, instead of going over an HTTP connection. +// This is used because the github graphql client expects an authentication token +type localRoundTripper struct { + handler http.Handler +} + +func (l localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + w := httptest.NewRecorder() + l.handler.ServeHTTP(w, req) + return w.Result(), nil +} diff --git a/internal/api/logger.go b/internal/api/logger.go new file mode 100644 index 0000000..d071e6c --- /dev/null +++ b/internal/api/logger.go @@ -0,0 +1,50 @@ +package api + +import ( + "errors" + "fmt" + "strings" + + "charm.land/log/v2" +) + +type HTTPLogger struct { + strBuilder *strings.Builder + limit int +} + +// NewHTTPLogger creates a new HTTPLogger. +// If limit is 0 the limit is 100000. +func NewHTTPLogger(limit int) HTTPLogger { + l := limit + if l == 0 { + limit = 100000 + } + logger := HTTPLogger{ + strBuilder: &strings.Builder{}, + limit: limit, + } + return logger +} + +func (logger *HTTPLogger) Write(p []byte) (n int, err error) { + s := string(p) + if strings.HasPrefix(s, "* Request at") { + logger.strBuilder.Reset() + } + + lp := len(p) + n = min(lp, logger.limit) + n, err = logger.strBuilder.Write(p[:n]) + if lp > logger.limit { + logger.strBuilder.WriteString("\n------------------ TRIMMED ------------------\n") + fmt.Fprintf(logger.strBuilder, "Content length: %d (limit is %d)\n", lp, logger.limit) + return logger.limit, errors.New("msg too long") + } + + if strings.HasPrefix(s, "* Request took") { + log.Debug("HTTPLogger", "msg", logger.strBuilder.String()) + } + + return n, err +} diff --git a/internal/api/testdata/prComments.json b/internal/api/testdata/prComments.json new file mode 100644 index 0000000..1f7aa6f --- /dev/null +++ b/internal/api/testdata/prComments.json @@ -0,0 +1,406 @@ +{ + "data": { + "resource": { + "title": "feat(terminal): replace `libvterm` with `libghostty-vt`", + "number": 39773, + "url": "https://github.com/neovim/neovim/pull/39773", + "repository": { + "nameWithOwner": "neovim/neovim" + }, + "merged": false, + "isDraft": true, + "closed": false, + "headRefName": "libvterm-to-libghostty", + "reviewThreads": { + "nodes": [ + { + "id": "PRRT_kwDOAPphoM6B3n0V", + "isResolved": true, + "isOutdated": true, + "path": "src/nvim/terminal.c", + "line": null, + "originalLine": 498, + "diffSide": "RIGHT", + "comments": { + "nodes": [ + { + "id": "PRRC_kwDOAPphoM7A77J7", + "body": "Maybe make this a macro instead?\n\n```c\n#define GHOSTTY_ASSERT(res) assert((res) == GHOSTTY_SUCCESS)\n```", + "author": { + "login": "gpanders" + }, + "createdAt": "2026-05-13T19:32:28Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3236934267", + "replyTo": null + }, + { + "id": "PRRC_kwDOAPphoM7A9Yao", + "body": "That would cause the wrapped function call to be compiled away in release builds.", + "author": { + "login": "noib3" + }, + "createdAt": "2026-05-13T20:40:33Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3237316264", + "replyTo": { + "id": "PRRC_kwDOAPphoM7A77J7" + } + }, + { + "id": "PRRC_kwDOAPphoM7BMi6M", + "body": "Ah that’s right. I’m too used to Lua", + "author": { + "login": "gpanders" + }, + "createdAt": "2026-05-14T12:23:57Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3241291404", + "replyTo": { + "id": "PRRC_kwDOAPphoM7A77J7" + } + } + ] + } + }, + { + "id": "PRRT_kwDOAPphoM6B3uQ9", + "isResolved": true, + "isOutdated": true, + "path": "src/nvim/tui/tui.c", + "line": null, + "originalLine": 728, + "diffSide": "RIGHT", + "comments": { + "nodes": [ + { + "id": "PRRC_kwDOAPphoM7A8DvK", + "body": "This shouldn't be related to vterm or libghostty. Can you separate this change out into a separate PR (and describe the problem and solution)?", + "author": { + "login": "gpanders" + }, + "createdAt": "2026-05-13T19:39:19Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3236969418", + "replyTo": null + }, + { + "id": "PRRC_kwDOAPphoM7BkjVj", + "body": "It's not related to the transition, but it was a fix for a bug that was discovered by it. I pulled that out into https://github.com/neovim/neovim/pull/39803 where I explain the problem and solution.", + "author": { + "login": "noib3" + }, + "createdAt": "2026-05-15T10:39:32Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3247584611", + "replyTo": { + "id": "PRRC_kwDOAPphoM7A8DvK" + } + }, + { + "id": "PRRC_kwDOAPphoM7akR5H", + "body": "Removed the fix, see https://github.com/neovim/neovim/pull/39803#issuecomment-5105850368", + "author": { + "login": "noib3" + }, + "createdAt": "2026-07-28T15:34:33Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3666943559", + "replyTo": { + "id": "PRRC_kwDOAPphoM7A8DvK" + } + } + ] + } + }, + { + "id": "PRRT_kwDOAPphoM6B3wbA", + "isResolved": true, + "isOutdated": true, + "path": "runtime/lua/vim/_core/defaults.lua", + "line": null, + "originalLine": 681, + "diffSide": "LEFT", + "comments": { + "nodes": [ + { + "id": "PRRC_kwDOAPphoM7A8Gpm", + "body": "Nice", + "author": { + "login": "gpanders" + }, + "createdAt": "2026-05-13T19:41:31Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3236981350", + "replyTo": null + }, + { + "id": "PRRC_kwDOAPphoM7BLPzz", + "body": "what allows us to delete this?", + "author": { + "login": "justinmk" + }, + "createdAt": "2026-05-14T11:16:41Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3240951027", + "replyTo": { + "id": "PRRC_kwDOAPphoM7A8Gpm" + } + }, + { + "id": "PRRC_kwDOAPphoM7BLSdP", + "body": "OSC 10/11 queries are handled by libghostty, see https://github.com/ghostty-org/ghostty/pull/12631.", + "author": { + "login": "noib3" + }, + "createdAt": "2026-05-14T11:19:01Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3240961871", + "replyTo": { + "id": "PRRC_kwDOAPphoM7A8Gpm" + } + } + ] + } + }, + { + "id": "PRRT_kwDOAPphoM6B6J7l", + "isResolved": true, + "isOutdated": true, + "path": "test/functional/terminal/scrollback_spec.lua", + "line": null, + "originalLine": 233, + "diffSide": "RIGHT", + "comments": { + "nodes": [ + { + "id": "PRRC_kwDOAPphoM7A_ZAO", + "body": "This doesn't look correct.", + "author": { + "login": "zeertzjq" + }, + "createdAt": "2026-05-13T22:37:03Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3237842958", + "replyTo": null + }, + { + "id": "PRRC_kwDOAPphoM7BqJFr", + "body": "fixed in 0b1bf57bd86766ced77003bd8947a46db2262934", + "author": { + "login": "noib3" + }, + "createdAt": "2026-05-15T14:58:33Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3249049963", + "replyTo": { + "id": "PRRC_kwDOAPphoM7A_ZAO" + } + } + ] + } + }, + { + "id": "PRRT_kwDOAPphoM6B6a16", + "isResolved": true, + "isOutdated": true, + "path": "test/functional/terminal/buffer_spec.lua", + "line": 1111, + "originalLine": 1100, + "diffSide": "LEFT", + "comments": { + "nodes": [ + { + "id": "PRRC_kwDOAPphoM7A_wZ-", + "body": "Why is this test deleted?", + "author": { + "login": "zeertzjq" + }, + "createdAt": "2026-05-13T23:03:33Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3237938814", + "replyTo": null + }, + { + "id": "PRRC_kwDOAPphoM7BHC8e", + "body": "libghostty treats U+FEFF as non-printing, so the screen would be empty.", + "author": { + "login": "noib3" + }, + "createdAt": "2026-05-14T07:39:47Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3239849758", + "replyTo": { + "id": "PRRC_kwDOAPphoM7A_wZ-" + } + } + ] + } + }, + { + "id": "PRRT_kwDOAPphoM6B6bno", + "isResolved": true, + "isOutdated": true, + "path": "test/functional/terminal/tui_spec.lua", + "line": null, + "originalLine": 907, + "diffSide": "RIGHT", + "comments": { + "nodes": [ + { + "id": "PRRC_kwDOAPphoM7A_xkR", + "body": "This doesn't look right.", + "author": { + "login": "zeertzjq" + }, + "createdAt": "2026-05-13T23:04:42Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3237943569", + "replyTo": null + }, + { + "id": "PRRC_kwDOAPphoM7BqJbz", + "body": "fixed in d8d8512b8ff2261d503a5cbb6a2f3a2b5c99db56", + "author": { + "login": "noib3" + }, + "createdAt": "2026-05-15T14:58:47Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3249051379", + "replyTo": { + "id": "PRRC_kwDOAPphoM7A_xkR" + } + } + ] + } + }, + { + "id": "PRRT_kwDOAPphoM6CCsqp", + "isResolved": true, + "isOutdated": true, + "path": "cmake.deps/deps.txt", + "line": null, + "originalLine": 55, + "diffSide": "RIGHT", + "comments": { + "nodes": [ + { + "id": "PRRC_kwDOAPphoM7BLKdu", + "body": "is `noib3` the right upstream for us to depend on ...?\n\n", + "author": { + "login": "justinmk" + }, + "createdAt": "2026-05-14T11:11:59Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3240929134", + "replyTo": null + }, + { + "id": "PRRC_kwDOAPphoM7BLO1O", + "body": "It is until the PRs I mentioned are merged upstream, then we can switch.", + "author": { + "login": "noib3" + }, + "createdAt": "2026-05-14T11:15:51Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3240947022", + "replyTo": { + "id": "PRRC_kwDOAPphoM7BLKdu" + } + } + ] + } + }, + { + "id": "PRRT_kwDOAPphoM6CCuKn", + "isResolved": true, + "isOutdated": true, + "path": "runtime/doc/dev_tools.txt", + "line": null, + "originalLine": 146, + "diffSide": "RIGHT", + "comments": { + "nodes": [ + { + "id": "PRRC_kwDOAPphoM7BLMa7", + "body": "these notes are still relevant, don't think any changes are needed in this doc file. though with ghostty \"inspector\" they are becoming less relevant.", + "author": { + "login": "justinmk" + }, + "createdAt": "2026-05-14T11:13:45Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3240937147", + "replyTo": null + }, + { + "id": "PRRC_kwDOAPphoM7Bqaie", + "body": "brought those back in 5191c48f65c4.", + "author": { + "login": "noib3" + }, + "createdAt": "2026-05-15T15:10:05Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3249121438", + "replyTo": { + "id": "PRRC_kwDOAPphoM7BLMa7" + } + } + ] + } + }, + { + "id": "PRRT_kwDOAPphoM6CCwCo", + "isResolved": true, + "isOutdated": true, + "path": "runtime/doc/terminal.txt", + "line": 213, + "originalLine": 201, + "diffSide": "LEFT", + "comments": { + "nodes": [ + { + "id": "PRRC_kwDOAPphoM7BLO4w", + "body": "it looks like code changes might support this now, but do we need a test for it? ", + "author": { + "login": "justinmk" + }, + "createdAt": "2026-05-14T11:15:54Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3240947248", + "replyTo": null + }, + { + "id": "PRRC_kwDOAPphoM7Bqbtr", + "body": "let Codex add a test for it in d8988c7f1b46", + "author": { + "login": "noib3" + }, + "createdAt": "2026-05-15T15:10:47Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3249126251", + "replyTo": { + "id": "PRRC_kwDOAPphoM7BLO4w" + } + } + ] + } + }, + { + "id": "PRRT_kwDOAPphoM6CC_XC", + "isResolved": true, + "isOutdated": true, + "path": "src/nvim/terminal.c", + "line": null, + "originalLine": 503, + "diffSide": "RIGHT", + "comments": { + "nodes": [ + { + "id": "PRRC_kwDOAPphoM7BLjF7", + "body": "```suggestion\nstatic void assert_ok(GhosttyResult res)\n```", + "author": { + "login": "justinmk" + }, + "createdAt": "2026-05-14T11:33:29Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3241030011", + "replyTo": null + }, + { + "id": "PRRC_kwDOAPphoM7BqcK9", + "body": "done in 3c057fc8e5cf", + "author": { + "login": "noib3" + }, + "createdAt": "2026-05-15T15:11:05Z", + "url": "https://github.com/neovim/neovim/pull/39773#discussion_r3249128125", + "replyTo": { + "id": "PRRC_kwDOAPphoM7BLjF7" + } + } + ] + } + } + ] + } + } + } +} diff --git a/internal/api/testdata/prDiff.diff b/internal/api/testdata/prDiff.diff new file mode 100644 index 0000000..3c0eb3a --- /dev/null +++ b/internal/api/testdata/prDiff.diff @@ -0,0 +1,19619 @@ +diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml +index a3ad2e98c5607f..a498dd980d804c 100644 +--- a/.github/actions/setup/action.yml ++++ b/.github/actions/setup/action.yml +@@ -27,5 +27,9 @@ runs: + run: ./.github/scripts/install_deps.sh ${INSTALL_FLAGS} + shell: bash + ++ - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 ++ with: ++ version: 0.16.0 ++ + - name: Cache + uses: ./.github/actions/cache +diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml +index 14e0c7276917a3..007de92b3ca2bb 100644 +--- a/.github/workflows/build.yml ++++ b/.github/workflows/build.yml +@@ -29,8 +29,8 @@ jobs: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: +- CMAKE_URL: 'https://cmake.org/files/v3.16/cmake-3.16.0-Linux-x86_64.sh' +- CMAKE_VERSION: '3.16.0' ++ CMAKE_URL: 'https://cmake.org/files/v3.19/cmake-3.19.0-Linux-x86_64.sh' ++ CMAKE_VERSION: '3.19.0' + steps: + - uses: actions/checkout@v7.0.1 + with: +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 72e7427393d53e..046f704283889a 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -7,7 +7,7 @@ + # - verbose output: cmake --build build --verbose + + # Version should match the tested CMAKE_URL in .github/workflows/build.yml. +-cmake_minimum_required(VERSION 3.16) ++cmake_minimum_required(VERSION 3.19) + + project(nvim C) + +@@ -53,7 +53,6 @@ endif() + #------------------------------------------------------------------------------- + set(FUNCS_DATA ${PROJECT_BINARY_DIR}/funcs_data.mpack) + set(TOUCHES_DIR ${PROJECT_BINARY_DIR}/touches) +-set(VTERM_TEST_FILE ${PROJECT_BINARY_DIR}/test/vterm_test_output) + + file(GLOB DOCFILES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/runtime/doc/*.txt) + +diff --git a/LICENSE.txt b/LICENSE.txt +index 7d06aaf8bced96..6da7d37c6ea9fd 100644 +--- a/LICENSE.txt ++++ b/LICENSE.txt +@@ -193,11 +193,11 @@ The externally maintained libraries used by Neovim are: + - LuaJIT: a Just-In-Time Compiler for Lua. Copyright Mike Pall. MIT license. + - Luv: Apache 2.0 license + - gettext ++ - libghostty-vt: MIT license + - libiconv + - libmpack: MIT license + - libtermkey: MIT license + - libuv. Copyright Joyent, Inc. and other Node contributors. Node.js license. +- - libvterm: MIT license + - lua-bitop: MIT license + - lua-cjson: MIT license + - lua-compat: MIT license +diff --git a/build.zig b/build.zig +index 3519786ec74766..0b949e6e102f02 100644 +--- a/build.zig ++++ b/build.zig +@@ -107,6 +107,14 @@ pub fn build(b: *std.Build) !void { + }; + const ci_build = b.option(bool, "ci-build", "CI build") orelse false; + ++ const ghostty = b.dependency("ghostty", .{ ++ .target = target, ++ .optimize = optimize, ++ .@"emit-lib-vt" = true, ++ .@"emit-xcframework" = false, ++ }); ++ const ghostty_vt = ghostty.artifact("ghostty-vt-static"); ++ + const ziglua = b.dependency("zlua", .{ + .target = target, + .optimize = optimize_lua, +@@ -227,7 +235,6 @@ pub fn build(b: *std.Build) !void { + "lua/", + "viml/", + "viml/parser/", +- "vterm/", + }; + + // source names _relative_ src/nvim/, not including other src/ subdircs +@@ -352,8 +359,6 @@ pub fn build(b: *std.Build) !void { + .HAVE_BUILTIN_ADD_OVERFLOW = true, + .HAVE_WIMPLICIT_FALLTHROUGH_FLAG = true, + .HAVE_BITSCANFORWARD64 = null, +- +- .VTERM_TEST_FILE = "test/vterm_test_output", // TODO(bfredl): revisit when porting libvterm tests + }); + + _ = gen_config.addCopyFile(sysconfig_step.getOutputFile(), "auto/config.h"); // run_preprocessor() workaronnd +@@ -458,6 +463,7 @@ pub fn build(b: *std.Build) !void { + if (iconv) |dep| { + try unittest_include_path.append(b.allocator, dep.artifact("iconv").getEmittedIncludeTree()); + } ++ try unittest_include_path.append(b.allocator, ghostty_vt.getEmittedIncludeTree()); + + const gen_headers, const funcs_data = try gen.nvim_gen_sources( + b, +@@ -535,7 +541,9 @@ pub fn build(b: *std.Build) !void { + if (is_windows) { + nvim_mod.linkSystemLibrary("netapi32", .{}); + } ++ nvim_mod.linkLibrary(ghostty_vt); + nvim_mod.addIncludePath(b.path("src")); ++ nvim_mod.addIncludePath(ghostty_vt.getEmittedIncludeTree()); + nvim_mod.addIncludePath(gen_config.getDirectory()); + nvim_mod.addIncludePath(gen_headers.getDirectory()); + try build_lua.add_lua_modules(b, t, nvim_mod, lpeg, use_luajit, false, sys_opts); +@@ -565,6 +573,7 @@ pub fn build(b: *std.Build) !void { + "-std=gnu99", + "-DZIG_BUILD", + "-D_GNU_SOURCE", ++ "-DGHOSTTY_STATIC", + if (support_unittests) "-DUNIT_TESTING" else "", + if (use_luajit) "" else "-DNVIM_VENDOR_BIT", + if (is_windows) "-DMSWIN" else "", +@@ -927,7 +936,6 @@ pub fn test_config(b: *std.Build) ![]u8 { + \\M.translations_enabled = "$ENABLE_TRANSLATIONS" == "ON" + \\M.is_asan = "$ENABLE_ASAN_UBSAN" == "ON" + \\M.is_zig_build = true +- \\M.vterm_test_file = "test/vterm_test_output" + \\M.test_build_dir = _G.nvim_build_dir -- command line arg + \\M.test_source_path = "{[src_path]s}" + \\M.test_lua_prg = "" +diff --git a/build.zig.zon b/build.zig.zon +index 22eaface168ead..5fca594d807817 100644 +--- a/build.zig.zon ++++ b/build.zig.zon +@@ -9,6 +9,10 @@ + .url = "git+https://github.com/natecraddock/ziglua.git#9ae39fa50b1ab8427d35f963216116d7bff1e584", + .hash = "zlua-0.1.0-hGRpC1mUBQCNQf4CnmRTM4I7gxE3YpCvZ-p2EarlcyER", + }, ++ .ghostty = .{ ++ .url = "https://github.com/ghostty-org/ghostty/archive/4133c6e48c4b99d19f5885478a19db4868994d07.tar.gz", ++ .hash = "ghostty-1.3.2-dev-5UdBC3iCJgWjJ0A5ilLHfKkkcNoWp4AkvwXNh8hJdA09", ++ }, + .lpeg = .{ + .url = "https://github.com/neovim/deps/raw/d495ee6f79e7962a53ad79670cb92488abe0b9b4/opt/lpeg-1.1.0.tar.gz", + .hash = "N-V-__8AAMnaAwCEutreuREG3QayBVEZqUTDQFY1Nsrv2OIt", +diff --git a/cmake.config/config.h.in b/cmake.config/config.h.in +index 833218abf9a4cb..fd712032c3b22b 100644 +--- a/cmake.config/config.h.in ++++ b/cmake.config/config.h.in +@@ -52,5 +52,3 @@ + #cmakedefine HAVE_BUILTIN_ADD_OVERFLOW + #cmakedefine HAVE_WIMPLICIT_FALLTHROUGH_FLAG + #cmakedefine HAVE_BITSCANFORWARD64 +- +-#define VTERM_TEST_FILE "@VTERM_TEST_FILE@" +diff --git a/cmake.deps/CMakeLists.txt b/cmake.deps/CMakeLists.txt +index 3d36db52bb19c5..d681e57cc101d6 100644 +--- a/cmake.deps/CMakeLists.txt ++++ b/cmake.deps/CMakeLists.txt +@@ -1,5 +1,5 @@ + # This is not meant to be included by the top-level. +-cmake_minimum_required(VERSION 3.16) ++cmake_minimum_required(VERSION 3.19) + project(NVIM_DEPS C) + + if(POLICY CMP0135) +@@ -38,6 +38,7 @@ option(USE_BUNDLED_TS "Use the bundled treesitter runtime." ${USE_BUNDLED}) + option(USE_BUNDLED_TS_PARSERS "Use the bundled treesitter parsers." ${USE_BUNDLED}) + option(USE_BUNDLED_UNIBILIUM "Use the bundled unibilium." ${USE_BUNDLED}) + option(USE_BUNDLED_UTF8PROC "Use the bundled utf8proc library." ${USE_BUNDLED}) ++option(USE_BUNDLED_GHOSTTY "Use bundled ghostty." ${USE_BUNDLED}) + + if(USE_BUNDLED AND MSVC) + option(USE_BUNDLED_GETTEXT "Use the bundled version of gettext." ON) +@@ -167,6 +168,10 @@ if(USE_BUNDLED_UTF8PROC) + include(BuildUTF8proc) + endif() + ++if(USE_BUNDLED_GHOSTTY) ++ include(BuildGhostty) ++endif() ++ + if(WIN32) + include(GetBinaryDeps) + +diff --git a/cmake.deps/CMakePresets.json b/cmake.deps/CMakePresets.json +index fdec38372c1958..40338e8144a401 100644 +--- a/cmake.deps/CMakePresets.json ++++ b/cmake.deps/CMakePresets.json +@@ -16,6 +16,7 @@ + "description": "Build neovim with external deps on ubuntu", + "cacheVariables": { + "USE_BUNDLED":"OFF", ++ "USE_BUNDLED_GHOSTTY":"ON", + "USE_BUNDLED_TS":"ON", + "USE_BUNDLED_UTF8PROC":"ON", + "ENABLE_WASMTIME":"OFF" +diff --git a/cmake.deps/cmake/BuildGhostty.cmake b/cmake.deps/cmake/BuildGhostty.cmake +new file mode 100644 +index 00000000000000..fdc6d7488b92fd +--- /dev/null ++++ b/cmake.deps/cmake/BuildGhostty.cmake +@@ -0,0 +1,19 @@ ++get_externalproject_options(ghostty ${DEPS_IGNORE_SHA}) ++ ++set(GHOSTTY_CMAKE_ARGS ${DEPS_CMAKE_ARGS}) ++ ++# Ghostty's Debug mode makes terminal tests slow enough for check_term_rep() in ++# test/functional/terminal/buffer_spec.lua to hit a 30-second timeout on some CI ++# runners, so we always build in release mode. ++list(APPEND GHOSTTY_CMAKE_ARGS -D CMAKE_BUILD_TYPE=RelWithDebInfo) ++ ++if(APPLE) ++ list(APPEND GHOSTTY_CMAKE_ARGS -DGHOSTTY_ZIG_BUILD_FLAGS=-Demit-xcframework=false) ++elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") ++ list(APPEND GHOSTTY_CMAKE_ARGS -DGHOSTTY_ZIG_BUILD_FLAGS=-Dcpu=baseline) ++endif() ++ ++ExternalProject_Add(ghostty ++ DOWNLOAD_DIR ${DEPS_DOWNLOAD_DIR}/ghostty ++ CMAKE_ARGS ${GHOSTTY_CMAKE_ARGS} ++ ${EXTERNALPROJECT_OPTIONS}) +diff --git a/cmake.deps/cmake/GettextCMakeLists.txt b/cmake.deps/cmake/GettextCMakeLists.txt +index b86f3c6a5231dc..8298a0d11b9799 100644 +--- a/cmake.deps/cmake/GettextCMakeLists.txt ++++ b/cmake.deps/cmake/GettextCMakeLists.txt +@@ -1,4 +1,4 @@ +-cmake_minimum_required(VERSION 3.16) ++cmake_minimum_required(VERSION 3.19) + project(gettext C) + + add_compile_options(-w) +diff --git a/cmake.deps/cmake/LibiconvCMakeLists.txt b/cmake.deps/cmake/LibiconvCMakeLists.txt +index 91813148839cef..73120fb3a06ef6 100644 +--- a/cmake.deps/cmake/LibiconvCMakeLists.txt ++++ b/cmake.deps/cmake/LibiconvCMakeLists.txt +@@ -1,4 +1,4 @@ +-cmake_minimum_required(VERSION 3.16) ++cmake_minimum_required(VERSION 3.19) + project(libiconv C) + + add_compile_options(-w) +diff --git a/cmake.deps/cmake/LpegCMakeLists.txt b/cmake.deps/cmake/LpegCMakeLists.txt +index ac1da8c062e8d3..a16bad1229649f 100644 +--- a/cmake.deps/cmake/LpegCMakeLists.txt ++++ b/cmake.deps/cmake/LpegCMakeLists.txt +@@ -1,4 +1,4 @@ +-cmake_minimum_required(VERSION 3.16) ++cmake_minimum_required(VERSION 3.19) + project (lpeg C) + + include(GNUInstallDirs) +diff --git a/cmake.deps/cmake/MarkdownParserCMakeLists.txt b/cmake.deps/cmake/MarkdownParserCMakeLists.txt +index 504acb74a59fb2..09629f01cf5886 100644 +--- a/cmake.deps/cmake/MarkdownParserCMakeLists.txt ++++ b/cmake.deps/cmake/MarkdownParserCMakeLists.txt +@@ -1,4 +1,4 @@ +-cmake_minimum_required(VERSION 3.16) ++cmake_minimum_required(VERSION 3.19) + project(${PARSERLANG} C) + + add_compile_options(-w) +diff --git a/cmake.deps/cmake/TreesitterParserCMakeLists.txt b/cmake.deps/cmake/TreesitterParserCMakeLists.txt +index 08b942a0ffa143..d06d313971cab4 100644 +--- a/cmake.deps/cmake/TreesitterParserCMakeLists.txt ++++ b/cmake.deps/cmake/TreesitterParserCMakeLists.txt +@@ -1,4 +1,4 @@ +-cmake_minimum_required(VERSION 3.16) ++cmake_minimum_required(VERSION 3.19) + project(parser C) + + add_compile_options(-w) +diff --git a/cmake.deps/deps.txt b/cmake.deps/deps.txt +index 05f4d4cd9d01b6..4ed3fdee9a30f5 100644 +--- a/cmake.deps/deps.txt ++++ b/cmake.deps/deps.txt +@@ -58,3 +58,6 @@ WASMTIME_SHA256 aa82e354f782125665b448997daec3d9b239c4f180887eb3035f71ab97433fa6 + + UNCRUSTIFY_URL https://github.com/uncrustify/uncrustify/archive/uncrustify-0.83.0.tar.gz + UNCRUSTIFY_SHA256 1d4d3ab3a991c66eb24cb0a3e690573b4882fd98bc6ec64a01c283998c20dc87 ++ ++GHOSTTY_URL https://github.com/ghostty-org/ghostty/archive/4133c6e48c4b99d19f5885478a19db4868994d07.tar.gz ++GHOSTTY_SHA256 cd7cd4ab7f13a703b88bd6d7cb091d56b325f061200564ccfd36b886e3371ac3 +diff --git a/cmake/FindGhostty.cmake b/cmake/FindGhostty.cmake +new file mode 100644 +index 00000000000000..7654d036373387 +--- /dev/null ++++ b/cmake/FindGhostty.cmake +@@ -0,0 +1,10 @@ ++find_path2(GHOSTTY_INCLUDE_DIR ghostty/vt.h) ++find_library2(GHOSTTY_LIBRARY ghostty-vt) ++ ++find_package_handle_standard_args(Ghostty REQUIRED_VARS GHOSTTY_INCLUDE_DIR GHOSTTY_LIBRARY) ++ ++add_library(ghostty INTERFACE) ++target_include_directories(ghostty SYSTEM BEFORE INTERFACE ${GHOSTTY_INCLUDE_DIR}) ++target_link_libraries(ghostty INTERFACE ${GHOSTTY_LIBRARY}) ++ ++mark_as_advanced(GHOSTTY_INCLUDE_DIR GHOSTTY_LIBRARY) +diff --git a/runtime/doc/dev_arch.txt b/runtime/doc/dev_arch.txt +index 4928ead1ca2217..e756403020087b 100644 +--- a/runtime/doc/dev_arch.txt ++++ b/runtime/doc/dev_arch.txt +@@ -26,8 +26,8 @@ C CODE + + - Nvim C code lives in `src/nvim/`. + - This includes third-party components which we have forked and fully +- "own", meaning we no longer track the upstream. In particular: `vterm/` +- and `tui/termkey/`. ++ "own", meaning we no longer track the upstream. In particular: ++ `tui/termkey/`. + - Vendored third-party components live in `src/*`. + - `src/tee/` and `src/xxd/` have their own build files. They are shipped + with the Windows artifact to ensure those tools exists there. +diff --git a/runtime/doc/news-0.11.txt b/runtime/doc/news-0.11.txt +index 2f5f842d83b569..2aaafc99c17fb2 100644 +--- a/runtime/doc/news-0.11.txt ++++ b/runtime/doc/news-0.11.txt +@@ -358,7 +358,7 @@ TERMINAL + status line in |terminal| windows. + • The terminal buffer now supports reflow (wrapped lines adapt when the buffer + is resized horizontally). Note: Lines that are not visible and kept in +- 'scrollback' are not reflown. ++ 'scrollback' are not reflowed. + • The |terminal| now supports OSC 8 escape sequences and will display + hyperlinks in supporting host terminals. + • The |terminal| now uses the actual cursor, rather than a "virtual" cursor. +diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt +index 5f32b79eb674c3..833eec9416d6f4 100644 +--- a/runtime/doc/news.txt ++++ b/runtime/doc/news.txt +@@ -416,7 +416,9 @@ STARTUP + + TERMINAL + +-• todo ++• The terminal emulator now uses libghostty-vt instead of libvterm. Lines in ++ the terminal scrollback region are now reflowed on resize like lines in the ++ visible area. + + TREESITTER + +diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt +index 8086129f6886db..156aa4d57ae9de 100644 +--- a/runtime/doc/options.txt ++++ b/runtime/doc/options.txt +@@ -5327,14 +5327,12 @@ A jump table for the options with a short description can be found at |Q_op|. + *'scrollback'* *'scbk'* + 'scrollback' 'scbk' number (default 10000) + local to buffer +- Maximum number of lines kept beyond the visible screen. Lines at the +- top are deleted if new lines exceed this limit. ++ Requested maximum number of lines kept beyond the visible screen. ++ The actual number may differ because the terminal emulator stores and ++ discards scrollback in larger chunks. + Minimum is 1, maximum is 1000000. + Only in |terminal| and |prompt-buffer| buffers. + +- Note: Lines that are not visible and kept in terminal scrollback are not +- reflown when the terminal buffer is resized horizontally. +- + *'scrollbind'* *'scb'* *'noscrollbind'* *'noscb'* + 'scrollbind' 'scb' boolean (default off) + local to window +diff --git a/runtime/doc/terminal.txt b/runtime/doc/terminal.txt +index 3f9c3f650fd802..94012984beb465 100644 +--- a/runtime/doc/terminal.txt ++++ b/runtime/doc/terminal.txt +@@ -6,7 +6,7 @@ + + Terminal emulator *terminal* *terminal-emulator* + +-Nvim embeds a VT220/xterm terminal emulator based on libvterm. The terminal is ++Nvim embeds a VT220/xterm terminal emulator based on libghostty. The terminal is + presented as a special 'buftype', asynchronously updated as data is received + from the connected program. + +@@ -209,9 +209,6 @@ clipboard. Reading from the system clipboard with OSC 52 is not supported, as + this would allow any arbitrary program in the :terminal to read the user's + clipboard. + +-OSC 52 sequences sent from the :terminal buffer do not emit a |TermRequest| +-event. The event is handled directly by Nvim and is not forwarded to plugins. +- + OSC 133: shell integration *terminal-osc133* *shell-prompt* + + Shells can emit semantic escape sequences (OSC 133) to mark where each prompt +diff --git a/runtime/lua/vim/_core/defaults.lua b/runtime/lua/vim/_core/defaults.lua +index 4414608cbfe79a..c854fb49a61c0f 100644 +--- a/runtime/lua/vim/_core/defaults.lua ++++ b/runtime/lua/vim/_core/defaults.lua +@@ -634,33 +634,6 @@ do + set_terminal_exitmsg(ev.buf, msg, pos) + end) + +- nvim_on('TermRequest', nvim_terminal_augroup, { +- desc = 'Handles OSC foreground/background color requests', +- }, function(ev) +- --- @type integer +- local channel = vim.bo[ev.buf].channel +- if channel == 0 then +- return +- end +- local fg_request = ev.data.sequence == '\027]10;?' +- local bg_request = ev.data.sequence == '\027]11;?' +- if fg_request or bg_request then +- -- WARN: This does not return the actual foreground/background color, +- -- but rather returns: +- -- - fg=white/bg=black when Nvim option 'background' is 'dark' +- -- - fg=black/bg=white when Nvim option 'background' is 'light' +- local red, green, blue = 0, 0, 0 +- local bg_option_dark = vim.o.background == 'dark' +- if (fg_request and bg_option_dark) or (bg_request and not bg_option_dark) then +- red, green, blue = 65535, 65535, 65535 +- end +- local command = fg_request and 10 or 11 +- local data = +- string.format('\027]%d;rgb:%04x/%04x/%04x%s', command, red, green, blue, ev.data.terminator) +- vim.api.nvim_chan_send(channel, data) +- end +- end) +- + local nvim_terminal_prompt_ns = vim.api.nvim_create_namespace('nvim.terminal.prompt') + nvim_on('TermRequest', nvim_terminal_augroup, { + desc = 'Mark shell prompts indicated by OSC 133 sequences for navigation', +diff --git a/runtime/lua/vim/_meta/options.gen.lua b/runtime/lua/vim/_meta/options.gen.lua +index 6932bbfca51a9f..6bb3390c1a9ff8 100644 +--- a/runtime/lua/vim/_meta/options.gen.lua ++++ b/runtime/lua/vim/_meta/options.gen.lua +@@ -5513,14 +5513,12 @@ vim.o.scr = vim.o.scroll + vim.wo.scroll = vim.o.scroll + vim.wo.scr = vim.wo.scroll + +---- Maximum number of lines kept beyond the visible screen. Lines at the +---- top are deleted if new lines exceed this limit. ++--- Requested maximum number of lines kept beyond the visible screen. ++--- The actual number may differ because the terminal emulator stores and ++--- discards scrollback in larger chunks. + --- Minimum is 1, maximum is 1000000. + --- Only in `terminal` and `prompt-buffer` buffers. + --- +---- Note: Lines that are not visible and kept in terminal scrollback are not +---- reflown when the terminal buffer is resized horizontally. +---- + --- @type integer + vim.o.scrollback = -1 + vim.o.scbk = vim.o.scrollback +diff --git a/src/clint.lua b/src/clint.lua +index dd668d8db47aed..2fc13acd9cafed 100755 +--- a/src/clint.lua ++++ b/src/clint.lua +@@ -788,7 +788,6 @@ local function check_includes(filename, lines, error) + 'mpack/object.h', + 'nvim/func_attr.h', + 'termkey/termkey.h', +- 'vterm/vterm.h', + 'xdiff/xdiff.h', + } + +diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt +index 046e1b65931fcb..13e56b84b5d173 100644 +--- a/src/nvim/CMakeLists.txt ++++ b/src/nvim/CMakeLists.txt +@@ -58,6 +58,9 @@ if(ENABLE_WASMTIME) + target_compile_definitions(nvim_bin PRIVATE HAVE_WASMTIME) + endif() + ++find_package(Ghostty REQUIRED) ++target_link_libraries(main_lib INTERFACE ghostty) ++ + # The unit test lib requires LuaJIT; it will be skipped if LuaJIT is missing. + option(PREFER_LUA "Prefer Lua over LuaJIT in the nvim executable." OFF) + if(PREFER_LUA) +@@ -419,7 +422,6 @@ foreach(subdir + msgpack_rpc + tui + tui/termkey +- vterm + event + eval + lua +diff --git a/src/nvim/auevents.lua b/src/nvim/auevents.lua +index 2497d90a171315..f0ae264ed44cc2 100644 +--- a/src/nvim/auevents.lua ++++ b/src/nvim/auevents.lua +@@ -123,7 +123,7 @@ return { + TermEnter = false, -- after entering Terminal mode + TermLeave = false, -- after leaving Terminal mode + TermOpen = false, -- after opening a terminal buffer +- TermRequest = false, -- after an unhandled OSC sequence is emitted ++ TermRequest = false, -- after an OSC, DCS, or APC sequence is emitted + TermResponse = false, -- after setting "v:termresponse" + TextChanged = true, -- text was modified + TextChangedI = true, -- text was modified in Insert mode(no popup) +diff --git a/src/nvim/highlight_group.c b/src/nvim/highlight_group.c +index 7f1b3f8e1ee58d..04a56fd110ac42 100644 +--- a/src/nvim/highlight_group.c ++++ b/src/nvim/highlight_group.c +@@ -47,6 +47,7 @@ + #include "nvim/os/time.h" + #include "nvim/runtime.h" + #include "nvim/strings.h" ++#include "nvim/terminal.h" + #include "nvim/types_defs.h" + #include "nvim/ui.h" + #include "nvim/ui_defs.h" +@@ -1019,10 +1020,12 @@ void set_hl_group(int id, HlAttrs attrs, Dict(highlight) *dict, int link_id) + if (did_changed) { + highlight_attr_set_all(); + } ++ terminal_update_colors_all(); + ui_default_colors_set(); + } else { + // a cursor style uses this syn_id, make sure its attribute is updated. + if (cursor_mode_uses_syn_id(id)) { ++ terminal_update_colors_all(); + ui_mode_info_set(); + } + } +@@ -1516,6 +1519,7 @@ void do_highlight(const char *line, const bool forceit, const bool init) + // redraw below will still handle usages of guibg=fg etc. + ui_default_colors_set(); + } ++ terminal_update_colors_all(); + did_highlight_changed = true; + redraw_all_later(UPD_NOT_VALID); + } else { +@@ -1984,6 +1988,7 @@ static void set_hl_attr(int idx) + + // a cursor style uses this syn_id, make sure its attribute is updated. + if (cursor_mode_uses_syn_id(idx + 1)) { ++ terminal_update_colors_all(); + ui_mode_info_set(); + } + } +diff --git a/src/nvim/option.c b/src/nvim/option.c +index 5e549e12367d14..6c300eff7cc89a 100644 +--- a/src/nvim/option.c ++++ b/src/nvim/option.c +@@ -2684,9 +2684,12 @@ static const char *did_set_scrollback(optset_T *args) + OptInt old_value = args->os_oldval.data.integer; + OptInt value = args->os_newval.data.integer; + +- if (buf->terminal && value < old_value) { +- // Force the scrollback to take immediate effect only when decreasing it. +- on_scrollback_option_changed(buf->terminal); ++ if (buf->terminal && !(args->os_flags & OPT_GLOBAL)) { ++ size_t old_limit = old_value < 1 ? SB_MAX : (size_t)old_value; ++ size_t limit = value < 1 ? SB_MAX : (size_t)value; ++ buf->b_p_scbk = (OptInt)limit; ++ // Force a terminal refresh when the value decreases. ++ on_scrollback_option_changed(buf->terminal, limit, limit < old_limit); + } + return NULL; + } +diff --git a/src/nvim/options.lua b/src/nvim/options.lua +index a0708612337168..e669064dc4c5dc 100644 +--- a/src/nvim/options.lua ++++ b/src/nvim/options.lua +@@ -7466,13 +7466,11 @@ local options = { + doc = '10000', + }, + desc = [=[ +- Maximum number of lines kept beyond the visible screen. Lines at the +- top are deleted if new lines exceed this limit. ++ Requested maximum number of lines kept beyond the visible screen. ++ The actual number may differ because the terminal emulator stores and ++ discards scrollback in larger chunks. + Minimum is 1, maximum is 1000000. + Only in |terminal| and |prompt-buffer| buffers. +- +- Note: Lines that are not visible and kept in terminal scrollback are not +- reflown when the terminal buffer is resized horizontally. + ]=], + full_name = 'scrollback', + redraw = { 'current_buffer' }, +diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c +index a224d2290e6759..d448e24fd66573 100644 +--- a/src/nvim/optionstr.c ++++ b/src/nvim/optionstr.c +@@ -591,6 +591,7 @@ const char *did_set_background(optset_T *args) + FOR_ALL_BUFFERS(buf) { + if (buf->terminal) { + terminal_notify_theme(buf->terminal, dark); ++ terminal_update_colors(buf->terminal); + } + } + +@@ -1322,6 +1323,8 @@ const char *did_set_guicursor(optset_T *args FUNC_ATTR_UNUSED) + if (errmsg != NULL) { + return errmsg; + } ++ terminal_update_colors_all(); ++ terminal_update_default_cursor_all(); + if (Visual.active) { + // In Visual mode cursor may be drawn differently. + redrawWinline(curwin, curwin->w_cursor.lnum); +diff --git a/src/nvim/os/shell.c b/src/nvim/os/shell.c +index 7683d6cbbcdca5..01b2d1ae734e92 100644 +--- a/src/nvim/os/shell.c ++++ b/src/nvim/os/shell.c +@@ -1151,7 +1151,7 @@ static void out_data_append_to_screen(const char *output, size_t *count, int fd, + // 2. we don't compose chars over buffer boundaries, even if we see an + // incomplete UTF-8 sequence that could be composing with the last + // complete sequence. +- // This will be corrected when we switch to vterm based implementation ++ // This can be corrected by handling grapheme clusters across buffer boundaries. + while (p < end) { + int i = *p ? utfc_ptr2len_len(p, (int)*count - (int)(p - output)) : 1; + if (!eof && i == 1 && utf8len_tab_zero[*(uint8_t *)p] > (end - p)) { +diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c +index 84a21521a962cb..43256fb3cd1194 100644 +--- a/src/nvim/terminal.c ++++ b/src/nvim/terminal.c +@@ -1,28 +1,16 @@ + // VT220/xterm-like terminal emulator. +-// Powered by libvterm http://www.leonerd.org.uk/code/libvterm ++// Powered by libghostty https://ghostty.org + // +-// libvterm is a pure C99 terminal emulation library with abstract input and +-// display. This means that the library needs to read data from the master fd +-// and feed VTerm instances, which will invoke user callbacks with screen +-// update instructions that must be mirrored to the real display. +-// +-// Keys are sent to VTerm instances by calling +-// vterm_keyboard_key/vterm_keyboard_unichar, which generates byte streams that +-// must be fed back to the master fd. ++// Keys are encoded into byte streams that must be fed back to the master fd. + // + // Nvim buffers are used as the display mechanism for both the visible screen + // and the scrollback buffer. + // +-// When a line becomes invisible due to a decrease in screen height or because +-// a line was pushed up during normal terminal output, we store the line +-// information in the scrollback buffer, which is mirrored in the nvim buffer +-// by appending lines just above the visible part of the buffer. +-// +-// When the screen height increases, libvterm will ask for a row in the +-// scrollback buffer, which is mirrored in the nvim buffer displaying lines +-// that were previously invisible. ++// libghostty owns the terminal screen and scrollback contents. Nvim buffers ++// mirror Ghostty's screen and the visible slice of Ghostty's history so ++// scrollback remains a normal buffer. + // +-// The vterm->nvim synchronization is performed in intervals of 10 milliseconds, ++// The terminal->nvim synchronization is performed in intervals of 10 milliseconds, + // to minimize screen updates when receiving large bursts of data. + // + // This module is decoupled from the processes that normally feed it data, so +@@ -34,6 +22,7 @@ + // Some code from pangoterm http://www.leonerd.org.uk/code/pangoterm + + #include ++#include + #include + #include + #include +@@ -46,6 +35,7 @@ + #include "nvim/ascii_defs.h" + #include "nvim/autocmd.h" + #include "nvim/autocmd_defs.h" ++#include "nvim/base64.h" + #include "nvim/buffer.h" + #include "nvim/buffer_defs.h" + #include "nvim/change.h" +@@ -96,14 +86,6 @@ + #include "nvim/types_defs.h" + #include "nvim/ui.h" + #include "nvim/vim_defs.h" +-#include "nvim/vterm/keyboard.h" +-#include "nvim/vterm/mouse.h" +-#include "nvim/vterm/parser.h" +-#include "nvim/vterm/pen.h" +-#include "nvim/vterm/screen.h" +-#include "nvim/vterm/state.h" +-#include "nvim/vterm/vterm.h" +-#include "nvim/vterm/vterm_keycodes_defs.h" + #include "nvim/window.h" + + typedef struct { +@@ -126,48 +108,100 @@ typedef struct { + OptInt save_w_p_siso; + } TerminalState; + ++typedef struct { ++ int16_t idx; ++ int rgb; ++ bool is_default; ++} TerminalColorAttrs; ++ ++typedef enum { ++ kTermRequestParserNormal = 0, ++ kTermRequestParserEsc, ++ kTermRequestParserCsi, ++ kTermRequestParserOsc, ++ kTermRequestParserDcs, ++ kTermRequestParserApc, ++ kTermRequestParserStringEsc, ++} TermRequestParserState; ++ ++typedef enum { ++ kTermRequestKindNone = 0, ++ kTermRequestKindOsc, ++ kTermRequestKindDcs, ++ kTermRequestKindApc, ++} TermRequestKind; ++ ++typedef enum { ++ kTermRequestTerminatorBel = 0, ++ kTermRequestTerminatorSt, ++} TermRequestTerminator; ++ ++typedef enum { ++ kTermRequestParserEventNone = 0, ++ kTermRequestParserEventStart, ++ kTermRequestParserEventFinish, ++ kTermRequestParserEventSyncOutput, ++} TermRequestParserEvent; ++ ++typedef struct { ++ uint16_t parameter; ++ bool parameter_set; ++ bool private; ++ bool has_mode; ++} SyncOutputParser; ++ ++typedef enum { ++ kTerminalClipboardRegister = 0, ++ kTerminalClipboardPrimary, ++} TerminalClipboardRegister; ++ + #include "terminal.c.generated.h" + +-// Delay for refreshing the terminal buffer after receiving updates from +-// libvterm. Improves performance when receiving large bursts of data. ++// Delay for refreshing the terminal buffer after receiving updates. Improves ++// performance when receiving large bursts of data. + #define REFRESH_DELAY 10 + +-#define TEXTBUF_SIZE 0x1fff +-#define SELECTIONBUF_SIZE 0x0400 ++#define TEXTBUF_SIZE 0x1fff ++ ++// Whether to include OSC 52/clipboard support in Ghostty's DA1 response. Functional tests ++// toggle this to force the runtime OSC 52 detection path to fall back to XTGETTCAP. ++DLLEXPORT int terminal_ghostty_da_clipboard = 1; + + static TimeWatcher refresh_timer; + static bool refresh_pending = false; + +-typedef struct { +- size_t cols; +- VTermScreenCell cells[]; +-} ScrollbackLine; +- + struct terminal { + TerminalOptions opts; // options passed to terminal_alloc() +- VTerm *vt; +- VTermScreen *vts; +- // buffer used to: +- // - convert VTermScreen cell arrays into utf8 strings +- // - receive data from libvterm as a result of key presses. ++ GhosttyTerminal ghostty; ++ GhosttyRenderState ghostty_render_state; ++ GhosttyRenderStateRowIterator ghostty_render_row_iterator; ++ GhosttyRenderStateRowCells ghostty_render_row_cells; ++ GhosttyKeyEncoder ghostty_key_encoder; ++ GhosttyKeyEvent ghostty_key_event; ++ GhosttyMouseEncoder ghostty_mouse_encoder; ++ GhosttyMouseEvent ghostty_mouse_event; ++ unsigned ghostty_mouse_buttons; ++ struct { ++ bool x10; ++ bool normal; ++ bool button; ++ bool any; ++ bool utf8; ++ bool sgr; ++ bool urxvt; ++ bool sgr_pixels; ++ } ghostty_mouse_modes; ++ // Buffer used to fetch Ghostty rows. + char textbuf[TEXTBUF_SIZE]; + +- ScrollbackLine **sb_buffer; ///< Scrollback storage. +- size_t sb_current; ///< Lines stored in sb_buffer. +- size_t sb_size; ///< Capacity of sb_buffer. +- /// "virtual index" that points to the first sb_buffer row that we need to +- /// push to the terminal buffer when refreshing the scrollback. +- int sb_pending; +- size_t sb_deleted; ///< Lines deleted from sb_buffer. +- size_t old_sb_deleted; ///< Value of sb_deleted on last refresh_scrollback(). +- /// Lines in the terminal buffer belonging to the screen instead of the scrollback. +- int old_height; +- +- char *title; // VTermStringFragment buffer +- size_t title_len; +- size_t title_size; +- +- // buf_T instance that acts as a "drawing surface" for libvterm ++ /// Previous active-screen origin, used to detect history added while Ghostty prunes old pages. ++ GhosttyTrackedGridRef ghostty_scrollback_anchor; ++ size_t scrollback_rows; ///< Ghostty history rows mirrored in the nvim buffer. ++ size_t scrollback_deleted; ///< Mirrored history rows deleted from the buffer top. ++ bool scrollback_clear_pending; ///< Ghostty processed CSI 3 J since the last refresh. ++ size_t scrollback_clear_rows; ///< Buffer rows that became history before CSI 3 J. ++ ++ // buf_T instance that acts as a "drawing surface" for the terminal. + // we can't store a direct reference to the buffer because the + // refresh_timer_cb may be called after the buffer was freed, and there's + // no way to know if the memory was reused. +@@ -180,12 +214,11 @@ struct terminal { + // when true, the terminal's destruction is already enqueued. + bool destroy; + +- // some vterm properties +- bool forward_mouse; +- int invalid_start, invalid_end; // invalid rows in libvterm screen ++ // some terminal properties ++ int invalid_start, invalid_end; // invalid rows in Ghostty screen + struct { + int row, col; +- int shape; ++ GhosttyRenderStateCursorVisualStyle shape; + bool visible; ///< Terminal wants to show cursor. + ///< `TerminalState.cursor_visible` indicates whether it is actually shown. + bool blink; +@@ -202,36 +235,18 @@ struct terminal { + bool theme_updates; ///< Send a theme update notification when 'bg' changes + bool synchronized_output; ///< Mode 2026: suppress redraws until end of synchronized update + bool sync_flush_pending; ///< Set when mode 2026 ends; triggers immediate buffer refresh ++ SyncOutputParser sync_output_parser; + + bool color_set[16]; + +- char *selection_buffer; ///< libvterm selection buffer +- StringBuilder selection; ///< Growable array containing full selection data +- + StringBuilder termrequest_buffer; ///< Growable array containing unfinished request sequence +- VTermTerminator termrequest_terminator; ///< Terminator (BEL or ST) used in the termrequest ++ TermRequestParserState termrequest_state; ///< Current OSC/DCS/APC TermRequest or CSI parser state. ++ TermRequestKind termrequest_kind; ///< Current OSC/DCS/APC sequence kind. ++ TermRequestTerminator termrequest_terminator; ///< Terminator (BEL or ST) used in request. + + size_t refcount; // reference count + }; + +-static VTermScreenCallbacks vterm_screen_callbacks = { +- .damage = term_damage, +- .moverect = term_moverect, +- .movecursor = term_movecursor, +- .settermprop = term_settermprop, +- .bell = term_bell, +- .theme = term_theme, +- .sb_pushline = term_sb_push, // Called before a line goes offscreen. +- .sb_popline = term_sb_pop, +- .sb_clear = term_sb_clear, +-}; +- +-static VTermSelectionCallbacks vterm_selection_callbacks = { +- .set = term_selection_set, +- // For security reasons we don't support querying the system clipboard from the embedded terminal +- .query = NULL, +-}; +- + static Set(ptr_t) invalidated_terminals = SET_INIT; + + static void emit_termrequest(void **argv) +@@ -242,8 +257,8 @@ static void emit_termrequest(void **argv) + StringBuilder *pending_send = argv[3]; + int row = (int)(intptr_t)argv[4]; + int col = (int)(intptr_t)argv[5]; +- size_t sb_deleted = (size_t)(intptr_t)argv[6]; +- VTermTerminator terminator = (VTermTerminator)(intptr_t)argv[7]; ++ size_t scrollback_deleted = (size_t)(intptr_t)argv[6]; ++ TermRequestTerminator terminator = (TermRequestTerminator)(intptr_t)argv[7]; + + buf_T *buf = handle_get_buffer(buf_handle); + if (!buf || buf->terminal == NULL) { // Terminal already closed. +@@ -254,20 +269,12 @@ static void emit_termrequest(void **argv) + } + Terminal *term = buf->terminal; + +- if (term->sb_pending > 0) { +- // Don't emit the event while there is pending scrollback because we need +- // the buffer contents to be fully updated. If this is the case, schedule +- // the event onto the pending queue where it will be executed after the +- // terminal is refreshed and the pending scrollback is cleared. +- multiqueue_put(term->pending.events, emit_termrequest, argv[0], argv[1], argv[2], +- argv[3], argv[4], argv[5], argv[6], argv[7]); +- return; +- } ++ refresh_terminal(term); + + set_vim_var_string(VV_TERMREQUEST, sequence, (ptrdiff_t)sequence_length); + + MAXSIZE_TEMP_ARRAY(cursor, 2); +- ADD_C(cursor, INTEGER_OBJ(row - (int64_t)(term->sb_deleted - sb_deleted))); ++ ADD_C(cursor, INTEGER_OBJ(row - (int64_t)(term->scrollback_deleted - scrollback_deleted))); + ADD_C(cursor, INTEGER_OBJ(col)); + + MAXSIZE_TEMP_DICT(data, 3); +@@ -276,7 +283,7 @@ static void emit_termrequest(void **argv) + PUT_C(data, "cursor", ARRAY_OBJ(cursor)); + PUT_C(data, "terminator", + terminator == +- VTERM_TERMINATOR_BEL ? STATIC_CSTR_AS_OBJ("\x07") : STATIC_CSTR_AS_OBJ("\x1b\\")); ++ kTermRequestTerminatorBel ? STATIC_CSTR_AS_OBJ("\x07") : STATIC_CSTR_AS_OBJ("\x1b\\")); + + term->refcount++; + apply_autocmds_group(EVENT_TERMREQUEST, NULL, NULL, true, AUGROUP_ALL, buf, NULL, +@@ -308,137 +315,245 @@ static void schedule_termrequest(Terminal *term) + term->pending.send = xmalloc(sizeof(StringBuilder)); + kv_init(*term->pending.send); + ++ terminal_ghostty_cursor_position_update(term); + int line = row_to_linenr(term, term->cursor.row); + multiqueue_put(main_loop.events, emit_termrequest, (void *)(intptr_t)term->buf_handle, + xmemdup(term->termrequest_buffer.items, term->termrequest_buffer.size), + (void *)(intptr_t)term->termrequest_buffer.size, term->pending.send, + (void *)(intptr_t)line, (void *)(intptr_t)term->cursor.col, +- (void *)(intptr_t)term->sb_deleted, ++ (void *)(intptr_t)term->scrollback_deleted, + (void *)(intptr_t)term->termrequest_terminator); + } + +-static int parse_osc8(const char *str, int *attr) ++static void terminal_termrequest_begin(Terminal *term, TermRequestKind kind) + FUNC_ATTR_NONNULL_ALL + { +- // Parse the URI from the OSC 8 sequence and add the URL to our URL set. +- // Skip the ID, we don't use it (for now) +- size_t i = 0; +- for (; str[i] != NUL; i++) { +- if (str[i] == ';') { +- break; +- } ++ term->termrequest_kind = kind; ++ term->termrequest_state = kind == kTermRequestKindOsc ++ ? kTermRequestParserOsc ++ : kind == kTermRequestKindDcs ++ ? kTermRequestParserDcs ++ : kTermRequestParserApc; ++ kv_size(term->termrequest_buffer) = 0; ++ switch (kind) { ++ case kTermRequestKindOsc: ++ kv_concat_len(term->termrequest_buffer, "\x1b]", 2); ++ break; ++ case kTermRequestKindDcs: ++ kv_concat_len(term->termrequest_buffer, "\x1bP", 2); ++ break; ++ case kTermRequestKindApc: ++ kv_concat_len(term->termrequest_buffer, "\x1b_", 2); ++ break; ++ case kTermRequestKindNone: ++ break; + } ++} + +- if (str[i] != ';') { +- // Invalid OSC sequence +- return 0; +- } ++static void terminal_termrequest_finish(Terminal *term, TermRequestTerminator terminator) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ term->termrequest_terminator = terminator; ++ term->termrequest_state = kTermRequestParserNormal; ++} + +- // Move past the semicolon +- i++; ++static void terminal_sync_output_parser_begin(Terminal *term) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ term->termrequest_state = kTermRequestParserCsi; ++ term->sync_output_parser = (SyncOutputParser) { 0 }; ++} + +- if (str[i] == NUL) { +- // Empty OSC 8, no URL +- *attr = 0; +- return 1; ++/// Returns true at the end of a DECSET/DECRST sequence containing mode 2026. ++/// ++/// Ghostty exposes the current mode only, so the caller samples it at this boundary to preserve ++/// transitions when a complete synchronized update arrives in one input write. ++static bool terminal_sync_output_parse_byte(Terminal *term, uint8_t c) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ if (c >= '0' && c <= '9') { ++ term->sync_output_parser.parameter = MIN((uint16_t)2027, ++ (uint16_t)(term->sync_output_parser.parameter * 10 ++ + (c - '0'))); ++ term->sync_output_parser.parameter_set = true; ++ return false; + } +- +- *attr = hl_add_url(0, str + i); +- return 1; ++ if (term->sync_output_parser.parameter_set) { ++ term->sync_output_parser.has_mode |= term->sync_output_parser.parameter == 2026; ++ term->sync_output_parser.parameter = 0; ++ term->sync_output_parser.parameter_set = false; ++ } ++ if (c == '?') { ++ term->sync_output_parser.private = true; ++ } ++ return term->sync_output_parser.private ++ && term->sync_output_parser.has_mode ++ && (c == 'h' || c == 'l'); + } + +-static int on_osc(int command, VTermStringFragment frag, void *user) ++static TermRequestParserEvent terminal_termrequest_parse_byte(Terminal *term, uint8_t c) + FUNC_ATTR_NONNULL_ALL + { +- Terminal *term = user; ++ switch (term->termrequest_state) { ++ case kTermRequestParserNormal: ++ switch (c) { ++ case 0x90: ++ terminal_termrequest_begin(term, kTermRequestKindDcs); ++ return kTermRequestParserEventStart; ++ case 0x9d: ++ terminal_termrequest_begin(term, kTermRequestKindOsc); ++ return kTermRequestParserEventStart; ++ case 0x9f: ++ terminal_termrequest_begin(term, kTermRequestKindApc); ++ return kTermRequestParserEventStart; ++ case 0x9b: ++ terminal_sync_output_parser_begin(term); ++ return kTermRequestParserEventNone; ++ case ESC: ++ term->termrequest_state = kTermRequestParserEsc; ++ return kTermRequestParserEventNone; ++ default: ++ return kTermRequestParserEventNone; ++ } + +- if (frag.str == NULL || frag.len == 0) { +- return 0; +- } ++ case kTermRequestParserEsc: ++ switch (c) { ++ case ']': ++ terminal_termrequest_begin(term, kTermRequestKindOsc); ++ return kTermRequestParserEventStart; ++ case 'P': ++ terminal_termrequest_begin(term, kTermRequestKindDcs); ++ return kTermRequestParserEventStart; ++ case '_': ++ terminal_termrequest_begin(term, kTermRequestKindApc); ++ return kTermRequestParserEventStart; ++ case '[': ++ case 0x9b: ++ terminal_sync_output_parser_begin(term); ++ return kTermRequestParserEventNone; ++ case ESC: ++ return kTermRequestParserEventNone; ++ default: ++ term->termrequest_state = kTermRequestParserNormal; ++ return kTermRequestParserEventNone; ++ } + +- if (command != 8 && !has_event(EVENT_TERMREQUEST)) { +- return 1; +- } ++ case kTermRequestParserCsi: ++ if (c == ESC) { ++ term->termrequest_state = kTermRequestParserEsc; ++ return kTermRequestParserEventNone; ++ } ++ if (c == 0x9b) { ++ terminal_sync_output_parser_begin(term); ++ return kTermRequestParserEventNone; ++ } ++ if (terminal_sync_output_parse_byte(term, c)) { ++ term->termrequest_state = kTermRequestParserNormal; ++ return kTermRequestParserEventSyncOutput; ++ } ++ if (c >= 0x40 && c <= 0x7e) { ++ term->termrequest_state = kTermRequestParserNormal; ++ } ++ return kTermRequestParserEventNone; ++ ++ case kTermRequestParserOsc: ++ case kTermRequestParserDcs: ++ case kTermRequestParserApc: ++ if (c == 0x9c) { ++ terminal_termrequest_finish(term, kTermRequestTerminatorSt); ++ return kTermRequestParserEventFinish; ++ } ++ if (term->termrequest_kind == kTermRequestKindOsc && c == BELL) { ++ terminal_termrequest_finish(term, kTermRequestTerminatorBel); ++ return kTermRequestParserEventFinish; ++ } ++ if (c == ESC) { ++ term->termrequest_state = kTermRequestParserStringEsc; ++ return kTermRequestParserEventNone; ++ } ++ kv_push(term->termrequest_buffer, (char)c); ++ return kTermRequestParserEventNone; + +- if (frag.initial) { +- kv_size(term->termrequest_buffer) = 0; +- kv_printf(term->termrequest_buffer, "\x1b]%d;", command); +- } +- kv_concat_len(term->termrequest_buffer, frag.str, frag.len); +- if (frag.final) { +- term->termrequest_terminator = frag.terminator; +- if (has_event(EVENT_TERMREQUEST)) { +- schedule_termrequest(term); +- } +- if (command == 8) { +- kv_push(term->termrequest_buffer, NUL); +- const size_t off = STRLEN_LITERAL("\x1b]8;"); +- int attr = 0; +- if (parse_osc8(term->termrequest_buffer.items + off, &attr)) { +- VTermState *state = vterm_obtain_state(term->vt); +- VTermValue value = { .number = attr }; +- vterm_state_set_penattr(state, VTERM_ATTR_URI, VTERM_VALUETYPE_INT, &value); +- } ++ case kTermRequestParserStringEsc: ++ if (c == '\\') { ++ terminal_termrequest_finish(term, kTermRequestTerminatorSt); ++ return kTermRequestParserEventFinish; ++ } ++ kv_push(term->termrequest_buffer, ESC); ++ if (c == 0x9c) { ++ terminal_termrequest_finish(term, kTermRequestTerminatorSt); ++ return kTermRequestParserEventFinish; + } ++ if (term->termrequest_kind == kTermRequestKindOsc && c == BELL) { ++ terminal_termrequest_finish(term, kTermRequestTerminatorBel); ++ return kTermRequestParserEventFinish; ++ } ++ kv_push(term->termrequest_buffer, (char)c); ++ term->termrequest_state = term->termrequest_kind == kTermRequestKindOsc ++ ? kTermRequestParserOsc ++ : term->termrequest_kind == kTermRequestKindDcs ++ ? kTermRequestParserDcs ++ : kTermRequestParserApc; ++ return kTermRequestParserEventNone; + } +- return 1; ++ return kTermRequestParserEventNone; + } + +-static int on_dcs(const char *command, size_t commandlen, VTermStringFragment frag, void *user) ++static TerminalClipboardRegister terminal_osc52_register(const char *selectors, size_t len) ++ FUNC_ATTR_NONNULL_ALL + { +- Terminal *term = user; +- +- if (command == NULL || frag.str == NULL) { +- return 0; +- } +- if (!has_event(EVENT_TERMREQUEST)) { +- return 1; ++ if (len == 0) { ++ return kTerminalClipboardRegister; + } + +- if (frag.initial) { +- kv_size(term->termrequest_buffer) = 0; +- kv_printf(term->termrequest_buffer, "\x1bP%.*s", (int)commandlen, command); +- } +- kv_concat_len(term->termrequest_buffer, frag.str, frag.len); +- if (frag.final) { +- term->termrequest_terminator = frag.terminator; +- schedule_termrequest(term); ++ for (size_t i = 0; i < len; i++) { ++ if (selectors[i] != 'p') { ++ return kTerminalClipboardRegister; ++ } + } +- return 1; ++ return kTerminalClipboardPrimary; + } + +-static int on_apc(VTermStringFragment frag, void *user) ++static void terminal_osc52_handle(Terminal *term) ++ FUNC_ATTR_NONNULL_ALL + { +- Terminal *term = user; +- if (frag.str == NULL || frag.len == 0) { +- return 0; ++ if (term->termrequest_kind != kTermRequestKindOsc) { ++ return; + } + +- if (!has_event(EVENT_TERMREQUEST)) { +- return 1; ++ const char *seq = term->termrequest_buffer.items; ++ size_t len = term->termrequest_buffer.size; ++ static const char prefix[] = "\033]52;"; ++ const size_t prefix_len = sizeof(prefix) - 1; ++ if (len < prefix_len || memcmp(seq, prefix, prefix_len) != 0) { ++ return; ++ } ++ ++ const char *selectors = seq + prefix_len; ++ size_t rest_len = len - prefix_len; ++ const char *sep = memchr(selectors, ';', rest_len); ++ if (sep == NULL) { ++ return; + } + +- if (frag.initial) { +- kv_size(term->termrequest_buffer) = 0; +- kv_printf(term->termrequest_buffer, "\x1b_"); ++ size_t selector_len = (size_t)(sep - selectors); ++ const char *payload = sep + 1; ++ size_t payload_len = rest_len - selector_len - 1; ++ if (payload_len == 1 && payload[0] == '?') { ++ return; + } +- kv_concat_len(term->termrequest_buffer, frag.str, frag.len); +- if (frag.final) { +- term->termrequest_terminator = frag.terminator; +- schedule_termrequest(term); ++ ++ size_t decoded_len = 0; ++ char *decoded = base64_decode(payload, payload_len, &decoded_len); ++ if (decoded == NULL) { ++ return; + } +- return 1; +-} + +-static VTermStateFallbacks vterm_fallbacks = { +- .control = NULL, +- .csi = NULL, +- .osc = on_osc, +- .dcs = on_dcs, +- .apc = on_apc, +- .pm = NULL, +- .sos = NULL, +-}; ++ char *data = xmemdupz(decoded, decoded_len); ++ xfree(decoded); ++ multiqueue_put(main_loop.events, term_clipboard_set, ++ (void *)(intptr_t)terminal_osc52_register(selectors, selector_len), data); ++} + + void terminal_init(void) + { +@@ -458,37 +573,344 @@ void terminal_teardown(void) + invalidated_terminals = (Set(ptr_t)) SET_INIT; + } + +-static void term_output_callback(const char *s, size_t len, void *user_data) ++static void assert_ok(GhosttyResult res) + { +- terminal_send((Terminal *)user_data, s, len); ++ assert(res == GHOSTTY_SUCCESS); + } + +-/// Allocates a terminal's scrollback buffer if it hasn't been allocated yet. +-/// Does nothing if it's already allocated, unlike adjust_scrollback(). +-/// +-/// @param term Terminal instance. +-/// @param buf The terminal's buffer, or NULL to get it from buf_handle. +-/// +-/// @return whether the terminal now has a scrollback buffer. +-static bool term_may_alloc_scrollback(Terminal *term, buf_T *buf) ++static bool terminal_ghostty_mode_get(Terminal *term, GhosttyMode mode) ++ FUNC_ATTR_NONNULL_ALL + { +- if (term->sb_buffer != NULL) { +- return true; +- } +- if (buf == NULL) { +- buf = handle_get_buffer(term->buf_handle); +- if (buf == NULL) { // No need to allocate scrollback if buffer is deleted. +- return false; +- } ++ bool enabled = false; ++ assert_ok(ghostty_terminal_mode_get(term->ghostty, mode, &enabled)); ++ return enabled; ++} ++ ++static void terminal_ghostty_sync_output_update(Terminal *term) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ bool synchronized_output = terminal_ghostty_mode_get(term, GHOSTTY_MODE_SYNC_OUTPUT); ++ if (term->synchronized_output && !synchronized_output) { ++ term->sync_flush_pending = true; + } ++ term->synchronized_output = synchronized_output; ++} ++ ++static bool terminal_mouse_tracking_enabled(Terminal *term) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ bool enabled = false; ++ assert_ok(ghostty_terminal_get(term->ghostty, ++ GHOSTTY_TERMINAL_DATA_MOUSE_TRACKING, ++ &enabled)); ++ return enabled; ++} + ++static size_t terminal_scrollback_limit(buf_T *buf) ++ FUNC_ATTR_NONNULL_ALL ++{ + if (buf->b_p_scbk < 1) { + buf->b_p_scbk = SB_MAX; + } +- // Configure the scrollback buffer. +- term->sb_size = (size_t)buf->b_p_scbk; +- term->sb_buffer = xmalloc(sizeof(ScrollbackLine *) * term->sb_size); +- return true; ++ return (size_t)buf->b_p_scbk; ++} ++ ++static void terminal_ghostty_size_get(Terminal *term, int *height, int *width) ++ FUNC_ATTR_NONNULL_ARG(1) ++{ ++ uint16_t rows = 0; ++ uint16_t cols = 0; ++ assert_ok(ghostty_terminal_get(term->ghostty, GHOSTTY_TERMINAL_DATA_ROWS, &rows)); ++ assert_ok(ghostty_terminal_get(term->ghostty, GHOSTTY_TERMINAL_DATA_COLS, &cols)); ++ if (height != NULL) { ++ *height = (int)rows; ++ } ++ if (width != NULL) { ++ *width = (int)cols; ++ } ++} ++ ++static void terminal_ghostty_cursor_position_update(Terminal *term) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ uint16_t col = 0; ++ uint16_t row = 0; ++ assert_ok(ghostty_terminal_get(term->ghostty, ++ GHOSTTY_TERMINAL_DATA_CURSOR_X, ++ &col)); ++ assert_ok(ghostty_terminal_get(term->ghostty, ++ GHOSTTY_TERMINAL_DATA_CURSOR_Y, ++ &row)); ++ term->cursor.col = (int)col; ++ term->cursor.row = (int)row; ++} ++ ++static void terminal_ghostty_cursor_viewport_position_update(Terminal *term) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ bool has_viewport_cursor = false; ++ assert_ok(ghostty_render_state_get(term->ghostty_render_state, ++ GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE, ++ &has_viewport_cursor)); ++ if (!has_viewport_cursor) { ++ terminal_ghostty_cursor_position_update(term); ++ return; ++ } ++ ++ uint16_t col = 0; ++ uint16_t row = 0; ++ assert_ok(ghostty_render_state_get(term->ghostty_render_state, ++ GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X, ++ &col)); ++ assert_ok(ghostty_render_state_get(term->ghostty_render_state, ++ GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y, ++ &row)); ++ term->cursor.col = (int)col; ++ term->cursor.row = (int)row; ++} ++ ++static void terminal_ghostty_cursor_update(Terminal *term) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ int old_row = term->cursor.row; ++ int old_col = term->cursor.col; ++ terminal_ghostty_cursor_viewport_position_update(term); ++ bool position_changed = term->cursor.row != old_row || term->cursor.col != old_col; ++ ++ bool visible = false; ++ assert_ok(ghostty_render_state_get(term->ghostty_render_state, ++ GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE, ++ &visible)); ++ ++ bool blink = false; ++ assert_ok(ghostty_render_state_get(term->ghostty_render_state, ++ GHOSTTY_RENDER_STATE_DATA_CURSOR_BLINKING, ++ &blink)); ++ ++ GhosttyRenderStateCursorVisualStyle shape = GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK; ++ assert_ok(ghostty_render_state_get(term->ghostty_render_state, ++ GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE, ++ &shape)); ++ ++ bool visibility_changed = term->cursor.visible != visible; ++ bool style_changed = term->cursor.blink != blink || term->cursor.shape != shape; ++ term->cursor.visible = visible; ++ term->cursor.blink = blink; ++ term->cursor.shape = shape; ++ ++ if (style_changed) { ++ term->pending.cursor = true; ++ } ++ if (position_changed || visibility_changed || style_changed) { ++ invalidate_terminal(term, -1, -1); ++ } ++} ++ ++static void terminal_ghostty_termprops_update(Terminal *term) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ terminal_ghostty_sync_output_update(term); ++ ++ GhosttyTerminalScreen screen = GHOSTTY_TERMINAL_SCREEN_PRIMARY; ++ assert_ok(ghostty_terminal_get(term->ghostty, ++ GHOSTTY_TERMINAL_DATA_ACTIVE_SCREEN, ++ &screen)); ++ bool in_altscreen = screen == GHOSTTY_TERMINAL_SCREEN_ALTERNATE; ++ if (term->in_altscreen != in_altscreen) { ++ int height; ++ terminal_ghostty_size_get(term, &height, NULL); ++ term->invalid_start = 0; ++ term->invalid_end = height; ++ invalidate_terminal(term, -1, -1); ++ } ++ term->in_altscreen = in_altscreen; ++ ++ term->theme_updates = terminal_ghostty_mode_get(term, GHOSTTY_MODE_COLOR_SCHEME_REPORT); ++} ++ ++static GhosttyTerminalCursorStyle terminal_default_cursor_style(void) ++{ ++ switch (shape_table[SHAPE_IDX_TERM].shape) { ++ case SHAPE_BLOCK: ++ return GHOSTTY_TERMINAL_CURSOR_STYLE_BLOCK; ++ case SHAPE_HOR: ++ return GHOSTTY_TERMINAL_CURSOR_STYLE_UNDERLINE; ++ case SHAPE_VER: ++ return GHOSTTY_TERMINAL_CURSOR_STYLE_BAR; ++ } ++ UNREACHABLE; ++} ++ ++static void terminal_update_default_cursor(Terminal *term) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ GhosttyTerminalCursorStyle style = terminal_default_cursor_style(); ++ assert_ok(ghostty_terminal_set(term->ghostty, ++ GHOSTTY_TERMINAL_OPT_DEFAULT_CURSOR_STYLE, ++ &style)); ++ ++ bool blink = shape_table[SHAPE_IDX_TERM].blinkon != 0 ++ && shape_table[SHAPE_IDX_TERM].blinkoff != 0; ++ assert_ok(ghostty_terminal_set(term->ghostty, ++ GHOSTTY_TERMINAL_OPT_DEFAULT_CURSOR_BLINK, ++ &blink)); ++} ++ ++void terminal_update_default_cursor_all(void) ++{ ++ FOR_ALL_BUFFERS(buf) { ++ if (buf->terminal) { ++ terminal_update_default_cursor(buf->terminal); ++ terminal_ghostty_render_state_update(buf->terminal); ++ } ++ } ++} ++ ++static size_t terminal_ghostty_scrollback_rows_get(Terminal *term) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ size_t rows = 0; ++ assert_ok(ghostty_terminal_get(term->ghostty, ++ GHOSTTY_TERMINAL_DATA_SCROLLBACK_ROWS, ++ &rows)); ++ return rows; ++} ++ ++static size_t terminal_scrollback_clear_sequence_len(const char *data, size_t len, size_t offset) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ if ((uint8_t)data[offset] == 0x9b && offset + 2 < len && data[offset + 1] == '3' ++ && data[offset + 2] == 'J') { ++ return 3; ++ } ++ if (data[offset] == ESC && offset + 3 < len && data[offset + 1] == '[' ++ && data[offset + 2] == '3' && data[offset + 3] == 'J') { ++ return 4; ++ } ++ return 0; ++} ++ ++static void terminal_scrollback_clear_record(Terminal *term) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ size_t history_rows = terminal_ghostty_scrollback_rows_get(term); ++ size_t clear_rows = history_rows; ++ if (!term->scrollback_clear_pending) { ++ clear_rows = MAX(term->scrollback_rows, history_rows); ++ } ++ ++ term->scrollback_clear_pending = true; ++ term->scrollback_clear_rows += clear_rows; ++} ++ ++static void terminal_mouse_encoder_set_size(Terminal *term, uint16_t width, uint16_t height) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ GhosttyMouseEncoderSize size = { ++ .size = sizeof(GhosttyMouseEncoderSize), ++ .screen_width = MAX((uint32_t)width, 1U), ++ .screen_height = MAX((uint32_t)height, 1U), ++ .cell_width = 1, ++ .cell_height = 1, ++ .padding_top = 0, ++ .padding_bottom = 0, ++ .padding_right = 0, ++ .padding_left = 0, ++ }; ++ ghostty_mouse_encoder_setopt(term->ghostty_mouse_encoder, GHOSTTY_MOUSE_ENCODER_OPT_SIZE, ++ &size); ++} ++ ++static void terminal_mouse_encoder_sync_config(Terminal *term) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ bool x10 = terminal_ghostty_mode_get(term, GHOSTTY_MODE_X10_MOUSE); ++ bool normal = terminal_ghostty_mode_get(term, GHOSTTY_MODE_NORMAL_MOUSE); ++ bool button = terminal_ghostty_mode_get(term, GHOSTTY_MODE_BUTTON_MOUSE); ++ bool any = terminal_ghostty_mode_get(term, GHOSTTY_MODE_ANY_MOUSE); ++ bool utf8 = terminal_ghostty_mode_get(term, GHOSTTY_MODE_UTF8_MOUSE); ++ bool sgr = terminal_ghostty_mode_get(term, GHOSTTY_MODE_SGR_MOUSE); ++ bool urxvt = terminal_ghostty_mode_get(term, GHOSTTY_MODE_URXVT_MOUSE); ++ bool sgr_pixels = terminal_ghostty_mode_get(term, GHOSTTY_MODE_SGR_PIXELS_MOUSE); ++ ++ if (x10 == term->ghostty_mouse_modes.x10 ++ && normal == term->ghostty_mouse_modes.normal ++ && button == term->ghostty_mouse_modes.button ++ && any == term->ghostty_mouse_modes.any ++ && utf8 == term->ghostty_mouse_modes.utf8 ++ && sgr == term->ghostty_mouse_modes.sgr ++ && urxvt == term->ghostty_mouse_modes.urxvt ++ && sgr_pixels == term->ghostty_mouse_modes.sgr_pixels) { ++ return; ++ } ++ ++ term->ghostty_mouse_modes.x10 = x10; ++ term->ghostty_mouse_modes.normal = normal; ++ term->ghostty_mouse_modes.button = button; ++ term->ghostty_mouse_modes.any = any; ++ term->ghostty_mouse_modes.utf8 = utf8; ++ term->ghostty_mouse_modes.sgr = sgr; ++ term->ghostty_mouse_modes.urxvt = urxvt; ++ term->ghostty_mouse_modes.sgr_pixels = sgr_pixels; ++ ++ // This resets motion deduplication, so call it only when mouse modes changed. ++ ghostty_mouse_encoder_setopt_from_terminal(term->ghostty_mouse_encoder, term->ghostty); ++} ++ ++static unsigned terminal_mouse_button_number(GhosttyMouseButton button) ++{ ++ static const unsigned button_numbers[] = { ++ [GHOSTTY_MOUSE_BUTTON_LEFT] = 1, ++ [GHOSTTY_MOUSE_BUTTON_RIGHT] = 3, ++ [GHOSTTY_MOUSE_BUTTON_MIDDLE] = 2, ++ [GHOSTTY_MOUSE_BUTTON_FOUR] = 4, ++ [GHOSTTY_MOUSE_BUTTON_FIVE] = 5, ++ [GHOSTTY_MOUSE_BUTTON_SIX] = 6, ++ [GHOSTTY_MOUSE_BUTTON_SEVEN] = 7, ++ [GHOSTTY_MOUSE_BUTTON_EIGHT] = 8, ++ [GHOSTTY_MOUSE_BUTTON_NINE] = 9, ++ [GHOSTTY_MOUSE_BUTTON_TEN] = 10, ++ [GHOSTTY_MOUSE_BUTTON_ELEVEN] = 11, ++ }; ++ unsigned index = (unsigned)button; ++ return index < ARRAY_SIZE(button_numbers) ? button_numbers[index] : 0; ++} ++ ++static GhosttyMouseButton terminal_mouse_button_from_number(unsigned button) ++{ ++ static const GhosttyMouseButton buttons[] = { ++ GHOSTTY_MOUSE_BUTTON_UNKNOWN, ++ GHOSTTY_MOUSE_BUTTON_LEFT, ++ GHOSTTY_MOUSE_BUTTON_MIDDLE, ++ GHOSTTY_MOUSE_BUTTON_RIGHT, ++ GHOSTTY_MOUSE_BUTTON_FOUR, ++ GHOSTTY_MOUSE_BUTTON_FIVE, ++ GHOSTTY_MOUSE_BUTTON_SIX, ++ GHOSTTY_MOUSE_BUTTON_SEVEN, ++ GHOSTTY_MOUSE_BUTTON_EIGHT, ++ GHOSTTY_MOUSE_BUTTON_NINE, ++ GHOSTTY_MOUSE_BUTTON_TEN, ++ GHOSTTY_MOUSE_BUTTON_ELEVEN, ++ }; ++ return button < ARRAY_SIZE(buttons) ? buttons[button] : GHOSTTY_MOUSE_BUTTON_UNKNOWN; ++} ++ ++static bool terminal_mouse_button_is_stateful(GhosttyMouseButton button) ++{ ++ unsigned number = terminal_mouse_button_number(button); ++ return (number >= 1 && number <= 3) || (number >= 8 && number <= 11); ++} ++ ++static bool terminal_mouse_get_pressed_button(Terminal *term, GhosttyMouseButton *button) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ for (unsigned number = 1; number <= 11; number++) { ++ if (term->ghostty_mouse_buttons & (1U << (number - 1))) { ++ *button = terminal_mouse_button_from_number(number); ++ return *button != GHOSTTY_MOUSE_BUTTON_UNKNOWN; ++ } ++ } ++ return false; + } + + // public API {{{ +@@ -512,50 +934,71 @@ Terminal *terminal_alloc(buf_T *buf, TerminalOptions opts) + // Associate the terminal instance with the new buffer + term->buf_handle = buf->handle; + buf->terminal = term; +- // Create VTerm +- term->vt = vterm_new(opts.height, opts.width); +- vterm_set_utf8(term->vt, 1); +- // Setup state +- VTermState *state = vterm_obtain_state(term->vt); +- // Set up screen +- term->vts = vterm_obtain_screen(term->vt); +- vterm_screen_enable_altscreen(term->vts, true); +- vterm_screen_enable_reflow(term->vts, true); +- // delete empty lines at the end of the buffer +- vterm_screen_set_callbacks(term->vts, &vterm_screen_callbacks, term); +- vterm_screen_set_unrecognised_fallbacks(term->vts, &vterm_fallbacks, term); +- vterm_screen_set_damage_merge(term->vts, VTERM_DAMAGE_SCROLL); +- vterm_screen_reset(term->vts, 1); +- vterm_output_set_callback(term->vt, term_output_callback, term); +- +- term->selection_buffer = xcalloc(SELECTIONBUF_SIZE, 1); +- vterm_state_set_selection_callbacks(state, &vterm_selection_callbacks, term, +- term->selection_buffer, SELECTIONBUF_SIZE); +- +- VTermValue cursor_shape; +- switch (shape_table[SHAPE_IDX_TERM].shape) { +- case SHAPE_BLOCK: +- cursor_shape.number = VTERM_PROP_CURSORSHAPE_BLOCK; +- break; +- case SHAPE_HOR: +- cursor_shape.number = VTERM_PROP_CURSORSHAPE_UNDERLINE; +- break; +- case SHAPE_VER: +- cursor_shape.number = VTERM_PROP_CURSORSHAPE_BAR_LEFT; +- break; +- } +- vterm_state_set_termprop(state, VTERM_PROP_CURSORSHAPE, &cursor_shape); +- +- VTermValue cursor_blink; +- if (shape_table[SHAPE_IDX_TERM].blinkon != 0 && shape_table[SHAPE_IDX_TERM].blinkoff != 0) { +- cursor_blink.boolean = true; +- } else { +- cursor_blink.boolean = false; +- } +- vterm_state_set_termprop(state, VTERM_PROP_CURSORBLINK, &cursor_blink); ++ // Create Ghostty ++ uint16_t ghostty_cols = MAX(opts.width, 1); ++ uint16_t ghostty_rows = MAX(opts.height, 1); ++ assert_ok(ghostty_terminal_new(NULL, &term->ghostty, ghostty_cols, ghostty_rows)); ++ // Ghostty has a default scrollback byte limit of 10k bytes, so we set it to NULL to disable it. ++ assert_ok(ghostty_terminal_set(term->ghostty, ++ GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_BYTES, ++ NULL)); ++ // Ghostty has no scrollback line limit by default, so we initially set it to the max value of the ++ // 'scrollback' option. The real value will be set when processing TermOpen. ++ assert_ok(ghostty_terminal_set(term->ghostty, ++ GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_LINES, ++ &(size_t){ SB_MAX })); ++ GhosttyPoint scrollback_anchor = { ++ .tag = GHOSTTY_POINT_TAG_ACTIVE, ++ .value.coordinate = { .x = 0, .y = 0 }, ++ }; ++ assert_ok(ghostty_terminal_grid_ref_track(term->ghostty, ++ scrollback_anchor, ++ &term->ghostty_scrollback_anchor)); ++ assert_ok(ghostty_terminal_mode_set(term->ghostty, ++ GHOSTTY_MODE_GRAPHEME_CLUSTER, ++ true)); ++ terminal_update_colors(term); ++ assert_ok(ghostty_render_state_new(NULL, &term->ghostty_render_state)); ++ assert_ok(ghostty_render_state_row_iterator_new(NULL, ++ &term->ghostty_render_row_iterator)); ++ assert_ok(ghostty_render_state_row_cells_new(NULL, ++ &term->ghostty_render_row_cells)); ++ assert_ok(ghostty_terminal_set(term->ghostty, GHOSTTY_TERMINAL_OPT_USERDATA, term)); ++ ++ // ghostty_terminal_set() takes option values as const void *, including ++ // callback options. ISO C does not allow converting function pointers to ++ // object pointers, so we briefly disable pedantic warnings. ++#if defined(__GNUC__) ++# pragma GCC diagnostic push ++# pragma GCC diagnostic ignored "-Wpedantic" ++#endif ++ assert_ok(ghostty_terminal_set(term->ghostty, GHOSTTY_TERMINAL_OPT_WRITE_PTY, ++ (const void *)on_ghostty_write_pty)); ++ assert_ok(ghostty_terminal_set(term->ghostty, GHOSTTY_TERMINAL_OPT_BELL, ++ (const void *)on_term_ghostty_bell)); ++ assert_ok(ghostty_terminal_set(term->ghostty, GHOSTTY_TERMINAL_OPT_TITLE_CHANGED, ++ (const void *)on_ghostty_title_changed)); ++ assert_ok(ghostty_terminal_set(term->ghostty, GHOSTTY_TERMINAL_OPT_COLOR_SCHEME, ++ (const void *)on_term_ghostty_color_scheme)); ++ assert_ok(ghostty_terminal_set(term->ghostty, GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES, ++ (const void *)on_term_ghostty_device_attributes)); ++#if defined(__GNUC__) ++# pragma GCC diagnostic pop ++#endif + +- // Force a initial refresh of the screen to ensure the buffer will always +- // have as many lines as screen rows when refresh_scrollback() is called. ++ assert_ok(ghostty_key_encoder_new(NULL, &term->ghostty_key_encoder)); ++ assert_ok(ghostty_key_event_new(NULL, &term->ghostty_key_event)); ++ assert_ok(ghostty_mouse_encoder_new(NULL, &term->ghostty_mouse_encoder)); ++ assert_ok(ghostty_mouse_event_new(NULL, &term->ghostty_mouse_event)); ++ terminal_mouse_encoder_set_size(term, ghostty_cols, ghostty_rows); ++ bool track_last_cell = true; ++ ghostty_mouse_encoder_setopt(term->ghostty_mouse_encoder, ++ GHOSTTY_MOUSE_ENCODER_OPT_TRACK_LAST_CELL, ++ &track_last_cell); ++ terminal_update_default_cursor(term); ++ terminal_ghostty_render_state_update(term); ++ ++ // Force an initial refresh so the buffer starts with one line per screen row. + term->invalid_start = 0; + term->invalid_end = opts.height; + +@@ -573,12 +1016,10 @@ Terminal *terminal_alloc(buf_T *buf, TerminalOptions opts) + } + deleted_lines_buf(buf, 1, line_count); + } +- term->old_height = 1; +- + return term; + } + +-/// Triggers TermOpen and allocates terminal scrollback buffer. ++/// Triggers TermOpen. + /// + /// @param termpp Pointer to the terminal channel's `term` field. + /// @param buf Buffer used for presentation of the terminal. +@@ -591,13 +1032,7 @@ void terminal_open(Terminal **termpp, buf_T *buf) + CtxSwitch aco = { 0 }; + ctx_switch(&aco, NULL, NULL, buf, 0); + +- if (term->sb_buffer != NULL) { +- // If scrollback has been allocated by autocommands between terminal_alloc() +- // and terminal_open(), it also needs to be refreshed. +- refresh_scrollback(term, buf); +- } else { +- assert(term->invalid_start >= 0); +- } ++ assert(term->invalid_start >= 0); + refresh_screen(term, buf); + buf->b_locked++; + set_option_value(kOptBuftype, STATIC_CSTR_AS_OBJ("terminal"), OPT_LOCAL); +@@ -610,10 +1045,6 @@ void terminal_open(Terminal **termpp, buf_T *buf) + // Reset cursor in current window. + curwin->w_cursor = (pos_T){ .lnum = 1, .col = 0, .coladd = 0 }; + +- // Apply TermOpen autocmds _before_ configuring the scrollback buffer, to avoid +- // over-allocating in case TermOpen reduces 'scrollback'. +- // In the rare case where TermOpen polls for events, the scrollback buffer will be +- // allocated anyway if needed. + apply_autocmds(EVENT_TERMOPEN, NULL, NULL, false, buf); + + ctx_restore(&aco); +@@ -622,36 +1053,45 @@ void terminal_open(Terminal **termpp, buf_T *buf) + return; // Terminal has already been destroyed. + } + +- // Local 'scrollback' _after_ autocmds. +- if (!term_may_alloc_scrollback(term, buf)) { +- abort(); +- } ++ size_t scrollback_limit = terminal_scrollback_limit(buf); ++ assert_ok(ghostty_terminal_set(term->ghostty, ++ GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_LINES, ++ &scrollback_limit)); ++ ++ GhosttyColorRgb palette[256]; ++ assert_ok(ghostty_terminal_get(term->ghostty, ++ GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT, ++ palette)); + +- VTermState *state = vterm_obtain_state(term->vt); + // Configure the color palette. Try to get the color from: + // + // - b:terminal_color_{NUM} + // - g:terminal_color_{NUM} +- // - the VTerm instance + for (int i = 0; i < 16; i++) { + char var[64]; + snprintf(var, sizeof(var), "terminal_color_%d", i); + char *name = get_config_string(buf, var); +- if (name) { +- int dummy; +- RgbValue color_val = name_to_color(name, &dummy); +- +- if (color_val != -1) { +- VTermColor color; +- vterm_color_rgb(&color, +- (uint8_t)((color_val >> 16) & 0xFF), +- (uint8_t)((color_val >> 8) & 0xFF), +- (uint8_t)((color_val >> 0) & 0xFF)); +- vterm_state_set_palette_color(state, i, &color); +- term->color_set[i] = true; +- } ++ if (!name) { ++ continue; + } ++ int dummy; ++ RgbValue color_val = name_to_color(name, &dummy); ++ if (color_val == -1) { ++ continue; ++ } ++ palette[i] = (GhosttyColorRgb){ ++ .r = (uint8_t)((color_val >> 16) & 0xFF), ++ .g = (uint8_t)((color_val >> 8) & 0xFF), ++ .b = (uint8_t)((color_val >> 0) & 0xFF), ++ }; ++ term->color_set[i] = true; + } ++ ++ assert_ok(ghostty_terminal_set(term->ghostty, ++ GHOSTTY_TERMINAL_OPT_COLOR_PALETTE, ++ palette)); ++ ++ terminal_update_colors(term); + } + + /// Closes the Terminal buffer. +@@ -777,7 +1217,7 @@ void terminal_check_size(Terminal *term) + } + + int curwidth, curheight; +- vterm_get_size(term->vt, &curheight, &curwidth); ++ terminal_ghostty_size_get(term, &curheight, &curwidth); + uint16_t width = 0; + uint16_t height = 0; + +@@ -801,8 +1241,9 @@ void terminal_check_size(Terminal *term) + return; + } + +- vterm_set_size(term->vt, height, width); +- vterm_screen_flush_damage(term->vts); ++ assert_ok(ghostty_terminal_resize(term->ghostty, width, height, 0, 0)); ++ terminal_ghostty_render_state_update(term); ++ terminal_mouse_encoder_set_size(term, width, height); + term->pending.resize = true; + invalidate_terminal(term, -1, -1); + } +@@ -1008,7 +1449,7 @@ static void terminal_check_cursor(void) + } else { + // Nudge cursor when returning to normal-mode. + int off = (State & MODE_TERMINAL) ? 0 : (curwin->w_p_rl ? 1 : -1); +- coladvance(curwin, MAX(0, term->cursor.col + off)); ++ coladvance(curwin, MAX(0, terminal_cursor_virtcol(term) + off)); + } + } + +@@ -1106,6 +1547,7 @@ static int terminal_execute(VimState *state, int key) + + // Check for certain control keys like Ctrl-C and Ctrl-\. We still send the + // unmerged key and modifiers to the terminal. ++ const int key_modifiers = mod_mask; + int tmp_mod_mask = mod_mask; + int mod_key = merge_modifiers(key, &tmp_mod_mask); + +@@ -1130,7 +1572,7 @@ static int terminal_execute(VimState *state, int key) + case K_MOUSELEFT: + case K_MOUSERIGHT: + case K_MOUSEMOVE: +- if (send_mouse_event(s->term, key)) { ++ if (send_mouse_event(s->term, mod_key, tmp_mod_mask)) { + return 0; + } + break; +@@ -1200,7 +1642,7 @@ static int terminal_execute(VimState *state, int key) + } + + s->got_bsl = false; +- terminal_send_key(s->term, key); ++ terminal_send_key(s->term, key, key_modifiers); + } + + return 1; +@@ -1226,16 +1668,17 @@ void terminal_destroy(Terminal **termpp) + unblock_autocmds(); + set_del(ptr_t, &invalidated_terminals, term); + } +- for (size_t i = 0; i < term->sb_current; i++) { +- xfree(term->sb_buffer[i]); +- } +- xfree(term->sb_buffer); +- xfree(term->title); +- xfree(term->selection_buffer); +- kv_destroy(term->selection); + kv_destroy(term->termrequest_buffer); +- vterm_free(term->vt); + multiqueue_free(term->pending.events); ++ ghostty_mouse_event_free(term->ghostty_mouse_event); ++ ghostty_mouse_encoder_free(term->ghostty_mouse_encoder); ++ ghostty_key_event_free(term->ghostty_key_event); ++ ghostty_key_encoder_free(term->ghostty_key_encoder); ++ ghostty_render_state_row_cells_free(term->ghostty_render_row_cells); ++ ghostty_render_state_row_iterator_free(term->ghostty_render_row_iterator); ++ ghostty_render_state_free(term->ghostty_render_state); ++ ghostty_tracked_grid_ref_free(term->ghostty_scrollback_anchor); ++ ghostty_terminal_free(term->ghostty); + xfree(term); + *termpp = NULL; // coverity[dead-store] + } +@@ -1285,15 +1728,23 @@ static bool is_filter_char(int c) + return !!(tpf_flags & flag); + } + ++static void terminal_send_bracketed_paste(Terminal *term, bool start) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ bool bracketed_paste = false; ++ assert_ok(ghostty_terminal_mode_get(term->ghostty, ++ GHOSTTY_MODE_BRACKETED_PASTE, ++ &bracketed_paste)); ++ if (bracketed_paste) { ++ terminal_send(term, start ? "\x1b[200~" : "\x1b[201~", 6); ++ } ++} ++ + void terminal_set_streamed_paste(Terminal *term, bool streamed) + FUNC_ATTR_NONNULL_ALL + { + if (term->streamed_paste != streamed) { +- if (streamed) { +- vterm_keyboard_start_paste(curbuf->terminal->vt); +- } else { +- vterm_keyboard_end_paste(curbuf->terminal->vt); +- } ++ terminal_send_bracketed_paste(term, streamed); + } + term->streamed_paste = streamed; + } +@@ -1304,7 +1755,7 @@ void terminal_paste(int count, String *y_array, size_t y_size) + return; + } + if (!curbuf->terminal->streamed_paste) { +- vterm_keyboard_start_paste(curbuf->terminal->vt); ++ terminal_send_bracketed_paste(curbuf->terminal, true); + } + size_t buff_len = y_array[0].size; + char *buff = xmalloc(buff_len); +@@ -1340,67 +1791,285 @@ void terminal_paste(int count, String *y_array, size_t y_size) + } + xfree(buff); + if (!curbuf->terminal->streamed_paste) { +- vterm_keyboard_end_paste(curbuf->terminal->vt); ++ terminal_send_bracketed_paste(curbuf->terminal, false); + } + } + +-static void terminal_send_key(Terminal *term, int c) ++static void terminal_key_encoder_sync_config(Terminal *term) ++ FUNC_ATTR_NONNULL_ALL + { +- VTermModifier mod = VTERM_MOD_NONE; +- +- // Convert K_ZERO back to ASCII +- if (c == K_ZERO) { +- c = Ctrl_AT; +- } ++ ghostty_key_encoder_setopt_from_terminal(term->ghostty_key_encoder, term->ghostty); + +- VTermKey key = convert_key(&c, &mod); +- +- if (key != VTERM_KEY_NONE) { +- vterm_keyboard_key(term->vt, key, mod); +- } else if (!IS_SPECIAL(c)) { +- vterm_keyboard_unichar(term->vt, (uint32_t)c, mod); +- } ++ // MOD_MASK_ALT is already terminal Alt, so always encode option as Alt. ++ GhosttyOptionAsAlt option_as_alt = GHOSTTY_OPTION_AS_ALT_TRUE; ++ ghostty_key_encoder_setopt(term->ghostty_key_encoder, ++ GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT, ++ &option_as_alt); + } + +-/// Callback scheduled on the main loop when a synchronized update ends. +-/// Refreshes a single terminal with full-screen damage. +-static void on_sync_flush(void **argv) ++/// Returns a known unshifted codepoint for a key event. ++/// ++/// @param utf8 Produced UTF-8 text for the key event. ++/// @param utf8_len Length of `utf8` in bytes. ++/// @return Unshifted codepoint, or 0 if unknown. ++static uint32_t terminal_key_known_unshifted_codepoint(const char *utf8, size_t utf8_len) + { +- if (exiting) { +- return; ++ if (utf8_len != 1) { ++ return 0; + } +- handle_T buf_handle = (handle_T)(intptr_t)argv[0]; +- buf_T *buf = handle_get_buffer(buf_handle); +- if (!buf || !buf->terminal) { +- return; ++ ++ uint8_t c = (uint8_t)utf8[0]; ++ if (c >= 'A' && c <= 'Z') { ++ return (uint32_t)(c + ('a' - 'A')); + } +- block_autocmds(); +- refresh_terminal(buf->terminal); +- unblock_autocmds(); ++ if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '[' || c == ']' ++ || c == '\\' || c == '/') { ++ return c; ++ } ++ return 0; + } + +-void terminal_receive(Terminal *term, const char *data, size_t len) ++/// Returns the UTF-8 text produced by a keypad keycode. ++/// ++/// @param c Neovim keycode for a keypad key. ++/// @param len Set to the returned text length in bytes, or 0 if `c` is not handled. ++/// @return Static UTF-8 text for the keypad key, or NULL if `c` is not handled. ++static const char *terminal_keypad_generated_utf8(int c, size_t *len) ++ FUNC_ATTR_NONNULL_ALL + { +- if (!data) { +- return; +- } +- +- if (term->opts.force_crlf) { +- StringBuilder crlf_data = KV_INITIAL_VALUE; +- +- for (size_t i = 0; i < len; i++) { +- if (data[i] == '\n' && (i == 0 || (i > 0 && data[i - 1] != '\r'))) { +- kv_push(crlf_data, '\r'); ++ *len = 1; ++ switch (c) { ++ case K_K0: ++ FALLTHROUGH; ++ case K_KINS: ++ return "0"; ++ case K_K1: ++ FALLTHROUGH; ++ case K_KEND: ++ return "1"; ++ case K_K2: ++ FALLTHROUGH; ++ case K_KDOWN: ++ return "2"; ++ case K_K3: ++ FALLTHROUGH; ++ case K_KPAGEDOWN: ++ return "3"; ++ case K_K4: ++ FALLTHROUGH; ++ case K_KLEFT: ++ return "4"; ++ case K_K5: ++ FALLTHROUGH; ++ case K_KORIGIN: ++ return "5"; ++ case K_K6: ++ FALLTHROUGH; ++ case K_KRIGHT: ++ return "6"; ++ case K_K7: ++ FALLTHROUGH; ++ case K_KHOME: ++ return "7"; ++ case K_K8: ++ FALLTHROUGH; ++ case K_KUP: ++ return "8"; ++ case K_K9: ++ FALLTHROUGH; ++ case K_KPAGEUP: ++ return "9"; ++ case K_KDEL: ++ FALLTHROUGH; ++ case K_KPOINT: ++ return "."; ++ case K_KPLUS: ++ return "+"; ++ case K_KMINUS: ++ return "-"; ++ case K_KMULTIPLY: ++ return "*"; ++ case K_KDIVIDE: ++ return "/"; ++ case K_KCOMMA: ++ return ","; ++ case K_KEQUAL: ++ return "="; ++ default: ++ *len = 0; ++ return NULL; ++ } ++} ++ ++static void terminal_key_encode_event(Terminal *term, GhosttyKey key, GhosttyMods mods, ++ const char *utf8, size_t utf8_len) ++ FUNC_ATTR_NONNULL_ARG(1) ++{ ++ terminal_key_encoder_sync_config(term); ++ ++ ghostty_key_event_set_action(term->ghostty_key_event, GHOSTTY_KEY_ACTION_PRESS); ++ ghostty_key_event_set_key(term->ghostty_key_event, key); ++ ghostty_key_event_set_mods(term->ghostty_key_event, mods); ++ ghostty_key_event_set_utf8(term->ghostty_key_event, utf8, utf8_len); ++ uint32_t unshifted_codepoint = terminal_key_known_unshifted_codepoint(utf8, utf8_len); ++ ghostty_key_event_set_unshifted_codepoint(term->ghostty_key_event, unshifted_codepoint); ++ ++ // Try encoding to a stack-allocated buffer first. ++ char buf[128]; ++ size_t len = 0; ++ GhosttyResult res = ghostty_key_encoder_encode(term->ghostty_key_encoder, ++ term->ghostty_key_event, ++ buf, ++ sizeof(buf), ++ &len); ++ ++ // If that was too small, allocate on the heap. ++ if (res == GHOSTTY_OUT_OF_SPACE) { ++ char *big_buf = xmalloc(len); ++ assert_ok(ghostty_key_encoder_encode(term->ghostty_key_encoder, ++ term->ghostty_key_event, ++ big_buf, ++ len, ++ &len)); ++ terminal_send(term, big_buf, len); ++ xfree(big_buf); ++ return; ++ } ++ ++ assert_ok(res); ++ if (len > 0) { ++ terminal_send(term, buf, len); ++ } ++} ++ ++static void terminal_send_key(Terminal *term, int c, int modifiers) ++{ ++ // Convert K_ZERO back to ASCII ++ if (c == K_ZERO) { ++ c = Ctrl_AT; ++ } ++ ++ GhosttyMods mods = convert_key_modifiers(c, modifiers); ++ if ((mods & GHOSTTY_MODS_CTRL) && !(mods & GHOSTTY_MODS_SHIFT) && c >= 'A' && c <= 'Z') { ++ c += ('a' - 'A'); ++ } ++ ++ GhosttyKey key = convert_key(c); ++ if (key != GHOSTTY_KEY_UNIDENTIFIED) { ++ size_t utf8_len = 0; ++ const char *utf8 = terminal_keypad_generated_utf8(c, &utf8_len); ++ terminal_key_encode_event(term, key, mods, utf8, utf8_len); ++ return; ++ } ++ ++ if (IS_SPECIAL(c)) { ++ return; ++ } ++ ++ if (c < 0x20 || c == DEL) { ++ char ctrl = (char)c; ++ terminal_send(term, &ctrl, 1); ++ return; ++ } ++ ++ char utf8[MB_MAXBYTES]; ++ int utf8_len = utf_char2bytes(c, utf8); ++ terminal_key_encode_event(term, GHOSTTY_KEY_UNIDENTIFIED, mods, utf8, (size_t)utf8_len); ++} ++ ++/// Callback scheduled on the main loop when a synchronized update ends. ++/// Refreshes a single terminal with full-screen damage. ++static void on_sync_flush(void **argv) ++{ ++ if (exiting) { ++ return; ++ } ++ handle_T buf_handle = (handle_T)(intptr_t)argv[0]; ++ buf_T *buf = handle_get_buffer(buf_handle); ++ if (!buf || !buf->terminal) { ++ return; ++ } ++ block_autocmds(); ++ refresh_terminal(buf->terminal); ++ unblock_autocmds(); ++} ++ ++static void terminal_process_vt_input(Terminal *term, const char *data, size_t len) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ size_t chunk_start = 0; ++ for (size_t i = 0; i < len; i++) { ++ if (term->termrequest_state == kTermRequestParserNormal) { ++ size_t clear_sequence_len = terminal_scrollback_clear_sequence_len(data, len, i); ++ if (clear_sequence_len > 0) { ++ if (i > chunk_start) { ++ ghostty_terminal_vt_write(term->ghostty, (const uint8_t *)(data + chunk_start), ++ i - chunk_start); ++ terminal_ghostty_render_state_update(term); ++ } ++ terminal_scrollback_clear_record(term); ++ ghostty_terminal_vt_write(term->ghostty, (const uint8_t *)(data + i), ++ clear_sequence_len); ++ i += clear_sequence_len - 1; ++ chunk_start = i + 1; ++ continue; ++ } ++ } ++ ++ TermRequestParserEvent event = terminal_termrequest_parse_byte(term, (uint8_t)data[i]); ++ if (event == kTermRequestParserEventStart) { ++ size_t request_start = i; ++ if (i > 0 && data[i - 1] == ESC ++ && ((uint8_t)data[i] == ']' || (uint8_t)data[i] == 'P' || (uint8_t)data[i] == '_')) { ++ request_start--; ++ } ++ ghostty_terminal_vt_write(term->ghostty, (const uint8_t *)(data + chunk_start), ++ request_start - chunk_start); ++ chunk_start = request_start; ++ } else if (event == kTermRequestParserEventFinish) { ++ ghostty_terminal_vt_write(term->ghostty, (const uint8_t *)(data + chunk_start), ++ i + 1 - chunk_start); ++ terminal_osc52_handle(term); ++ if (has_event(EVENT_TERMREQUEST)) { ++ schedule_termrequest(term); ++ } ++ term->termrequest_state = kTermRequestParserNormal; ++ term->termrequest_kind = kTermRequestKindNone; ++ kv_size(term->termrequest_buffer) = 0; ++ chunk_start = i + 1; ++ } else if (event == kTermRequestParserEventSyncOutput) { ++ ghostty_terminal_vt_write(term->ghostty, (const uint8_t *)(data + chunk_start), ++ i + 1 - chunk_start); ++ terminal_ghostty_sync_output_update(term); ++ chunk_start = i + 1; ++ } ++ } ++ ghostty_terminal_vt_write(term->ghostty, (const uint8_t *)(data + chunk_start), ++ len - chunk_start); ++ terminal_ghostty_render_state_update(term); ++} ++ ++void terminal_receive(Terminal *term, const char *data, size_t len) ++{ ++ if (!data || len == 0) { ++ return; ++ } ++ ++ if (term->opts.force_crlf) { ++ StringBuilder crlf_data = KV_INITIAL_VALUE; ++ ++ for (size_t i = 0; i < len; i++) { ++ if (data[i] == '\n' && (i == 0 || (i > 0 && data[i - 1] != '\r'))) { ++ kv_push(crlf_data, '\r'); + } + kv_push(crlf_data, data[i]); + } + +- vterm_input_write(term->vt, crlf_data.items, kv_size(crlf_data)); ++ terminal_process_vt_input(term, crlf_data.items, kv_size(crlf_data)); + kv_destroy(crlf_data); + } else { +- vterm_input_write(term->vt, data, len); ++ terminal_process_vt_input(term, data, len); + } +- vterm_screen_flush_damage(term->vts); + + // When a synchronized update just ended, refresh the buffer immediately + // instead of waiting for the 10ms timer. This eliminates the window where +@@ -1411,7 +2080,7 @@ void terminal_receive(Terminal *term, const char *data, size_t len) + // Force full-screen damage so every row is updated, not just + // the rows with accumulated damage from individual callbacks. + int height; +- vterm_get_size(term->vt, &height, NULL); ++ terminal_ghostty_size_get(term, &height, NULL); + term->invalid_start = 0; + term->invalid_end = height; + multiqueue_put(main_loop.events, on_sync_flush, +@@ -1419,414 +2088,672 @@ void terminal_receive(Terminal *term, const char *data, size_t len) + } + } + +-static int get_rgb(VTermState *state, VTermColor color) ++static bool terminal_ghostty_grid_ref(Terminal *term, GhosttyPointTag tag, uint32_t row, int col, ++ GhosttyGridRef *ref) ++ FUNC_ATTR_NONNULL_ALL + { +- vterm_state_convert_color_to_rgb(state, &color); +- return RGB_(color.rgb.red, color.rgb.green, color.rgb.blue); ++ if (col < 0 || col > UINT16_MAX) { ++ return false; ++ } ++ ++ *ref = GHOSTTY_INIT_SIZED(GhosttyGridRef); ++ GhosttyPoint point = { ++ .tag = tag, ++ .value = { ++ .coordinate = { ++ .x = (uint16_t)col, ++ .y = row, ++ }, ++ }, ++ }; ++ ++ GhosttyResult result = ghostty_terminal_grid_ref(term->ghostty, point, ref); ++ if (result == GHOSTTY_INVALID_VALUE || result == GHOSTTY_NO_VALUE) { ++ return false; ++ } ++ assert_ok(result); ++ return true; + } + +-static int get_underline_hl_flag(VTermScreenCellAttrs attrs) ++static bool render_active_row_cells(Terminal *term, uint32_t row, GhosttyRenderStateRowCells *cells) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ assert_ok(ghostty_render_state_get(term->ghostty_render_state, ++ GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR, ++ &term->ghostty_render_row_iterator)); ++ ++ for (uint32_t row_idx = 0; row_idx <= row; row_idx++) { ++ if (!ghostty_render_state_row_iterator_next(term->ghostty_render_row_iterator)) { ++ return false; ++ } ++ } ++ ++ *cells = term->ghostty_render_row_cells; ++ assert_ok(ghostty_render_state_row_get(term->ghostty_render_row_iterator, ++ GHOSTTY_RENDER_STATE_ROW_DATA_CELLS, ++ cells)); ++ return true; ++} ++ ++static int terminal_ghostty_underline_hl_flag(int underline) + { +- switch (attrs.underline) { +- case VTERM_UNDERLINE_OFF: ++ switch (underline) { ++ case GHOSTTY_SGR_UNDERLINE_NONE: + return 0; +- case VTERM_UNDERLINE_SINGLE: ++ case GHOSTTY_SGR_UNDERLINE_SINGLE: + return HL_UNDERLINE; +- case VTERM_UNDERLINE_DOUBLE: ++ case GHOSTTY_SGR_UNDERLINE_DOUBLE: + return HL_UNDERDOUBLE; +- case VTERM_UNDERLINE_CURLY: ++ case GHOSTTY_SGR_UNDERLINE_CURLY: + return HL_UNDERCURL; ++ case GHOSTTY_SGR_UNDERLINE_DOTTED: ++ return HL_UNDERDOTTED; ++ case GHOSTTY_SGR_UNDERLINE_DASHED: ++ return HL_UNDERDASHED; + default: + return HL_UNDERLINE; + } + } + +-void terminal_get_line_attributes(Terminal *term, win_T *wp, int linenr, int *term_attrs) ++static int terminal_ghostty_rgb(GhosttyColorRgb color) + { +- int height, width; +- vterm_get_size(term->vt, &height, &width); +- VTermState *state = vterm_obtain_state(term->vt); +- assert(linenr); +- int row = linenr_to_row(term, linenr); +- if (row >= height) { +- // Terminal height was decreased but the change wasn't reflected into the +- // buffer yet +- return; +- } ++ return RGB_(color.r, color.g, color.b); ++} + +- width = MIN(TERM_ATTRS_MAX, width); +- for (int col = 0; col < width; col++) { +- VTermScreenCell cell; +- bool color_valid = fetch_cell(term, row, col, &cell); +- bool fg_default = !color_valid || VTERM_COLOR_IS_DEFAULT_FG(&cell.fg); +- bool bg_default = !color_valid || VTERM_COLOR_IS_DEFAULT_BG(&cell.bg); +- +- // Get the rgb value set by libvterm. +- int vt_fg = fg_default ? -1 : get_rgb(state, cell.fg); +- int vt_bg = bg_default ? -1 : get_rgb(state, cell.bg); +- +- bool fg_indexed = VTERM_COLOR_IS_INDEXED(&cell.fg); +- bool bg_indexed = VTERM_COLOR_IS_INDEXED(&cell.bg); +- +- int16_t vt_fg_idx = ((!fg_default && fg_indexed) ? cell.fg.indexed.idx + 1 : 0); +- int16_t vt_bg_idx = ((!bg_default && bg_indexed) ? cell.bg.indexed.idx + 1 : 0); +- +- bool fg_set = vt_fg_idx && vt_fg_idx <= 16 && term->color_set[vt_fg_idx - 1]; +- bool bg_set = vt_bg_idx && vt_bg_idx <= 16 && term->color_set[vt_bg_idx - 1]; +- +- int hl_attrs = (cell.attrs.bold ? HL_BOLD : 0) +- | (cell.attrs.dim ? HL_DIM : 0) +- | (cell.attrs.blink ? HL_BLINK : 0) +- | (cell.attrs.conceal ? HL_CONCEALED : 0) +- | (cell.attrs.overline ? HL_OVERLINE : 0) +- | (cell.attrs.italic ? HL_ITALIC : 0) +- | (cell.attrs.reverse ? HL_INVERSE : 0) +- | get_underline_hl_flag(cell.attrs) +- | (cell.attrs.strike ? HL_STRIKETHROUGH : 0) +- | ((fg_indexed && !fg_set) ? HL_FG_INDEXED : 0) +- | ((bg_indexed && !bg_set) ? HL_BG_INDEXED : 0); +- +- int attr_id = 0; +- +- if (hl_attrs || !fg_default || !bg_default) { +- attr_id = hl_get_term_attr(&(HlAttrs) { +- .cterm_ae_attr = (int32_t)hl_attrs, +- .cterm_fg_color = vt_fg_idx, +- .cterm_bg_color = vt_bg_idx, +- .rgb_ae_attr = (int32_t)hl_attrs, +- .rgb_fg_color = vt_fg, +- .rgb_bg_color = vt_bg, +- .rgb_sp_color = -1, +- .hl_blend = -1, +- .url = -1, +- }); +- } +- +- if (cell.uri > 0) { +- attr_id = hl_combine_attr(attr_id, cell.uri); +- } ++/// Converts an RGB value to Ghostty's RGB representation. ++static GhosttyColorRgb rgb_value_to_ghostty_color(RgbValue color) ++{ ++ return (GhosttyColorRgb) { ++ .r = (uint8_t)((color >> 16) & 0xff), ++ .g = (uint8_t)((color >> 8) & 0xff), ++ .b = (uint8_t)(color & 0xff), ++ }; ++} + +- term_attrs[col] = attr_id; ++static int terminal_cell_hl_attr(Terminal *term, int hl_attrs, int16_t fg_idx, int16_t bg_idx, ++ int fg, int bg, int sp, bool fg_default, bool bg_default, ++ int url_attr) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ bool fg_indexed = fg_idx != 0; ++ bool bg_indexed = bg_idx != 0; ++ bool fg_set = fg_idx && fg_idx <= 16 && term->color_set[fg_idx - 1]; ++ bool bg_set = bg_idx && bg_idx <= 16 && term->color_set[bg_idx - 1]; ++ ++ hl_attrs |= ((fg_indexed && !fg_set) ? HL_FG_INDEXED : 0) ++ | ((bg_indexed && !bg_set) ? HL_BG_INDEXED : 0); ++ ++ int attr_id = 0; ++ if (hl_attrs || !fg_default || !bg_default) { ++ attr_id = hl_get_term_attr(&(HlAttrs) { ++ .cterm_ae_attr = (int32_t)hl_attrs, ++ .cterm_fg_color = fg_idx, ++ .cterm_bg_color = bg_idx, ++ .rgb_ae_attr = (int32_t)hl_attrs, ++ .rgb_fg_color = fg, ++ .rgb_bg_color = bg, ++ .rgb_sp_color = sp, ++ .hl_blend = -1, ++ .url = -1, ++ }); + } ++ ++ return url_attr > 0 ? hl_combine_attr(attr_id, url_attr) : attr_id; + } + +-Buffer terminal_buf(const Terminal *term) +- FUNC_ATTR_NONNULL_ALL ++static int16_t terminal_ghostty_style_color_index(GhosttyStyleColor color) + { +- return term->buf_handle; ++ if (color.tag != GHOSTTY_STYLE_COLOR_PALETTE) { ++ return 0; ++ } ++ return (int16_t)(color.value.palette + 1); + } + +-bool terminal_running(const Terminal *term) +- FUNC_ATTR_NONNULL_ALL ++static int terminal_ghostty_style_color_rgb(const GhosttyColorRgb palette[256], ++ GhosttyStyleColor color) + { +- return !term->closed; ++ switch (color.tag) { ++ case GHOSTTY_STYLE_COLOR_NONE: ++ return -1; ++ case GHOSTTY_STYLE_COLOR_PALETTE: ++ return terminal_ghostty_rgb(palette[color.value.palette]); ++ case GHOSTTY_STYLE_COLOR_RGB: ++ return terminal_ghostty_rgb(color.value.rgb); ++ default: ++ return -1; ++ } + } + +-bool terminal_suspended(const Terminal *term) ++static TerminalColorAttrs terminal_ghostty_style_color_attrs(const GhosttyColorRgb palette[256], ++ GhosttyStyleColor color) + FUNC_ATTR_NONNULL_ALL + { +- return term->suspended; ++ return (TerminalColorAttrs) { ++ .idx = terminal_ghostty_style_color_index(color), ++ .rgb = terminal_ghostty_style_color_rgb(palette, color), ++ .is_default = color.tag == GHOSTTY_STYLE_COLOR_NONE, ++ }; + } + +-void terminal_notify_theme(Terminal *term, bool dark) ++static TerminalColorAttrs terminal_ghostty_cell_bg_attrs(GhosttyCell cell, GhosttyStyleColor color, ++ const GhosttyColorRgb palette[256]) + FUNC_ATTR_NONNULL_ALL + { +- if (!term->theme_updates) { +- return; ++ TerminalColorAttrs attrs = terminal_ghostty_style_color_attrs(palette, color); ++ if (color.tag != GHOSTTY_STYLE_COLOR_NONE) { ++ return attrs; + } + +- char buf[10]; +- ssize_t ret = snprintf(buf, sizeof(buf), "\x1b[997;%cn", dark ? '1' : '2'); +- assert(ret > 0); +- assert((size_t)ret <= sizeof(buf)); +- terminal_send(term, buf, (size_t)ret); ++ GhosttyCellContentTag content_tag = GHOSTTY_CELL_CONTENT_CODEPOINT; ++ assert_ok(ghostty_cell_get(cell, GHOSTTY_CELL_DATA_CONTENT_TAG, &content_tag)); ++ if (content_tag == GHOSTTY_CELL_CONTENT_BG_COLOR_PALETTE) { ++ GhosttyColorPaletteIndex palette_index = 0; ++ assert_ok(ghostty_cell_get(cell, GHOSTTY_CELL_DATA_COLOR_PALETTE, ++ &palette_index)); ++ return (TerminalColorAttrs) { ++ .idx = (int16_t)(palette_index + 1), ++ .rgb = terminal_ghostty_rgb(palette[palette_index]), ++ .is_default = false, ++ }; ++ } ++ ++ if (content_tag == GHOSTTY_CELL_CONTENT_BG_COLOR_RGB) { ++ GhosttyColorRgb bg = { 0 }; ++ assert_ok(ghostty_cell_get(cell, GHOSTTY_CELL_DATA_COLOR_RGB, &bg)); ++ attrs.rgb = terminal_ghostty_rgb(bg); ++ attrs.is_default = false; ++ } ++ return attrs; + } + +-static void terminal_focus(const Terminal *term, bool focus) ++static int terminal_ghostty_cell_url_attr(const GhosttyGridRef *ref) + FUNC_ATTR_NONNULL_ALL + { +- VTermState *state = vterm_obtain_state(term->vt); +- if (focus) { +- vterm_state_focus_in(state); +- } else { +- vterm_state_focus_out(state); ++ size_t uri_len = 0; ++ GhosttyResult result = ghostty_grid_ref_hyperlink_uri(ref, NULL, 0, &uri_len); ++ if (result == GHOSTTY_SUCCESS && uri_len == 0) { ++ return 0; ++ } ++ if (result != GHOSTTY_OUT_OF_SPACE) { ++ assert_ok(result); + } +-} + +-// }}} +-// libvterm callbacks {{{ ++ char *uri = xmalloc(uri_len + 1); ++ result = ghostty_grid_ref_hyperlink_uri(ref, (uint8_t *)uri, uri_len, &uri_len); ++ assert_ok(result); ++ uri[uri_len] = NUL; ++ int attr = hl_add_url(0, uri); ++ xfree(uri); ++ return attr; ++} + +-static int term_damage(VTermRect rect, void *data) ++static int cell_raw_attr(Terminal *term, GhosttyPointTag tag, uint32_t row, int col, ++ GhosttyCell cell, GhosttyStyle style, const GhosttyColorRgb palette[256]) ++ FUNC_ATTR_NONNULL_ALL + { +- invalidate_terminal(data, rect.start_row, rect.end_row); +- return 1; ++ TerminalColorAttrs fg = terminal_ghostty_style_color_attrs(palette, style.fg_color); ++ TerminalColorAttrs bg = terminal_ghostty_cell_bg_attrs(cell, style.bg_color, palette); ++ int underline = terminal_ghostty_underline_hl_flag(style.underline); ++ int sp = underline ? terminal_ghostty_style_color_rgb(palette, style.underline_color) : -1; ++ ++ int hl_attrs = (style.bold ? HL_BOLD : 0) ++ | (style.faint ? HL_DIM : 0) ++ | (style.blink ? HL_BLINK : 0) ++ | (style.invisible ? HL_CONCEALED : 0) ++ | (style.overline ? HL_OVERLINE : 0) ++ | (style.italic ? HL_ITALIC : 0) ++ | (style.inverse ? HL_INVERSE : 0) ++ | underline ++ | (style.strikethrough ? HL_STRIKETHROUGH : 0); ++ ++ int url_attr = 0; ++ bool has_hyperlink = false; ++ assert_ok(ghostty_cell_get(cell, GHOSTTY_CELL_DATA_HAS_HYPERLINK, &has_hyperlink)); ++ if (has_hyperlink) { ++ GhosttyGridRef ref = { 0 }; ++ if (terminal_ghostty_grid_ref(term, tag, row, col, &ref)) { ++ url_attr = terminal_ghostty_cell_url_attr(&ref); ++ } ++ } ++ ++ return terminal_cell_hl_attr(term, hl_attrs, fg.idx, bg.idx, fg.rgb, bg.rgb, sp, ++ fg.is_default, bg.is_default, url_attr); + } + +-static int term_moverect(VTermRect dest, VTermRect src, void *data) ++static int terminal_ghostty_cell_attr(Terminal *term, GhosttyPointTag tag, uint32_t row, int col, ++ const GhosttyColorRgb palette[256]) ++ FUNC_ATTR_NONNULL_ALL + { +- invalidate_terminal(data, MIN(dest.start_row, src.start_row), +- MAX(dest.end_row, src.end_row)); +- return 1; ++ GhosttyGridRef ref = { 0 }; ++ if (!terminal_ghostty_grid_ref(term, tag, row, col, &ref)) { ++ return 0; ++ } ++ ++ GhosttyCell cell = 0; ++ assert_ok(ghostty_grid_ref_cell(&ref, &cell)); ++ ++ GhosttyStyle style = GHOSTTY_INIT_SIZED(GhosttyStyle); ++ assert_ok(ghostty_grid_ref_style(&ref, &style)); ++ ++ return cell_raw_attr(term, tag, row, col, cell, style, palette); + } + +-static int term_movecursor(VTermPos new_pos, VTermPos old_pos, int visible, void *data) ++/// Returns the number of Nvim display cells used by text mirrored from a Ghostty cell. ++static int terminal_ghostty_cell_display_width(const GhosttyGridRef *ref) ++ FUNC_ATTR_NONNULL_ALL + { +- Terminal *term = data; +- term->cursor.row = new_pos.row; +- term->cursor.col = new_pos.col; +- invalidate_terminal(term, -1, -1); +- return 1; ++ size_t grapheme_len = 0; ++ GhosttyResult result = ghostty_grid_ref_graphemes(ref, NULL, 0, &grapheme_len); ++ if (grapheme_len == 0) { ++ return 1; ++ } ++ if (result != GHOSTTY_OUT_OF_SPACE) { ++ assert_ok(result); ++ } ++ ++ uint32_t stack[16]; ++ uint32_t *graphemes = stack; ++ if (grapheme_len > ARRAY_SIZE(stack)) { ++ graphemes = xmalloc(sizeof(*graphemes) * grapheme_len); ++ } ++ ++ assert_ok(ghostty_grid_ref_graphemes(ref, graphemes, grapheme_len, ++ &grapheme_len)); ++ char buf[MB_MAXBYTES * ARRAY_SIZE(stack)]; ++ char *text = buf; ++ if (grapheme_len > ARRAY_SIZE(stack)) { ++ text = xmalloc(MB_MAXBYTES * grapheme_len); ++ } ++ ++ size_t len = 0; ++ for (size_t i = 0; i < grapheme_len; i++) { ++ len += (size_t)utf_char2bytes((int)graphemes[i], text + len); ++ } ++ ++ int width = utf_ptr2cells_len(text, (int)len); ++ ++ if (text != buf) { ++ xfree(text); ++ } ++ if (graphemes != stack) { ++ xfree(graphemes); ++ } ++ ++ return width; + } + +-static void buf_set_term_title(buf_T *buf, const char *title, size_t len) ++static int render_cell_display_width(GhosttyRenderStateRowCells cells) ++ FUNC_ATTR_NONNULL_ALL + { +- if (!buf) { +- return; // In case of receiving OSC 2 between buffer close and job exit. ++ uint32_t grapheme_len = 0; ++ assert_ok(ghostty_render_state_row_cells_get(cells, ++ GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN, ++ &grapheme_len)); ++ if (grapheme_len == 0) { ++ return 1; + } + +- Error err = ERROR_INIT; +- buf->b_locked++; +- dict_set_var(buf->b_vars, +- STATIC_CSTR_AS_STRING("term_title"), +- STRING_OBJ(((String){ .data = (char *)title, .size = len })), +- false, +- false, +- NULL, +- &err); +- buf->b_locked--; +- api_clear_error(&err); +- status_redraw_buf(buf); ++ uint32_t stack[16]; ++ uint32_t *graphemes = stack; ++ if (grapheme_len > ARRAY_SIZE(stack)) { ++ graphemes = xmalloc(sizeof(*graphemes) * grapheme_len); ++ } ++ ++ assert_ok(ghostty_render_state_row_cells_get(cells, ++ GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF, ++ graphemes)); ++ char buf[MB_MAXBYTES * ARRAY_SIZE(stack)]; ++ char *text = buf; ++ if (grapheme_len > ARRAY_SIZE(stack)) { ++ text = xmalloc(MB_MAXBYTES * grapheme_len); ++ } ++ ++ size_t len = 0; ++ for (uint32_t i = 0; i < grapheme_len; i++) { ++ len += (size_t)utf_char2bytes((int)graphemes[i], text + len); ++ } ++ ++ int width = utf_ptr2cells_len(text, (int)len); ++ ++ if (text != buf) { ++ xfree(text); ++ } ++ if (graphemes != stack) { ++ xfree(graphemes); ++ } ++ ++ return width; + } + +-static int term_settermprop(VTermProp prop, VTermValue *val, void *data) ++/// Converts Ghostty's cursor column on the active row to the virtual column in the buffer. ++static int terminal_cursor_virtcol(Terminal *term) ++ FUNC_ATTR_NONNULL_ALL + { +- Terminal *term = data; ++ if (term->cursor.col == 0) { ++ return 0; ++ } + +- switch (prop) { +- case VTERM_PROP_ALTSCREEN: +- term->in_altscreen = val->boolean; +- break; ++ int width = 0; ++ terminal_ghostty_size_get(term, NULL, &width); + +- case VTERM_PROP_CURSORVISIBLE: +- term->cursor.visible = val->boolean; +- invalidate_terminal(term, -1, -1); +- break; ++ int vcol = 0; ++ int col = 0; ++ GhosttyRenderStateRowCells cells = NULL; ++ if (render_active_row_cells(term, (uint32_t)term->cursor.row, &cells)) { ++ while (col < term->cursor.col && col < width) { ++ assert_ok(ghostty_render_state_row_cells_select(cells, (uint16_t)col)); ++ ++ GhosttyCell cell = 0; ++ assert_ok(ghostty_render_state_row_cells_get(cells, ++ GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW, ++ &cell)); ++ ++ GhosttyCellWide wide = GHOSTTY_CELL_WIDE_NARROW; ++ assert_ok(ghostty_cell_get(cell, GHOSTTY_CELL_DATA_WIDE, &wide)); ++ int cell_width = wide == GHOSTTY_CELL_WIDE_WIDE ? 2 : 1; ++ int next_col = col + cell_width; ++ int display_width = render_cell_display_width(cells); ++ ++ if (next_col > term->cursor.col) { ++ vcol += MIN(display_width, term->cursor.col - col); ++ break; ++ } + +- case VTERM_PROP_TITLE: { +- buf_T *buf = handle_get_buffer(term->buf_handle); // May be NULL +- VTermStringFragment frag = val->string; ++ vcol += display_width; ++ col = next_col; ++ } + +- if (frag.initial && frag.final) { +- buf_set_term_title(buf, frag.str, frag.len); ++ return vcol + term->cursor.col - col; ++ } ++ ++ while (col < term->cursor.col && col < width) { ++ GhosttyGridRef ref = { 0 }; ++ if (!terminal_ghostty_grid_ref(term, GHOSTTY_POINT_TAG_ACTIVE, (uint32_t)term->cursor.row, col, ++ &ref)) { + break; + } + +- if (frag.initial) { +- term->title_len = 0; +- term->title_size = MAX(frag.len, 1024); +- term->title = xmalloc(sizeof(char *) * term->title_size); +- } else if (term->title_len + frag.len > term->title_size) { +- term->title_size *= 2; +- term->title = xrealloc(term->title, sizeof(char *) * term->title_size); +- } ++ GhosttyCell cell = 0; ++ assert_ok(ghostty_grid_ref_cell(&ref, &cell)); + +- memcpy(term->title + term->title_len, frag.str, frag.len); +- term->title_len += frag.len; ++ GhosttyCellWide wide = GHOSTTY_CELL_WIDE_NARROW; ++ assert_ok(ghostty_cell_get(cell, GHOSTTY_CELL_DATA_WIDE, &wide)); ++ int cell_width = wide == GHOSTTY_CELL_WIDE_WIDE ? 2 : 1; ++ int next_col = col + cell_width; ++ int display_width = terminal_ghostty_cell_display_width(&ref); + +- if (frag.final) { +- buf_set_term_title(buf, term->title, term->title_len); +- xfree(term->title); +- term->title = NULL; ++ if (next_col > term->cursor.col) { ++ vcol += MIN(display_width, term->cursor.col - col); ++ break; + } +- break; +- } + +- case VTERM_PROP_MOUSE: +- term->forward_mouse = (bool)val->number; +- break; ++ vcol += display_width; ++ col = next_col; ++ } + +- case VTERM_PROP_CURSORBLINK: +- term->cursor.blink = val->boolean; +- term->pending.cursor = true; +- invalidate_terminal(term, -1, -1); +- break; ++ return vcol + term->cursor.col - col; ++} + +- case VTERM_PROP_CURSORSHAPE: +- term->cursor.shape = val->number; +- term->pending.cursor = true; +- invalidate_terminal(term, -1, -1); +- break; ++void terminal_get_line_attributes(Terminal *term, win_T *wp, int linenr, int *term_attrs) ++{ ++ (void)wp; ++ int height, width; ++ terminal_ghostty_size_get(term, &height, &width); ++ assert(linenr); ++ if (linenr < 1) { ++ return; ++ } + +- case VTERM_PROP_THEMEUPDATES: +- term->theme_updates = val->boolean; +- break; ++ size_t screen_row = (size_t)(linenr - 1); ++ if (screen_row >= term->scrollback_rows + (size_t)height) { ++ // Terminal height was decreased but the change wasn't reflected into the ++ // buffer yet ++ return; ++ } + +- case VTERM_PROP_SYNCOUTPUT: +- term->synchronized_output = val->boolean; +- if (!val->boolean) { +- // Mark that sync just ended; terminal_receive() will flush +- // the buffer immediately rather than waiting for the 10ms timer. +- term->sync_flush_pending = true; ++ width = MIN(TERM_ATTRS_MAX, width); ++ GhosttyRenderStateColors colors = GHOSTTY_INIT_SIZED(GhosttyRenderStateColors); ++ assert_ok(ghostty_render_state_colors_get(term->ghostty_render_state, &colors)); ++ ++ if (screen_row >= term->scrollback_rows) { ++ uint32_t row = (uint32_t)(screen_row - term->scrollback_rows); ++ GhosttyRenderStateRowCells cells = NULL; ++ if (render_active_row_cells(term, row, &cells)) { ++ int col = 0; ++ while (col < width && ghostty_render_state_row_cells_next(cells)) { ++ GhosttyCell cell = 0; ++ assert_ok(ghostty_render_state_row_cells_get(cells, ++ GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW, ++ &cell)); ++ ++ GhosttyStyle style = GHOSTTY_INIT_SIZED(GhosttyStyle); ++ assert_ok(ghostty_render_state_row_cells_get(cells, ++ GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE, ++ &style)); ++ ++ term_attrs[col] = cell_raw_attr(term, GHOSTTY_POINT_TAG_ACTIVE, row, col, ++ cell, style, colors.palette); ++ col++; ++ } ++ return; + } +- break; ++ } + +- default: +- return 0; ++ GhosttyPointTag tag = GHOSTTY_POINT_TAG_ACTIVE; ++ uint32_t row = 0; ++ if (screen_row < term->scrollback_rows) { ++ tag = GHOSTTY_POINT_TAG_SCREEN; ++ row = (uint32_t)screen_row; ++ } else { ++ row = (uint32_t)(screen_row - term->scrollback_rows); + } + +- return 1; ++ for (int col = 0; col < width; col++) { ++ int attr_id = terminal_ghostty_cell_attr(term, tag, row, col, colors.palette); ++ term_attrs[col] = attr_id; ++ } + } + +-/// Called when the terminal wants to ring the system bell. +-static int term_bell(void *data) ++Buffer terminal_buf(const Terminal *term) ++ FUNC_ATTR_NONNULL_ALL + { +- vim_beep(kOptBoFlagTerm); +- return 1; ++ return term->buf_handle; + } + +-/// Called when the terminal wants to query the system theme. +-static int term_theme(bool *dark, void *data) ++bool terminal_running(const Terminal *term) + FUNC_ATTR_NONNULL_ALL + { +- *dark = (*p_bg == 'd'); +- return 1; ++ return !term->closed; ++} ++ ++bool terminal_suspended(const Terminal *term) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ return term->suspended; ++} ++ ++void terminal_notify_theme(Terminal *term, bool dark) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ if (!term->theme_updates) { ++ return; ++ } ++ ++ char buf[10]; ++ ssize_t ret = snprintf(buf, sizeof(buf), "\x1b[997;%cn", dark ? '1' : '2'); ++ assert(ret > 0); ++ assert((size_t)ret <= sizeof(buf)); ++ terminal_send(term, buf, (size_t)ret); + } + +-/// Scrollback push handler: called just before a line goes offscreen (and libvterm will forget it), +-/// giving us a chance to store it. +-/// +-/// Code adapted from pangoterm. +-static int term_sb_push(int cols, const VTermScreenCell *cells, void *data) ++/// Updates the terminal's default foreground, background, and cursor colors. ++void terminal_update_colors(Terminal *term) ++ FUNC_ATTR_NONNULL_ALL + { +- Terminal *term = data; +- +- if (!term_may_alloc_scrollback(term, NULL)) { +- return 0; +- } +- assert(term->sb_size > 0); +- +- // copy vterm cells into sb_buffer +- size_t c = (size_t)cols; +- ScrollbackLine *sbrow = NULL; +- if (term->sb_current == term->sb_size) { +- if (term->sb_buffer[term->sb_current - 1]->cols == c) { +- // Recycle old row if it's the right size +- sbrow = term->sb_buffer[term->sb_current - 1]; +- } else { +- xfree(term->sb_buffer[term->sb_current - 1]); ++ bool dark = (*p_bg == 'd'); ++ ++ // Set the foreground color. ++ RgbValue fg = (p_tgc && normal_fg >= 0) ? normal_fg : (dark ? 0xffffff : 0x000000); ++ GhosttyColorRgb fg_ghostty = rgb_value_to_ghostty_color(fg); ++ assert_ok(ghostty_terminal_set(term->ghostty, GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND, ++ &fg_ghostty)); ++ ++ // Set the background color. ++ RgbValue bg = (p_tgc && normal_bg >= 0) ? normal_bg : (dark ? 0x000000 : 0xffffff); ++ GhosttyColorRgb bg_ghostty = rgb_value_to_ghostty_color(bg); ++ assert_ok(ghostty_terminal_set(term->ghostty, GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND, ++ &bg_ghostty)); ++ ++ // Set the cursor color. ++ if (*p_guicursor != NUL && shape_table[SHAPE_IDX_TERM].id != 0) { ++ HlAttrs attrs = syn_attr2entry(syn_id2attr(shape_table[SHAPE_IDX_TERM].id)); ++ if (attrs.hl_blend != 100 && !(attrs.rgb_ae_attr & HL_INVERSE) && attrs.rgb_bg_color >= 0) { ++ GhosttyColorRgb cursor_ghostty = rgb_value_to_ghostty_color(attrs.rgb_bg_color); ++ assert_ok(ghostty_terminal_set(term->ghostty, GHOSTTY_TERMINAL_OPT_COLOR_CURSOR, ++ &cursor_ghostty)); ++ return; + } +- term->sb_deleted++; +- +- // Make room at the start by shifting to the right. +- memmove(term->sb_buffer + 1, term->sb_buffer, +- sizeof(term->sb_buffer[0]) * (term->sb_current - 1)); +- } else if (term->sb_current > 0) { +- // Make room at the start by shifting to the right. +- memmove(term->sb_buffer + 1, term->sb_buffer, +- sizeof(term->sb_buffer[0]) * term->sb_current); +- } +- +- if (!sbrow) { +- sbrow = xmalloc(sizeof(ScrollbackLine) + c * sizeof(sbrow->cells[0])); +- sbrow->cols = c; + } + +- // New row is added at the start of the storage buffer. +- term->sb_buffer[0] = sbrow; +- if (term->sb_current < term->sb_size) { +- term->sb_current++; +- } +- +- if (term->sb_pending < (int)term->sb_size) { +- term->sb_pending++; +- } ++ assert_ok(ghostty_terminal_set(term->ghostty, GHOSTTY_TERMINAL_OPT_COLOR_CURSOR, ++ NULL)); ++} + +- memcpy(sbrow->cells, cells, sizeof(cells[0]) * c); +- if (!term->synchronized_output) { +- set_put(ptr_t, &invalidated_terminals, term); ++/// Updates the default colors for every open terminal buffer. ++void terminal_update_colors_all(void) ++{ ++ FOR_ALL_BUFFERS(buf) { ++ if (buf->terminal) { ++ terminal_update_colors(buf->terminal); ++ } + } +- +- return 1; + } + +-/// Scrollback pop handler (from pangoterm). +-/// +-/// @param cols +-/// @param cells VTerm state to update. +-/// @param data Terminal +-static int term_sb_pop(int cols, VTermScreenCell *cells, void *data) ++static void terminal_focus(Terminal *term, bool focus) ++ FUNC_ATTR_NONNULL_ALL + { +- Terminal *term = data; ++ bool report_focus = false; + +- if (!term->sb_current) { +- return 0; +- } ++ assert_ok(ghostty_terminal_mode_get(term->ghostty, ++ GHOSTTY_MODE_FOCUS_EVENT, ++ &report_focus)); + +- if (term->sb_pending > 0) { +- term->sb_pending--; +- } else { +- term->old_height++; ++ // Return early if focus reporting is not enabled. ++ if (!report_focus) { ++ return; + } + +- ScrollbackLine *sbrow = term->sb_buffer[0]; +- term->sb_current--; +- // Forget the "popped" row by shifting the rest onto it. +- memmove(term->sb_buffer, term->sb_buffer + 1, +- sizeof(term->sb_buffer[0]) * (term->sb_current)); ++ enum { FOCUS_BUF_SIZE = 3, }; ++ char buf[FOCUS_BUF_SIZE]; ++ size_t len = 0; ++ assert_ok(ghostty_focus_encode(focus ? GHOSTTY_FOCUS_GAINED : GHOSTTY_FOCUS_LOST, ++ buf, ++ FOCUS_BUF_SIZE, ++ &len)); + +- size_t cols_to_copy = MIN((size_t)cols, sbrow->cols); ++ terminal_send(term, buf, len); ++} + +- // copy to vterm state +- memcpy(cells, sbrow->cells, sizeof(cells[0]) * cols_to_copy); +- for (size_t col = cols_to_copy; col < (size_t)cols; col++) { +- cells[col].schar = 0; +- cells[col].width = 1; +- } ++// }}} ++// libghostty callbacks {{{ + +- xfree(sbrow); +- if (!term->synchronized_output) { +- set_put(ptr_t, &invalidated_terminals, term); +- } ++/// Called when Ghostty needs to write the response for a terminal query. ++static void on_ghostty_write_pty(GhosttyTerminal ghostty FUNC_ATTR_UNUSED, void *user_data, ++ const uint8_t *data, size_t len) ++{ ++ Terminal *term = (Terminal *)user_data; ++ terminal_send(term, (const char *)data, len); ++} + +- return 1; ++/// Called when the terminal program wants to set the title. ++static void on_ghostty_title_changed(GhosttyTerminal ghostty, void *user_data) ++{ ++ Terminal *term = (Terminal *)user_data; ++ GhosttyString title = { 0 }; ++ assert_ok(ghostty_terminal_get(ghostty, GHOSTTY_TERMINAL_DATA_TITLE, &title)); ++ ++ buf_T *buf = handle_get_buffer(term->buf_handle); ++ buf_set_term_title(buf, title.ptr == NULL ? "" : (const char *)title.ptr, title.len); + } + +-static int term_sb_clear(void *data) ++/// Called when the terminal program wants to ring the system bell. ++static void on_term_ghostty_bell(GhosttyTerminal ghostty FUNC_ATTR_UNUSED, ++ void *user_data FUNC_ATTR_UNUSED) + { +- Terminal *term = data; ++ vim_beep(kOptBoFlagTerm); ++} + +- if (term->in_altscreen || !term->sb_size || !term->sb_current) { +- return 1; +- } ++/// Called when the terminal program wants to know the terminal device attributes. ++static bool on_term_ghostty_device_attributes(GhosttyTerminal ghostty, void *user_data, ++ GhosttyDeviceAttributes *out_attrs) ++{ ++ (void)ghostty; ++ (void)user_data; ++ ++ GhosttyDeviceAttributes attrs = { ++ .primary = { ++ .conformance_level = GHOSTTY_DA_CONFORMANCE_VT220, ++ .features = { GHOSTTY_DA_FEATURE_ANSI_COLOR }, ++ .num_features = 1, ++ }, ++ .secondary = { ++ .device_type = GHOSTTY_DA_DEVICE_TYPE_VT220, ++ .firmware_version = 10, ++ .rom_cartridge = 0, ++ }, ++ .tertiary = { ++ .unit_id = 0, ++ }, ++ }; ++ if (terminal_ghostty_da_clipboard) { ++ attrs.primary.features[attrs.primary.num_features++] = GHOSTTY_DA_FEATURE_CLIPBOARD; ++ } ++ *out_attrs = attrs; ++ return true; ++} + +- for (size_t i = 0; i < term->sb_current; i++) { +- xfree(term->sb_buffer[i]); ++static void buf_set_term_title(buf_T *buf, const char *title, size_t len) ++{ ++ if (!buf) { ++ return; // In case of receiving OSC 2 between buffer close and job exit. + } + +- term->sb_deleted += term->sb_current; +- term->sb_current = 0; +- term->sb_pending = 0; +- invalidate_terminal(term, -1, -1); ++ Error err = ERROR_INIT; ++ buf->b_locked++; ++ dict_set_var(buf->b_vars, ++ STATIC_CSTR_AS_STRING("term_title"), ++ STRING_OBJ(((String){ .data = (char *)title, .size = len })), ++ false, ++ false, ++ NULL, ++ &err); ++ buf->b_locked--; ++ api_clear_error(&err); ++ status_redraw_buf(buf); ++} + +- return 1; ++/// Called when the terminal program wants to query the system theme. ++static bool on_term_ghostty_color_scheme(GhosttyTerminal ghostty FUNC_ATTR_UNUSED, ++ void *user_data FUNC_ATTR_UNUSED, ++ GhosttyColorScheme *out_scheme) ++{ ++ *out_scheme = (*p_bg == 'd') ? GHOSTTY_COLOR_SCHEME_DARK : GHOSTTY_COLOR_SCHEME_LIGHT; ++ return true; + } + + static void term_clipboard_set(void **argv) + { +- VTermSelectionMask mask = (VTermSelectionMask)(long)argv[0]; ++ TerminalClipboardRegister reg = (TerminalClipboardRegister)(intptr_t)argv[0]; + char *data = argv[1]; + + char regname; +- switch (mask) { +- case VTERM_SELECTION_CLIPBOARD: ++ switch (reg) { ++ case kTerminalClipboardRegister: + regname = '+'; + break; +- case VTERM_SELECTION_PRIMARY: ++ case kTerminalClipboardPrimary: + regname = '*'; + break; +- default: +- regname = '+'; +- break; + } + + // Split the payload into readfile()-style list (:h chansend()). +@@ -1851,43 +2778,32 @@ static void term_clipboard_set(void **argv) + eval_call_provider("clipboard", "set", args, true); + } + +-static int term_selection_set(VTermSelectionMask mask, VTermStringFragment frag, void *user) +-{ +- Terminal *term = user; +- if (frag.initial) { +- kv_size(term->selection) = 0; +- } +- +- kv_concat_len(term->selection, frag.str, frag.len); +- +- if (frag.final) { +- char *data = xmemdupz(term->selection.items, kv_size(term->selection)); +- multiqueue_put(main_loop.events, term_clipboard_set, (void *)mask, data); +- } +- +- return 1; +-} +- + // }}} + // input handling {{{ + +-static void convert_modifiers(int *key, VTermModifier *statep) ++static GhosttyMods convert_mouse_modifiers(int modifiers) + { +- if (mod_mask & MOD_MASK_SHIFT) { +- *statep |= VTERM_MOD_SHIFT; ++ GhosttyMods mods = 0; ++ if (modifiers & MOD_MASK_SHIFT) { ++ mods |= GHOSTTY_MODS_SHIFT; + } +- if (mod_mask & MOD_MASK_CTRL) { +- *statep |= VTERM_MOD_CTRL; +- if (!(mod_mask & MOD_MASK_SHIFT) && *key >= 'A' && *key <= 'Z') { +- // vterm interprets CTRL+A as SHIFT+CTRL, change to CTRL+a +- *key += ('a' - 'A'); +- } ++ if (modifiers & MOD_MASK_CTRL) { ++ mods |= GHOSTTY_MODS_CTRL; + } +- if (mod_mask & MOD_MASK_ALT) { +- *statep |= VTERM_MOD_ALT; ++ if (modifiers & MOD_MASK_ALT) { ++ mods |= GHOSTTY_MODS_ALT; + } ++ if (modifiers & MOD_MASK_CMD) { ++ mods |= GHOSTTY_MODS_SUPER; ++ } ++ return mods; ++} ++ ++static GhosttyMods convert_key_modifiers(int key, int modifiers) ++{ ++ GhosttyMods mods = convert_mouse_modifiers(modifiers); + +- switch (*key) { ++ switch (key) { + case K_S_TAB: + case K_S_UP: + case K_S_DOWN: +@@ -1907,300 +2823,306 @@ static void convert_modifiers(int *key, VTermModifier *statep) + case K_S_F10: + case K_S_F11: + case K_S_F12: +- *statep |= VTERM_MOD_SHIFT; ++ mods |= GHOSTTY_MODS_SHIFT; + break; + + case K_C_LEFT: + case K_C_RIGHT: + case K_C_HOME: + case K_C_END: +- *statep |= VTERM_MOD_CTRL; ++ mods |= GHOSTTY_MODS_CTRL; + break; + } ++ ++ return mods; + } + +-static VTermKey convert_key(int *key, VTermModifier *statep) ++static GhosttyKey convert_key(int key) + { +- convert_modifiers(key, statep); +- +- switch (*key) { ++ switch (key) { + case K_BS: +- return VTERM_KEY_BACKSPACE; ++ return GHOSTTY_KEY_BACKSPACE; + case K_S_TAB: + FALLTHROUGH; + case TAB: +- return VTERM_KEY_TAB; ++ return GHOSTTY_KEY_TAB; + case Ctrl_M: +- return VTERM_KEY_ENTER; ++ return GHOSTTY_KEY_ENTER; + case ESC: +- return VTERM_KEY_ESCAPE; ++ return GHOSTTY_KEY_ESCAPE; + + case K_S_UP: + FALLTHROUGH; + case K_UP: +- return VTERM_KEY_UP; ++ return GHOSTTY_KEY_ARROW_UP; + case K_S_DOWN: + FALLTHROUGH; + case K_DOWN: +- return VTERM_KEY_DOWN; ++ return GHOSTTY_KEY_ARROW_DOWN; + case K_S_LEFT: + FALLTHROUGH; + case K_C_LEFT: + FALLTHROUGH; + case K_LEFT: +- return VTERM_KEY_LEFT; ++ return GHOSTTY_KEY_ARROW_LEFT; + case K_S_RIGHT: + FALLTHROUGH; + case K_C_RIGHT: + FALLTHROUGH; + case K_RIGHT: +- return VTERM_KEY_RIGHT; ++ return GHOSTTY_KEY_ARROW_RIGHT; + + case K_INS: +- return VTERM_KEY_INS; ++ return GHOSTTY_KEY_INSERT; + case K_DEL: +- return VTERM_KEY_DEL; ++ return GHOSTTY_KEY_DELETE; + case K_S_HOME: + FALLTHROUGH; + case K_C_HOME: + FALLTHROUGH; + case K_HOME: +- return VTERM_KEY_HOME; ++ return GHOSTTY_KEY_HOME; + case K_S_END: + FALLTHROUGH; + case K_C_END: + FALLTHROUGH; + case K_END: +- return VTERM_KEY_END; ++ return GHOSTTY_KEY_END; + case K_PAGEUP: +- return VTERM_KEY_PAGEUP; ++ return GHOSTTY_KEY_PAGE_UP; + case K_PAGEDOWN: +- return VTERM_KEY_PAGEDOWN; ++ return GHOSTTY_KEY_PAGE_DOWN; + + case K_K0: + FALLTHROUGH; + case K_KINS: +- return VTERM_KEY_KP_0; ++ return GHOSTTY_KEY_NUMPAD_0; + case K_K1: + FALLTHROUGH; + case K_KEND: +- return VTERM_KEY_KP_1; ++ return GHOSTTY_KEY_NUMPAD_1; + case K_K2: + FALLTHROUGH; + case K_KDOWN: +- return VTERM_KEY_KP_2; ++ return GHOSTTY_KEY_NUMPAD_2; + case K_K3: + FALLTHROUGH; + case K_KPAGEDOWN: +- return VTERM_KEY_KP_3; ++ return GHOSTTY_KEY_NUMPAD_3; + case K_K4: + FALLTHROUGH; + case K_KLEFT: +- return VTERM_KEY_KP_4; ++ return GHOSTTY_KEY_NUMPAD_4; + case K_K5: + FALLTHROUGH; + case K_KORIGIN: +- return VTERM_KEY_KP_5; ++ return GHOSTTY_KEY_NUMPAD_5; + case K_K6: + FALLTHROUGH; + case K_KRIGHT: +- return VTERM_KEY_KP_6; ++ return GHOSTTY_KEY_NUMPAD_6; + case K_K7: + FALLTHROUGH; + case K_KHOME: +- return VTERM_KEY_KP_7; ++ return GHOSTTY_KEY_NUMPAD_7; + case K_K8: + FALLTHROUGH; + case K_KUP: +- return VTERM_KEY_KP_8; ++ return GHOSTTY_KEY_NUMPAD_8; + case K_K9: + FALLTHROUGH; + case K_KPAGEUP: +- return VTERM_KEY_KP_9; ++ return GHOSTTY_KEY_NUMPAD_9; + case K_KDEL: + FALLTHROUGH; + case K_KPOINT: +- return VTERM_KEY_KP_PERIOD; ++ return GHOSTTY_KEY_NUMPAD_DECIMAL; + case K_KENTER: +- return VTERM_KEY_KP_ENTER; ++ return GHOSTTY_KEY_NUMPAD_ENTER; + case K_KPLUS: +- return VTERM_KEY_KP_PLUS; ++ return GHOSTTY_KEY_NUMPAD_ADD; + case K_KMINUS: +- return VTERM_KEY_KP_MINUS; ++ return GHOSTTY_KEY_NUMPAD_SUBTRACT; + case K_KMULTIPLY: +- return VTERM_KEY_KP_MULT; ++ return GHOSTTY_KEY_NUMPAD_MULTIPLY; + case K_KDIVIDE: +- return VTERM_KEY_KP_DIVIDE; ++ return GHOSTTY_KEY_NUMPAD_DIVIDE; ++ case K_KCOMMA: ++ return GHOSTTY_KEY_NUMPAD_COMMA; ++ case K_KEQUAL: ++ return GHOSTTY_KEY_NUMPAD_EQUAL; + + case K_S_F1: + FALLTHROUGH; + case K_F1: +- return VTERM_KEY_FUNCTION(1); ++ return GHOSTTY_KEY_F1; + case K_S_F2: + FALLTHROUGH; + case K_F2: +- return VTERM_KEY_FUNCTION(2); ++ return GHOSTTY_KEY_F2; + case K_S_F3: + FALLTHROUGH; + case K_F3: +- return VTERM_KEY_FUNCTION(3); ++ return GHOSTTY_KEY_F3; + case K_S_F4: + FALLTHROUGH; + case K_F4: +- return VTERM_KEY_FUNCTION(4); ++ return GHOSTTY_KEY_F4; + case K_S_F5: + FALLTHROUGH; + case K_F5: +- return VTERM_KEY_FUNCTION(5); ++ return GHOSTTY_KEY_F5; + case K_S_F6: + FALLTHROUGH; + case K_F6: +- return VTERM_KEY_FUNCTION(6); ++ return GHOSTTY_KEY_F6; + case K_S_F7: + FALLTHROUGH; + case K_F7: +- return VTERM_KEY_FUNCTION(7); ++ return GHOSTTY_KEY_F7; + case K_S_F8: + FALLTHROUGH; + case K_F8: +- return VTERM_KEY_FUNCTION(8); ++ return GHOSTTY_KEY_F8; + case K_S_F9: + FALLTHROUGH; + case K_F9: +- return VTERM_KEY_FUNCTION(9); ++ return GHOSTTY_KEY_F9; + case K_S_F10: + FALLTHROUGH; + case K_F10: +- return VTERM_KEY_FUNCTION(10); ++ return GHOSTTY_KEY_F10; + case K_S_F11: + FALLTHROUGH; + case K_F11: +- return VTERM_KEY_FUNCTION(11); ++ return GHOSTTY_KEY_F11; + case K_S_F12: + FALLTHROUGH; + case K_F12: +- return VTERM_KEY_FUNCTION(12); +- ++ return GHOSTTY_KEY_F12; + case K_F13: +- return VTERM_KEY_FUNCTION(13); ++ return GHOSTTY_KEY_F13; + case K_F14: +- return VTERM_KEY_FUNCTION(14); ++ return GHOSTTY_KEY_F14; + case K_F15: +- return VTERM_KEY_FUNCTION(15); ++ return GHOSTTY_KEY_F15; + case K_F16: +- return VTERM_KEY_FUNCTION(16); ++ return GHOSTTY_KEY_F16; + case K_F17: +- return VTERM_KEY_FUNCTION(17); ++ return GHOSTTY_KEY_F17; + case K_F18: +- return VTERM_KEY_FUNCTION(18); ++ return GHOSTTY_KEY_F18; + case K_F19: +- return VTERM_KEY_FUNCTION(19); ++ return GHOSTTY_KEY_F19; + case K_F20: +- return VTERM_KEY_FUNCTION(20); ++ return GHOSTTY_KEY_F20; + case K_F21: +- return VTERM_KEY_FUNCTION(21); ++ return GHOSTTY_KEY_F21; + case K_F22: +- return VTERM_KEY_FUNCTION(22); ++ return GHOSTTY_KEY_F22; + case K_F23: +- return VTERM_KEY_FUNCTION(23); ++ return GHOSTTY_KEY_F23; + case K_F24: +- return VTERM_KEY_FUNCTION(24); ++ return GHOSTTY_KEY_F24; + case K_F25: +- return VTERM_KEY_FUNCTION(25); +- case K_F26: +- return VTERM_KEY_FUNCTION(26); +- case K_F27: +- return VTERM_KEY_FUNCTION(27); +- case K_F28: +- return VTERM_KEY_FUNCTION(28); +- case K_F29: +- return VTERM_KEY_FUNCTION(29); +- case K_F30: +- return VTERM_KEY_FUNCTION(30); +- case K_F31: +- return VTERM_KEY_FUNCTION(31); +- case K_F32: +- return VTERM_KEY_FUNCTION(32); +- case K_F33: +- return VTERM_KEY_FUNCTION(33); +- case K_F34: +- return VTERM_KEY_FUNCTION(34); +- case K_F35: +- return VTERM_KEY_FUNCTION(35); +- case K_F36: +- return VTERM_KEY_FUNCTION(36); +- case K_F37: +- return VTERM_KEY_FUNCTION(37); +- case K_F38: +- return VTERM_KEY_FUNCTION(38); +- case K_F39: +- return VTERM_KEY_FUNCTION(39); +- case K_F40: +- return VTERM_KEY_FUNCTION(40); +- case K_F41: +- return VTERM_KEY_FUNCTION(41); +- case K_F42: +- return VTERM_KEY_FUNCTION(42); +- case K_F43: +- return VTERM_KEY_FUNCTION(43); +- case K_F44: +- return VTERM_KEY_FUNCTION(44); +- case K_F45: +- return VTERM_KEY_FUNCTION(45); +- case K_F46: +- return VTERM_KEY_FUNCTION(46); +- case K_F47: +- return VTERM_KEY_FUNCTION(47); +- case K_F48: +- return VTERM_KEY_FUNCTION(48); +- case K_F49: +- return VTERM_KEY_FUNCTION(49); +- case K_F50: +- return VTERM_KEY_FUNCTION(50); +- case K_F51: +- return VTERM_KEY_FUNCTION(51); +- case K_F52: +- return VTERM_KEY_FUNCTION(52); +- case K_F53: +- return VTERM_KEY_FUNCTION(53); +- case K_F54: +- return VTERM_KEY_FUNCTION(54); +- case K_F55: +- return VTERM_KEY_FUNCTION(55); +- case K_F56: +- return VTERM_KEY_FUNCTION(56); +- case K_F57: +- return VTERM_KEY_FUNCTION(57); +- case K_F58: +- return VTERM_KEY_FUNCTION(58); +- case K_F59: +- return VTERM_KEY_FUNCTION(59); +- case K_F60: +- return VTERM_KEY_FUNCTION(60); +- case K_F61: +- return VTERM_KEY_FUNCTION(61); +- case K_F62: +- return VTERM_KEY_FUNCTION(62); +- case K_F63: +- return VTERM_KEY_FUNCTION(63); ++ return GHOSTTY_KEY_F25; + + default: +- return VTERM_KEY_NONE; ++ return GHOSTTY_KEY_UNIDENTIFIED; ++ } ++} ++ ++static void terminal_mouse_encode_event(Terminal *term, GhosttyMouseAction action, bool has_button, ++ GhosttyMouseButton button, int row, int col, ++ GhosttyMods mods, bool any_button_pressed) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ ghostty_mouse_encoder_setopt(term->ghostty_mouse_encoder, ++ GHOSTTY_MOUSE_ENCODER_OPT_ANY_BUTTON_PRESSED, ++ &any_button_pressed); ++ ghostty_mouse_event_set_action(term->ghostty_mouse_event, action); ++ ghostty_mouse_event_set_mods(term->ghostty_mouse_event, mods); ++ ghostty_mouse_event_set_position(term->ghostty_mouse_event, (GhosttyMousePosition) { ++ .x = (float)col, ++ .y = (float)row, ++ }); ++ ++ if (has_button) { ++ ghostty_mouse_event_set_button(term->ghostty_mouse_event, button); ++ } else { ++ ghostty_mouse_event_clear_button(term->ghostty_mouse_event); ++ } ++ ++ // Try encoding to a stack-allocated buffer first. ++ char buf[128]; ++ size_t len = 0; ++ GhosttyResult res = ghostty_mouse_encoder_encode(term->ghostty_mouse_encoder, ++ term->ghostty_mouse_event, ++ buf, ++ sizeof(buf), ++ &len); ++ ++ // If that was too small, allocate on the heap. ++ if (res == GHOSTTY_OUT_OF_SPACE) { ++ char *big_buf = xmalloc(len); ++ assert_ok(ghostty_mouse_encoder_encode(term->ghostty_mouse_encoder, ++ term->ghostty_mouse_event, ++ big_buf, ++ len, ++ &len)); ++ terminal_send(term, big_buf, len); ++ xfree(big_buf); ++ return; ++ } ++ ++ assert_ok(res); ++ if (len > 0) { ++ terminal_send(term, buf, len); + } + } + +-static void mouse_action(Terminal *term, int button, int row, int col, bool pressed, +- VTermModifier mod) ++static void mouse_action(Terminal *term, bool has_button, GhosttyMouseButton button, int row, ++ int col, bool pressed, GhosttyMods mods) + { +- vterm_mouse_move(term->vt, row, col, mod); +- if (button) { +- vterm_mouse_button(term->vt, button, pressed, mod); ++ terminal_mouse_encoder_sync_config(term); ++ ++ GhosttyMouseButton motion_button = GHOSTTY_MOUSE_BUTTON_UNKNOWN; ++ bool any_button_pressed = terminal_mouse_get_pressed_button(term, &motion_button); ++ terminal_mouse_encode_event(term, GHOSTTY_MOUSE_ACTION_MOTION, any_button_pressed, ++ motion_button, row, col, mods, any_button_pressed); ++ ++ if (!has_button) { ++ return; ++ } ++ ++ unsigned button_number = terminal_mouse_button_number(button); ++ if (button_number == 0) { ++ return; ++ } ++ ++ unsigned old_buttons = term->ghostty_mouse_buttons; ++ if (terminal_mouse_button_is_stateful(button)) { ++ unsigned mask = 1U << (button_number - 1); ++ if (pressed) { ++ term->ghostty_mouse_buttons |= mask; ++ } else { ++ term->ghostty_mouse_buttons &= ~mask; ++ } + } ++ ++ if (term->ghostty_mouse_buttons == old_buttons && (button_number < 4 || button_number > 7)) { ++ return; ++ } ++ ++ any_button_pressed = term->ghostty_mouse_buttons != 0 || pressed; ++ terminal_mouse_encode_event(term, ++ pressed ? GHOSTTY_MOUSE_ACTION_PRESS : GHOSTTY_MOUSE_ACTION_RELEASE, ++ true, button, row, col, mods, any_button_pressed); + } + + // process a mouse event while the terminal is focused. return true if the + // terminal should lose focus +-static bool send_mouse_event(Terminal *term, int c) ++static bool send_mouse_event(Terminal *term, int c, int modifiers) + { + int row = mouse_row; + int col = mouse_col; +@@ -2212,13 +3134,14 @@ static bool send_mouse_event(Terminal *term, int c) + + int offset; + if (!term->suspended && !term->closed +- && term->forward_mouse && mouse_win->w_buffer->terminal == term && row >= 0 ++ && terminal_mouse_tracking_enabled(term) && mouse_win->w_buffer->terminal == term && row >= 0 + && (grid > 1 || row + mouse_win->w_winbar_height < mouse_win->w_height) + && col >= (offset = win_col_off(mouse_win)) + && (grid > 1 || col < mouse_win->w_width)) { + // event in the terminal window and mouse events was enabled by the + // program. translate and forward the event +- int button; ++ GhosttyMouseButton button = GHOSTTY_MOUSE_BUTTON_UNKNOWN; ++ bool has_button = true; + bool pressed = false; + + switch (c) { +@@ -2226,44 +3149,57 @@ static bool send_mouse_event(Terminal *term, int c) + case K_LEFTMOUSE: + pressed = true; FALLTHROUGH; + case K_LEFTRELEASE: +- button = 1; break; ++ button = GHOSTTY_MOUSE_BUTTON_LEFT; ++ break; + case K_MIDDLEDRAG: + case K_MIDDLEMOUSE: + pressed = true; FALLTHROUGH; + case K_MIDDLERELEASE: +- button = 2; break; ++ button = GHOSTTY_MOUSE_BUTTON_MIDDLE; ++ break; + case K_RIGHTDRAG: + case K_RIGHTMOUSE: + pressed = true; FALLTHROUGH; + case K_RIGHTRELEASE: +- button = 3; break; ++ button = GHOSTTY_MOUSE_BUTTON_RIGHT; ++ break; + case K_X1DRAG: + case K_X1MOUSE: + pressed = true; FALLTHROUGH; + case K_X1RELEASE: +- button = 8; break; ++ button = GHOSTTY_MOUSE_BUTTON_EIGHT; ++ break; + case K_X2DRAG: + case K_X2MOUSE: + pressed = true; FALLTHROUGH; + case K_X2RELEASE: +- button = 9; break; ++ button = GHOSTTY_MOUSE_BUTTON_NINE; ++ break; + case K_MOUSEDOWN: +- pressed = true; button = 4; break; ++ pressed = true; ++ button = GHOSTTY_MOUSE_BUTTON_FOUR; ++ break; + case K_MOUSEUP: +- pressed = true; button = 5; break; +- case K_MOUSELEFT: +- pressed = true; button = 7; break; ++ pressed = true; ++ button = GHOSTTY_MOUSE_BUTTON_FIVE; ++ break; + case K_MOUSERIGHT: +- pressed = true; button = 6; break; ++ pressed = true; ++ button = GHOSTTY_MOUSE_BUTTON_SIX; ++ break; ++ case K_MOUSELEFT: ++ pressed = true; ++ button = GHOSTTY_MOUSE_BUTTON_SEVEN; ++ break; + case K_MOUSEMOVE: +- button = 0; break; ++ has_button = false; ++ break; + default: + return false; + } + +- VTermModifier mod = VTERM_MOD_NONE; +- convert_modifiers(&c, &mod); +- mouse_action(term, button, row, col - offset, pressed, mod); ++ GhosttyMods mods = convert_mouse_modifiers(modifiers); ++ mouse_action(term, has_button, button, row, col - offset, pressed, mods); + return false; + } + +@@ -2323,47 +3259,183 @@ static bool send_mouse_event(Terminal *term, int c) + // }}} + // terminal buffer refresh & misc {{{ + +-static void fetch_row(Terminal *term, int row, int end_col) ++static bool terminal_ghostty_append_codepoint(Terminal *term, char **ptr, size_t *cell_len, ++ uint32_t codepoint) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ if (*cell_len >= MAX_SCHAR_SIZE - 4 ++ || (size_t)(*ptr - term->textbuf) + MB_MAXBYTES >= TEXTBUF_SIZE) { ++ return false; ++ } ++ ++ char *cell_start = *ptr; ++ *ptr += utf_char2bytes((int)codepoint, *ptr); ++ *cell_len += (size_t)(*ptr - cell_start); ++ return true; ++} ++ ++static void terminal_ghostty_append_cell_text(Terminal *term, const GhosttyGridRef *ref, ++ GhosttyCell cell, char **ptr, size_t *line_len) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ size_t grapheme_len = 0; ++ GhosttyResult result = ghostty_grid_ref_graphemes(ref, NULL, 0, &grapheme_len); ++ if (grapheme_len == 0) { ++ if ((size_t)(*ptr - term->textbuf) < TEXTBUF_SIZE - 1) { ++ *(*ptr)++ = ' '; ++ } ++ bool has_styling = false; ++ assert_ok(ghostty_cell_get(cell, GHOSTTY_CELL_DATA_HAS_STYLING, &has_styling)); ++ if (has_styling) { ++ *line_len = (size_t)(*ptr - term->textbuf); ++ } ++ return; ++ } ++ if (result != GHOSTTY_OUT_OF_SPACE) { ++ assert_ok(result); ++ } ++ ++ uint32_t stack[16]; ++ uint32_t *graphemes = stack; ++ if (grapheme_len > ARRAY_SIZE(stack)) { ++ graphemes = xmalloc(sizeof(*graphemes) * grapheme_len); ++ } ++ ++ assert_ok(ghostty_grid_ref_graphemes(ref, graphemes, grapheme_len, ++ &grapheme_len)); ++ size_t cell_len = 0; ++ for (size_t i = 0; i < grapheme_len; i++) { ++ if (!terminal_ghostty_append_codepoint(term, ptr, &cell_len, graphemes[i])) { ++ break; ++ } ++ } ++ *line_len = (size_t)(*ptr - term->textbuf); ++ ++ if (graphemes != stack) { ++ xfree(graphemes); ++ } ++} ++ ++static void append_render_cell_text(Terminal *term, GhosttyRenderStateRowCells cells, ++ GhosttyCell cell, char **ptr, size_t *line_len) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ uint32_t grapheme_len = 0; ++ assert_ok(ghostty_render_state_row_cells_get(cells, ++ GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN, ++ &grapheme_len)); ++ if (grapheme_len == 0) { ++ if ((size_t)(*ptr - term->textbuf) < TEXTBUF_SIZE - 1) { ++ *(*ptr)++ = ' '; ++ } ++ bool has_styling = false; ++ assert_ok(ghostty_cell_get(cell, GHOSTTY_CELL_DATA_HAS_STYLING, &has_styling)); ++ if (has_styling) { ++ *line_len = (size_t)(*ptr - term->textbuf); ++ } ++ return; ++ } ++ ++ uint32_t stack[16]; ++ uint32_t *graphemes = stack; ++ if (grapheme_len > ARRAY_SIZE(stack)) { ++ graphemes = xmalloc(sizeof(*graphemes) * grapheme_len); ++ } ++ ++ assert_ok(ghostty_render_state_row_cells_get(cells, ++ GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF, ++ graphemes)); ++ size_t cell_len = 0; ++ for (uint32_t i = 0; i < grapheme_len; i++) { ++ if (!terminal_ghostty_append_codepoint(term, ptr, &cell_len, graphemes[i])) { ++ break; ++ } ++ } ++ *line_len = (size_t)(*ptr - term->textbuf); ++ ++ if (graphemes != stack) { ++ xfree(graphemes); ++ } ++} ++ ++static size_t fetch_render_row_cells(Terminal *term, GhosttyRenderStateRowCells cells, int end_col) ++ FUNC_ATTR_NONNULL_ALL + { + int col = 0; + size_t line_len = 0; + char *ptr = term->textbuf; + +- while (col < end_col) { +- VTermScreenCell cell; +- fetch_cell(term, row, col, &cell); +- if (cell.schar) { +- schar_get_adv(&ptr, cell.schar); +- line_len = (size_t)(ptr - term->textbuf); +- } else { +- *ptr++ = ' '; ++ while (col < end_col && ghostty_render_state_row_cells_next(cells)) { ++ GhosttyCell cell = 0; ++ assert_ok(ghostty_render_state_row_cells_get(cells, ++ GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW, ++ &cell)); ++ GhosttyCellWide wide = GHOSTTY_CELL_WIDE_NARROW; ++ assert_ok(ghostty_cell_get(cell, GHOSTTY_CELL_DATA_WIDE, &wide)); ++ if (wide != GHOSTTY_CELL_WIDE_SPACER_TAIL) { ++ append_render_cell_text(term, cells, cell, &ptr, &line_len); + } +- col += cell.width; ++ col++; + } + +- // end of line + term->textbuf[line_len] = NUL; ++ return line_len; + } + +-static bool fetch_cell(Terminal *term, int row, int col, VTermScreenCell *cell) ++/// Mirrors Ghostty scrollback rows [row, end) into the buffer. ++/// ++/// Scrollback rows are displayed as buffer lines, so scrolling the window does not move Ghostty's ++/// viewport. Because render states only expose the current viewport, we temporarily move it through ++/// the history in screen-sized steps, read each viewport, then restore it to the active area. ++/// ++/// @param insert Append rows if true, otherwise replace existing lines. ++static void mirror_scrollback_rows(Terminal *term, buf_T *buf, size_t row, size_t end, int width, ++ int height, bool insert) ++ FUNC_ATTR_NONNULL_ALL + { +- if (row < 0) { +- ScrollbackLine *sbrow = term->sb_buffer[-row - 1]; +- if ((size_t)col < sbrow->cols) { +- *cell = sbrow->cells[col]; +- } else { +- // fill the pointer with an empty cell +- *cell = (VTermScreenCell) { +- .schar = 0, +- .width = 1, +- }; +- return false; ++ while (row < end) { ++ GhosttyTerminalScrollViewport viewport = { ++ .tag = GHOSTTY_SCROLL_VIEWPORT_ROW, ++ .value.row = row, ++ }; ++ ghostty_terminal_scroll_viewport(term->ghostty, viewport); ++ assert_ok(ghostty_render_state_update(term->ghostty_render_state, term->ghostty)); ++ assert_ok(ghostty_render_state_get(term->ghostty_render_state, ++ GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR, ++ &term->ghostty_render_row_iterator)); ++ ++ size_t chunk_end = MIN(row + (size_t)height, end); ++ while (row < chunk_end) { ++ if (!ghostty_render_state_row_iterator_next(term->ghostty_render_row_iterator)) { ++ break; ++ } ++ GhosttyRenderStateRowCells cells = term->ghostty_render_row_cells; ++ assert_ok(ghostty_render_state_row_get(term->ghostty_render_row_iterator, ++ GHOSTTY_RENDER_STATE_ROW_DATA_CELLS, ++ &cells)); ++ (void)fetch_render_row_cells(term, cells, width); ++ linenr_T linenr = (linenr_T)row + 1; ++ if (insert) { ++ ml_append_buf(buf, (linenr_T)row, term->textbuf, 0, false); ++ appended_lines_buf(buf, (linenr_T)row, 1); ++ } else if (linenr <= buf->b_ml.ml_line_count) { ++ ml_replace_buf(buf, linenr, term->textbuf, true, false); ++ } else { ++ ml_append_buf(buf, buf->b_ml.ml_line_count, term->textbuf, 0, false); ++ appended_lines_buf(buf, buf->b_ml.ml_line_count, 1); ++ } ++ row++; ++ } ++ if (row < chunk_end) { ++ break; + } +- } else { +- vterm_screen_get_cell(term->vts, (VTermPos){ .row = row, .col = col }, +- cell); + } +- return true; ++ ghostty_terminal_scroll_viewport(term->ghostty, ++ (GhosttyTerminalScrollViewport) { ++ .tag = GHOSTTY_SCROLL_VIEWPORT_BOTTOM, ++ }); ++ // Restore the render state before refresh_screen() reads it. ++ assert_ok(ghostty_render_state_update(term->ghostty_render_state, term->ghostty)); + } + + // queue a terminal instance for refresh +@@ -2387,6 +3459,75 @@ static void invalidate_terminal(Terminal *term, int start_row, int end_row) + } + } + ++/// Invalidates the terminal rows Ghostty reports as dirty. ++/// ++/// We're currently handling invalid terminal rows as a single range, so partial Ghostty damage is ++/// collapsed to the smallest range covering every dirty row. After this returns, Ghostty's render ++/// state has been fully reset to not dirty. ++static void terminal_ghostty_render_state_update(Terminal *term) ++ FUNC_ATTR_NONNULL_ALL ++{ ++ assert_ok(ghostty_render_state_update(term->ghostty_render_state, ++ term->ghostty)); ++ terminal_ghostty_cursor_update(term); ++ terminal_ghostty_termprops_update(term); ++ ++ GhosttyRenderStateDirty dirty_state = GHOSTTY_RENDER_STATE_DIRTY_FALSE; ++ assert_ok(ghostty_render_state_get(term->ghostty_render_state, ++ GHOSTTY_RENDER_STATE_DATA_DIRTY, ++ &dirty_state)); ++ // Nothing to re-render, so we're done. ++ if (dirty_state == GHOSTTY_RENDER_STATE_DIRTY_FALSE) { ++ return; ++ } ++ ++ int dirty_start = INT_MAX; ++ int dirty_end = -1; ++ ++ // The whole screen is dirty, so the dirty range spans the full height. ++ if (dirty_state == GHOSTTY_RENDER_STATE_DIRTY_FULL) { ++ uint16_t rows = 0; ++ assert_ok(ghostty_render_state_get(term->ghostty_render_state, ++ GHOSTTY_RENDER_STATE_DATA_ROWS, ++ &rows)); ++ dirty_start = 0; ++ dirty_end = rows; ++ } ++ ++ assert_ok(ghostty_render_state_get(term->ghostty_render_state, ++ GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR, ++ &term->ghostty_render_row_iterator)); ++ ++ int row_idx = 0; ++ ++ while (ghostty_render_state_row_iterator_next(term->ghostty_render_row_iterator)) { ++ if (dirty_state == GHOSTTY_RENDER_STATE_DIRTY_PARTIAL) { ++ bool row_dirty = false; ++ assert_ok(ghostty_render_state_row_get(term->ghostty_render_row_iterator, ++ GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY, ++ &row_dirty)); ++ if (row_dirty) { ++ dirty_start = MIN(dirty_start, row_idx); ++ dirty_end = row_idx + 1; ++ } ++ } ++ ++ // Mark the row as clean. ++ bool dirty = false; ++ assert_ok(ghostty_render_state_row_set(term->ghostty_render_row_iterator, ++ GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY, ++ &dirty)); ++ row_idx++; ++ } ++ ++ invalidate_terminal(term, dirty_start, dirty_end); ++ ++ dirty_state = GHOSTTY_RENDER_STATE_DIRTY_FALSE; ++ assert_ok(ghostty_render_state_set(term->ghostty_render_state, ++ GHOSTTY_RENDER_STATE_OPTION_DIRTY, ++ &dirty_state)); ++} ++ + /// Normally refresh_timer_cb() is called when processing main_loop.events, but with + /// partial mappings main_loop.events isn't processed, while terminal buffers still + /// need refreshing after processing a key, so call this function before redrawing. +@@ -2405,7 +3546,7 @@ static void refresh_terminal(Terminal *term) + linenr_T ml_before = buf->b_ml.ml_line_count; + + bool resized = refresh_size(term, buf); +- refresh_scrollback(term, buf); ++ refresh_scrollback(term, buf, resized); + refresh_screen(term, buf); + + int ml_added = buf->b_ml.ml_line_count - ml_before; +@@ -2458,17 +3599,20 @@ static void refresh_cursor(Terminal *term, bool *cursor_visible) + } + + switch (term->cursor.shape) { +- case VTERM_PROP_CURSORSHAPE_BLOCK: ++ case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK: ++ case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK_HOLLOW: + shape_table[SHAPE_IDX_TERM].shape = SHAPE_BLOCK; + break; +- case VTERM_PROP_CURSORSHAPE_UNDERLINE: ++ case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_UNDERLINE: + shape_table[SHAPE_IDX_TERM].shape = SHAPE_HOR; + shape_table[SHAPE_IDX_TERM].percentage = 20; + break; +- case VTERM_PROP_CURSORSHAPE_BAR_LEFT: ++ case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BAR: + shape_table[SHAPE_IDX_TERM].shape = SHAPE_VER; + shape_table[SHAPE_IDX_TERM].percentage = 25; + break; ++ case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_MAX_VALUE: ++ abort(); + } + + ui_mode_info_set(); +@@ -2504,107 +3648,174 @@ static void refresh_timer_cb(TimeWatcher *watcher, void *data) + + static bool refresh_size(Terminal *term, buf_T *buf) + { ++ (void)buf; + if (!term->pending.resize || term->closed) { + return false; + } + + term->pending.resize = false; + int width, height; +- vterm_get_size(term->vt, &height, &width); ++ terminal_ghostty_size_get(term, &height, &width); + term->invalid_start = 0; + term->invalid_end = height; + term->opts.resize_cb((uint16_t)width, (uint16_t)height, term->opts.data); + return true; + } + +-void on_scrollback_option_changed(Terminal *term) ++void on_scrollback_option_changed(Terminal *term, size_t limit, bool force_refresh) + { +- // Scrollback buffer may not exist yet, e.g. if 'scrollback' is set in a TermOpen autocmd. +- if (term->sb_buffer != NULL) { ++ assert_ok(ghostty_terminal_set(term->ghostty, ++ GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_LINES, ++ &limit)); ++ if (force_refresh) { + refresh_terminal(term); + } + } + +-/// Adjusts scrollback storage and the terminal buffer scrollback lines +-static void adjust_scrollback(Terminal *term, buf_T *buf) ++// Refresh the scrollback of an invalidated terminal. ++static void refresh_scrollback(Terminal *term, buf_T *buf, bool resized) + { +- if (buf->b_p_scbk < 1) { // Local 'scrollback' was set to -1. +- buf->b_p_scbk = SB_MAX; +- } +- const size_t scbk = (size_t)buf->b_p_scbk; +- assert(term->sb_current < SIZE_MAX); +- if (term->sb_pending > 0) { // Pending rows must be processed first. +- abort(); +- } ++ // Buffer update callbacks may poll for uv events. ++ // Avoid polling for output to the same terminal as the one being refreshed. ++ term->opts.read_pause_cb(true, term->opts.data); + +- // Delete lines exceeding the new 'scrollback' limit. +- if (scbk < term->sb_current) { +- size_t diff = term->sb_current - scbk; +- for (size_t i = 0; i < diff; i++) { +- ml_delete_buf(buf, 1, false); +- term->sb_current--; +- xfree(term->sb_buffer[term->sb_current]); ++ int width, height; ++ terminal_ghostty_size_get(term, &height, &width); ++ ++ if (term->in_altscreen) { ++ linenr_T target_line_count = (linenr_T)(term->scrollback_rows + (size_t)height); ++ if (term->scrollback_rows > 0 && buf->b_ml.ml_line_count > target_line_count) { ++ target_line_count++; + } +- mark_adjust_buf(buf, 1, (linenr_T)diff, MAXLNUM, -(linenr_T)diff, true, +- kMarkAdjustTerm, kExtmarkUndo); +- deleted_lines_buf(buf, 1, (linenr_T)diff); ++ FOR_ALL_TAB_WINDOWS(tp, wp) { ++ if (!is_ctx_win(wp) && wp->w_buffer == buf) { ++ target_line_count = MAX(target_line_count, ++ (linenr_T)(term->scrollback_rows + (size_t)wp->w_view_height)); ++ } ++ } ++ while (buf->b_ml.ml_line_count > target_line_count && buf->b_ml.ml_line_count > 1) { ++ ml_delete_buf(buf, buf->b_ml.ml_line_count, false); ++ deleted_lines_buf(buf, buf->b_ml.ml_line_count, 1); ++ } ++ while (buf->b_ml.ml_line_count < target_line_count) { ++ ml_append_buf(buf, buf->b_ml.ml_line_count, "", 0, false); ++ appended_lines_buf(buf, buf->b_ml.ml_line_count, 1); ++ } ++ term->opts.read_pause_cb(false, term->opts.data); ++ return; + } + +- // Resize the scrollback storage. +- size_t sb_region = sizeof(ScrollbackLine *) * scbk; +- if (scbk != term->sb_size) { +- term->sb_buffer = xrealloc(term->sb_buffer, sb_region); +- } ++ size_t scrollback_rows = terminal_ghostty_scrollback_rows_get(term); + +- term->sb_size = scbk; +-} ++ size_t old_scrollback_rows = term->scrollback_rows; ++ bool scrollback_cleared = term->scrollback_clear_pending; ++ size_t scrollback_clear_rows = term->scrollback_clear_rows; ++ term->scrollback_clear_pending = false; ++ term->scrollback_clear_rows = 0; + +-// Refresh the scrollback of an invalidated terminal. +-static void refresh_scrollback(Terminal *term, buf_T *buf) +-{ +- // Buffer update callbacks may poll for uv events. +- // Avoid polling for output to the same terminal as the one being refreshed. +- term->opts.read_pause_cb(true, term->opts.data); ++ size_t scrollback_added = 0; ++ if (!resized) { ++ GhosttyPointCoordinate anchor = { 0 }; ++ GhosttyResult result = ghostty_tracked_grid_ref_point(term->ghostty_scrollback_anchor, ++ GHOSTTY_POINT_TAG_SCREEN, ++ &anchor); ++ if (result == GHOSTTY_SUCCESS) { ++ if (anchor.y <= scrollback_rows) { ++ scrollback_added = scrollback_rows - anchor.y; ++ } ++ } else if (result == GHOSTTY_NO_VALUE) { ++ // The anchor was pruned or reset, so none of the old mirrored history remains. ++ scrollback_added = SIZE_MAX; ++ } else { ++ assert_ok(result); ++ } ++ } ++ ++ if (!resized && scrollback_cleared) { ++ size_t max_deleted = buf->b_ml.ml_line_count > 1 ? (size_t)buf->b_ml.ml_line_count - 1 : 0; ++ size_t deleted = MIN(scrollback_clear_rows, max_deleted); ++ if (deleted > 0) { ++ mark_adjust_buf(buf, 1, (linenr_T)deleted, MAXLNUM, -(linenr_T)deleted, true, ++ kMarkAdjustTerm, kExtmarkUndo); ++ term->scrollback_deleted += deleted; ++ } ++ while (deleted > 0 && buf->b_ml.ml_line_count > 1) { ++ ml_delete_buf(buf, 1, false); ++ deleted_lines_buf(buf, 1, 1); ++ deleted--; ++ } ++ old_scrollback_rows = 0; ++ } else if (!resized) { ++ size_t total_rows = scrollback_added == SIZE_MAX ++ ? SIZE_MAX ++ : scrollback_added + old_scrollback_rows; ++ size_t deleted = total_rows > scrollback_rows ++ ? total_rows - scrollback_rows ++ : 0; ++ if (deleted > 0) { ++ mark_adjust_buf(buf, 1, (linenr_T)deleted, MAXLNUM, -(linenr_T)deleted, true, ++ kMarkAdjustTerm, kExtmarkUndo); ++ term->scrollback_deleted += deleted; ++ } ++ deleted = MIN(deleted, old_scrollback_rows); ++ while (deleted > 0 && buf->b_ml.ml_line_count > 1) { ++ ml_delete_buf(buf, 1, false); ++ deleted_lines_buf(buf, 1, 1); ++ old_scrollback_rows--; ++ deleted--; ++ } ++ } + +- linenr_T deleted = (linenr_T)(term->sb_deleted - term->old_sb_deleted); +- deleted = MIN(deleted, buf->b_ml.ml_line_count); +- mark_adjust_buf(buf, 1, deleted, MAXLNUM, -deleted, true, kMarkAdjustTerm, kExtmarkUndo); +- term->old_sb_deleted = term->sb_deleted; ++ term->scrollback_rows = scrollback_rows; + +- int old_height = term->old_height; +- int width, height; +- vterm_get_size(term->vt, &height, &width); ++ if (!resized && old_scrollback_rows < scrollback_rows) { ++ mirror_scrollback_rows(term, buf, old_scrollback_rows, scrollback_rows, width, height, true); ++ old_scrollback_rows = scrollback_rows; ++ } + +- // Remove deleted scrollback lines at the top, but don't unnecessarily remove +- // lines that will be overwritten by refresh_screen(). +- while (deleted > 0 && buf->b_ml.ml_line_count > old_height) { +- ml_delete_buf(buf, 1, false); +- deleted_lines_buf(buf, 1, 1); +- deleted--; ++ if (!resized) { ++ while (old_scrollback_rows > scrollback_rows && buf->b_ml.ml_line_count > 1) { ++ mark_adjust_buf(buf, 1, 1, MAXLNUM, -1, true, kMarkAdjustTerm, kExtmarkUndo); ++ term->scrollback_deleted++; ++ ml_delete_buf(buf, 1, false); ++ deleted_lines_buf(buf, 1, 1); ++ old_scrollback_rows--; ++ } + } + +- // Clamp old_height in case buffer lines have been deleted by the user. +- old_height = MIN(old_height, buf->b_ml.ml_line_count); +- while (term->sb_pending > 0) { +- // This means that either the window height has decreased or the screen +- // became full and libvterm had to push all rows up. Convert the first +- // pending scrollback row into a string and append it just above the visible +- // section of the buffer. +- fetch_row(term, -term->sb_pending, width); +- int buf_index = buf->b_ml.ml_line_count - old_height; +- ml_append_buf(buf, buf_index, term->textbuf, 0, false); +- appended_lines_buf(buf, buf_index, 1); +- term->sb_pending--; ++ linenr_T target_line_count = (linenr_T)(scrollback_rows + (size_t)height); ++ while (buf->b_ml.ml_line_count < target_line_count) { ++ ml_append_buf(buf, buf->b_ml.ml_line_count, "", 0, false); ++ appended_lines_buf(buf, buf->b_ml.ml_line_count, 1); ++ } ++ while (buf->b_ml.ml_line_count > target_line_count && buf->b_ml.ml_line_count > 1) { ++ if (resized && scrollback_rows >= old_scrollback_rows) { ++ // The active screen shrank while history was preserved. Remove obsolete ++ // screen rows from the bottom without shifting saved window views. ++ ml_delete_buf(buf, buf->b_ml.ml_line_count, false); ++ deleted_lines_buf(buf, buf->b_ml.ml_line_count, 1); ++ } else { ++ mark_adjust_buf(buf, 1, 1, MAXLNUM, -1, true, kMarkAdjustTerm, kExtmarkUndo); ++ term->scrollback_deleted++; ++ ml_delete_buf(buf, 1, false); ++ deleted_lines_buf(buf, 1, 1); ++ } + } + +- int max_line_count = (int)term->sb_current + height; +- // Remove extra lines at the bottom. +- while (buf->b_ml.ml_line_count > max_line_count) { +- ml_delete_buf(buf, buf->b_ml.ml_line_count, false); +- deleted_lines_buf(buf, buf->b_ml.ml_line_count, 1); ++ if (resized) { ++ mirror_scrollback_rows(term, buf, 0, scrollback_rows, width, height, false); ++ if (scrollback_rows > 0) { ++ changed_lines(buf, 1, 0, (linenr_T)scrollback_rows + 1, 0, true); ++ } + } + +- adjust_scrollback(term, buf); ++ GhosttyPoint scrollback_anchor = { ++ .tag = GHOSTTY_POINT_TAG_ACTIVE, ++ .value.coordinate = { .x = 0, .y = 0 }, ++ }; ++ assert_ok(ghostty_tracked_grid_ref_set(term->ghostty_scrollback_anchor, ++ term->ghostty, ++ scrollback_anchor)); + + term->opts.read_pause_cb(false, term->opts.data); + } +@@ -2617,7 +3828,7 @@ static void refresh_screen(Terminal *term, buf_T *buf) + int added = 0; + int height; + int width; +- vterm_get_size(term->vt, &height, &width); ++ terminal_ghostty_size_get(term, &height, &width); + // Terminal height may have decreased before `invalid_end` reflects it. + term->invalid_end = MIN(term->invalid_end, height); + +@@ -2628,10 +3839,34 @@ static void refresh_screen(Terminal *term, buf_T *buf) + return; + } + +- for (int r = term->invalid_start, linenr = row_to_linenr(term, r); +- r < term->invalid_end; r++, linenr++) { +- fetch_row(term, r, width); ++ assert_ok(ghostty_render_state_get(term->ghostty_render_state, ++ GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR, ++ &term->ghostty_render_row_iterator)); ++ ++ bool has_row = true; ++ for (int r = 0; r < term->invalid_end; r++) { ++ if (has_row) { ++ has_row = ghostty_render_state_row_iterator_next(term->ghostty_render_row_iterator); ++ } ++ if (r < term->invalid_start) { ++ continue; ++ } ++ ++ if (has_row) { ++ GhosttyRenderStateRowCells cells = term->ghostty_render_row_cells; ++ assert_ok(ghostty_render_state_row_get(term->ghostty_render_row_iterator, ++ GHOSTTY_RENDER_STATE_ROW_DATA_CELLS, ++ &cells)); ++ (void)fetch_render_row_cells(term, cells, width); ++ } else { ++ term->textbuf[0] = NUL; ++ } + ++ int linenr = row_to_linenr(term, r); ++ while (buf->b_ml.ml_line_count < linenr - 1) { ++ ml_append_buf(buf, buf->b_ml.ml_line_count, "", 0, false); ++ added++; ++ } + if (linenr <= buf->b_ml.ml_line_count) { + ml_replace_buf(buf, linenr, term->textbuf, true, false); + changed++; +@@ -2640,8 +3875,6 @@ static void refresh_screen(Terminal *term, buf_T *buf) + added++; + } + } +- term->old_height = height; +- + int change_start = row_to_linenr(term, term->invalid_start); + int change_end = change_start + changed; + term->invalid_start = INT_MAX; +@@ -2690,12 +3923,7 @@ static void adjust_topline_cursor(Terminal *term, buf_T *buf, int added) + + static int row_to_linenr(Terminal *term, int row) + { +- return row != INT_MAX ? row + (int)term->sb_current + 1 : INT_MAX; +-} +- +-static int linenr_to_row(Terminal *term, int linenr) +-{ +- return linenr - (int)term->sb_current - 1; ++ return row != INT_MAX ? row + (int)term->scrollback_rows + 1 : INT_MAX; + } + + static bool is_focused(Terminal *term) +diff --git a/src/nvim/vterm/LICENSE b/src/nvim/vterm/LICENSE +deleted file mode 100644 +index 0d051634b28b6b..00000000000000 +--- a/src/nvim/vterm/LICENSE ++++ /dev/null +@@ -1,23 +0,0 @@ +- +- +-The MIT License +- +-Copyright (c) 2008 Paul Evans +- +-Permission is hereby granted, free of charge, to any person obtaining a copy +-of this software and associated documentation files (the "Software"), to deal +-in the Software without restriction, including without limitation the rights +-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +-copies of the Software, and to permit persons to whom the Software is +-furnished to do so, subject to the following conditions: +- +-The above copyright notice and this permission notice shall be included in +-all copies or substantial portions of the Software. +- +-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +-THE SOFTWARE. +diff --git a/src/nvim/vterm/README.md b/src/nvim/vterm/README.md +deleted file mode 100644 +index 4cc43bfb94b8da..00000000000000 +--- a/src/nvim/vterm/README.md ++++ /dev/null +@@ -1 +0,0 @@ +-Adopted from [libvterm](https://www.leonerd.org.uk/code/libvterm/) +diff --git a/src/nvim/vterm/encoding.c b/src/nvim/vterm/encoding.c +deleted file mode 100644 +index 06bf0be694d27c..00000000000000 +--- a/src/nvim/vterm/encoding.c ++++ /dev/null +@@ -1,269 +0,0 @@ +-#include "nvim/vterm/encoding.h" +-#include "nvim/vterm/vterm_internal_defs.h" +- +-#include "vterm/encoding.c.generated.h" +- +-#define UNICODE_INVALID 0xFFFD +- +-#if defined(DEBUG) && DEBUG > 1 +-# define DEBUG_PRINT_UTF8 +-#endif +- +-struct UTF8DecoderData { +- // number of bytes remaining in this codepoint +- int bytes_remaining; +- +- // number of bytes total in this codepoint once it's finished +- // (for detecting overlongs) +- int bytes_total; +- +- int this_cp; +-}; +- +-static void init_utf8(VTermEncoding *enc, void *data_) +-{ +- struct UTF8DecoderData *data = data_; +- +- data->bytes_remaining = 0; +- data->bytes_total = 0; +-} +- +-static void decode_utf8(VTermEncoding *enc, void *data_, uint32_t cp[], int *cpi, int cplen, +- const char bytes[], size_t *pos, size_t bytelen) +-{ +- struct UTF8DecoderData *data = data_; +- +-#ifdef DEBUG_PRINT_UTF8 +- printf("BEGIN UTF-8\n"); +-#endif +- +- for (; *pos < bytelen && *cpi < cplen; (*pos)++) { +- uint8_t c = (uint8_t)bytes[*pos]; +- +-#ifdef DEBUG_PRINT_UTF8 +- printf(" pos=%zd c=%02x rem=%d\n", *pos, c, data->bytes_remaining); +-#endif +- +- if (c < 0x20) { // C0 +- return; +- } else if (c >= 0x20 && c < 0x7f) { +- if (data->bytes_remaining) { +- cp[(*cpi)++] = UNICODE_INVALID; +- } +- +- cp[(*cpi)++] = c; +-#ifdef DEBUG_PRINT_UTF8 +- printf(" UTF-8 char: U+%04x\n", c); +-#endif +- data->bytes_remaining = 0; +- } else if (c == 0x7f) { // DEL +- return; +- } else if (c >= 0x80 && c < 0xc0) { +- if (!data->bytes_remaining) { +- cp[(*cpi)++] = UNICODE_INVALID; +- continue; +- } +- +- data->this_cp <<= 6; +- data->this_cp |= c & 0x3f; +- data->bytes_remaining--; +- +- if (!data->bytes_remaining) { +-#ifdef DEBUG_PRINT_UTF8 +- printf(" UTF-8 raw char U+%04x bytelen=%d ", data->this_cp, data->bytes_total); +-#endif +- // Check for overlong sequences +- switch (data->bytes_total) { +- case 2: +- if (data->this_cp < 0x0080) { +- data->this_cp = UNICODE_INVALID; +- } +- break; +- case 3: +- if (data->this_cp < 0x0800) { +- data->this_cp = UNICODE_INVALID; +- } +- break; +- case 4: +- if (data->this_cp < 0x10000) { +- data->this_cp = UNICODE_INVALID; +- } +- break; +- case 5: +- if (data->this_cp < 0x200000) { +- data->this_cp = UNICODE_INVALID; +- } +- break; +- case 6: +- if (data->this_cp < 0x4000000) { +- data->this_cp = UNICODE_INVALID; +- } +- break; +- } +- // Now look for plain invalid ones +- if ((data->this_cp >= 0xD800 && data->this_cp <= 0xDFFF) +- || data->this_cp == 0xFFFE +- || data->this_cp == 0xFFFF) { +- data->this_cp = UNICODE_INVALID; +- } +-#ifdef DEBUG_PRINT_UTF8 +- printf(" char: U+%04x\n", data->this_cp); +-#endif +- cp[(*cpi)++] = (uint32_t)data->this_cp; +- } +- } else if (c >= 0xc0 && c < 0xe0) { +- if (data->bytes_remaining) { +- cp[(*cpi)++] = UNICODE_INVALID; +- } +- +- data->this_cp = c & 0x1f; +- data->bytes_total = 2; +- data->bytes_remaining = 1; +- } else if (c >= 0xe0 && c < 0xf0) { +- if (data->bytes_remaining) { +- cp[(*cpi)++] = UNICODE_INVALID; +- } +- +- data->this_cp = c & 0x0f; +- data->bytes_total = 3; +- data->bytes_remaining = 2; +- } else if (c >= 0xf0 && c < 0xf8) { +- if (data->bytes_remaining) { +- cp[(*cpi)++] = UNICODE_INVALID; +- } +- +- data->this_cp = c & 0x07; +- data->bytes_total = 4; +- data->bytes_remaining = 3; +- } else if (c >= 0xf8 && c < 0xfc) { +- if (data->bytes_remaining) { +- cp[(*cpi)++] = UNICODE_INVALID; +- } +- +- data->this_cp = c & 0x03; +- data->bytes_total = 5; +- data->bytes_remaining = 4; +- } else if (c >= 0xfc && c < 0xfe) { +- if (data->bytes_remaining) { +- cp[(*cpi)++] = UNICODE_INVALID; +- } +- +- data->this_cp = c & 0x01; +- data->bytes_total = 6; +- data->bytes_remaining = 5; +- } else { +- cp[(*cpi)++] = UNICODE_INVALID; +- } +- } +-} +- +-static VTermEncoding encoding_utf8 = { +- .init = &init_utf8, +- .decode = &decode_utf8, +-}; +- +-static void decode_usascii(VTermEncoding *enc, void *data, uint32_t cp[], int *cpi, int cplen, +- const char bytes[], size_t *pos, size_t bytelen) +-{ +- int is_gr = bytes[*pos] & 0x80; +- +- for (; *pos < bytelen && *cpi < cplen; (*pos)++) { +- uint8_t c = (uint8_t)(bytes[*pos] ^ is_gr); +- +- if (c < 0x20 || c == 0x7f || c >= 0x80) { +- return; +- } +- +- cp[(*cpi)++] = c; +- } +-} +- +-static VTermEncoding encoding_usascii = { +- .decode = &decode_usascii, +-}; +- +-struct StaticTableEncoding { +- const VTermEncoding enc; +- const uint32_t chars[128]; +-}; +- +-static void decode_table(VTermEncoding *enc, void *data, uint32_t cp[], int *cpi, int cplen, +- const char bytes[], size_t *pos, size_t bytelen) +-{ +- struct StaticTableEncoding *table = (struct StaticTableEncoding *)enc; +- int is_gr = bytes[*pos] & 0x80; +- +- for (; *pos < bytelen && *cpi < cplen; (*pos)++) { +- uint8_t c = (uint8_t)(bytes[*pos] ^ is_gr); +- +- if (c < 0x20 || c == 0x7f || c >= 0x80) { +- return; +- } +- +- if (table->chars[c]) { +- cp[(*cpi)++] = table->chars[c]; +- } else { +- cp[(*cpi)++] = c; +- } +- } +-} +- +-// https://en.wikipedia.org/wiki/DEC_Special_Graphics +-static const struct StaticTableEncoding encoding_DECdrawing = { +- { .decode = &decode_table }, +- { +- [0x60] = 0x25C6, // BLACK DIAMOND +- [0x61] = 0x2592, // MEDIUM SHADE (checkerboard) +- [0x62] = 0x2409, // SYMBOL FOR HORIZONTAL TAB +- [0x63] = 0x240C, // SYMBOL FOR FORM FEED +- [0x64] = 0x240D, // SYMBOL FOR CARRIAGE RETURN +- [0x65] = 0x240A, // SYMBOL FOR LINE FEED +- [0x66] = 0x00B0, // DEGREE SIGN +- [0x67] = 0x00B1, // PLUS-MINUS SIGN (plus or minus) +- [0x68] = 0x2424, // SYMBOL FOR NEW LINE +- [0x69] = 0x240B, // SYMBOL FOR VERTICAL TAB +- [0x6a] = 0x2518, // BOX DRAWINGS LIGHT UP AND LEFT (bottom-right corner) +- [0x6b] = 0x2510, // BOX DRAWINGS LIGHT DOWN AND LEFT (top-right corner) +- [0x6c] = 0x250C, // BOX DRAWINGS LIGHT DOWN AND RIGHT (top-left corner) +- [0x6d] = 0x2514, // BOX DRAWINGS LIGHT UP AND RIGHT (bottom-left corner) +- [0x6e] = 0x253C, // BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL (crossing lines) +- [0x6f] = 0x23BA, // HORIZONTAL SCAN LINE-1 +- [0x70] = 0x23BB, // HORIZONTAL SCAN LINE-3 +- [0x71] = 0x2500, // BOX DRAWINGS LIGHT HORIZONTAL +- [0x72] = 0x23BC, // HORIZONTAL SCAN LINE-7 +- [0x73] = 0x23BD, // HORIZONTAL SCAN LINE-9 +- [0x74] = 0x251C, // BOX DRAWINGS LIGHT VERTICAL AND RIGHT +- [0x75] = 0x2524, // BOX DRAWINGS LIGHT VERTICAL AND LEFT +- [0x76] = 0x2534, // BOX DRAWINGS LIGHT UP AND HORIZONTAL +- [0x77] = 0x252C, // BOX DRAWINGS LIGHT DOWN AND HORIZONTAL +- [0x78] = 0x2502, // BOX DRAWINGS LIGHT VERTICAL +- [0x79] = 0x2A7D, // LESS-THAN OR SLANTED EQUAL-TO +- [0x7a] = 0x2A7E, // GREATER-THAN OR SLANTED EQUAL-TO +- [0x7b] = 0x03C0, // GREEK SMALL LETTER PI +- [0x7c] = 0x2260, // NOT EQUAL TO +- [0x7d] = 0x00A3, // POUND SIGN +- [0x7e] = 0x00B7, // MIDDLE DOT +- } +-}; +- +-static struct { +- VTermEncodingType type; +- char designation; +- VTermEncoding *enc; +-} +-encodings[] = { +- { ENC_UTF8, 'u', &encoding_utf8 }, +- { ENC_SINGLE_94, '0', (VTermEncoding *)&encoding_DECdrawing }, +- { ENC_SINGLE_94, 'B', &encoding_usascii }, +- { 0 }, +-}; +- +-VTermEncoding *vterm_lookup_encoding(VTermEncodingType type, char designation) +-{ +- for (int i = 0; encodings[i].designation; i++) { +- if (encodings[i].type == type && encodings[i].designation == designation) { +- return encodings[i].enc; +- } +- } +- return NULL; +-} +diff --git a/src/nvim/vterm/encoding.h b/src/nvim/vterm/encoding.h +deleted file mode 100644 +index da418a487549db..00000000000000 +--- a/src/nvim/vterm/encoding.h ++++ /dev/null +@@ -1,8 +0,0 @@ +-#pragma once +- +-#include +- +-#include "nvim/vterm/vterm_defs.h" +-#include "nvim/vterm/vterm_internal_defs.h" +- +-#include "vterm/encoding.h.generated.h" +diff --git a/src/nvim/vterm/keyboard.c b/src/nvim/vterm/keyboard.c +deleted file mode 100644 +index e1ae0c9d1f0150..00000000000000 +--- a/src/nvim/vterm/keyboard.c ++++ /dev/null +@@ -1,316 +0,0 @@ +-#include +- +-#include "nvim/ascii_defs.h" +-#include "nvim/tui/termkey/termkey.h" +-#include "nvim/vterm/keyboard.h" +-#include "nvim/vterm/vterm.h" +-#include "nvim/vterm/vterm_internal_defs.h" +- +-#include "vterm/keyboard.c.generated.h" +- +-static VTermKeyEncodingFlags vterm_state_get_key_encoding_flags(const VTermState *state) +-{ +- int screen = state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY; +- const struct VTermKeyEncodingStack *stack = &state->key_encoding_stacks[screen]; +- assert(stack->size > 0); +- return stack->items[stack->size - 1]; +-} +- +-void vterm_keyboard_unichar(VTerm *vt, uint32_t c, VTermModifier mod) +-{ +- bool passthru = false; +- if (c == ' ') { +- // Space is passed through only when there are no modifiers (including shift) +- passthru = mod == VTERM_MOD_NONE; +- } else { +- // Otherwise pass through when there are no modifiers (ignoring shift) +- passthru = (mod & (unsigned)~VTERM_MOD_SHIFT) == 0; +- } +- +- if (passthru) { +- char str[6]; +- int seqlen = fill_utf8((int)c, str); +- vterm_push_output_bytes(vt, str, (size_t)seqlen); +- return; +- } +- +- VTermKeyEncodingFlags flags = vterm_state_get_key_encoding_flags(vt->state); +- if (flags.disambiguate) { +- // Always use unshifted codepoint +- if (c >= 'A' && c <= 'Z') { +- c += 'a' - 'A'; +- mod |= VTERM_MOD_SHIFT; +- } +- +- vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%du", c, mod + 1); +- return; +- } +- +- if (mod & VTERM_MOD_CTRL) { +- // Handle special cases. These are taken from kitty, but seem mostly +- // consistent across terminals. +- switch (c) { +- case '2': +- case ' ': +- // Ctrl+2 is NUL to match Ctrl+@ (which is Shift+2 on US keyboards) +- // Ctrl+Space is also NUL for some reason +- c = 0x00; +- break; +- case '3': +- case '4': +- case '5': +- case '6': +- case '7': +- // Ctrl+3 through Ctrl+7 are sequential starting from 0x1b. Importantly, +- // this means that Ctrl+6 emits 0x1e (the same as Ctrl+^ on US keyboards) +- c = 0x1b + c - '3'; +- break; +- case '8': +- // Ctrl+8 is DEL +- c = 0x7f; +- break; +- case '/': +- // Ctrl+/ is equivalent to Ctrl+_ for historic reasons +- c = 0x1f; +- break; +- default: +- if (c >= '@' && c <= 0x7f) { +- c &= 0x1f; +- } +- break; +- } +- } +- +- vterm_push_output_sprintf(vt, "%s%c", mod & VTERM_MOD_ALT ? ESC_S : "", c); +-} +- +-typedef struct { +- enum { +- KEYCODE_NONE, +- KEYCODE_LITERAL, +- KEYCODE_TAB, +- KEYCODE_ENTER, +- KEYCODE_SS3, +- KEYCODE_CSI, +- KEYCODE_CSI_CURSOR, +- KEYCODE_CSINUM, +- KEYCODE_KEYPAD, +- } type; +- int literal; +- int csinum; +-} keycodes_s; +- +-static keycodes_s keycodes[] = { +- { KEYCODE_NONE, NUL, 0 }, // NONE +- +- { KEYCODE_ENTER, '\r', 0 }, // ENTER +- { KEYCODE_TAB, '\t', 0 }, // TAB +- { KEYCODE_LITERAL, '\x7f', 0 }, // BACKSPACE == ASCII DEL +- { KEYCODE_LITERAL, '\x1b', 0 }, // ESCAPE +- +- { KEYCODE_CSI_CURSOR, 'A', 0 }, // UP +- { KEYCODE_CSI_CURSOR, 'B', 0 }, // DOWN +- { KEYCODE_CSI_CURSOR, 'D', 0 }, // LEFT +- { KEYCODE_CSI_CURSOR, 'C', 0 }, // RIGHT +- +- { KEYCODE_CSINUM, '~', 2 }, // INS +- { KEYCODE_CSINUM, '~', 3 }, // DEL +- { KEYCODE_CSI_CURSOR, 'H', 0 }, // HOME +- { KEYCODE_CSI_CURSOR, 'F', 0 }, // END +- { KEYCODE_CSINUM, '~', 5 }, // PAGEUP +- { KEYCODE_CSINUM, '~', 6 }, // PAGEDOWN +-}; +- +-static keycodes_s keycodes_fn[] = { +- { KEYCODE_NONE, NUL, 0 }, // F0 - shouldn't happen +- { KEYCODE_SS3, 'P', 0 }, // F1 +- { KEYCODE_SS3, 'Q', 0 }, // F2 +- { KEYCODE_SS3, 'R', 0 }, // F3 +- { KEYCODE_SS3, 'S', 0 }, // F4 +- { KEYCODE_CSINUM, '~', 15 }, // F5 +- { KEYCODE_CSINUM, '~', 17 }, // F6 +- { KEYCODE_CSINUM, '~', 18 }, // F7 +- { KEYCODE_CSINUM, '~', 19 }, // F8 +- { KEYCODE_CSINUM, '~', 20 }, // F9 +- { KEYCODE_CSINUM, '~', 21 }, // F10 +- { KEYCODE_CSINUM, '~', 23 }, // F11 +- { KEYCODE_CSINUM, '~', 24 }, // F12 +-}; +- +-static keycodes_s keycodes_kp[] = { +- { KEYCODE_KEYPAD, '0', 'p' }, // KP_0 +- { KEYCODE_KEYPAD, '1', 'q' }, // KP_1 +- { KEYCODE_KEYPAD, '2', 'r' }, // KP_2 +- { KEYCODE_KEYPAD, '3', 's' }, // KP_3 +- { KEYCODE_KEYPAD, '4', 't' }, // KP_4 +- { KEYCODE_KEYPAD, '5', 'u' }, // KP_5 +- { KEYCODE_KEYPAD, '6', 'v' }, // KP_6 +- { KEYCODE_KEYPAD, '7', 'w' }, // KP_7 +- { KEYCODE_KEYPAD, '8', 'x' }, // KP_8 +- { KEYCODE_KEYPAD, '9', 'y' }, // KP_9 +- { KEYCODE_KEYPAD, '*', 'j' }, // KP_MULT +- { KEYCODE_KEYPAD, '+', 'k' }, // KP_PLUS +- { KEYCODE_KEYPAD, ',', 'l' }, // KP_COMMA +- { KEYCODE_KEYPAD, '-', 'm' }, // KP_MINUS +- { KEYCODE_KEYPAD, '.', 'n' }, // KP_PERIOD +- { KEYCODE_KEYPAD, '/', 'o' }, // KP_DIVIDE +- { KEYCODE_KEYPAD, '\n', 'M' }, // KP_ENTER +- { KEYCODE_KEYPAD, '=', 'X' }, // KP_EQUAL +-}; +- +-static keycodes_s keycodes_kp_csiu[] = { +- { KEYCODE_KEYPAD, 57399, 'p' }, // KP_0 +- { KEYCODE_KEYPAD, 57400, 'q' }, // KP_1 +- { KEYCODE_KEYPAD, 57401, 'r' }, // KP_2 +- { KEYCODE_KEYPAD, 57402, 's' }, // KP_3 +- { KEYCODE_KEYPAD, 57403, 't' }, // KP_4 +- { KEYCODE_KEYPAD, 57404, 'u' }, // KP_5 +- { KEYCODE_KEYPAD, 57405, 'v' }, // KP_6 +- { KEYCODE_KEYPAD, 57406, 'w' }, // KP_7 +- { KEYCODE_KEYPAD, 57407, 'x' }, // KP_8 +- { KEYCODE_KEYPAD, 57408, 'y' }, // KP_9 +- { KEYCODE_KEYPAD, 57411, 'j' }, // KP_MULT +- { KEYCODE_KEYPAD, 57413, 'k' }, // KP_PLUS +- { KEYCODE_KEYPAD, 57416, 'l' }, // KP_COMMA +- { KEYCODE_KEYPAD, 57412, 'm' }, // KP_MINUS +- { KEYCODE_KEYPAD, 57409, 'n' }, // KP_PERIOD +- { KEYCODE_KEYPAD, 57410, 'o' }, // KP_DIVIDE +- { KEYCODE_KEYPAD, 57414, 'M' }, // KP_ENTER +- { KEYCODE_KEYPAD, 57415, 'X' }, // KP_EQUAL +-}; +- +-void vterm_keyboard_key(VTerm *vt, VTermKey key, VTermModifier mod) +-{ +- if (key == VTERM_KEY_NONE) { +- return; +- } +- +- VTermKeyEncodingFlags flags = vterm_state_get_key_encoding_flags(vt->state); +- +- keycodes_s k; +- if (key < VTERM_KEY_FUNCTION_0) { +- if (key >= sizeof(keycodes)/sizeof(keycodes[0])) { +- return; +- } +- k = keycodes[key]; +- } else if (key >= VTERM_KEY_FUNCTION_0 && key <= VTERM_KEY_FUNCTION_MAX) { +- if ((key - VTERM_KEY_FUNCTION_0) >= sizeof(keycodes_fn)/sizeof(keycodes_fn[0])) { +- return; +- } +- k = keycodes_fn[key - VTERM_KEY_FUNCTION_0]; +- } else if (key >= VTERM_KEY_KP_0) { +- if ((key - VTERM_KEY_KP_0) >= sizeof(keycodes_kp)/sizeof(keycodes_kp[0])) { +- return; +- } +- +- if (flags.disambiguate) { +- k = keycodes_kp_csiu[key - VTERM_KEY_KP_0]; +- } else { +- k = keycodes_kp[key - VTERM_KEY_KP_0]; +- } +- } +- +- switch (k.type) { +- case KEYCODE_NONE: +- break; +- +- case KEYCODE_TAB: +- // Shift-Tab is CSI Z but plain Tab is 0x09 +- if (flags.disambiguate) { +- goto case_LITERAL; +- } else if (mod == VTERM_MOD_SHIFT) { +- vterm_push_output_sprintf_ctrl(vt, C1_CSI, "Z"); +- } else if (mod & VTERM_MOD_SHIFT) { +- vterm_push_output_sprintf_ctrl(vt, C1_CSI, "1;%dZ", mod + 1); +- } else { +- goto case_LITERAL; +- } +- break; +- +- case KEYCODE_ENTER: +- // Enter is CRLF in newline mode, but just LF in linefeed +- if (vt->state->mode.newline) { +- vterm_push_output_sprintf(vt, "\r\n"); +- } else { +- goto case_LITERAL; +- } +- break; +- +- case KEYCODE_LITERAL: +- case_LITERAL: +- if (flags.disambiguate) { +- switch (key) { +- case VTERM_KEY_TAB: +- case VTERM_KEY_ENTER: +- case VTERM_KEY_BACKSPACE: +- // If there are no mods then leave these as-is +- flags.disambiguate = mod != VTERM_MOD_NONE; +- break; +- default: +- break; +- } +- } +- +- if (flags.disambiguate) { +- vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%du", k.literal, mod + 1); +- } else { +- vterm_push_output_sprintf(vt, mod & VTERM_MOD_ALT ? ESC_S "%c" : "%c", k.literal); +- } +- break; +- +- case KEYCODE_SS3: +- case_SS3: +- if (mod == 0) { +- vterm_push_output_sprintf_ctrl(vt, C1_SS3, "%c", k.literal); +- } else { +- goto case_CSI; +- } +- break; +- +- case KEYCODE_CSI: +- case_CSI: +- if (mod == 0) { +- vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%c", k.literal); +- } else { +- vterm_push_output_sprintf_ctrl(vt, C1_CSI, "1;%d%c", mod + 1, k.literal); +- } +- break; +- +- case KEYCODE_CSINUM: +- if (mod == 0) { +- vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d%c", k.csinum, k.literal); +- } else { +- vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%d%c", k.csinum, mod + 1, k.literal); +- } +- break; +- +- case KEYCODE_CSI_CURSOR: +- if (vt->state->mode.cursor) { +- goto case_SS3; +- } else { +- goto case_CSI; +- } +- +- case KEYCODE_KEYPAD: +- if (vt->state->mode.keypad) { +- k.literal = k.csinum; +- goto case_SS3; +- } else { +- goto case_LITERAL; +- } +- } +-} +- +-void vterm_keyboard_start_paste(VTerm *vt) +-{ +- if (vt->state->mode.bracketpaste) { +- vterm_push_output_sprintf_ctrl(vt, C1_CSI, "200~"); +- } +-} +- +-void vterm_keyboard_end_paste(VTerm *vt) +-{ +- if (vt->state->mode.bracketpaste) { +- vterm_push_output_sprintf_ctrl(vt, C1_CSI, "201~"); +- } +-} +diff --git a/src/nvim/vterm/keyboard.h b/src/nvim/vterm/keyboard.h +deleted file mode 100644 +index 77ecfd7e1d3081..00000000000000 +--- a/src/nvim/vterm/keyboard.h ++++ /dev/null +@@ -1,8 +0,0 @@ +-#pragma once +- +-#include +- +-#include "nvim/vterm/vterm_defs.h" +-#include "nvim/vterm/vterm_keycodes_defs.h" +- +-#include "vterm/keyboard.h.generated.h" +diff --git a/src/nvim/vterm/mouse.c b/src/nvim/vterm/mouse.c +deleted file mode 100644 +index 558e67e151b44c..00000000000000 +--- a/src/nvim/vterm/mouse.c ++++ /dev/null +@@ -1,122 +0,0 @@ +-#include "nvim/math.h" +-#include "nvim/tui/termkey/termkey.h" +-#include "nvim/vterm/mouse.h" +-#include "nvim/vterm/vterm.h" +-#include "nvim/vterm/vterm_internal_defs.h" +- +-#include "vterm/mouse.c.generated.h" +- +-static void output_mouse(VTermState *state, int code, int pressed, int modifiers, int col, int row) +-{ +- modifiers <<= 2; +- +- switch (state->mouse_protocol) { +- case MOUSE_X10: +- if (col + 0x21 > 0xff) { +- col = 0xff - 0x21; +- } +- if (row + 0x21 > 0xff) { +- row = 0xff - 0x21; +- } +- +- if (!pressed) { +- code = 3; +- } +- +- if (code & 0x80) { +- break; +- } +- vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "M%c%c%c", +- (code | modifiers) + 0x20, col + 0x21, row + 0x21); +- break; +- +- case MOUSE_UTF8: { +- char utf8[18]; +- size_t len = 0; +- +- if (!pressed) { +- code = 3; +- } +- +- len += (size_t)fill_utf8((code | modifiers) + 0x20, utf8 + len); +- len += (size_t)fill_utf8(col + 0x21, utf8 + len); +- len += (size_t)fill_utf8(row + 0x21, utf8 + len); +- utf8[len] = 0; +- +- vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "M%s", utf8); +- } +- break; +- +- case MOUSE_SGR: +- vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "<%d;%d;%d%c", +- code | modifiers, col + 1, row + 1, pressed ? 'M' : 'm'); +- break; +- +- case MOUSE_RXVT: +- if (!pressed) { +- code = 3; +- } +- +- vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%d;%d;%dM", +- code | modifiers, col + 1, row + 1); +- break; +- } +-} +- +-void vterm_mouse_move(VTerm *vt, int row, int col, VTermModifier mod) +-{ +- VTermState *state = vt->state; +- +- if (col == state->mouse_col && row == state->mouse_row) { +- return; +- } +- +- state->mouse_col = col; +- state->mouse_row = row; +- +- if ((state->mouse_flags & MOUSE_WANT_DRAG && state->mouse_buttons) +- || (state->mouse_flags & MOUSE_WANT_MOVE)) { +- if (state->mouse_buttons) { +- int button = xctz((uint64_t)state->mouse_buttons) + 1; +- if (button < 4) { +- output_mouse(state, button - 1 + 0x20, 1, (int)mod, col, row); +- } else if (button >= 8 && button < 12) { +- output_mouse(state, button - 8 + 0x80 + 0x20, 1, (int)mod, col, row); +- } +- } else { +- output_mouse(state, 3 + 0x20, 1, (int)mod, col, row); +- } +- } +-} +- +-void vterm_mouse_button(VTerm *vt, int button, bool pressed, VTermModifier mod) +-{ +- VTermState *state = vt->state; +- +- int old_buttons = state->mouse_buttons; +- +- if ((button > 0 && button <= 3) || (button >= 8 && button <= 11)) { +- if (pressed) { +- state->mouse_buttons |= (1 << (button - 1)); +- } else { +- state->mouse_buttons &= ~(1 << (button - 1)); +- } +- } +- +- // Most of the time we don't get button releases from 4/5/6/7 +- if (state->mouse_buttons == old_buttons && (button < 4 || button > 7)) { +- return; +- } +- +- if (!state->mouse_flags) { +- return; +- } +- +- if (button < 4) { +- output_mouse(state, button - 1, pressed, (int)mod, state->mouse_col, state->mouse_row); +- } else if (button < 8) { +- output_mouse(state, button - 4 + 0x40, pressed, (int)mod, state->mouse_col, state->mouse_row); +- } else if (button < 12) { +- output_mouse(state, button - 8 + 0x80, pressed, (int)mod, state->mouse_col, state->mouse_row); +- } +-} +diff --git a/src/nvim/vterm/mouse.h b/src/nvim/vterm/mouse.h +deleted file mode 100644 +index fb40d523f0133a..00000000000000 +--- a/src/nvim/vterm/mouse.h ++++ /dev/null +@@ -1,8 +0,0 @@ +-#pragma once +- +-#include +- +-#include "nvim/vterm/vterm_defs.h" +-#include "nvim/vterm/vterm_keycodes_defs.h" +- +-#include "vterm/mouse.h.generated.h" +diff --git a/src/nvim/vterm/parser.c b/src/nvim/vterm/parser.c +deleted file mode 100644 +index 91cf2dc8f89a14..00000000000000 +--- a/src/nvim/vterm/parser.c ++++ /dev/null +@@ -1,414 +0,0 @@ +-#include +-#include +-#include +- +-#include "nvim/vterm/parser.h" +-#include "nvim/vterm/vterm.h" +-#include "nvim/vterm/vterm_internal_defs.h" +- +-#include "vterm/parser.c.generated.h" +- +-#undef DEBUG_PARSER +- +-static bool is_intermed(uint8_t c) +-{ +- return c >= 0x20 && c <= 0x2f; +-} +- +-static void do_control(VTerm *vt, uint8_t control) +-{ +- if (vt->parser.callbacks && vt->parser.callbacks->control) { +- if ((*vt->parser.callbacks->control)(control, vt->parser.cbdata)) { +- return; +- } +- } +- +- DEBUG_LOG("libvterm: Unhandled control 0x%02x\n", control); +-} +- +-static void do_csi(VTerm *vt, char command) +-{ +-#ifdef DEBUG_PARSER +- printf("Parsed CSI args as:\n", arglen, args); +- printf(" leader: %s\n", vt->parser.v.csi.leader); +- for (int argi = 0; argi < vt->parser.v.csi.argi; argi++) { +- printf(" %lu", CSI_ARG(vt->parser.v.csi.args[argi])); +- if (!CSI_ARG_HAS_MORE(vt->parser.v.csi.args[argi])) { +- printf("\n"); +- } +- printf(" intermed: %s\n", vt->parser.intermed); +- } +-#endif +- +- if (vt->parser.callbacks && vt->parser.callbacks->csi) { +- if ((*vt->parser.callbacks->csi)(vt->parser.v.csi.leaderlen ? vt->parser.v.csi.leader : NULL, +- vt->parser.v.csi.args, +- vt->parser.v.csi.argi, +- vt->parser.intermedlen ? vt->parser.intermed : NULL, +- command, +- vt->parser.cbdata)) { +- return; +- } +- } +- +- DEBUG_LOG("libvterm: Unhandled CSI %c\n", command); +-} +- +-static void do_escape(VTerm *vt, char command) +-{ +- char seq[INTERMED_MAX + 1]; +- +- size_t len = (size_t)vt->parser.intermedlen; +- strncpy(seq, vt->parser.intermed, len); // NOLINT(runtime/printf) +- seq[len++] = command; +- seq[len] = 0; +- +- if (vt->parser.callbacks && vt->parser.callbacks->escape) { +- if ((*vt->parser.callbacks->escape)(seq, len, vt->parser.cbdata)) { +- return; +- } +- } +- +- DEBUG_LOG("libvterm: Unhandled escape ESC 0x%02x\n", command); +-} +- +-static void string_fragment(VTerm *vt, const char *str, size_t len, bool final, +- VTermTerminator terminator) +-{ +- VTermStringFragment frag = { +- .str = str, +- .len = len, +- .initial = vt->parser.string_initial, +- .final = final, +- .terminator = terminator, +- }; +- +- switch (vt->parser.state) { +- case OSC: +- if (vt->parser.callbacks && vt->parser.callbacks->osc) { +- (*vt->parser.callbacks->osc)(vt->parser.v.osc.command, frag, vt->parser.cbdata); +- } +- break; +- +- case DCS_VTERM: +- if (vt->parser.callbacks && vt->parser.callbacks->dcs) { +- (*vt->parser.callbacks->dcs)(vt->parser.v.dcs.command, (size_t)vt->parser.v.dcs.commandlen, +- frag, +- vt->parser.cbdata); +- } +- break; +- +- case APC: +- if (vt->parser.callbacks && vt->parser.callbacks->apc) { +- (*vt->parser.callbacks->apc)(frag, vt->parser.cbdata); +- } +- break; +- +- case PM: +- if (vt->parser.callbacks && vt->parser.callbacks->pm) { +- (*vt->parser.callbacks->pm)(frag, vt->parser.cbdata); +- } +- break; +- +- case SOS: +- if (vt->parser.callbacks && vt->parser.callbacks->sos) { +- (*vt->parser.callbacks->sos)(frag, vt->parser.cbdata); +- } +- break; +- +- case NORMAL: +- case CSI_LEADER: +- case CSI_ARGS: +- case CSI_INTERMED: +- case OSC_COMMAND: +- case DCS_COMMAND: +- return; +- } +- +- vt->parser.string_initial = false; +-} +- +-size_t vterm_input_write(VTerm *vt, const char *bytes, size_t len) +-{ +- size_t pos = 0; +- const char *string_start; +- +- switch (vt->parser.state) { +- case NORMAL: +- case CSI_LEADER: +- case CSI_ARGS: +- case CSI_INTERMED: +- case OSC_COMMAND: +- case DCS_COMMAND: +- string_start = NULL; +- break; +- case OSC: +- case DCS_VTERM: +- case APC: +- case PM: +- case SOS: +- string_start = bytes; +- break; +- } +- +-#define ENTER_STATE(st) do { vt->parser.state = st; string_start = NULL; } while (0) +-#define ENTER_NORMAL_STATE() ENTER_STATE(NORMAL) +- +-#define IS_STRING_STATE() (vt->parser.state >= OSC_COMMAND) +- +- for (; pos < len; pos++) { +- uint8_t c = (uint8_t)bytes[pos]; +- bool c1_allowed = !vt->mode.utf8; +- +- if (c == 0x00 || c == 0x7f) { // NUL, DEL +- if (IS_STRING_STATE()) { +- string_fragment(vt, string_start, (size_t)(bytes + pos - string_start), false, +- VTERM_TERMINATOR_ST); +- string_start = bytes + pos + 1; +- } +- if (vt->parser.emit_nul) { +- do_control(vt, c); +- } +- continue; +- } +- if (c == 0x18 || c == 0x1a) { // CAN, SUB +- vt->parser.in_esc = false; +- ENTER_NORMAL_STATE(); +- if (vt->parser.emit_nul) { +- do_control(vt, c); +- } +- continue; +- } else if (c == 0x1b) { // ESC +- vt->parser.intermedlen = 0; +- if (!IS_STRING_STATE()) { +- vt->parser.state = NORMAL; +- } +- vt->parser.in_esc = true; +- continue; +- } else if (c == 0x07 // BEL, can stand for ST in OSC or DCS state +- && IS_STRING_STATE()) {} else if (c < 0x20) { // other C0 +- if (vt->parser.state == SOS) { +- continue; // All other C0s permitted in SOS +- } +- if (IS_STRING_STATE()) { +- string_fragment(vt, string_start, (size_t)(bytes + pos - string_start), false, +- VTERM_TERMINATOR_ST); +- } +- do_control(vt, c); +- if (IS_STRING_STATE()) { +- string_start = bytes + pos + 1; +- } +- continue; +- } +- +- size_t string_len = (size_t)(bytes + pos - string_start); +- +- if (vt->parser.in_esc) { +- // Hoist an ESC letter into a C1 if we're not in a string mode +- // Always accept ESC \ == ST even in string mode +- if (!vt->parser.intermedlen +- && c >= 0x40 && c < 0x60 +- && ((!IS_STRING_STATE() || c == 0x5c))) { +- c += 0x40; +- c1_allowed = true; +- if (string_len) { +- assert(string_len > 0); +- string_len -= 1; +- } +- vt->parser.in_esc = false; +- } else { +- string_start = NULL; +- vt->parser.state = NORMAL; +- } +- } +- +- switch (vt->parser.state) { +- case CSI_LEADER: +- // Extract leader bytes 0x3c to 0x3f +- if (c >= 0x3c && c <= 0x3f) { +- if (vt->parser.v.csi.leaderlen < CSI_LEADER_MAX - 1) { +- vt->parser.v.csi.leader[vt->parser.v.csi.leaderlen++] = (char)c; +- } +- break; +- } +- +- vt->parser.v.csi.leader[vt->parser.v.csi.leaderlen] = 0; +- +- vt->parser.v.csi.argi = 0; +- vt->parser.v.csi.args[0] = CSI_ARG_MISSING; +- vt->parser.state = CSI_ARGS; +- +- FALLTHROUGH; +- case CSI_ARGS: +- // Numerical value of argument +- if (c >= '0' && c <= '9') { +- if (vt->parser.v.csi.args[vt->parser.v.csi.argi] == CSI_ARG_MISSING) { +- vt->parser.v.csi.args[vt->parser.v.csi.argi] = 0; +- } +- vt->parser.v.csi.args[vt->parser.v.csi.argi] *= 10; +- vt->parser.v.csi.args[vt->parser.v.csi.argi] += c - '0'; +- break; +- } +- if (c == ':') { +- vt->parser.v.csi.args[vt->parser.v.csi.argi] |= CSI_ARG_FLAG_MORE; +- c = ';'; +- } +- if (c == ';') { +- vt->parser.v.csi.argi++; +- vt->parser.v.csi.args[vt->parser.v.csi.argi] = CSI_ARG_MISSING; +- break; +- } +- +- vt->parser.v.csi.argi++; +- vt->parser.intermedlen = 0; +- vt->parser.state = CSI_INTERMED; +- FALLTHROUGH; +- case CSI_INTERMED: +- if (is_intermed(c)) { +- if (vt->parser.intermedlen < INTERMED_MAX - 1) { +- vt->parser.intermed[vt->parser.intermedlen++] = (char)c; +- } +- break; +- } else if (c == 0x1b) { +- // ESC in CSI cancels +- } else if (c >= 0x40 && c <= 0x7e) { +- vt->parser.intermed[vt->parser.intermedlen] = 0; +- do_csi(vt, (char)c); +- } +- // else was invalid CSI +- +- ENTER_NORMAL_STATE(); +- break; +- +- case OSC_COMMAND: +- // Numerical value of command +- if (c >= '0' && c <= '9') { +- if (vt->parser.v.osc.command == -1) { +- vt->parser.v.osc.command = 0; +- } else { +- vt->parser.v.osc.command *= 10; +- } +- vt->parser.v.osc.command += c - '0'; +- break; +- } +- if (c == ';') { +- vt->parser.state = OSC; +- string_start = bytes + pos + 1; +- break; +- } +- +- string_start = bytes + pos; +- string_len = 0; +- vt->parser.state = OSC; +- goto string_state; +- +- case DCS_COMMAND: +- if (vt->parser.v.dcs.commandlen < CSI_LEADER_MAX) { +- vt->parser.v.dcs.command[vt->parser.v.dcs.commandlen++] = (char)c; +- } +- +- if (c >= 0x40 && c <= 0x7e) { +- string_start = bytes + pos + 1; +- vt->parser.state = DCS_VTERM; +- } +- break; +- +-string_state: +- case OSC: +- case DCS_VTERM: +- case APC: +- case PM: +- case SOS: +- if (c == 0x07 || (c1_allowed && c == 0x9c)) { +- string_fragment(vt, string_start, string_len, true, +- c == 0x07 ? VTERM_TERMINATOR_BEL : VTERM_TERMINATOR_ST); +- ENTER_NORMAL_STATE(); +- } +- break; +- +- case NORMAL: +- if (vt->parser.in_esc) { +- if (is_intermed(c)) { +- if (vt->parser.intermedlen < INTERMED_MAX - 1) { +- vt->parser.intermed[vt->parser.intermedlen++] = (char)c; +- } +- } else if (c >= 0x30 && c < 0x7f) { +- do_escape(vt, (char)c); +- vt->parser.in_esc = 0; +- ENTER_NORMAL_STATE(); +- } else { +- DEBUG_LOG("TODO: Unhandled byte %02x in Escape\n", c); +- } +- break; +- } +- if (c1_allowed && c >= 0x80 && c < 0xa0) { +- switch (c) { +- case 0x90: // DCS +- vt->parser.string_initial = true; +- vt->parser.v.dcs.commandlen = 0; +- ENTER_STATE(DCS_COMMAND); +- break; +- case 0x98: // SOS +- vt->parser.string_initial = true; +- ENTER_STATE(SOS); +- string_start = bytes + pos + 1; +- break; +- case 0x9b: // CSI +- vt->parser.v.csi.leaderlen = 0; +- ENTER_STATE(CSI_LEADER); +- break; +- case 0x9d: // OSC +- vt->parser.v.osc.command = -1; +- vt->parser.string_initial = true; +- ENTER_STATE(OSC_COMMAND); +- break; +- case 0x9e: // PM +- vt->parser.string_initial = true; +- ENTER_STATE(PM); +- string_start = bytes + pos + 1; +- break; +- case 0x9f: // APC +- vt->parser.string_initial = true; +- ENTER_STATE(APC); +- string_start = bytes + pos + 1; +- break; +- default: +- do_control(vt, c); +- break; +- } +- } else { +- size_t eaten = 0; +- if (vt->parser.callbacks && vt->parser.callbacks->text) { +- eaten = (size_t)(*vt->parser.callbacks->text)(bytes + pos, len - pos, vt->parser.cbdata); +- } +- +- if (!eaten) { +- DEBUG_LOG("libvterm: Text callback did not consume any input\n"); +- // force it to make progress +- eaten = 1; +- } +- +- pos += (eaten - 1); // we'll ++ it again in a moment +- } +- break; +- } +- } +- +- if (string_start) { +- size_t string_len = (size_t)(bytes + pos - string_start); +- if (string_len > 0) { +- if (vt->parser.in_esc) { +- string_len -= 1; +- } +- string_fragment(vt, string_start, string_len, false, VTERM_TERMINATOR_ST); +- } +- } +- +- return len; +-} +- +-void vterm_parser_set_callbacks(VTerm *vt, const VTermParserCallbacks *callbacks, void *user) +-{ +- vt->parser.callbacks = callbacks; +- vt->parser.cbdata = user; +-} +diff --git a/src/nvim/vterm/parser.h b/src/nvim/vterm/parser.h +deleted file mode 100644 +index 6532504efe714b..00000000000000 +--- a/src/nvim/vterm/parser.h ++++ /dev/null +@@ -1,7 +0,0 @@ +-#pragma once +- +-#include +- +-#include "nvim/vterm/vterm_defs.h" +- +-#include "vterm/parser.h.generated.h" +diff --git a/src/nvim/vterm/pen.c b/src/nvim/vterm/pen.c +deleted file mode 100644 +index f2054633d57ea9..00000000000000 +--- a/src/nvim/vterm/pen.c ++++ /dev/null +@@ -1,677 +0,0 @@ +-#include +- +-#include "nvim/vterm/pen.h" +-#include "nvim/vterm/vterm.h" +-#include "nvim/vterm/vterm_internal_defs.h" +- +-#include "vterm/pen.c.generated.h" +- +-// Structure used to store RGB triples without the additional metadata stored in VTermColor. +-typedef struct { +- uint8_t red, green, blue; +-} VTermRGB; +- +-static const VTermRGB ansi_colors[] = { +- // R G B +- { 0, 0, 0 }, // black +- { 224, 0, 0 }, // red +- { 0, 224, 0 }, // green +- { 224, 224, 0 }, // yellow +- { 0, 0, 224 }, // blue +- { 224, 0, 224 }, // magenta +- { 0, 224, 224 }, // cyan +- { 224, 224, 224 }, // white == light grey +- +- // high intensity +- { 128, 128, 128 }, // black +- { 255, 64, 64 }, // red +- { 64, 255, 64 }, // green +- { 255, 255, 64 }, // yellow +- { 64, 64, 255 }, // blue +- { 255, 64, 255 }, // magenta +- { 64, 255, 255 }, // cyan +- { 255, 255, 255 }, // white for real +-}; +- +-static uint8_t ramp6[] = { +- 0x00, 0x33, 0x66, 0x99, 0xCC, 0xFF, +-}; +- +-static uint8_t ramp24[] = { +- 0x00, 0x0B, 0x16, 0x21, 0x2C, 0x37, 0x42, 0x4D, 0x58, 0x63, 0x6E, 0x79, +- 0x85, 0x90, 0x9B, 0xA6, 0xB1, 0xBC, 0xC7, 0xD2, 0xDD, 0xE8, 0xF3, 0xFF, +-}; +- +-static void lookup_default_colour_ansi(long idx, VTermColor *col) +-{ +- if (idx >= 0 && idx < 16) { +- vterm_color_rgb(col, +- ansi_colors[idx].red, ansi_colors[idx].green, ansi_colors[idx].blue); +- } +-} +- +-static bool lookup_colour_ansi(const VTermState *state, long index, VTermColor *col) +-{ +- if (index >= 0 && index < 16) { +- *col = state->colors[index]; +- return true; +- } +- +- return false; +-} +- +-static bool lookup_colour_palette(const VTermState *state, long index, VTermColor *col) +-{ +- if (index >= 0 && index < 16) { +- // Normal 8 colours or high intensity - parse as palette 0 +- return lookup_colour_ansi(state, index, col); +- } else if (index >= 16 && index < 232) { +- // 216-colour cube +- index -= 16; +- +- vterm_color_rgb(col, ramp6[index/6/6 % 6], +- ramp6[index/6 % 6], +- ramp6[index % 6]); +- +- return true; +- } else if (index >= 232 && index < 256) { +- // 24 greyscales +- index -= 232; +- +- vterm_color_rgb(col, ramp24[index], ramp24[index], ramp24[index]); +- +- return true; +- } +- +- return false; +-} +- +-static int lookup_colour(const VTermState *state, int palette, const long args[], int argcount, +- VTermColor *col) +-{ +- switch (palette) { +- case 2: // RGB mode - 3 args contain colour values directly +- if (argcount < 3) { +- return argcount; +- } +- +- vterm_color_rgb(col, (uint8_t)CSI_ARG(args[0]), (uint8_t)CSI_ARG(args[1]), +- (uint8_t)CSI_ARG(args[2])); +- +- return 3; +- +- case 5: // XTerm 256-colour mode +- if (!argcount || CSI_ARG_IS_MISSING(args[0])) { +- return argcount ? 1 : 0; +- } +- +- vterm_color_indexed(col, (uint8_t)args[0]); +- +- return argcount ? 1 : 0; +- +- default: +- DEBUG_LOG("Unrecognised colour palette %d\n", palette); +- return 0; +- } +-} +- +-// Some conveniences +- +-static void setpenattr(VTermState *state, VTermAttr attr, VTermValueType type, VTermValue *val) +-{ +-#ifdef DEBUG +- if (type != vterm_get_attr_type(attr)) { +- DEBUG_LOG("Cannot set attr %d as it has type %d, not type %d\n", +- attr, vterm_get_attr_type(attr), type); +- return; +- } +-#endif +- if (state->callbacks && state->callbacks->setpenattr) { +- (*state->callbacks->setpenattr)(attr, val, state->cbdata); +- } +-} +- +-static void setpenattr_bool(VTermState *state, VTermAttr attr, int boolean) +-{ +- VTermValue val = { .boolean = boolean }; +- setpenattr(state, attr, VTERM_VALUETYPE_BOOL, &val); +-} +- +-static void setpenattr_int(VTermState *state, VTermAttr attr, int number) +-{ +- VTermValue val = { .number = number }; +- setpenattr(state, attr, VTERM_VALUETYPE_INT, &val); +-} +- +-static void setpenattr_col(VTermState *state, VTermAttr attr, VTermColor color) +-{ +- VTermValue val = { .color = color }; +- setpenattr(state, attr, VTERM_VALUETYPE_COLOR, &val); +-} +- +-static void set_pen_col_ansi(VTermState *state, VTermAttr attr, long col) +-{ +- VTermColor *colp = (attr == VTERM_ATTR_BACKGROUND) ? &state->pen.bg : &state->pen.fg; +- +- vterm_color_indexed(colp, (uint8_t)col); +- +- setpenattr_col(state, attr, *colp); +-} +- +-void vterm_state_newpen(VTermState *state) +-{ +- // 90% grey so that pure white is brighter +- vterm_color_rgb(&state->default_fg, 240, 240, 240); +- vterm_color_rgb(&state->default_bg, 0, 0, 0); +- vterm_state_set_default_colors(state, &state->default_fg, &state->default_bg); +- +- for (int col = 0; col < 16; col++) { +- lookup_default_colour_ansi(col, &state->colors[col]); +- } +-} +- +-void vterm_state_resetpen(VTermState *state) +-{ +- state->pen.bold = 0; setpenattr_bool(state, VTERM_ATTR_BOLD, 0); +- state->pen.underline = 0; setpenattr_int(state, VTERM_ATTR_UNDERLINE, 0); +- state->pen.italic = 0; setpenattr_bool(state, VTERM_ATTR_ITALIC, 0); +- state->pen.blink = 0; setpenattr_bool(state, VTERM_ATTR_BLINK, 0); +- state->pen.reverse = 0; setpenattr_bool(state, VTERM_ATTR_REVERSE, 0); +- state->pen.conceal = 0; setpenattr_bool(state, VTERM_ATTR_CONCEAL, 0); +- state->pen.strike = 0; setpenattr_bool(state, VTERM_ATTR_STRIKE, 0); +- state->pen.font = 0; setpenattr_int(state, VTERM_ATTR_FONT, 0); +- state->pen.small = 0; setpenattr_bool(state, VTERM_ATTR_SMALL, 0); +- state->pen.baseline = 0; setpenattr_int(state, VTERM_ATTR_BASELINE, 0); +- state->pen.dim = 0; setpenattr_bool(state, VTERM_ATTR_DIM, 0); +- state->pen.overline = 0; setpenattr_bool(state, VTERM_ATTR_OVERLINE, 0); +- +- state->pen.fg = state->default_fg; +- setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->default_fg); +- state->pen.bg = state->default_bg; +- setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->default_bg); +- +- state->pen.uri = 0; setpenattr_int(state, VTERM_ATTR_URI, 0); +-} +- +-void vterm_state_savepen(VTermState *state, int save) +-{ +- if (save) { +- state->saved.pen = state->pen; +- } else { +- state->pen = state->saved.pen; +- +- setpenattr_bool(state, VTERM_ATTR_BOLD, state->pen.bold); +- setpenattr_int(state, VTERM_ATTR_UNDERLINE, state->pen.underline); +- setpenattr_bool(state, VTERM_ATTR_ITALIC, state->pen.italic); +- setpenattr_bool(state, VTERM_ATTR_BLINK, state->pen.blink); +- setpenattr_bool(state, VTERM_ATTR_REVERSE, state->pen.reverse); +- setpenattr_bool(state, VTERM_ATTR_CONCEAL, state->pen.conceal); +- setpenattr_bool(state, VTERM_ATTR_STRIKE, state->pen.strike); +- setpenattr_int(state, VTERM_ATTR_FONT, state->pen.font); +- setpenattr_bool(state, VTERM_ATTR_SMALL, state->pen.small); +- setpenattr_int(state, VTERM_ATTR_BASELINE, state->pen.baseline); +- setpenattr_bool(state, VTERM_ATTR_DIM, state->pen.dim); +- setpenattr_bool(state, VTERM_ATTR_OVERLINE, state->pen.overline); +- +- setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->pen.fg); +- setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->pen.bg); +- +- setpenattr_int(state, VTERM_ATTR_URI, state->pen.uri); +- } +-} +- +-void vterm_state_set_default_colors(VTermState *state, const VTermColor *default_fg, +- const VTermColor *default_bg) +-{ +- if (default_fg) { +- state->default_fg = *default_fg; +- state->default_fg.type = (state->default_fg.type & ~VTERM_COLOR_DEFAULT_MASK) +- | VTERM_COLOR_DEFAULT_FG; +- } +- +- if (default_bg) { +- state->default_bg = *default_bg; +- state->default_bg.type = (state->default_bg.type & ~VTERM_COLOR_DEFAULT_MASK) +- | VTERM_COLOR_DEFAULT_BG; +- } +-} +- +-void vterm_state_set_palette_color(VTermState *state, int index, const VTermColor *col) +-{ +- if (index >= 0 && index < 16) { +- state->colors[index] = *col; +- } +-} +- +-/// Makes sure that the given color `col` is indeed an RGB colour. After this +-/// function returns, VTERM_COLOR_IS_RGB(col) will return true, while all other +-/// flags stored in `col->type` will have been reset. +-/// +-/// @param state is the VTermState instance from which the colour palette should +-/// be extracted. +-/// @param col is a pointer at the VTermColor instance that should be converted +-/// to an RGB colour. +-void vterm_state_convert_color_to_rgb(const VTermState *state, VTermColor *col) +-{ +- if (VTERM_COLOR_IS_INDEXED(col)) { // Convert indexed colors to RGB +- lookup_colour_palette(state, col->indexed.idx, col); +- } +- col->type &= VTERM_COLOR_TYPE_MASK; // Reset any metadata but the type +-} +- +-void vterm_state_setpen(VTermState *state, const long args[], int argcount) +-{ +- // SGR - ECMA-48 8.3.117 +- +- int argi = 0; +- int value; +- +- while (argi < argcount) { +- // This logic is easier to do 'done' backwards; set it true, and make it +- // false again in the 'default' case +- int done = 1; +- +- long arg; +- switch (arg = CSI_ARG(args[argi])) { +- case CSI_ARG_MISSING: +- case 0: // Reset +- vterm_state_resetpen(state); +- break; +- +- case 1: { // Bold on +- const VTermColor *fg = &state->pen.fg; +- state->pen.bold = 1; +- setpenattr_bool(state, VTERM_ATTR_BOLD, 1); +- if (!VTERM_COLOR_IS_DEFAULT_FG(fg) && VTERM_COLOR_IS_INDEXED(fg) && fg->indexed.idx < 8 +- && state->bold_is_highbright) { +- set_pen_col_ansi(state, VTERM_ATTR_FOREGROUND, fg->indexed.idx + (state->pen.bold ? 8 : 0)); +- } +- break; +- } +- +- case 2: // Dim/faint on +- state->pen.dim = 1; +- setpenattr_bool(state, VTERM_ATTR_DIM, 1); +- break; +- +- case 3: // Italic on +- state->pen.italic = 1; +- setpenattr_bool(state, VTERM_ATTR_ITALIC, 1); +- break; +- +- case 4: // Underline +- state->pen.underline = VTERM_UNDERLINE_SINGLE; +- if (CSI_ARG_HAS_MORE(args[argi])) { +- argi++; +- switch (CSI_ARG(args[argi])) { +- case 0: +- state->pen.underline = 0; +- break; +- case 1: +- state->pen.underline = VTERM_UNDERLINE_SINGLE; +- break; +- case 2: +- state->pen.underline = VTERM_UNDERLINE_DOUBLE; +- break; +- case 3: +- state->pen.underline = VTERM_UNDERLINE_CURLY; +- break; +- } +- } +- setpenattr_int(state, VTERM_ATTR_UNDERLINE, state->pen.underline); +- break; +- +- case 5: // Blink +- state->pen.blink = 1; +- setpenattr_bool(state, VTERM_ATTR_BLINK, 1); +- break; +- +- case 7: // Reverse on +- state->pen.reverse = 1; +- setpenattr_bool(state, VTERM_ATTR_REVERSE, 1); +- break; +- +- case 8: // Conceal on +- state->pen.conceal = 1; +- setpenattr_bool(state, VTERM_ATTR_CONCEAL, 1); +- break; +- +- case 9: // Strikethrough on +- state->pen.strike = 1; +- setpenattr_bool(state, VTERM_ATTR_STRIKE, 1); +- break; +- +- case 10: +- case 11: +- case 12: +- case 13: +- case 14: +- case 15: +- case 16: +- case 17: +- case 18: +- case 19: // Select font +- state->pen.font = CSI_ARG(args[argi]) - 10; +- setpenattr_int(state, VTERM_ATTR_FONT, state->pen.font); +- break; +- +- case 21: // Underline double +- state->pen.underline = VTERM_UNDERLINE_DOUBLE; +- setpenattr_int(state, VTERM_ATTR_UNDERLINE, state->pen.underline); +- break; +- +- case 22: // Normal intensity (bold and dim off) +- state->pen.bold = 0; +- setpenattr_bool(state, VTERM_ATTR_BOLD, 0); +- state->pen.dim = 0; +- setpenattr_bool(state, VTERM_ATTR_DIM, 0); +- break; +- +- case 23: // Italic and Gothic (currently unsupported) off +- state->pen.italic = 0; +- setpenattr_bool(state, VTERM_ATTR_ITALIC, 0); +- break; +- +- case 24: // Underline off +- state->pen.underline = 0; +- setpenattr_int(state, VTERM_ATTR_UNDERLINE, 0); +- break; +- +- case 25: // Blink off +- state->pen.blink = 0; +- setpenattr_bool(state, VTERM_ATTR_BLINK, 0); +- break; +- +- case 27: // Reverse off +- state->pen.reverse = 0; +- setpenattr_bool(state, VTERM_ATTR_REVERSE, 0); +- break; +- +- case 28: // Conceal off (Reveal) +- state->pen.conceal = 0; +- setpenattr_bool(state, VTERM_ATTR_CONCEAL, 0); +- break; +- +- case 29: // Strikethrough off +- state->pen.strike = 0; +- setpenattr_bool(state, VTERM_ATTR_STRIKE, 0); +- break; +- +- case 30: +- case 31: +- case 32: +- case 33: +- case 34: +- case 35: +- case 36: +- case 37: // Foreground colour palette +- value = CSI_ARG(args[argi]) - 30; +- if (state->pen.bold && state->bold_is_highbright) { +- value += 8; +- } +- set_pen_col_ansi(state, VTERM_ATTR_FOREGROUND, value); +- break; +- +- case 38: // Foreground colour alternative palette +- if (argcount - argi < 1) { +- return; +- } +- argi += 1 + lookup_colour(state, CSI_ARG(args[argi + 1]), args + argi + 2, +- argcount - argi - 2, &state->pen.fg); +- setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->pen.fg); +- break; +- +- case 39: // Foreground colour default +- state->pen.fg = state->default_fg; +- setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->pen.fg); +- break; +- +- case 40: +- case 41: +- case 42: +- case 43: +- case 44: +- case 45: +- case 46: +- case 47: // Background colour palette +- value = CSI_ARG(args[argi]) - 40; +- set_pen_col_ansi(state, VTERM_ATTR_BACKGROUND, value); +- break; +- +- case 48: // Background colour alternative palette +- if (argcount - argi < 1) { +- return; +- } +- argi += 1 + lookup_colour(state, CSI_ARG(args[argi + 1]), args + argi + 2, +- argcount - argi - 2, &state->pen.bg); +- setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->pen.bg); +- break; +- +- case 49: // Default background +- state->pen.bg = state->default_bg; +- setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->pen.bg); +- break; +- +- case 53: // Overline on +- state->pen.overline = 1; +- setpenattr_bool(state, VTERM_ATTR_OVERLINE, 1); +- break; +- +- case 55: // Overline off +- state->pen.overline = 0; +- setpenattr_bool(state, VTERM_ATTR_OVERLINE, 0); +- break; +- +- case 73: // Superscript +- case 74: // Subscript +- case 75: // Superscript/subscript off +- state->pen.small = (arg != 75); +- state->pen.baseline = +- (arg == 73) ? VTERM_BASELINE_RAISE +- : (arg == 74) ? VTERM_BASELINE_LOWER +- : VTERM_BASELINE_NORMAL; +- setpenattr_bool(state, VTERM_ATTR_SMALL, state->pen.small); +- setpenattr_int(state, VTERM_ATTR_BASELINE, state->pen.baseline); +- break; +- +- case 90: +- case 91: +- case 92: +- case 93: +- case 94: +- case 95: +- case 96: +- case 97: // Foreground colour high-intensity palette +- value = CSI_ARG(args[argi]) - 90 + 8; +- set_pen_col_ansi(state, VTERM_ATTR_FOREGROUND, value); +- break; +- +- case 100: +- case 101: +- case 102: +- case 103: +- case 104: +- case 105: +- case 106: +- case 107: // Background colour high-intensity palette +- value = CSI_ARG(args[argi]) - 100 + 8; +- set_pen_col_ansi(state, VTERM_ATTR_BACKGROUND, value); +- break; +- +- default: +- done = 0; +- break; +- } +- +- if (!done) { +- DEBUG_LOG("libvterm: Unhandled CSI SGR %ld\n", arg); +- } +- +- while (CSI_ARG_HAS_MORE(args[argi++])) {} +- } +-} +- +-static int vterm_state_getpen_color(const VTermColor *col, int argi, long args[], int fg) +-{ +- // Do nothing if the given color is the default color +- if ((fg && VTERM_COLOR_IS_DEFAULT_FG(col)) +- || (!fg && VTERM_COLOR_IS_DEFAULT_BG(col))) { +- return argi; +- } +- +- // Decide whether to send an indexed color or an RGB color +- if (VTERM_COLOR_IS_INDEXED(col)) { +- const uint8_t idx = col->indexed.idx; +- if (idx < 8) { +- args[argi++] = (idx + (fg ? 30 : 40)); +- } else if (idx < 16) { +- args[argi++] = (idx - 8 + (fg ? 90 : 100)); +- } else { +- args[argi++] = CSI_ARG_FLAG_MORE | (fg ? 38 : 48); +- args[argi++] = CSI_ARG_FLAG_MORE | 5; +- args[argi++] = idx; +- } +- } else if (VTERM_COLOR_IS_RGB(col)) { +- args[argi++] = CSI_ARG_FLAG_MORE | (fg ? 38 : 48); +- args[argi++] = CSI_ARG_FLAG_MORE | 2; +- args[argi++] = CSI_ARG_FLAG_MORE | col->rgb.red; +- args[argi++] = CSI_ARG_FLAG_MORE | col->rgb.green; +- args[argi++] = col->rgb.blue; +- } +- return argi; +-} +- +-int vterm_state_getpen(VTermState *state, long args[], int argcount) +-{ +- int argi = 0; +- +- if (state->pen.bold) { +- args[argi++] = 1; +- } +- +- if (state->pen.dim) { +- args[argi++] = 2; +- } +- +- if (state->pen.italic) { +- args[argi++] = 3; +- } +- +- if (state->pen.underline == VTERM_UNDERLINE_SINGLE) { +- args[argi++] = 4; +- } +- if (state->pen.underline == VTERM_UNDERLINE_CURLY) { +- args[argi++] = 4 | CSI_ARG_FLAG_MORE, args[argi++] = 3; +- } +- +- if (state->pen.blink) { +- args[argi++] = 5; +- } +- +- if (state->pen.reverse) { +- args[argi++] = 7; +- } +- +- if (state->pen.conceal) { +- args[argi++] = 8; +- } +- +- if (state->pen.strike) { +- args[argi++] = 9; +- } +- +- if (state->pen.font) { +- args[argi++] = 10 + state->pen.font; +- } +- +- if (state->pen.underline == VTERM_UNDERLINE_DOUBLE) { +- args[argi++] = 21; +- } +- +- argi = vterm_state_getpen_color(&state->pen.fg, argi, args, true); +- +- argi = vterm_state_getpen_color(&state->pen.bg, argi, args, false); +- +- if (state->pen.overline) { +- args[argi++] = 53; +- } +- +- if (state->pen.small) { +- if (state->pen.baseline == VTERM_BASELINE_RAISE) { +- args[argi++] = 73; +- } else if (state->pen.baseline == VTERM_BASELINE_LOWER) { +- args[argi++] = 74; +- } +- } +- +- return argi; +-} +- +-int vterm_state_set_penattr(VTermState *state, VTermAttr attr, VTermValueType type, VTermValue *val) +-{ +- if (!val) { +- return 0; +- } +- +- if (type != vterm_get_attr_type(attr)) { +- DEBUG_LOG("Cannot set attr %d as it has type %d, not type %d\n", +- attr, vterm_get_attr_type(attr), type); +- return 0; +- } +- +- switch (attr) { +- case VTERM_ATTR_BOLD: +- state->pen.bold = (unsigned)val->boolean; +- break; +- case VTERM_ATTR_UNDERLINE: +- state->pen.underline = (unsigned)val->number; +- break; +- case VTERM_ATTR_ITALIC: +- state->pen.italic = (unsigned)val->boolean; +- break; +- case VTERM_ATTR_BLINK: +- state->pen.blink = (unsigned)val->boolean; +- break; +- case VTERM_ATTR_REVERSE: +- state->pen.reverse = (unsigned)val->boolean; +- break; +- case VTERM_ATTR_CONCEAL: +- state->pen.conceal = (unsigned)val->boolean; +- break; +- case VTERM_ATTR_STRIKE: +- state->pen.strike = (unsigned)val->boolean; +- break; +- case VTERM_ATTR_FONT: +- state->pen.font = (unsigned)val->number; +- break; +- case VTERM_ATTR_FOREGROUND: +- state->pen.fg = val->color; +- break; +- case VTERM_ATTR_BACKGROUND: +- state->pen.bg = val->color; +- break; +- case VTERM_ATTR_SMALL: +- state->pen.small = (unsigned)val->boolean; +- break; +- case VTERM_ATTR_BASELINE: +- state->pen.baseline = (unsigned)val->number; +- break; +- case VTERM_ATTR_URI: +- state->pen.uri = val->number; +- break; +- case VTERM_ATTR_DIM: +- state->pen.dim = (unsigned)val->boolean; +- break; +- case VTERM_ATTR_OVERLINE: +- state->pen.overline = (unsigned)val->boolean; +- break; +- default: +- return 0; +- } +- +- if (state->callbacks && state->callbacks->setpenattr) { +- (*state->callbacks->setpenattr)(attr, val, state->cbdata); +- } +- +- return 1; +-} +diff --git a/src/nvim/vterm/pen.h b/src/nvim/vterm/pen.h +deleted file mode 100644 +index 54269d0f9815f6..00000000000000 +--- a/src/nvim/vterm/pen.h ++++ /dev/null +@@ -1,7 +0,0 @@ +-#pragma once +- +-#include +- +-#include "nvim/vterm/vterm_defs.h" +- +-#include "vterm/pen.h.generated.h" +diff --git a/src/nvim/vterm/screen.c b/src/nvim/vterm/screen.c +deleted file mode 100644 +index 4e8de114282f9c..00000000000000 +--- a/src/nvim/vterm/screen.c ++++ /dev/null +@@ -1,1125 +0,0 @@ +-#include +-#include +- +-#include "nvim/grid.h" +-#include "nvim/mbyte.h" +-#include "nvim/tui/termkey/termkey.h" +-#include "nvim/vterm/pen.h" +-#include "nvim/vterm/screen.h" +-#include "nvim/vterm/state.h" +-#include "nvim/vterm/vterm.h" +-#include "nvim/vterm/vterm_defs.h" +-#include "nvim/vterm/vterm_internal_defs.h" +- +-#include "vterm/screen.c.generated.h" +- +-#define UNICODE_SPACE 0x20 +-#define UNICODE_LINEFEED 0x0a +- +-#undef DEBUG_REFLOW +- +-static inline void clearcell(const VTermScreen *screen, ScreenCell *cell) +-{ +- cell->schar = 0; +- cell->pen = screen->pen; +-} +- +-ScreenCell *getcell(const VTermScreen *screen, int row, int col) +-{ +- if (row < 0 || row >= screen->rows) { +- return NULL; +- } +- if (col < 0 || col >= screen->cols) { +- return NULL; +- } +- return screen->buffer + (screen->cols * row) + col; +-} +- +-static ScreenCell *alloc_buffer(VTermScreen *screen, int rows, int cols) +-{ +- ScreenCell *new_buffer = vterm_allocator_malloc(screen->vt, +- sizeof(ScreenCell) * (size_t)rows * (size_t)cols); +- +- for (int row = 0; row < rows; row++) { +- for (int col = 0; col < cols; col++) { +- clearcell(screen, &new_buffer[row * cols + col]); +- } +- } +- +- return new_buffer; +-} +- +-static void damagerect(VTermScreen *screen, VTermRect rect) +-{ +- VTermRect emit; +- +- switch (screen->damage_merge) { +- case VTERM_DAMAGE_CELL: +- // Always emit damage event +- emit = rect; +- break; +- +- case VTERM_DAMAGE_ROW: +- // Emit damage longer than one row. Try to merge with existing damage in the same row +- if (rect.end_row > rect.start_row + 1) { +- // Bigger than 1 line - flush existing, emit this +- vterm_screen_flush_damage(screen); +- emit = rect; +- } else if (screen->damaged.start_row == -1) { +- // None stored yet +- screen->damaged = rect; +- return; +- } else if (rect.start_row == screen->damaged.start_row) { +- // Merge with the stored line +- if (screen->damaged.start_col > rect.start_col) { +- screen->damaged.start_col = rect.start_col; +- } +- if (screen->damaged.end_col < rect.end_col) { +- screen->damaged.end_col = rect.end_col; +- } +- return; +- } else { +- // Emit the currently stored line, store a new one +- emit = screen->damaged; +- screen->damaged = rect; +- } +- break; +- +- case VTERM_DAMAGE_SCREEN: +- case VTERM_DAMAGE_SCROLL: +- // Never emit damage event +- if (screen->damaged.start_row == -1) { +- screen->damaged = rect; +- } else { +- rect_expand(&screen->damaged, &rect); +- } +- return; +- +- default: +- DEBUG_LOG("TODO: Maybe merge damage for level %d\n", screen->damage_merge); +- return; +- } +- +- if (screen->callbacks && screen->callbacks->damage) { +- (*screen->callbacks->damage)(emit, screen->cbdata); +- } +-} +- +-static void damagescreen(VTermScreen *screen) +-{ +- VTermRect rect = { +- .start_row = 0, +- .end_row = screen->rows, +- .start_col = 0, +- .end_col = screen->cols, +- }; +- +- damagerect(screen, rect); +-} +- +-static int putglyph(VTermGlyphInfo *info, VTermPos pos, void *user) +-{ +- VTermScreen *screen = user; +- ScreenCell *cell = getcell(screen, pos.row, pos.col); +- +- if (!cell) { +- return 0; +- } +- +- cell->schar = info->schar; +- if (info->schar != 0) { +- cell->pen = screen->pen; +- } +- +- for (int col = 1; col < info->width; col++) { +- getcell(screen, pos.row, pos.col + col)->schar = (uint32_t)-1; +- } +- +- VTermRect rect = { +- .start_row = pos.row, +- .end_row = pos.row + 1, +- .start_col = pos.col, +- .end_col = pos.col + info->width, +- }; +- +- cell->pen.protected_cell = info->protected_cell; +- cell->pen.dwl = info->dwl; +- cell->pen.dhl = info->dhl; +- +- damagerect(screen, rect); +- +- return 1; +-} +- +-static void sb_pushline_from_row(VTermScreen *screen, int row) +-{ +- VTermPos pos = { .row = row }; +- for (pos.col = 0; pos.col < screen->cols; pos.col++) { +- vterm_screen_get_cell(screen, pos, screen->sb_buffer + pos.col); +- } +- +- (screen->callbacks->sb_pushline)(screen->cols, screen->sb_buffer, screen->cbdata); +-} +- +-static int moverect_internal(VTermRect dest, VTermRect src, void *user) +-{ +- VTermScreen *screen = user; +- +- if (screen->callbacks && screen->callbacks->sb_pushline +- && dest.start_row == 0 && dest.start_col == 0 // starts top-left corner +- && dest.end_col == screen->cols // full width +- && screen->buffer == screen->buffers[BUFIDX_PRIMARY]) { // not altscreen +- for (int row = 0; row < src.start_row; row++) { +- sb_pushline_from_row(screen, row); +- } +- } +- +- int cols = src.end_col - src.start_col; +- int downward = src.start_row - dest.start_row; +- +- int init_row, test_row, inc_row; +- if (downward < 0) { +- init_row = dest.end_row - 1; +- test_row = dest.start_row - 1; +- inc_row = -1; +- } else { +- init_row = dest.start_row; +- test_row = dest.end_row; +- inc_row = +1; +- } +- +- for (int row = init_row; row != test_row; row += inc_row) { +- memmove(getcell(screen, row, dest.start_col), +- getcell(screen, row + downward, src.start_col), +- (size_t)cols * sizeof(ScreenCell)); +- } +- +- return 1; +-} +- +-static int moverect_user(VTermRect dest, VTermRect src, void *user) +-{ +- VTermScreen *screen = user; +- +- if (screen->callbacks && screen->callbacks->moverect) { +- if (screen->damage_merge != VTERM_DAMAGE_SCROLL) { +- // Avoid an infinite loop +- vterm_screen_flush_damage(screen); +- } +- +- if ((*screen->callbacks->moverect)(dest, src, screen->cbdata)) { +- return 1; +- } +- } +- +- damagerect(screen, dest); +- +- return 1; +-} +- +-static int erase_internal(VTermRect rect, int selective, void *user) +-{ +- VTermScreen *screen = user; +- +- for (int row = rect.start_row; row < screen->state->rows && row < rect.end_row; row++) { +- const VTermLineInfo *info = vterm_state_get_lineinfo(screen->state, row); +- +- for (int col = rect.start_col; col < rect.end_col; col++) { +- ScreenCell *cell = getcell(screen, row, col); +- +- if (selective && cell->pen.protected_cell) { +- continue; +- } +- +- cell->schar = 0; +- cell->pen = (ScreenPen){ +- // Only copy .fg and .bg; leave things like rv in reset state +- .fg = screen->pen.fg, +- .bg = screen->pen.bg, +- }; +- cell->pen.dwl = info->doublewidth; +- cell->pen.dhl = info->doubleheight; +- } +- } +- +- return 1; +-} +- +-static int erase_user(VTermRect rect, int selective, void *user) +-{ +- VTermScreen *screen = user; +- +- damagerect(screen, rect); +- +- return 1; +-} +- +-static int erase(VTermRect rect, int selective, void *user) +-{ +- erase_internal(rect, selective, user); +- return erase_user(rect, 0, user); +-} +- +-static int scrollrect(VTermRect rect, int downward, int rightward, void *user) +-{ +- VTermScreen *screen = user; +- +- if (screen->damage_merge != VTERM_DAMAGE_SCROLL) { +- vterm_scroll_rect(rect, downward, rightward, +- moverect_internal, erase_internal, screen); +- +- vterm_screen_flush_damage(screen); +- +- vterm_scroll_rect(rect, downward, rightward, +- moverect_user, erase_user, screen); +- +- return 1; +- } +- +- if (screen->damaged.start_row != -1 +- && !rect_intersects(&rect, &screen->damaged)) { +- vterm_screen_flush_damage(screen); +- } +- +- if (screen->pending_scrollrect.start_row == -1) { +- screen->pending_scrollrect = rect; +- screen->pending_scroll_downward = downward; +- screen->pending_scroll_rightward = rightward; +- } else if (rect_equal(&screen->pending_scrollrect, &rect) +- && ((screen->pending_scroll_downward == 0 && downward == 0) +- || (screen->pending_scroll_rightward == 0 && rightward == 0))) { +- screen->pending_scroll_downward += downward; +- screen->pending_scroll_rightward += rightward; +- } else { +- vterm_screen_flush_damage(screen); +- +- screen->pending_scrollrect = rect; +- screen->pending_scroll_downward = downward; +- screen->pending_scroll_rightward = rightward; +- } +- +- vterm_scroll_rect(rect, downward, rightward, +- moverect_internal, erase_internal, screen); +- +- if (screen->damaged.start_row == -1) { +- return 1; +- } +- +- if (rect_contains(&rect, &screen->damaged)) { +- // Scroll region entirely contains the damage; just move it +- vterm_rect_move(&screen->damaged, -downward, -rightward); +- rect_clip(&screen->damaged, &rect); +- } +- // There are a number of possible cases here, but lets restrict this to only the common case where +- // we might actually gain some performance by optimising it. Namely, a vertical scroll that neatly +- // cuts the damage region in half. +- else if (rect.start_col <= screen->damaged.start_col +- && rect.end_col >= screen->damaged.end_col +- && rightward == 0) { +- if (screen->damaged.start_row >= rect.start_row +- && screen->damaged.start_row < rect.end_row) { +- screen->damaged.start_row -= downward; +- if (screen->damaged.start_row < rect.start_row) { +- screen->damaged.start_row = rect.start_row; +- } +- if (screen->damaged.start_row > rect.end_row) { +- screen->damaged.start_row = rect.end_row; +- } +- } +- if (screen->damaged.end_row >= rect.start_row +- && screen->damaged.end_row < rect.end_row) { +- screen->damaged.end_row -= downward; +- if (screen->damaged.end_row < rect.start_row) { +- screen->damaged.end_row = rect.start_row; +- } +- if (screen->damaged.end_row > rect.end_row) { +- screen->damaged.end_row = rect.end_row; +- } +- } +- } else { +- DEBUG_LOG("TODO: Just flush and redo damaged=" STRFrect " rect=" STRFrect "\n", +- ARGSrect(screen->damaged), ARGSrect(rect)); +- } +- +- return 1; +-} +- +-static int movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user) +-{ +- VTermScreen *screen = user; +- +- if (screen->callbacks && screen->callbacks->movecursor) { +- return (*screen->callbacks->movecursor)(pos, oldpos, visible, screen->cbdata); +- } +- +- return 0; +-} +- +-static int setpenattr(VTermAttr attr, VTermValue *val, void *user) +-{ +- VTermScreen *screen = user; +- +- switch (attr) { +- case VTERM_ATTR_BOLD: +- screen->pen.bold = (unsigned)val->boolean; +- return 1; +- case VTERM_ATTR_UNDERLINE: +- screen->pen.underline = (unsigned)val->number; +- return 1; +- case VTERM_ATTR_ITALIC: +- screen->pen.italic = (unsigned)val->boolean; +- return 1; +- case VTERM_ATTR_BLINK: +- screen->pen.blink = (unsigned)val->boolean; +- return 1; +- case VTERM_ATTR_REVERSE: +- screen->pen.reverse = (unsigned)val->boolean; +- return 1; +- case VTERM_ATTR_CONCEAL: +- screen->pen.conceal = (unsigned)val->boolean; +- return 1; +- case VTERM_ATTR_STRIKE: +- screen->pen.strike = (unsigned)val->boolean; +- return 1; +- case VTERM_ATTR_FONT: +- screen->pen.font = (unsigned)val->number; +- return 1; +- case VTERM_ATTR_FOREGROUND: +- screen->pen.fg = val->color; +- return 1; +- case VTERM_ATTR_BACKGROUND: +- screen->pen.bg = val->color; +- return 1; +- case VTERM_ATTR_SMALL: +- screen->pen.small = (unsigned)val->boolean; +- return 1; +- case VTERM_ATTR_BASELINE: +- screen->pen.baseline = (unsigned)val->number; +- return 1; +- case VTERM_ATTR_URI: +- screen->pen.uri = val->number; +- return 1; +- case VTERM_ATTR_DIM: +- screen->pen.dim = (unsigned)val->boolean; +- return 1; +- case VTERM_ATTR_OVERLINE: +- screen->pen.overline = (unsigned)val->boolean; +- return 1; +- +- case VTERM_N_ATTRS: +- return 0; +- } +- +- return 0; +-} +- +-static int settermprop(VTermProp prop, VTermValue *val, void *user) +-{ +- VTermScreen *screen = user; +- +- switch (prop) { +- case VTERM_PROP_ALTSCREEN: +- if (val->boolean && !screen->buffers[BUFIDX_ALTSCREEN]) { +- return 0; +- } +- +- screen->buffer = +- val->boolean ? screen->buffers[BUFIDX_ALTSCREEN] : screen->buffers[BUFIDX_PRIMARY]; +- // only send a damage event on disable; because during enable there's an erase that sends a +- // damage anyway +- if (!val->boolean) { +- damagescreen(screen); +- } +- break; +- case VTERM_PROP_REVERSE: +- screen->global_reverse = (unsigned)val->boolean; +- damagescreen(screen); +- break; +- default: +- ; // ignore +- } +- +- if (screen->callbacks && screen->callbacks->settermprop) { +- return (*screen->callbacks->settermprop)(prop, val, screen->cbdata); +- } +- +- return 1; +-} +- +-static int bell(void *user) +-{ +- VTermScreen *screen = user; +- +- if (screen->callbacks && screen->callbacks->bell) { +- return (*screen->callbacks->bell)(screen->cbdata); +- } +- +- return 0; +-} +- +-/// How many cells are non-blank Returns the position of the first blank cell in the trailing blank +-/// end +-static int line_popcount(ScreenCell *buffer, int row, int rows, int cols) +-{ +- int col = cols - 1; +- while (col >= 0 && buffer[row * cols + col].schar == 0) { +- col--; +- } +- return col + 1; +-} +- +-static void resize_buffer(VTermScreen *screen, int bufidx, int new_rows, int new_cols, bool active, +- VTermStateFields *statefields) +-{ +- int old_rows = screen->rows; +- int old_cols = screen->cols; +- +- ScreenCell *old_buffer = screen->buffers[bufidx]; +- VTermLineInfo *old_lineinfo = statefields->lineinfos[bufidx]; +- +- ScreenCell *new_buffer = vterm_allocator_malloc(screen->vt, +- sizeof(ScreenCell) * (size_t)new_rows * +- (size_t)new_cols); +- VTermLineInfo *new_lineinfo = vterm_allocator_malloc(screen->vt, +- sizeof(new_lineinfo[0]) * (size_t)new_rows); +- +- int old_row = old_rows - 1; +- int new_row = new_rows - 1; +- +- VTermPos old_cursor = statefields->pos; +- VTermPos new_cursor = { -1, -1 }; +- +-#ifdef DEBUG_REFLOW +- fprintf(stderr, "Resizing from %dx%d to %dx%d; cursor was at (%d,%d)\n", +- old_cols, old_rows, new_cols, new_rows, old_cursor.col, old_cursor.row); +-#endif +- +- // Keep track of the final row that is known to be blank, so we know what spare space we have for +- // scrolling into +- int final_blank_row = new_rows; +- +- bool do_reflow = screen->reflow && (bufidx == BUFIDX_PRIMARY); +- +- while (old_row >= 0) { +- int old_row_end = old_row; +- // TODO(vterm): Stop if dwl or dhl +- while (do_reflow && old_lineinfo && old_row > 0 && old_lineinfo[old_row].continuation) { +- old_row--; +- } +- int old_row_start = old_row; +- +- int width = 0; +- for (int row = old_row_start; row <= old_row_end; row++) { +- if (do_reflow && row < (old_rows - 1) && old_lineinfo[row + 1].continuation) { +- width += old_cols; +- } else { +- width += line_popcount(old_buffer, row, old_rows, old_cols); +- } +- } +- +- if (final_blank_row == (new_row + 1) && width == 0) { +- final_blank_row = new_row; +- } +- +- int new_height = do_reflow +- ? width ? (width + new_cols - 1) / new_cols : 1 +- : 1; +- +- int new_row_end = new_row; +- int new_row_start = new_row - new_height + 1; +- +- old_row = old_row_start; +- int old_col = 0; +- +- int spare_rows = new_rows - final_blank_row; +- +- if (new_row_start < 0 // we'd fall off the top +- && spare_rows >= 0 // we actually have spare rows +- && (!active || new_cursor.row == -1 || (new_cursor.row - new_row_start) < new_rows)) { +- // Attempt to scroll content down into the blank rows at the bottom to make it fit +- int downwards = -new_row_start; +- if (downwards > spare_rows) { +- downwards = spare_rows; +- } +- int rowcount = new_rows - downwards; +- +-#ifdef DEBUG_REFLOW +- fprintf(stderr, " scroll %d rows +%d downwards\n", rowcount, downwards); +-#endif +- +- memmove(&new_buffer[downwards * new_cols], &new_buffer[0], +- (size_t)rowcount * (size_t)new_cols * sizeof(ScreenCell)); +- memmove(&new_lineinfo[downwards], &new_lineinfo[0], +- (size_t)rowcount * sizeof(new_lineinfo[0])); +- +- new_row += downwards; +- new_row_start += downwards; +- new_row_end += downwards; +- +- if (new_cursor.row >= 0) { +- new_cursor.row += downwards; +- } +- +- final_blank_row += downwards; +- } +- +-#ifdef DEBUG_REFLOW +- fprintf(stderr, " rows [%d..%d] <- [%d..%d] width=%d\n", +- new_row_start, new_row_end, old_row_start, old_row_end, width); +-#endif +- +- if (new_row_start < 0) { +- if (old_row_start <= old_cursor.row && old_cursor.row <= old_row_end) { +- new_cursor.row = 0; +- new_cursor.col = old_cursor.col; +- if (new_cursor.col >= new_cols) { +- new_cursor.col = new_cols - 1; +- } +- } +- break; +- } +- +- for (new_row = new_row_start, old_row = old_row_start; new_row <= new_row_end; new_row++) { +- int count = width >= new_cols ? new_cols : width; +- width -= count; +- +- int new_col = 0; +- +- while (count) { +- // TODO(vterm): This could surely be done a lot faster by memcpy()'ing the entire range +- new_buffer[new_row * new_cols + new_col] = old_buffer[old_row * old_cols + old_col]; +- +- if (old_cursor.row == old_row && old_cursor.col == old_col) { +- new_cursor.row = new_row, new_cursor.col = new_col; +- } +- +- old_col++; +- if (old_col == old_cols) { +- old_row++; +- +- if (!do_reflow) { +- new_col++; +- break; +- } +- old_col = 0; +- } +- +- new_col++; +- count--; +- } +- +- if (old_row <= old_row_end && old_cursor.row == old_row && old_cursor.col >= old_col) { +- new_cursor.row = new_row, new_cursor.col = (old_cursor.col - old_col + new_col); +- if (new_cursor.col >= new_cols) { +- new_cursor.col = new_cols - 1; +- } +- } +- +- while (new_col < new_cols) { +- clearcell(screen, &new_buffer[new_row * new_cols + new_col]); +- new_col++; +- } +- +- new_lineinfo[new_row].continuation = (new_row > new_row_start); +- } +- +- old_row = old_row_start - 1; +- new_row = new_row_start - 1; +- } +- +- if (old_cursor.row <= old_row) { +- // cursor would have moved entirely off the top of the screen; lets just bring it within range +- new_cursor.row = 0, new_cursor.col = old_cursor.col; +- if (new_cursor.col >= new_cols) { +- new_cursor.col = new_cols - 1; +- } +- } +- +- // We really expect the cursor position to be set by now +- if (active && (new_cursor.row == -1 || new_cursor.col == -1)) { +- fprintf(stderr, "screen_resize failed to update cursor position\n"); +- abort(); +- } +- +- if (old_row >= 0 && bufidx == BUFIDX_PRIMARY) { +- // Push spare lines to scrollback buffer +- if (screen->callbacks && screen->callbacks->sb_pushline) { +- for (int row = 0; row <= old_row; row++) { +- sb_pushline_from_row(screen, row); +- } +- } +- if (active) { +- statefields->pos.row -= (old_row + 1); +- } +- } +- if (new_row >= 0 && bufidx == BUFIDX_PRIMARY +- && screen->callbacks && screen->callbacks->sb_popline) { +- // Try to backfill rows by popping scrollback buffer +- while (new_row >= 0) { +- if (!(screen->callbacks->sb_popline(old_cols, screen->sb_buffer, screen->cbdata))) { +- break; +- } +- +- VTermPos pos = { .row = new_row }; +- for (pos.col = 0; pos.col < old_cols && pos.col < new_cols; +- pos.col += screen->sb_buffer[pos.col].width) { +- VTermScreenCell *src = &screen->sb_buffer[pos.col]; +- ScreenCell *dst = &new_buffer[pos.row * new_cols + pos.col]; +- +- dst->schar = src->schar; +- +- dst->pen.bold = src->attrs.bold; +- dst->pen.underline = src->attrs.underline; +- dst->pen.italic = src->attrs.italic; +- dst->pen.blink = src->attrs.blink; +- dst->pen.reverse = src->attrs.reverse ^ screen->global_reverse; +- dst->pen.conceal = src->attrs.conceal; +- dst->pen.strike = src->attrs.strike; +- dst->pen.font = src->attrs.font; +- dst->pen.small = src->attrs.small; +- dst->pen.baseline = src->attrs.baseline; +- dst->pen.dim = src->attrs.dim; +- dst->pen.overline = src->attrs.overline; +- +- dst->pen.fg = src->fg; +- dst->pen.bg = src->bg; +- +- dst->pen.uri = src->uri; +- +- if (src->width == 2 && pos.col < (new_cols - 1)) { +- (dst + 1)->schar = (uint32_t)-1; +- } +- } +- for (; pos.col < new_cols; pos.col++) { +- clearcell(screen, &new_buffer[pos.row * new_cols + pos.col]); +- } +- new_row--; +- +- if (active) { +- statefields->pos.row++; +- } +- } +- } +- if (new_row >= 0) { +- // Scroll new rows back up to the top and fill in blanks at the bottom +- int moverows = new_rows - new_row - 1; +- memmove(&new_buffer[0], &new_buffer[(new_row + 1) * new_cols], +- (size_t)moverows * (size_t)new_cols * sizeof(ScreenCell)); +- memmove(&new_lineinfo[0], &new_lineinfo[new_row + 1], +- (size_t)moverows * sizeof(new_lineinfo[0])); +- +- new_cursor.row -= (new_row + 1); +- +- for (new_row = moverows; new_row < new_rows; new_row++) { +- for (int col = 0; col < new_cols; col++) { +- clearcell(screen, &new_buffer[new_row * new_cols + col]); +- } +- new_lineinfo[new_row] = (VTermLineInfo){ 0 }; +- } +- } +- +- vterm_allocator_free(screen->vt, old_buffer); +- screen->buffers[bufidx] = new_buffer; +- +- vterm_allocator_free(screen->vt, old_lineinfo); +- statefields->lineinfos[bufidx] = new_lineinfo; +- +- if (active) { +- statefields->pos = new_cursor; +- } +-} +- +-static int resize(int new_rows, int new_cols, VTermStateFields *fields, void *user) +-{ +- VTermScreen *screen = user; +- +- int altscreen_active = (screen->buffers[BUFIDX_ALTSCREEN] +- && screen->buffer == screen->buffers[BUFIDX_ALTSCREEN]); +- +- int old_rows = screen->rows; +- int old_cols = screen->cols; +- +- if (new_cols > old_cols) { +- // Ensure that ->sb_buffer is large enough for a new or and old row +- if (screen->sb_buffer) { +- vterm_allocator_free(screen->vt, screen->sb_buffer); +- } +- +- screen->sb_buffer = vterm_allocator_malloc(screen->vt, +- sizeof(VTermScreenCell) * (size_t)new_cols); +- } +- +- resize_buffer(screen, 0, new_rows, new_cols, !altscreen_active, fields); +- if (screen->buffers[BUFIDX_ALTSCREEN]) { +- resize_buffer(screen, 1, new_rows, new_cols, altscreen_active, fields); +- } else if (new_rows != old_rows) { +- // We don't need a full resize of the altscreen because it isn't enabled but we should at least +- // keep the lineinfo the right size +- vterm_allocator_free(screen->vt, fields->lineinfos[BUFIDX_ALTSCREEN]); +- +- VTermLineInfo *new_lineinfo = vterm_allocator_malloc(screen->vt, +- sizeof(new_lineinfo[0]) * +- (size_t)new_rows); +- for (int row = 0; row < new_rows; row++) { +- new_lineinfo[row] = (VTermLineInfo){ 0 }; +- } +- +- fields->lineinfos[BUFIDX_ALTSCREEN] = new_lineinfo; +- } +- +- screen->buffer = +- altscreen_active ? screen->buffers[BUFIDX_ALTSCREEN] : screen->buffers[BUFIDX_PRIMARY]; +- +- screen->rows = new_rows; +- screen->cols = new_cols; +- +- if (new_cols <= old_cols) { +- if (screen->sb_buffer) { +- vterm_allocator_free(screen->vt, screen->sb_buffer); +- } +- +- screen->sb_buffer = vterm_allocator_malloc(screen->vt, +- sizeof(VTermScreenCell) * (size_t)new_cols); +- } +- +- // TODO(vterm): Maaaaybe we can optimise this if there's no reflow happening +- damagescreen(screen); +- +- if (screen->callbacks && screen->callbacks->resize) { +- return (*screen->callbacks->resize)(new_rows, new_cols, screen->cbdata); +- } +- +- return 1; +-} +- +-static int theme(bool *dark, void *user) +-{ +- VTermScreen *screen = user; +- +- if (screen->callbacks && screen->callbacks->theme) { +- return (*screen->callbacks->theme)(dark, screen->cbdata); +- } +- +- return 1; +-} +- +-static int setlineinfo(int row, const VTermLineInfo *newinfo, const VTermLineInfo *oldinfo, +- void *user) +-{ +- VTermScreen *screen = user; +- +- if (newinfo->doublewidth != oldinfo->doublewidth +- || newinfo->doubleheight != oldinfo->doubleheight) { +- for (int col = 0; col < screen->cols; col++) { +- ScreenCell *cell = getcell(screen, row, col); +- cell->pen.dwl = newinfo->doublewidth; +- cell->pen.dhl = newinfo->doubleheight; +- } +- +- VTermRect rect = { +- .start_row = row, +- .end_row = row + 1, +- .start_col = 0, +- .end_col = newinfo->doublewidth ? screen->cols / 2 : screen->cols, +- }; +- damagerect(screen, rect); +- +- if (newinfo->doublewidth) { +- rect.start_col = screen->cols / 2; +- rect.end_col = screen->cols; +- +- erase_internal(rect, 0, user); +- } +- } +- +- return 1; +-} +- +-static int sb_clear(void *user) +-{ +- VTermScreen *screen = user; +- +- if (screen->callbacks && screen->callbacks->sb_clear) { +- if ((*screen->callbacks->sb_clear)(screen->cbdata)) { +- return 1; +- } +- } +- +- return 0; +-} +- +-static VTermStateCallbacks state_cbs = { +- .putglyph = &putglyph, +- .movecursor = &movecursor, +- .scrollrect = &scrollrect, +- .erase = &erase, +- .setpenattr = &setpenattr, +- .settermprop = &settermprop, +- .bell = &bell, +- .resize = &resize, +- .theme = &theme, +- .setlineinfo = &setlineinfo, +- .sb_clear = &sb_clear, +-}; +- +-static VTermScreen *screen_new(VTerm *vt) +-{ +- VTermState *state = vterm_obtain_state(vt); +- if (!state) { +- return NULL; +- } +- +- VTermScreen *screen = vterm_allocator_malloc(vt, sizeof(VTermScreen)); +- int rows, cols; +- +- vterm_get_size(vt, &rows, &cols); +- +- screen->vt = vt; +- screen->state = state; +- +- screen->damage_merge = VTERM_DAMAGE_CELL; +- screen->damaged.start_row = -1; +- screen->pending_scrollrect.start_row = -1; +- +- screen->rows = rows; +- screen->cols = cols; +- +- screen->global_reverse = false; +- screen->reflow = false; +- +- screen->callbacks = NULL; +- screen->cbdata = NULL; +- +- screen->buffers[BUFIDX_PRIMARY] = alloc_buffer(screen, rows, cols); +- +- screen->buffer = screen->buffers[BUFIDX_PRIMARY]; +- +- screen->sb_buffer = vterm_allocator_malloc(screen->vt, sizeof(VTermScreenCell) * (size_t)cols); +- +- vterm_state_set_callbacks(screen->state, &state_cbs, screen); +- +- return screen; +-} +- +-void vterm_screen_free(VTermScreen *screen) +-{ +- vterm_allocator_free(screen->vt, screen->buffers[BUFIDX_PRIMARY]); +- if (screen->buffers[BUFIDX_ALTSCREEN]) { +- vterm_allocator_free(screen->vt, screen->buffers[BUFIDX_ALTSCREEN]); +- } +- +- vterm_allocator_free(screen->vt, screen->sb_buffer); +- +- vterm_allocator_free(screen->vt, screen); +-} +- +-void vterm_screen_reset(VTermScreen *screen, int hard) +-{ +- screen->damaged.start_row = -1; +- screen->pending_scrollrect.start_row = -1; +- vterm_state_reset(screen->state, hard); +- vterm_screen_flush_damage(screen); +-} +- +-// Copy internal to external representation of a screen cell +-int vterm_screen_get_cell(const VTermScreen *screen, VTermPos pos, VTermScreenCell *cell) +-{ +- ScreenCell *intcell = getcell(screen, pos.row, pos.col); +- if (!intcell) { +- return 0; +- } +- +- cell->schar = (intcell->schar == (uint32_t)-1) ? 0 : intcell->schar; +- +- cell->attrs.bold = intcell->pen.bold; +- cell->attrs.underline = intcell->pen.underline; +- cell->attrs.italic = intcell->pen.italic; +- cell->attrs.blink = intcell->pen.blink; +- cell->attrs.reverse = intcell->pen.reverse ^ screen->global_reverse; +- cell->attrs.conceal = intcell->pen.conceal; +- cell->attrs.strike = intcell->pen.strike; +- cell->attrs.font = intcell->pen.font; +- cell->attrs.small = intcell->pen.small; +- cell->attrs.baseline = intcell->pen.baseline; +- cell->attrs.dim = intcell->pen.dim; +- cell->attrs.overline = intcell->pen.overline; +- +- cell->attrs.dwl = intcell->pen.dwl; +- cell->attrs.dhl = intcell->pen.dhl; +- +- cell->fg = intcell->pen.fg; +- cell->bg = intcell->pen.bg; +- +- cell->uri = intcell->pen.uri; +- +- if (pos.col < (screen->cols - 1) +- && getcell(screen, pos.row, pos.col + 1)->schar == (uint32_t)-1) { +- cell->width = 2; +- } else { +- cell->width = 1; +- } +- +- return 1; +-} +- +-VTermScreen *vterm_obtain_screen(VTerm *vt) +-{ +- if (vt->screen) { +- return vt->screen; +- } +- +- VTermScreen *screen = screen_new(vt); +- vt->screen = screen; +- +- return screen; +-} +- +-void vterm_screen_enable_reflow(VTermScreen *screen, bool reflow) +-{ +- screen->reflow = reflow; +-} +- +-#undef vterm_screen_set_reflow +-void vterm_screen_set_reflow(VTermScreen *screen, bool reflow) +-{ +- vterm_screen_enable_reflow(screen, reflow); +-} +- +-void vterm_screen_enable_altscreen(VTermScreen *screen, int altscreen) +-{ +- if (!screen->buffers[BUFIDX_ALTSCREEN] && altscreen) { +- int rows, cols; +- vterm_get_size(screen->vt, &rows, &cols); +- +- screen->buffers[BUFIDX_ALTSCREEN] = alloc_buffer(screen, rows, cols); +- } +-} +- +-void vterm_screen_set_callbacks(VTermScreen *screen, const VTermScreenCallbacks *callbacks, +- void *user) +-{ +- screen->callbacks = callbacks; +- screen->cbdata = user; +-} +- +-void vterm_screen_set_unrecognised_fallbacks(VTermScreen *screen, +- const VTermStateFallbacks *fallbacks, void *user) +-{ +- vterm_state_set_unrecognised_fallbacks(screen->state, fallbacks, user); +-} +- +-void vterm_screen_flush_damage(VTermScreen *screen) +-{ +- if (screen->pending_scrollrect.start_row != -1) { +- vterm_scroll_rect(screen->pending_scrollrect, screen->pending_scroll_downward, +- screen->pending_scroll_rightward, +- moverect_user, erase_user, screen); +- +- screen->pending_scrollrect.start_row = -1; +- } +- +- if (screen->damaged.start_row != -1) { +- if (screen->callbacks && screen->callbacks->damage) { +- (*screen->callbacks->damage)(screen->damaged, screen->cbdata); +- } +- +- screen->damaged.start_row = -1; +- } +-} +- +-void vterm_screen_set_damage_merge(VTermScreen *screen, VTermDamageSize size) +-{ +- vterm_screen_flush_damage(screen); +- screen->damage_merge = size; +-} +- +-/// Same as vterm_state_convert_color_to_rgb(), but takes a `screen` instead of a `state` instance. +-void vterm_screen_convert_color_to_rgb(const VTermScreen *screen, VTermColor *col) +-{ +- vterm_state_convert_color_to_rgb(screen->state, col); +-} +- +-// Some utility functions on VTermRect structures +- +-#define STRFrect "(%d,%d-%d,%d)" +-#define ARGSrect(r) (r).start_row, (r).start_col, (r).end_row, (r).end_col +- +-// Expand dst to contain src as well +-void rect_expand(VTermRect *dst, VTermRect *src) +-{ +- if (dst->start_row > src->start_row) { +- dst->start_row = src->start_row; +- } +- if (dst->start_col > src->start_col) { +- dst->start_col = src->start_col; +- } +- if (dst->end_row < src->end_row) { +- dst->end_row = src->end_row; +- } +- if (dst->end_col < src->end_col) { +- dst->end_col = src->end_col; +- } +-} +- +-// Clip the dst to ensure it does not step outside of bounds +-void rect_clip(VTermRect *dst, VTermRect *bounds) +-{ +- if (dst->start_row < bounds->start_row) { +- dst->start_row = bounds->start_row; +- } +- if (dst->start_col < bounds->start_col) { +- dst->start_col = bounds->start_col; +- } +- if (dst->end_row > bounds->end_row) { +- dst->end_row = bounds->end_row; +- } +- if (dst->end_col > bounds->end_col) { +- dst->end_col = bounds->end_col; +- } +- // Ensure it doesn't end up negatively-sized +- if (dst->end_row < dst->start_row) { +- dst->end_row = dst->start_row; +- } +- if (dst->end_col < dst->start_col) { +- dst->end_col = dst->start_col; +- } +-} +- +-// True if the two rectangles are equal +-int rect_equal(VTermRect *a, VTermRect *b) +-{ +- return (a->start_row == b->start_row) +- && (a->start_col == b->start_col) +- && (a->end_row == b->end_row) +- && (a->end_col == b->end_col); +-} +- +-// True if small is contained entirely within big +-int rect_contains(VTermRect *big, VTermRect *small) +-{ +- if (small->start_row < big->start_row) { +- return 0; +- } +- if (small->start_col < big->start_col) { +- return 0; +- } +- if (small->end_row > big->end_row) { +- return 0; +- } +- if (small->end_col > big->end_col) { +- return 0; +- } +- return 1; +-} +- +-// True if the rectangles overlap at all +-int rect_intersects(VTermRect *a, VTermRect *b) +-{ +- if (a->start_row > b->end_row || b->start_row > a->end_row) { +- return 0; +- } +- if (a->start_col > b->end_col || b->start_col > a->end_col) { +- return 0; +- } +- return 1; +-} +diff --git a/src/nvim/vterm/screen.h b/src/nvim/vterm/screen.h +deleted file mode 100644 +index 7f281fa1964c06..00000000000000 +--- a/src/nvim/vterm/screen.h ++++ /dev/null +@@ -1,7 +0,0 @@ +-#pragma once +- +-#include +- +-#include "nvim/vterm/vterm_defs.h" +- +-#include "vterm/screen.h.generated.h" +diff --git a/src/nvim/vterm/state.c b/src/nvim/vterm/state.c +deleted file mode 100644 +index fe5a0020e820dd..00000000000000 +--- a/src/nvim/vterm/state.c ++++ /dev/null +@@ -1,2484 +0,0 @@ +-#include +-#include +-#include +- +-#include "nvim/grid.h" +-#include "nvim/mbyte.h" +-#include "nvim/vterm/encoding.h" +-#include "nvim/vterm/parser.h" +-#include "nvim/vterm/pen.h" +-#include "nvim/vterm/state.h" +-#include "nvim/vterm/vterm.h" +-#include "nvim/vterm/vterm_internal_defs.h" +- +-#include "vterm/state.c.generated.h" +- +-#define strneq(a, b, n) (strncmp(a, b, n) == 0) +- +-// Primary Device Attributes (DA1) response. +-// We make this a global (extern) variable so that we can override it with FFI +-// in tests. +-DLLEXPORT char vterm_primary_device_attr[] = "61;22;52"; +- +-// Some convenient wrappers to make callback functions easier +- +-static void putglyph(VTermState *state, const schar_T schar, int width, VTermPos pos) +-{ +- VTermGlyphInfo info = { +- .schar = schar, +- .width = width, +- .protected_cell = state->protected_cell, +- .dwl = state->lineinfo[pos.row].doublewidth, +- .dhl = state->lineinfo[pos.row].doubleheight, +- }; +- +- if (state->callbacks && state->callbacks->putglyph) { +- if ((*state->callbacks->putglyph)(&info, pos, state->cbdata)) { +- return; +- } +- } +- +- DEBUG_LOG("libvterm: Unhandled putglyph U+%04x at (%d,%d)\n", chars[0], pos.col, pos.row); +-} +- +-static void updatecursor(VTermState *state, VTermPos *oldpos, int cancel_phantom) +-{ +- if (state->pos.col == oldpos->col && state->pos.row == oldpos->row) { +- return; +- } +- +- if (cancel_phantom) { +- state->at_phantom = 0; +- } +- +- if (state->callbacks && state->callbacks->movecursor) { +- if ((*state->callbacks->movecursor)(state->pos, *oldpos, state->mode.cursor_visible, +- state->cbdata)) { +- return; +- } +- } +-} +- +-static void erase(VTermState *state, VTermRect rect, int selective) +-{ +- if (rect.end_col == state->cols) { +- // If we're erasing the final cells of any lines, cancel the continuation marker on the +- // subsequent line +- for (int row = rect.start_row + 1; row < rect.end_row + 1 && row < state->rows; row++) { +- state->lineinfo[row].continuation = 0; +- } +- } +- +- if (state->callbacks && state->callbacks->erase) { +- if ((*state->callbacks->erase)(rect, selective, state->cbdata)) { +- return; +- } +- } +-} +- +-static VTermState *vterm_state_new(VTerm *vt) +-{ +- VTermState *state = vterm_allocator_malloc(vt, sizeof(VTermState)); +- +- state->vt = vt; +- +- state->rows = vt->rows; +- state->cols = vt->cols; +- +- state->mouse_col = 0; +- state->mouse_row = 0; +- state->mouse_buttons = 0; +- +- state->mouse_protocol = MOUSE_X10; +- +- state->callbacks = NULL; +- state->cbdata = NULL; +- +- state->selection.callbacks = NULL; +- state->selection.user = NULL; +- state->selection.buffer = NULL; +- +- vterm_state_newpen(state); +- +- state->bold_is_highbright = 0; +- +- state->combine_pos.row = -1; +- +- state->tabstops = vterm_allocator_malloc(state->vt, ((size_t)state->cols + 7) / 8); +- +- state->lineinfos[BUFIDX_PRIMARY] = vterm_allocator_malloc(state->vt, +- (size_t)state->rows * +- sizeof(VTermLineInfo)); +- // TODO(vterm): Make an 'enable' function +- state->lineinfos[BUFIDX_ALTSCREEN] = vterm_allocator_malloc(state->vt, +- (size_t)state->rows * +- sizeof(VTermLineInfo)); +- state->lineinfo = state->lineinfos[BUFIDX_PRIMARY]; +- +- state->encoding_utf8.enc = vterm_lookup_encoding(ENC_UTF8, 'u'); +- if (*state->encoding_utf8.enc->init) { +- (*state->encoding_utf8.enc->init)(state->encoding_utf8.enc, state->encoding_utf8.data); +- } +- +- for (size_t i = 0; i < ARRAY_SIZE(state->key_encoding_stacks); i++) { +- struct VTermKeyEncodingStack *stack = &state->key_encoding_stacks[i]; +- for (size_t j = 0; j < ARRAY_SIZE(stack->items); j++) { +- memset(&stack->items[j], 0, sizeof(stack->items[j])); +- } +- +- stack->size = 1; +- } +- +- return state; +-} +- +-void vterm_state_free(VTermState *state) +-{ +- vterm_allocator_free(state->vt, state->tabstops); +- vterm_allocator_free(state->vt, state->lineinfos[BUFIDX_PRIMARY]); +- if (state->lineinfos[BUFIDX_ALTSCREEN]) { +- vterm_allocator_free(state->vt, state->lineinfos[BUFIDX_ALTSCREEN]); +- } +- vterm_allocator_free(state->vt, state); +-} +- +-static void scroll(VTermState *state, VTermRect rect, int downward, int rightward) +-{ +- if (!downward && !rightward) { +- return; +- } +- +- int rows = rect.end_row - rect.start_row; +- if (downward > rows) { +- downward = rows; +- } else if (downward < -rows) { +- downward = -rows; +- } +- +- int cols = rect.end_col - rect.start_col; +- if (rightward > cols) { +- rightward = cols; +- } else if (rightward < -cols) { +- rightward = -cols; +- } +- +- // Update lineinfo if full line +- if (rect.start_col == 0 && rect.end_col == state->cols && rightward == 0) { +- int height = rect.end_row - rect.start_row - abs(downward); +- +- if (downward > 0) { +- memmove(state->lineinfo + rect.start_row, +- state->lineinfo + rect.start_row + downward, +- (size_t)height * sizeof(state->lineinfo[0])); +- for (int row = rect.end_row - downward; row < rect.end_row; row++) { +- state->lineinfo[row] = (VTermLineInfo){ 0 }; +- } +- } else { +- memmove(state->lineinfo + rect.start_row - downward, +- state->lineinfo + rect.start_row, +- (size_t)height * sizeof(state->lineinfo[0])); +- for (int row = rect.start_row; row < rect.start_row - downward; row++) { +- state->lineinfo[row] = (VTermLineInfo){ 0 }; +- } +- } +- } +- +- if (state->callbacks && state->callbacks->scrollrect) { +- if ((*state->callbacks->scrollrect)(rect, downward, rightward, state->cbdata)) { +- return; +- } +- } +- +- if (state->callbacks) { +- vterm_scroll_rect(rect, downward, rightward, +- state->callbacks->moverect, state->callbacks->erase, state->cbdata); +- } +-} +- +-static void linefeed(VTermState *state) +-{ +- if (state->pos.row == SCROLLREGION_BOTTOM(state) - 1) { +- VTermRect rect = { +- .start_row = state->scrollregion_top, +- .end_row = SCROLLREGION_BOTTOM(state), +- .start_col = SCROLLREGION_LEFT(state), +- .end_col = SCROLLREGION_RIGHT(state), +- }; +- +- scroll(state, rect, 1, 0); +- } else if (state->pos.row < state->rows - 1) { +- state->pos.row++; +- } +-} +- +-static void set_col_tabstop(VTermState *state, int col) +-{ +- uint8_t mask = (uint8_t)(1 << (col & 7)); +- state->tabstops[col >> 3] |= mask; +-} +- +-static void clear_col_tabstop(VTermState *state, int col) +-{ +- uint8_t mask = (uint8_t)(1 << (col & 7)); +- state->tabstops[col >> 3] &= ~mask; +-} +- +-static int is_col_tabstop(VTermState *state, int col) +-{ +- uint8_t mask = (uint8_t)(1 << (col & 7)); +- return state->tabstops[col >> 3] & mask; +-} +- +-static int is_cursor_in_scrollregion(const VTermState *state) +-{ +- if (state->pos.row < state->scrollregion_top +- || state->pos.row >= SCROLLREGION_BOTTOM(state)) { +- return 0; +- } +- if (state->pos.col < SCROLLREGION_LEFT(state) +- || state->pos.col >= SCROLLREGION_RIGHT(state)) { +- return 0; +- } +- +- return 1; +-} +- +-static void tab(VTermState *state, int count, int direction) +-{ +- while (count > 0) { +- if (direction > 0) { +- if (state->pos.col >= THISROWWIDTH(state) - 1) { +- return; +- } +- +- state->pos.col++; +- } else if (direction < 0) { +- if (state->pos.col < 1) { +- return; +- } +- +- state->pos.col--; +- } +- +- if (is_col_tabstop(state, state->pos.col)) { +- count--; +- } +- } +-} +- +-#define NO_FORCE 0 +-#define FORCE 1 +- +-#define DWL_OFF 0 +-#define DWL_ON 1 +- +-#define DHL_OFF 0 +-#define DHL_TOP 1 +-#define DHL_BOTTOM 2 +- +-static void set_lineinfo(VTermState *state, int row, int force, int dwl, int dhl) +-{ +- VTermLineInfo info = state->lineinfo[row]; +- +- if (dwl == DWL_OFF) { +- info.doublewidth = DWL_OFF; +- } else if (dwl == DWL_ON) { +- info.doublewidth = DWL_ON; +- } +- // else -1 to ignore +- +- if (dhl == DHL_OFF) { +- info.doubleheight = DHL_OFF; +- } else if (dhl == DHL_TOP) { +- info.doubleheight = DHL_TOP; +- } else if (dhl == DHL_BOTTOM) { +- info.doubleheight = DHL_BOTTOM; +- } +- +- if ((state->callbacks +- && state->callbacks->setlineinfo +- && (*state->callbacks->setlineinfo)(row, &info, state->lineinfo + row, state->cbdata)) +- || force) { +- state->lineinfo[row] = info; +- } +-} +- +-static int on_text(const char bytes[], size_t len, void *user) +-{ +- VTermState *state = user; +- +- VTermPos oldpos = state->pos; +- +- uint32_t *codepoints = (uint32_t *)(state->vt->tmpbuffer); +- size_t maxpoints = (state->vt->tmpbuffer_len) / sizeof(uint32_t); +- +- int npoints = 0; +- size_t eaten = 0; +- +- VTermEncodingInstance *encoding = +- state->gsingle_set ? &state->encoding[state->gsingle_set] +- : !(bytes[eaten] & 0x80) ? &state->encoding[state->gl_set] +- : state->vt->mode.utf8 ? &state->encoding_utf8 +- : &state->encoding[state-> +- gr_set]; +- if (encoding->enc == state->encoding_utf8.enc) { +- encoding = &state->encoding_utf8; // Only use one UTF-8 encoding state. +- } +- +- (*encoding->enc->decode)(encoding->enc, encoding->data, +- codepoints, &npoints, state->gsingle_set ? 1 : (int)maxpoints, +- bytes, &eaten, len); +- +- // There's a chance an encoding (e.g. UTF-8) hasn't found enough bytes yet for even a single codepoint +- if (!npoints) { +- return (int)eaten; +- } +- +- if (state->gsingle_set && npoints) { +- state->gsingle_set = 0; +- } +- +- int i = 0; +- GraphemeState grapheme_state = GRAPHEME_STATE_INIT; +- size_t grapheme_len = 0; +- bool recombine = false; +- +- // See if the cursor has moved since +- if (state->pos.row == state->combine_pos.row +- && state->pos.col >= state->combine_pos.col +- && state->pos.col <= state->combine_pos.col + state->combine_width) { +- // This is a combining char. that needs to be merged with the previous glyph output +- if (utf_iscomposing((int)state->grapheme_last, (int)codepoints[i], &state->grapheme_state)) { +- // Find where we need to append these combining chars +- grapheme_len = state->grapheme_len; +- grapheme_state = state->grapheme_state; +- state->pos.col = state->combine_pos.col; +- state->at_phantom = 0; +- recombine = true; +- } else { +- DEBUG_LOG("libvterm: TODO: Skip over split char+combining\n"); +- } +- } +- +- while (i < npoints) { +- // Try to find combining characters following this +- do { +- if (grapheme_len < sizeof(state->grapheme_buf) - 4) { +- grapheme_len += (size_t)utf_char2bytes((int)codepoints[i], +- state->grapheme_buf + grapheme_len); +- } +- i++; +- } while (i < npoints && utf_iscomposing((int)codepoints[i - 1], (int)codepoints[i], +- &grapheme_state)); +- +- int width = utf_ptr2cells_len(state->grapheme_buf, (int)grapheme_len); +- +- if (state->at_phantom || state->pos.col + width > THISROWWIDTH(state)) { +- linefeed(state); +- state->pos.col = 0; +- state->at_phantom = 0; +- state->lineinfo[state->pos.row].continuation = 1; +- } +- +- if (state->mode.insert && !recombine) { +- // TODO(vterm): This will be a little inefficient for large bodies of text, as it'll have to +- // 'ICH' effectively before every glyph. We should scan ahead and ICH as many times as +- // required +- VTermRect rect = { +- .start_row = state->pos.row, +- .end_row = state->pos.row + 1, +- .start_col = state->pos.col, +- .end_col = THISROWWIDTH(state), +- }; +- scroll(state, rect, 0, -1); +- } +- +- schar_T sc = schar_from_buf(state->grapheme_buf, grapheme_len); +- putglyph(state, sc, width, state->pos); +- +- if (i == npoints) { +- // End of the buffer. Save the chars in case we have to combine with more on the next call +- state->grapheme_len = grapheme_len; +- state->grapheme_last = codepoints[i - 1]; +- state->grapheme_state = grapheme_state; +- state->combine_width = width; +- state->combine_pos = state->pos; +- } else { +- grapheme_len = 0; +- recombine = false; +- } +- +- if (state->pos.col + width >= THISROWWIDTH(state)) { +- if (state->mode.autowrap) { +- state->at_phantom = 1; +- } +- } else { +- state->pos.col += width; +- } +- } +- +- updatecursor(state, &oldpos, 0); +- +-#ifdef DEBUG +- if (state->pos.row < 0 || state->pos.row >= state->rows +- || state->pos.col < 0 || state->pos.col >= state->cols) { +- fprintf(stderr, "Position out of bounds after text: (%d,%d)\n", +- state->pos.row, state->pos.col); +- abort(); +- } +-#endif +- +- return (int)eaten; +-} +- +-static int on_control(uint8_t control, void *user) +-{ +- VTermState *state = user; +- +- VTermPos oldpos = state->pos; +- +- switch (control) { +- case 0x07: // BEL - ECMA-48 8.3.3 +- if (state->callbacks && state->callbacks->bell) { +- (*state->callbacks->bell)(state->cbdata); +- } +- break; +- +- case 0x08: // BS - ECMA-48 8.3.5 +- if (state->pos.col > 0) { +- state->pos.col--; +- } +- break; +- +- case 0x09: // HT - ECMA-48 8.3.60 +- tab(state, 1, +1); +- break; +- +- case 0x0a: // LF - ECMA-48 8.3.74 +- case 0x0b: // VT +- case 0x0c: // FF +- linefeed(state); +- if (state->mode.newline) { +- state->pos.col = 0; +- } +- break; +- +- case 0x0d: // CR - ECMA-48 8.3.15 +- state->pos.col = 0; +- break; +- +- case 0x0e: // LS1 - ECMA-48 8.3.76 +- state->gl_set = 1; +- break; +- +- case 0x0f: // LS0 - ECMA-48 8.3.75 +- state->gl_set = 0; +- break; +- +- case 0x84: // IND - DEPRECATED but implemented for completeness +- linefeed(state); +- break; +- +- case 0x85: // NEL - ECMA-48 8.3.86 +- linefeed(state); +- state->pos.col = 0; +- break; +- +- case 0x88: // HTS - ECMA-48 8.3.62 +- set_col_tabstop(state, state->pos.col); +- break; +- +- case 0x8d: // RI - ECMA-48 8.3.104 +- if (state->pos.row == state->scrollregion_top) { +- VTermRect rect = { +- .start_row = state->scrollregion_top, +- .end_row = SCROLLREGION_BOTTOM(state), +- .start_col = SCROLLREGION_LEFT(state), +- .end_col = SCROLLREGION_RIGHT(state), +- }; +- +- scroll(state, rect, -1, 0); +- } else if (state->pos.row > 0) { +- state->pos.row--; +- } +- break; +- +- case 0x8e: // SS2 - ECMA-48 8.3.141 +- state->gsingle_set = 2; +- break; +- +- case 0x8f: // SS3 - ECMA-48 8.3.142 +- state->gsingle_set = 3; +- break; +- +- default: +- if (state->fallbacks && state->fallbacks->control) { +- if ((*state->fallbacks->control)(control, state->fbdata)) { +- return 1; +- } +- } +- +- return 0; +- } +- +- updatecursor(state, &oldpos, 1); +- +-#ifdef DEBUG +- if (state->pos.row < 0 || state->pos.row >= state->rows +- || state->pos.col < 0 || state->pos.col >= state->cols) { +- fprintf(stderr, "Position out of bounds after Ctrl %02x: (%d,%d)\n", +- control, state->pos.row, state->pos.col); +- abort(); +- } +-#endif +- +- return 1; +-} +- +-static int settermprop_bool(VTermState *state, VTermProp prop, int v) +-{ +- VTermValue val = { .boolean = v }; +- return vterm_state_set_termprop(state, prop, &val); +-} +- +-static int settermprop_int(VTermState *state, VTermProp prop, int v) +-{ +- VTermValue val = { .number = v }; +- return vterm_state_set_termprop(state, prop, &val); +-} +- +-static int settermprop_string(VTermState *state, VTermProp prop, VTermStringFragment frag) +-{ +- VTermValue val = { .string = frag }; +- return vterm_state_set_termprop(state, prop, &val); +-} +- +-static void savecursor(VTermState *state, int save) +-{ +- if (save) { +- state->saved.pos = state->pos; +- state->saved.mode.cursor_visible = state->mode.cursor_visible; +- state->saved.mode.cursor_blink = state->mode.cursor_blink; +- state->saved.mode.cursor_shape = state->mode.cursor_shape; +- +- vterm_state_savepen(state, 1); +- } else { +- VTermPos oldpos = state->pos; +- +- state->pos = state->saved.pos; +- +- settermprop_bool(state, VTERM_PROP_CURSORVISIBLE, state->saved.mode.cursor_visible); +- settermprop_bool(state, VTERM_PROP_CURSORBLINK, state->saved.mode.cursor_blink); +- settermprop_int(state, VTERM_PROP_CURSORSHAPE, state->saved.mode.cursor_shape); +- +- vterm_state_savepen(state, 0); +- +- updatecursor(state, &oldpos, 1); +- } +-} +- +-static int on_escape(const char *bytes, size_t len, void *user) +-{ +- VTermState *state = user; +- +- // Easier to decode this from the first byte, even though the final byte terminates it +- switch (bytes[0]) { +- case ' ': +- if (len != 2) { +- return 0; +- } +- +- switch (bytes[1]) { +- case 'F': // S7C1T +- state->vt->mode.ctrl8bit = 0; +- break; +- +- case 'G': // S8C1T +- state->vt->mode.ctrl8bit = 1; +- break; +- +- default: +- return 0; +- } +- return 2; +- +- case '#': +- if (len != 2) { +- return 0; +- } +- +- switch (bytes[1]) { +- case '3': // DECDHL top +- if (state->mode.leftrightmargin) { +- break; +- } +- set_lineinfo(state, state->pos.row, NO_FORCE, DWL_ON, DHL_TOP); +- break; +- +- case '4': // DECDHL bottom +- if (state->mode.leftrightmargin) { +- break; +- } +- set_lineinfo(state, state->pos.row, NO_FORCE, DWL_ON, DHL_BOTTOM); +- break; +- +- case '5': // DECSWL +- if (state->mode.leftrightmargin) { +- break; +- } +- set_lineinfo(state, state->pos.row, NO_FORCE, DWL_OFF, DHL_OFF); +- break; +- +- case '6': // DECDWL +- if (state->mode.leftrightmargin) { +- break; +- } +- set_lineinfo(state, state->pos.row, NO_FORCE, DWL_ON, DHL_OFF); +- break; +- +- case '8': // DECALN +- { +- VTermPos pos; +- schar_T E = schar_from_ascii('E'); // E +- for (pos.row = 0; pos.row < state->rows; pos.row++) { +- for (pos.col = 0; pos.col < ROWWIDTH(state, pos.row); pos.col++) { +- putglyph(state, E, 1, pos); +- } +- } +- break; +- } +- +- default: +- return 0; +- } +- return 2; +- +- case '(': +- case ')': +- case '*': +- case '+': // SCS +- if (len != 2) { +- return 0; +- } +- +- { +- int setnum = bytes[0] - 0x28; +- VTermEncoding *newenc = vterm_lookup_encoding(ENC_SINGLE_94, bytes[1]); +- +- if (newenc) { +- state->encoding[setnum].enc = newenc; +- +- if (newenc->init) { +- (*newenc->init)(newenc, state->encoding[setnum].data); +- } +- } +- } +- +- return 2; +- +- case '7': // DECSC +- savecursor(state, 1); +- return 1; +- +- case '8': // DECRC +- savecursor(state, 0); +- return 1; +- +- case '<': // Ignored by VT100. Used in VT52 mode to switch up to VT100 +- return 1; +- +- case '=': // DECKPAM +- state->mode.keypad = 1; +- return 1; +- +- case '>': // DECKPNM +- state->mode.keypad = 0; +- return 1; +- +- case 'c': // RIS - ECMA-48 8.3.105 +- { +- VTermPos oldpos = state->pos; +- vterm_state_reset(state, 1); +- if (state->callbacks && state->callbacks->movecursor) { +- (*state->callbacks->movecursor)(state->pos, oldpos, state->mode.cursor_visible, +- state->cbdata); +- } +- return 1; +- } +- +- case 'n': // LS2 - ECMA-48 8.3.78 +- state->gl_set = 2; +- return 1; +- +- case 'o': // LS3 - ECMA-48 8.3.80 +- state->gl_set = 3; +- return 1; +- +- case '~': // LS1R - ECMA-48 8.3.77 +- state->gr_set = 1; +- return 1; +- +- case '}': // LS2R - ECMA-48 8.3.79 +- state->gr_set = 2; +- return 1; +- +- case '|': // LS3R - ECMA-48 8.3.81 +- state->gr_set = 3; +- return 1; +- +- default: +- return 0; +- } +-} +- +-static void set_mode(VTermState *state, int num, int val) +-{ +- switch (num) { +- case 4: // IRM - ECMA-48 7.2.10 +- state->mode.insert = (unsigned)val; +- break; +- +- case 20: // LNM - ANSI X3.4-1977 +- state->mode.newline = (unsigned)val; +- break; +- +- default: +- DEBUG_LOG("libvterm: Unknown mode %d\n", num); +- return; +- } +-} +- +-static void set_dec_mode(VTermState *state, int num, int val) +-{ +- switch (num) { +- case 1: +- state->mode.cursor = (unsigned)val; +- break; +- +- case 5: // DECSCNM - screen mode +- settermprop_bool(state, VTERM_PROP_REVERSE, val); +- break; +- +- case 6: // DECOM - origin mode +- { +- VTermPos oldpos = state->pos; +- state->mode.origin = (unsigned)val; +- state->pos.row = state->mode.origin ? state->scrollregion_top : 0; +- state->pos.col = state->mode.origin ? SCROLLREGION_LEFT(state) : 0; +- updatecursor(state, &oldpos, 1); +- } +- break; +- +- case 7: +- state->mode.autowrap = (unsigned)val; +- break; +- +- case 12: +- settermprop_bool(state, VTERM_PROP_CURSORBLINK, val); +- break; +- +- case 25: +- settermprop_bool(state, VTERM_PROP_CURSORVISIBLE, val); +- break; +- +- case 69: // DECVSSM - vertical split screen mode +- // DECLRMM - left/right margin mode +- state->mode.leftrightmargin = (unsigned)val; +- if (val) { +- // Setting DECVSSM must clear doublewidth/doubleheight state of every line +- for (int row = 0; row < state->rows; row++) { +- set_lineinfo(state, row, FORCE, DWL_OFF, DHL_OFF); +- } +- } +- +- break; +- +- case 1000: +- case 1002: +- case 1003: +- settermprop_int(state, VTERM_PROP_MOUSE, +- !val ? VTERM_PROP_MOUSE_NONE +- : (num == 1000) ? VTERM_PROP_MOUSE_CLICK +- : (num == 1002) ? VTERM_PROP_MOUSE_DRAG +- : VTERM_PROP_MOUSE_MOVE); +- break; +- +- case 1004: +- settermprop_bool(state, VTERM_PROP_FOCUSREPORT, val); +- state->mode.report_focus = (unsigned)val; +- break; +- +- case 1005: +- state->mouse_protocol = val ? MOUSE_UTF8 : MOUSE_X10; +- break; +- +- case 1006: +- state->mouse_protocol = val ? MOUSE_SGR : MOUSE_X10; +- break; +- +- case 1015: +- state->mouse_protocol = val ? MOUSE_RXVT : MOUSE_X10; +- break; +- +- case 1047: +- settermprop_bool(state, VTERM_PROP_ALTSCREEN, val); +- break; +- +- case 1048: +- savecursor(state, val); +- break; +- +- case 1049: +- settermprop_bool(state, VTERM_PROP_ALTSCREEN, val); +- savecursor(state, val); +- break; +- +- case 2004: +- state->mode.bracketpaste = (unsigned)val; +- break; +- +- case 2026: // Synchronized output +- settermprop_bool(state, VTERM_PROP_SYNCOUTPUT, val); +- break; +- +- case 2031: +- settermprop_bool(state, VTERM_PROP_THEMEUPDATES, val); +- break; +- +- default: +- DEBUG_LOG("libvterm: Unknown DEC mode %d\n", num); +- return; +- } +-} +- +-static void request_dec_mode(VTermState *state, int num) +-{ +- int reply; +- +- switch (num) { +- case 1: +- reply = state->mode.cursor; +- break; +- +- case 5: +- reply = state->mode.screen; +- break; +- +- case 6: +- reply = state->mode.origin; +- break; +- +- case 7: +- reply = state->mode.autowrap; +- break; +- +- case 12: +- reply = state->mode.cursor_blink; +- break; +- +- case 25: +- reply = state->mode.cursor_visible; +- break; +- +- case 69: +- reply = state->mode.leftrightmargin; +- break; +- +- case 1000: +- reply = state->mouse_flags == MOUSE_WANT_CLICK; +- break; +- +- case 1002: +- reply = state->mouse_flags == (MOUSE_WANT_CLICK|MOUSE_WANT_DRAG); +- break; +- +- case 1003: +- reply = state->mouse_flags == (MOUSE_WANT_CLICK|MOUSE_WANT_MOVE); +- break; +- +- case 1004: +- reply = state->mode.report_focus; +- break; +- +- case 1005: +- reply = state->mouse_protocol == MOUSE_UTF8; +- break; +- +- case 1006: +- reply = state->mouse_protocol == MOUSE_SGR; +- break; +- +- case 1015: +- reply = state->mouse_protocol == MOUSE_RXVT; +- break; +- +- case 1047: +- reply = state->mode.alt_screen; +- break; +- +- case 2004: +- reply = state->mode.bracketpaste; +- break; +- +- case 2026: +- reply = state->mode.synchronized_output; +- break; +- +- case 2031: +- reply = state->mode.theme_updates; +- break; +- +- default: +- vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%d;%d$y", num, 0); +- return; +- } +- +- vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%d;%d$y", num, reply ? 1 : 2); +-} +- +-static void request_version_string(VTermState *state) +-{ +- vterm_push_output_sprintf_str(state->vt, C1_DCS, true, ">|libvterm(%d.%d)", +- VTERM_VERSION_MAJOR, VTERM_VERSION_MINOR); +-} +- +-static void request_key_encoding_flags(VTermState *state) +-{ +- int screen = state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY; +- struct VTermKeyEncodingStack *stack = &state->key_encoding_stacks[screen]; +- +- int reply = 0; +- +- assert(stack->size > 0); +- VTermKeyEncodingFlags flags = stack->items[stack->size - 1]; +- +- if (flags.disambiguate) { +- reply |= KEY_ENCODING_DISAMBIGUATE; +- } +- +- if (flags.report_events) { +- reply |= KEY_ENCODING_REPORT_EVENTS; +- } +- +- if (flags.report_alternate) { +- reply |= KEY_ENCODING_REPORT_ALTERNATE; +- } +- +- if (flags.report_all_keys) { +- reply |= KEY_ENCODING_REPORT_ALL_KEYS; +- } +- +- if (flags.report_associated) { +- reply |= KEY_ENCODING_REPORT_ASSOCIATED; +- } +- +- vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%du", reply); +-} +- +-static void set_key_encoding_flags(VTermState *state, int arg, int mode) +-{ +- // When mode is 3, bits set in arg reset the corresponding mode +- bool set = mode != 3; +- +- // When mode is 1, unset bits are reset +- bool reset_unset = mode == 1; +- +- struct VTermKeyEncodingFlags flags = { 0 }; +- if (arg & KEY_ENCODING_DISAMBIGUATE) { +- flags.disambiguate = set; +- } else if (reset_unset) { +- flags.disambiguate = false; +- } +- +- if (arg & KEY_ENCODING_REPORT_EVENTS) { +- flags.report_events = set; +- } else if (reset_unset) { +- flags.report_events = false; +- } +- +- if (arg & KEY_ENCODING_REPORT_ALTERNATE) { +- flags.report_alternate = set; +- } else if (reset_unset) { +- flags.report_alternate = false; +- } +- if (arg & KEY_ENCODING_REPORT_ALL_KEYS) { +- flags.report_all_keys = set; +- } else if (reset_unset) { +- flags.report_all_keys = false; +- } +- +- if (arg & KEY_ENCODING_REPORT_ASSOCIATED) { +- flags.report_associated = set; +- } else if (reset_unset) { +- flags.report_associated = false; +- } +- +- int screen = state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY; +- struct VTermKeyEncodingStack *stack = &state->key_encoding_stacks[screen]; +- assert(stack->size > 0); +- stack->items[stack->size - 1] = flags; +-} +- +-static void push_key_encoding_flags(VTermState *state, int arg) +-{ +- int screen = state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY; +- struct VTermKeyEncodingStack *stack = &state->key_encoding_stacks[screen]; +- assert(stack->size <= ARRAY_SIZE(stack->items)); +- +- if (stack->size == ARRAY_SIZE(stack->items)) { +- // Evict oldest entry when stack is full +- for (size_t i = 0; i < ARRAY_SIZE(stack->items) - 1; i++) { +- stack->items[i] = stack->items[i + 1]; +- } +- } else { +- stack->size++; +- } +- +- set_key_encoding_flags(state, arg, 1); +-} +- +-static void pop_key_encoding_flags(VTermState *state, int arg) +-{ +- int screen = state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY; +- struct VTermKeyEncodingStack *stack = &state->key_encoding_stacks[screen]; +- if (arg >= stack->size) { +- stack->size = 1; +- +- // If a pop request is received that empties the stack, all flags are reset. +- memset(&stack->items[0], 0, sizeof(stack->items[0])); +- } else if (arg > 0) { +- stack->size -= arg; +- } +-} +- +-static int on_csi(const char *leader, const long args[], int argcount, const char *intermed, +- char command, void *user) +-{ +- VTermState *state = user; +- int leader_byte = 0; +- int intermed_byte = 0; +- int cancel_phantom = 1; +- +- if (leader && leader[0]) { +- if (leader[1]) { // longer than 1 char +- return 0; +- } +- +- switch (leader[0]) { +- case '?': +- case '>': +- case '<': +- case '=': +- leader_byte = (int)leader[0]; +- break; +- default: +- return 0; +- } +- } +- +- if (intermed && intermed[0]) { +- if (intermed[1]) { // longer than 1 char +- return 0; +- } +- +- switch (intermed[0]) { +- case ' ': +- case '!': +- case '"': +- case '$': +- case '\'': +- intermed_byte = (int)intermed[0]; +- break; +- default: +- return 0; +- } +- } +- +- VTermPos oldpos = state->pos; +- +- // Some temporaries for later code +- int count, val; +- int row, col; +- VTermRect rect; +- int selective; +- +-#define LBOUND(v, min) if ((v) < (min))(v) = (min) +-#define UBOUND(v, max) if ((v) > (max))(v) = (max) +- +-#define LEADER(l, b) ((l << 8) | b) +-#define INTERMED(i, b) ((i << 16) | b) +- +- switch (intermed_byte << 16 | leader_byte << 8 | command) { +- case 0x40: // ICH - ECMA-48 8.3.64 +- count = CSI_ARG_COUNT(args[0]); +- +- if (!is_cursor_in_scrollregion(state)) { +- break; +- } +- +- rect.start_row = state->pos.row; +- rect.end_row = state->pos.row + 1; +- rect.start_col = state->pos.col; +- if (state->mode.leftrightmargin) { +- rect.end_col = SCROLLREGION_RIGHT(state); +- } else { +- rect.end_col = THISROWWIDTH(state); +- } +- +- scroll(state, rect, 0, -count); +- +- break; +- +- case 0x41: // CUU - ECMA-48 8.3.22 +- count = CSI_ARG_COUNT(args[0]); +- state->pos.row -= count; +- state->at_phantom = 0; +- break; +- +- case 0x42: // CUD - ECMA-48 8.3.19 +- count = CSI_ARG_COUNT(args[0]); +- state->pos.row += count; +- state->at_phantom = 0; +- break; +- +- case 0x43: // CUF - ECMA-48 8.3.20 +- count = CSI_ARG_COUNT(args[0]); +- state->pos.col += count; +- state->at_phantom = 0; +- break; +- +- case 0x44: // CUB - ECMA-48 8.3.18 +- count = CSI_ARG_COUNT(args[0]); +- state->pos.col -= count; +- state->at_phantom = 0; +- break; +- +- case 0x45: // CNL - ECMA-48 8.3.12 +- count = CSI_ARG_COUNT(args[0]); +- state->pos.col = 0; +- state->pos.row += count; +- state->at_phantom = 0; +- break; +- +- case 0x46: // CPL - ECMA-48 8.3.13 +- count = CSI_ARG_COUNT(args[0]); +- state->pos.col = 0; +- state->pos.row -= count; +- state->at_phantom = 0; +- break; +- +- case 0x47: // CHA - ECMA-48 8.3.9 +- val = CSI_ARG_OR(args[0], 1); +- state->pos.col = val - 1; +- state->at_phantom = 0; +- break; +- +- case 0x48: // CUP - ECMA-48 8.3.21 +- row = CSI_ARG_OR(args[0], 1); +- col = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? 1 : CSI_ARG(args[1]); +- // zero-based +- state->pos.row = row - 1; +- state->pos.col = col - 1; +- if (state->mode.origin) { +- state->pos.row += state->scrollregion_top; +- state->pos.col += SCROLLREGION_LEFT(state); +- } +- state->at_phantom = 0; +- break; +- +- case 0x49: // CHT - ECMA-48 8.3.10 +- count = CSI_ARG_COUNT(args[0]); +- tab(state, count, +1); +- break; +- +- case 0x4a: // ED - ECMA-48 8.3.39 +- case LEADER('?', 0x4a): // DECSED - Selective Erase in Display +- selective = (leader_byte == '?'); +- switch (CSI_ARG(args[0])) { +- case CSI_ARG_MISSING: +- case 0: +- rect.start_row = state->pos.row; rect.end_row = state->pos.row + 1; +- rect.start_col = state->pos.col; rect.end_col = state->cols; +- if (rect.end_col > rect.start_col) { +- erase(state, rect, selective); +- } +- +- rect.start_row = state->pos.row + 1; rect.end_row = state->rows; +- rect.start_col = 0; +- for (int row_ = rect.start_row; row_ < rect.end_row; row_++) { +- set_lineinfo(state, row_, FORCE, DWL_OFF, DHL_OFF); +- } +- if (rect.end_row > rect.start_row) { +- erase(state, rect, selective); +- } +- break; +- +- case 1: +- rect.start_row = 0; rect.end_row = state->pos.row; +- rect.start_col = 0; rect.end_col = state->cols; +- for (int row_ = rect.start_row; row_ < rect.end_row; row_++) { +- set_lineinfo(state, row_, FORCE, DWL_OFF, DHL_OFF); +- } +- if (rect.end_col > rect.start_col) { +- erase(state, rect, selective); +- } +- +- rect.start_row = state->pos.row; rect.end_row = state->pos.row + 1; +- rect.end_col = state->pos.col + 1; +- if (rect.end_row > rect.start_row) { +- erase(state, rect, selective); +- } +- break; +- +- case 2: +- rect.start_row = 0; rect.end_row = state->rows; +- rect.start_col = 0; rect.end_col = state->cols; +- for (int row_ = rect.start_row; row_ < rect.end_row; row_++) { +- set_lineinfo(state, row_, FORCE, DWL_OFF, DHL_OFF); +- } +- erase(state, rect, selective); +- break; +- +- case 3: +- if (state->callbacks && state->callbacks->sb_clear) { +- if ((*state->callbacks->sb_clear)(state->cbdata)) { +- return 1; +- } +- } +- break; +- } +- break; +- +- case 0x4b: // EL - ECMA-48 8.3.41 +- case LEADER('?', 0x4b): // DECSEL - Selective Erase in Line +- selective = (leader_byte == '?'); +- rect.start_row = state->pos.row; +- rect.end_row = state->pos.row + 1; +- +- switch (CSI_ARG(args[0])) { +- case CSI_ARG_MISSING: +- case 0: +- rect.start_col = state->pos.col; rect.end_col = THISROWWIDTH(state); break; +- case 1: +- rect.start_col = 0; rect.end_col = state->pos.col + 1; break; +- case 2: +- rect.start_col = 0; rect.end_col = THISROWWIDTH(state); break; +- default: +- return 0; +- } +- +- if (rect.end_col > rect.start_col) { +- erase(state, rect, selective); +- } +- +- break; +- +- case 0x4c: // IL - ECMA-48 8.3.67 +- count = CSI_ARG_COUNT(args[0]); +- +- if (!is_cursor_in_scrollregion(state)) { +- break; +- } +- +- rect.start_row = state->pos.row; +- rect.end_row = SCROLLREGION_BOTTOM(state); +- rect.start_col = SCROLLREGION_LEFT(state); +- rect.end_col = SCROLLREGION_RIGHT(state); +- +- scroll(state, rect, -count, 0); +- +- break; +- +- case 0x4d: // DL - ECMA-48 8.3.32 +- count = CSI_ARG_COUNT(args[0]); +- +- if (!is_cursor_in_scrollregion(state)) { +- break; +- } +- +- rect.start_row = state->pos.row; +- rect.end_row = SCROLLREGION_BOTTOM(state); +- rect.start_col = SCROLLREGION_LEFT(state); +- rect.end_col = SCROLLREGION_RIGHT(state); +- +- scroll(state, rect, count, 0); +- +- break; +- +- case 0x50: // DCH - ECMA-48 8.3.26 +- count = CSI_ARG_COUNT(args[0]); +- +- if (!is_cursor_in_scrollregion(state)) { +- break; +- } +- +- rect.start_row = state->pos.row; +- rect.end_row = state->pos.row + 1; +- rect.start_col = state->pos.col; +- if (state->mode.leftrightmargin) { +- rect.end_col = SCROLLREGION_RIGHT(state); +- } else { +- rect.end_col = THISROWWIDTH(state); +- } +- +- scroll(state, rect, 0, count); +- +- break; +- +- case 0x53: // SU - ECMA-48 8.3.147 +- count = CSI_ARG_COUNT(args[0]); +- +- rect.start_row = state->scrollregion_top; +- rect.end_row = SCROLLREGION_BOTTOM(state); +- rect.start_col = SCROLLREGION_LEFT(state); +- rect.end_col = SCROLLREGION_RIGHT(state); +- +- scroll(state, rect, count, 0); +- +- break; +- +- case 0x54: // SD - ECMA-48 8.3.113 +- count = CSI_ARG_COUNT(args[0]); +- +- rect.start_row = state->scrollregion_top; +- rect.end_row = SCROLLREGION_BOTTOM(state); +- rect.start_col = SCROLLREGION_LEFT(state); +- rect.end_col = SCROLLREGION_RIGHT(state); +- +- scroll(state, rect, -count, 0); +- +- break; +- +- case 0x58: // ECH - ECMA-48 8.3.38 +- count = CSI_ARG_COUNT(args[0]); +- +- rect.start_row = state->pos.row; +- rect.end_row = state->pos.row + 1; +- rect.start_col = state->pos.col; +- rect.end_col = state->pos.col + count; +- UBOUND(rect.end_col, THISROWWIDTH(state)); +- +- erase(state, rect, 0); +- break; +- +- case 0x5a: // CBT - ECMA-48 8.3.7 +- count = CSI_ARG_COUNT(args[0]); +- tab(state, count, -1); +- break; +- +- case 0x60: // HPA - ECMA-48 8.3.57 +- col = CSI_ARG_OR(args[0], 1); +- state->pos.col = col - 1; +- state->at_phantom = 0; +- break; +- +- case 0x61: // HPR - ECMA-48 8.3.59 +- count = CSI_ARG_COUNT(args[0]); +- state->pos.col += count; +- state->at_phantom = 0; +- break; +- +- case 0x62: { // REP - ECMA-48 8.3.103 +- const int row_width = THISROWWIDTH(state); +- count = CSI_ARG_COUNT(args[0]); +- col = state->pos.col + count; +- UBOUND(col, row_width); +- schar_T sc = schar_from_buf(state->grapheme_buf, state->grapheme_len); +- while (state->pos.col < col) { +- putglyph(state, sc, state->combine_width, state->pos); +- state->pos.col += state->combine_width; +- } +- if (state->pos.col + state->combine_width >= row_width) { +- if (state->mode.autowrap) { +- state->at_phantom = 1; +- cancel_phantom = 0; +- } +- } +- break; +- } +- +- case 0x63: // DA - ECMA-48 8.3.24 +- val = CSI_ARG_OR(args[0], 0); +- if (val == 0) { +- // DEC VT100 response +- vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%sc", vterm_primary_device_attr); +- } +- break; +- +- case LEADER('>', 0x63): // DEC secondary Device Attributes +- vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, ">%d;%d;%dc", 0, 100, 0); +- break; +- +- case 0x64: // VPA - ECMA-48 8.3.158 +- row = CSI_ARG_OR(args[0], 1); +- state->pos.row = row - 1; +- if (state->mode.origin) { +- state->pos.row += state->scrollregion_top; +- } +- state->at_phantom = 0; +- break; +- +- case 0x65: // VPR - ECMA-48 8.3.160 +- count = CSI_ARG_COUNT(args[0]); +- state->pos.row += count; +- state->at_phantom = 0; +- break; +- +- case 0x66: // HVP - ECMA-48 8.3.63 +- row = CSI_ARG_OR(args[0], 1); +- col = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? 1 : CSI_ARG(args[1]); +- // zero-based +- state->pos.row = row - 1; +- state->pos.col = col - 1; +- if (state->mode.origin) { +- state->pos.row += state->scrollregion_top; +- state->pos.col += SCROLLREGION_LEFT(state); +- } +- state->at_phantom = 0; +- break; +- +- case 0x67: // TBC - ECMA-48 8.3.154 +- val = CSI_ARG_OR(args[0], 0); +- +- switch (val) { +- case 0: +- clear_col_tabstop(state, state->pos.col); +- break; +- case 3: +- case 5: +- for (col = 0; col < state->cols; col++) { +- clear_col_tabstop(state, col); +- } +- break; +- case 1: +- case 2: +- case 4: +- break; +- // TODO(vterm): 1, 2 and 4 aren't meaningful yet without line tab stops +- default: +- return 0; +- } +- break; +- +- case 0x68: // SM - ECMA-48 8.3.125 +- if (!CSI_ARG_IS_MISSING(args[0])) { +- set_mode(state, CSI_ARG(args[0]), 1); +- } +- break; +- +- case LEADER('?', 0x68): // DEC private mode set +- for (int i = 0; i < argcount; i++) { +- if (!CSI_ARG_IS_MISSING(args[i])) { +- set_dec_mode(state, CSI_ARG(args[i]), 1); +- } +- } +- break; +- +- case 0x6a: // HPB - ECMA-48 8.3.58 +- count = CSI_ARG_COUNT(args[0]); +- state->pos.col -= count; +- state->at_phantom = 0; +- break; +- +- case 0x6b: // VPB - ECMA-48 8.3.159 +- count = CSI_ARG_COUNT(args[0]); +- state->pos.row -= count; +- state->at_phantom = 0; +- break; +- +- case 0x6c: // RM - ECMA-48 8.3.106 +- if (!CSI_ARG_IS_MISSING(args[0])) { +- set_mode(state, CSI_ARG(args[0]), 0); +- } +- break; +- +- case LEADER('?', 0x6c): // DEC private mode reset +- for (int i = 0; i < argcount; i++) { +- if (!CSI_ARG_IS_MISSING(args[i])) { +- set_dec_mode(state, CSI_ARG(args[i]), 0); +- } +- } +- break; +- +- case 0x6d: // SGR - ECMA-48 8.3.117 +- vterm_state_setpen(state, args, argcount); +- break; +- +- case LEADER('?', 0x6d): // DECSGR +- // No actual DEC terminal recognised these, but some printers did. These are alternative ways to +- // request subscript/superscript/off +- for (int argi = 0; argi < argcount; argi++) { +- long arg; +- switch (arg = CSI_ARG(args[argi])) { +- case 4: // Superscript on +- arg = 73; +- vterm_state_setpen(state, &arg, 1); +- break; +- case 5: // Subscript on +- arg = 74; +- vterm_state_setpen(state, &arg, 1); +- break; +- case 24: // Super+subscript off +- arg = 75; +- vterm_state_setpen(state, &arg, 1); +- break; +- } +- } +- break; +- +- case 0x6e: // DSR - ECMA-48 8.3.35 +- case LEADER('?', 0x6e): // DECDSR +- val = CSI_ARG_OR(args[0], 0); +- +- { +- char *qmark = (leader_byte == '?') ? "?" : ""; +- bool dark = false; +- +- switch (val) { +- case 0: +- case 1: +- case 2: +- case 3: +- case 4: +- // ignore - these are replies +- break; +- case 5: +- vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%s0n", qmark); +- break; +- case 6: // CPR - cursor position report +- vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%s%d;%dR", qmark, state->pos.row + 1, +- state->pos.col + 1); +- break; +- case 996: +- if (state->callbacks && state->callbacks->theme) { +- if (state->callbacks->theme(&dark, state->cbdata)) { +- vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?997;%cn", dark ? '1' : '2'); +- } +- } +- break; +- } +- } +- break; +- +- case INTERMED('!', 0x70): // DECSTR - DEC soft terminal reset +- vterm_state_reset(state, 0); +- break; +- +- case LEADER('?', INTERMED('$', 0x70)): +- request_dec_mode(state, CSI_ARG(args[0])); +- break; +- +- case LEADER('>', 0x71): // XTVERSION - xterm query version string +- request_version_string(state); +- break; +- +- case INTERMED(' ', 0x71): // DECSCUSR - DEC set cursor shape +- val = CSI_ARG_OR(args[0], 1); +- +- switch (val) { +- case 0: +- case 1: +- settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1); +- settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BLOCK); +- break; +- case 2: +- settermprop_bool(state, VTERM_PROP_CURSORBLINK, 0); +- settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BLOCK); +- break; +- case 3: +- settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1); +- settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_UNDERLINE); +- break; +- case 4: +- settermprop_bool(state, VTERM_PROP_CURSORBLINK, 0); +- settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_UNDERLINE); +- break; +- case 5: +- settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1); +- settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BAR_LEFT); +- break; +- case 6: +- settermprop_bool(state, VTERM_PROP_CURSORBLINK, 0); +- settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BAR_LEFT); +- break; +- } +- +- break; +- +- case INTERMED('"', 0x71): // DECSCA - DEC select character protection attribute +- val = CSI_ARG_OR(args[0], 0); +- +- switch (val) { +- case 0: +- case 2: +- state->protected_cell = 0; +- break; +- case 1: +- state->protected_cell = 1; +- break; +- } +- +- break; +- +- case 0x72: // DECSTBM - DEC custom +- state->scrollregion_top = CSI_ARG_OR(args[0], 1) - 1; +- state->scrollregion_bottom = argcount < 2 +- || CSI_ARG_IS_MISSING(args[1]) ? -1 : CSI_ARG(args[1]); +- LBOUND(state->scrollregion_top, 0); +- UBOUND(state->scrollregion_top, state->rows); +- LBOUND(state->scrollregion_bottom, -1); +- if (state->scrollregion_top == 0 && state->scrollregion_bottom == state->rows) { +- state->scrollregion_bottom = -1; +- } else { +- UBOUND(state->scrollregion_bottom, state->rows); +- } +- +- if (SCROLLREGION_BOTTOM(state) <= state->scrollregion_top) { +- // Invalid +- state->scrollregion_top = 0; +- state->scrollregion_bottom = -1; +- } +- +- // Setting the scrolling region restores the cursor to the home position +- state->pos.row = 0; +- state->pos.col = 0; +- if (state->mode.origin) { +- state->pos.row += state->scrollregion_top; +- state->pos.col += SCROLLREGION_LEFT(state); +- } +- +- break; +- +- case 0x73: // DECSLRM - DEC custom +- // Always allow setting these margins, just they won't take effect without DECVSSM +- state->scrollregion_left = CSI_ARG_OR(args[0], 1) - 1; +- state->scrollregion_right = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? -1 : CSI_ARG(args[1]); +- LBOUND(state->scrollregion_left, 0); +- UBOUND(state->scrollregion_left, state->cols); +- LBOUND(state->scrollregion_right, -1); +- if (state->scrollregion_left == 0 && state->scrollregion_right == state->cols) { +- state->scrollregion_right = -1; +- } else { +- UBOUND(state->scrollregion_right, state->cols); +- } +- +- if (state->scrollregion_right > -1 +- && state->scrollregion_right <= state->scrollregion_left) { +- // Invalid +- state->scrollregion_left = 0; +- state->scrollregion_right = -1; +- } +- +- // Setting the scrolling region restores the cursor to the home position +- state->pos.row = 0; +- state->pos.col = 0; +- if (state->mode.origin) { +- state->pos.row += state->scrollregion_top; +- state->pos.col += SCROLLREGION_LEFT(state); +- } +- +- break; +- +- case LEADER('?', 0x75): // Kitty query +- request_key_encoding_flags(state); +- break; +- +- case LEADER('>', 0x75): // Kitty push flags +- push_key_encoding_flags(state, CSI_ARG_OR(args[0], 0)); +- break; +- +- case LEADER('<', 0x75): // Kitty pop flags +- pop_key_encoding_flags(state, CSI_ARG_OR(args[0], 1)); +- break; +- +- case LEADER('=', 0x75): // Kitty set flags +- val = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? 1 : CSI_ARG(args[1]); +- set_key_encoding_flags(state, CSI_ARG_OR(args[0], 0), val); +- break; +- +- case INTERMED('\'', 0x7D): // DECIC +- count = CSI_ARG_COUNT(args[0]); +- +- if (!is_cursor_in_scrollregion(state)) { +- break; +- } +- +- rect.start_row = state->scrollregion_top; +- rect.end_row = SCROLLREGION_BOTTOM(state); +- rect.start_col = state->pos.col; +- rect.end_col = SCROLLREGION_RIGHT(state); +- +- scroll(state, rect, 0, -count); +- +- break; +- +- case INTERMED('\'', 0x7E): // DECDC +- count = CSI_ARG_COUNT(args[0]); +- +- if (!is_cursor_in_scrollregion(state)) { +- break; +- } +- +- rect.start_row = state->scrollregion_top; +- rect.end_row = SCROLLREGION_BOTTOM(state); +- rect.start_col = state->pos.col; +- rect.end_col = SCROLLREGION_RIGHT(state); +- +- scroll(state, rect, 0, count); +- +- break; +- +- default: +- if (state->fallbacks && state->fallbacks->csi) { +- if ((*state->fallbacks->csi)(leader, args, argcount, intermed, command, state->fbdata)) { +- return 1; +- } +- } +- +- return 0; +- } +- +- if (state->mode.origin) { +- LBOUND(state->pos.row, state->scrollregion_top); +- UBOUND(state->pos.row, SCROLLREGION_BOTTOM(state) - 1); +- LBOUND(state->pos.col, SCROLLREGION_LEFT(state)); +- UBOUND(state->pos.col, SCROLLREGION_RIGHT(state) - 1); +- } else { +- LBOUND(state->pos.row, 0); +- UBOUND(state->pos.row, state->rows - 1); +- LBOUND(state->pos.col, 0); +- UBOUND(state->pos.col, THISROWWIDTH(state) - 1); +- } +- +- updatecursor(state, &oldpos, cancel_phantom); +- +-#ifdef DEBUG +- if (state->pos.row < 0 || state->pos.row >= state->rows +- || state->pos.col < 0 || state->pos.col >= state->cols) { +- fprintf(stderr, "Position out of bounds after CSI %c: (%d,%d)\n", +- command, state->pos.row, state->pos.col); +- abort(); +- } +- +- if (SCROLLREGION_BOTTOM(state) <= state->scrollregion_top) { +- fprintf(stderr, "Scroll region height out of bounds after CSI %c: %d <= %d\n", +- command, SCROLLREGION_BOTTOM(state), state->scrollregion_top); +- abort(); +- } +- +- if (SCROLLREGION_RIGHT(state) <= SCROLLREGION_LEFT(state)) { +- fprintf(stderr, "Scroll region width out of bounds after CSI %c: %d <= %d\n", +- command, SCROLLREGION_RIGHT(state), SCROLLREGION_LEFT(state)); +- abort(); +- } +-#endif +- +- return 1; +-} +- +-static uint8_t unbase64one(char c) +-{ +- if (c >= 'A' && c <= 'Z') { +- return (uint8_t)c - 'A'; +- } else if (c >= 'a' && c <= 'z') { +- return (uint8_t)c - 'a' + 26; +- } else if (c >= '0' && c <= '9') { +- return (uint8_t)c - '0' + 52; +- } else if (c == '+') { +- return 62; +- } else if (c == '/') { +- return 63; +- } +- +- return 0xFF; +-} +- +-static void osc_selection(VTermState *state, VTermStringFragment frag) +-{ +- if (frag.initial) { +- state->tmp.selection.mask = 0; +- state->tmp.selection.state = SELECTION_INITIAL; +- } +- +- while (!state->tmp.selection.state && frag.len) { +- // Parse selection parameter +- switch (frag.str[0]) { +- case 'c': +- state->tmp.selection.mask |= VTERM_SELECTION_CLIPBOARD; +- break; +- case 'p': +- state->tmp.selection.mask |= VTERM_SELECTION_PRIMARY; +- break; +- case 'q': +- state->tmp.selection.mask |= VTERM_SELECTION_SECONDARY; +- break; +- case 's': +- state->tmp.selection.mask |= VTERM_SELECTION_SELECT; +- break; +- case '0': +- case '1': +- case '2': +- case '3': +- case '4': +- case '5': +- case '6': +- case '7': +- state->tmp.selection.mask |= (VTERM_SELECTION_CUT0 << (frag.str[0] - '0')); +- break; +- +- case ';': +- state->tmp.selection.state = SELECTION_SELECTED; +- if (!state->tmp.selection.mask) { +- state->tmp.selection.mask = VTERM_SELECTION_SELECT|VTERM_SELECTION_CUT0; +- } +- break; +- } +- +- frag.str++; +- frag.len--; +- } +- +- if (!frag.len) { +- // Clear selection if we're already finished but didn't do anything +- if (frag.final && state->selection.callbacks->set) { +- (*state->selection.callbacks->set)(state->tmp.selection.mask, (VTermStringFragment){ +- .str = NULL, +- .len = 0, +- .initial = state->tmp.selection.state != SELECTION_SET, +- .final = true, +- }, state->selection.user); +- } +- return; +- } +- +- if (state->tmp.selection.state == SELECTION_SELECTED) { +- if (frag.str[0] == '?') { +- state->tmp.selection.state = SELECTION_QUERY; +- } else { +- state->tmp.selection.state = SELECTION_SET_INITIAL; +- state->tmp.selection.recvpartial = 0; +- } +- } +- +- if (state->tmp.selection.state == SELECTION_QUERY) { +- if (state->selection.callbacks->query) { +- (*state->selection.callbacks->query)(state->tmp.selection.mask, state->selection.user); +- } +- return; +- } +- +- if (state->tmp.selection.state == SELECTION_INVALID) { +- return; +- } +- +- if (state->selection.callbacks->set) { +- size_t bufcur = 0; +- char *buffer = state->selection.buffer; +- +- uint32_t x = 0; // Current decoding value +- int n = 0; // Number of sextets consumed +- +- if (state->tmp.selection.recvpartial) { +- n = state->tmp.selection.recvpartial >> 24; +- x = state->tmp.selection.recvpartial & 0x03FFFF; // could be up to 18 bits of state in here +- +- state->tmp.selection.recvpartial = 0; +- } +- +- while ((state->selection.buflen - bufcur) >= 3 && frag.len) { +- if (frag.str[0] == '=') { +- if (n == 2) { +- buffer[0] = (char)(x >> 4 & 0xFF); +- buffer += 1, bufcur += 1; +- } +- if (n == 3) { +- buffer[0] = (char)(x >> 10 & 0xFF); +- buffer[1] = (char)(x >> 2 & 0xFF); +- buffer += 2, bufcur += 2; +- } +- +- while (frag.len && frag.str[0] == '=') { +- frag.str++, frag.len--; +- } +- +- n = 0; +- } else { +- uint8_t b = unbase64one(frag.str[0]); +- if (b == 0xFF) { +- DEBUG_LOG("base64decode bad input %02X\n", (uint8_t)frag.str[0]); +- +- state->tmp.selection.state = SELECTION_INVALID; +- if (state->selection.callbacks->set) { +- (*state->selection.callbacks->set)(state->tmp.selection.mask, (VTermStringFragment){ +- .str = NULL, +- .len = 0, +- .initial = true, +- .final = true, +- }, state->selection.user); +- } +- break; +- } +- +- x = (x << 6) | b; +- n++; +- frag.str++, frag.len--; +- +- if (n == 4) { +- buffer[0] = (char)(x >> 16 & 0xFF); +- buffer[1] = (char)(x >> 8 & 0xFF); +- buffer[2] = (char)(x >> 0 & 0xFF); +- +- buffer += 3, bufcur += 3; +- x = 0; +- n = 0; +- } +- } +- +- if (!frag.len || (state->selection.buflen - bufcur) < 3) { +- if (bufcur) { +- (*state->selection.callbacks->set)(state->tmp.selection.mask, (VTermStringFragment){ +- .str = state->selection.buffer, +- .len = bufcur, +- .initial = state->tmp.selection.state == SELECTION_SET_INITIAL, +- .final = frag.final && !frag.len, +- }, state->selection.user); +- state->tmp.selection.state = SELECTION_SET; +- } +- +- buffer = state->selection.buffer; +- bufcur = 0; +- } +- } +- +- if (n) { +- state->tmp.selection.recvpartial = (uint32_t)(n << 24) | x; +- } +- } +-} +- +-static int on_osc(int command, VTermStringFragment frag, void *user) +-{ +- VTermState *state = user; +- +- switch (command) { +- case 0: +- settermprop_string(state, VTERM_PROP_ICONNAME, frag); +- settermprop_string(state, VTERM_PROP_TITLE, frag); +- break; +- +- case 1: +- settermprop_string(state, VTERM_PROP_ICONNAME, frag); +- break; +- +- case 2: +- settermprop_string(state, VTERM_PROP_TITLE, frag); +- break; +- +- case 52: +- if (state->selection.callbacks) { +- osc_selection(state, frag); +- } +- break; +- } +- +- if (state->fallbacks && state->fallbacks->osc) { +- if ((*state->fallbacks->osc)(command, frag, state->fbdata)) { +- return 1; +- } +- } +- +- return 0; +-} +- +-static void request_status_string(VTermState *state, VTermStringFragment frag) +-{ +- VTerm *vt = state->vt; +- +- char *tmp = state->tmp.decrqss; +- +- if (frag.initial) { +- tmp[0] = tmp[1] = tmp[2] = tmp[3] = 0; +- } +- +- size_t i = 0; +- while (i < sizeof(state->tmp.decrqss) - 1 && tmp[i]) { +- i++; +- } +- while (i < sizeof(state->tmp.decrqss) - 1 && frag.len--) { +- tmp[i++] = (frag.str++)[0]; +- } +- tmp[i] = 0; +- +- if (!frag.final) { +- return; +- } +- +- switch (tmp[0] | tmp[1] << 8 | tmp[2] << 16) { +- case 'm': { +- // Query SGR +- long args[20]; +- int argc = vterm_state_getpen(state, args, sizeof(args)/sizeof(args[0])); +- size_t cur = 0; +- +- cur += (size_t)snprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, +- vt->mode.ctrl8bit ? "\x90" "1$r" : ESC_S "P" "1$r"); // DCS 1$r ... +- if (cur >= vt->tmpbuffer_len) { +- return; +- } +- +- for (int argi = 0; argi < argc; argi++) { +- cur += (size_t)snprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, +- argi == argc - 1 ? "%ld" +- : CSI_ARG_HAS_MORE(args[argi]) ? "%ld:" +- : "%ld;", +- CSI_ARG(args[argi])); +- if (cur >= vt->tmpbuffer_len) { +- return; +- } +- } +- +- cur += (size_t)snprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, +- vt->mode.ctrl8bit ? "m" "\x9C" : "m" ESC_S "\\"); // ... m ST +- if (cur >= vt->tmpbuffer_len) { +- return; +- } +- +- vterm_push_output_bytes(vt, vt->tmpbuffer, cur); +- return; +- } +- +- case 'r': +- // Query DECSTBM +- vterm_push_output_sprintf_str(vt, C1_DCS, true, +- "1$r%d;%dr", state->scrollregion_top + 1, +- SCROLLREGION_BOTTOM(state)); +- return; +- +- case 's': +- // Query DECSLRM +- vterm_push_output_sprintf_str(vt, C1_DCS, true, +- "1$r%d;%ds", SCROLLREGION_LEFT(state) + 1, +- SCROLLREGION_RIGHT(state)); +- return; +- +- case ' '|('q' << 8): { +- // Query DECSCUSR +- int reply = 0; +- switch (state->mode.cursor_shape) { +- case VTERM_PROP_CURSORSHAPE_BLOCK: +- reply = 2; break; +- case VTERM_PROP_CURSORSHAPE_UNDERLINE: +- reply = 4; break; +- case VTERM_PROP_CURSORSHAPE_BAR_LEFT: +- reply = 6; break; +- } +- if (state->mode.cursor_blink) { +- reply--; +- } +- vterm_push_output_sprintf_str(vt, C1_DCS, true, +- "1$r%d q", reply); +- return; +- } +- +- case '\"'|('q' << 8): +- // Query DECSCA +- vterm_push_output_sprintf_str(vt, C1_DCS, true, +- "1$r%d\"q", state->protected_cell ? 1 : 2); +- return; +- } +- +- vterm_push_output_sprintf_str(state->vt, C1_DCS, true, "0$r"); +-} +- +-static int on_dcs(const char *command, size_t commandlen, VTermStringFragment frag, void *user) +-{ +- VTermState *state = user; +- +- if (commandlen == 2 && strneq(command, "$q", 2)) { +- request_status_string(state, frag); +- return 1; +- } else if (state->fallbacks && state->fallbacks->dcs) { +- if ((*state->fallbacks->dcs)(command, commandlen, frag, state->fbdata)) { +- return 1; +- } +- } +- +- DEBUG_LOG("libvterm: Unhandled DCS %.*s\n", (int)commandlen, command); +- return 0; +-} +- +-static int on_apc(VTermStringFragment frag, void *user) +-{ +- VTermState *state = user; +- +- if (state->fallbacks && state->fallbacks->apc) { +- if ((*state->fallbacks->apc)(frag, state->fbdata)) { +- return 1; +- } +- } +- +- // No DEBUG_LOG because all APCs are unhandled +- return 0; +-} +- +-static int on_pm(VTermStringFragment frag, void *user) +-{ +- VTermState *state = user; +- +- if (state->fallbacks && state->fallbacks->pm) { +- if ((*state->fallbacks->pm)(frag, state->fbdata)) { +- return 1; +- } +- } +- +- // No DEBUG_LOG because all PMs are unhandled +- return 0; +-} +- +-static int on_sos(VTermStringFragment frag, void *user) +-{ +- VTermState *state = user; +- +- if (state->fallbacks && state->fallbacks->sos) { +- if ((*state->fallbacks->sos)(frag, state->fbdata)) { +- return 1; +- } +- } +- +- // No DEBUG_LOG because all SOSs are unhandled +- return 0; +-} +- +-static int on_resize(int rows, int cols, void *user) +-{ +- VTermState *state = user; +- VTermPos oldpos = state->pos; +- +- if (cols != state->cols) { +- uint8_t *newtabstops = vterm_allocator_malloc(state->vt, ((size_t)cols + 7) / 8); +- +- // TODO(vterm): This can all be done much more efficiently bytewise +- int col; +- for (col = 0; col < state->cols && col < cols; col++) { +- uint8_t mask = (uint8_t)(1 << (col & 7)); +- if (state->tabstops[col >> 3] & mask) { +- newtabstops[col >> 3] |= mask; +- } else { +- newtabstops[col >> 3] &= ~mask; +- } +- } +- +- for (; col < cols; col++) { +- uint8_t mask = (uint8_t)(1 << (col & 7)); +- if (col % 8 == 0) { +- newtabstops[col >> 3] |= mask; +- } else { +- newtabstops[col >> 3] &= ~mask; +- } +- } +- +- vterm_allocator_free(state->vt, state->tabstops); +- state->tabstops = newtabstops; +- } +- +- state->rows = rows; +- state->cols = cols; +- +- if (state->scrollregion_bottom > -1) { +- UBOUND(state->scrollregion_bottom, state->rows); +- } +- if (state->scrollregion_right > -1) { +- UBOUND(state->scrollregion_right, state->cols); +- } +- +- VTermStateFields fields = { +- .pos = state->pos, +- .lineinfos = {[0] = state->lineinfos[0], [1] = state->lineinfos[1] }, +- }; +- +- if (state->callbacks && state->callbacks->resize) { +- (*state->callbacks->resize)(rows, cols, &fields, state->cbdata); +- state->pos = fields.pos; +- +- state->lineinfos[0] = fields.lineinfos[0]; +- state->lineinfos[1] = fields.lineinfos[1]; +- } else { +- if (rows != state->rows) { +- for (int bufidx = BUFIDX_PRIMARY; bufidx <= BUFIDX_ALTSCREEN; bufidx++) { +- VTermLineInfo *oldlineinfo = state->lineinfos[bufidx]; +- if (!oldlineinfo) { +- continue; +- } +- +- VTermLineInfo *newlineinfo = vterm_allocator_malloc(state->vt, +- (size_t)rows * sizeof(VTermLineInfo)); +- +- int row; +- for (row = 0; row < state->rows && row < rows; row++) { +- newlineinfo[row] = oldlineinfo[row]; +- } +- +- for (; row < rows; row++) { +- newlineinfo[row] = (VTermLineInfo){ +- .doublewidth = 0, +- }; +- } +- +- vterm_allocator_free(state->vt, state->lineinfos[bufidx]); +- state->lineinfos[bufidx] = newlineinfo; +- } +- } +- } +- +- state->lineinfo = state->lineinfos[state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY]; +- +- if (state->at_phantom && state->pos.col < cols - 1) { +- state->at_phantom = 0; +- state->pos.col++; +- } +- +- if (state->pos.row < 0) { +- state->pos.row = 0; +- } +- if (state->pos.row >= rows) { +- state->pos.row = rows - 1; +- } +- if (state->pos.col < 0) { +- state->pos.col = 0; +- } +- if (state->pos.col >= cols) { +- state->pos.col = cols - 1; +- } +- +- updatecursor(state, &oldpos, 1); +- +- return 1; +-} +- +-static const VTermParserCallbacks parser_callbacks = { +- .text = on_text, +- .control = on_control, +- .escape = on_escape, +- .csi = on_csi, +- .osc = on_osc, +- .dcs = on_dcs, +- .apc = on_apc, +- .pm = on_pm, +- .sos = on_sos, +- .resize = on_resize, +-}; +- +-VTermState *vterm_obtain_state(VTerm *vt) +-{ +- if (vt->state) { +- return vt->state; +- } +- +- VTermState *state = vterm_state_new(vt); +- vt->state = state; +- +- vterm_parser_set_callbacks(vt, &parser_callbacks, state); +- +- return state; +-} +- +-void vterm_state_reset(VTermState *state, int hard) +-{ +- state->scrollregion_top = 0; +- state->scrollregion_bottom = -1; +- state->scrollregion_left = 0; +- state->scrollregion_right = -1; +- +- state->mode.keypad = 0; +- state->mode.cursor = 0; +- state->mode.autowrap = 1; +- state->mode.insert = 0; +- state->mode.newline = 0; +- state->mode.alt_screen = 0; +- state->mode.origin = 0; +- state->mode.leftrightmargin = 0; +- state->mode.bracketpaste = 0; +- state->mode.report_focus = 0; +- +- state->mouse_flags = 0; +- +- state->vt->mode.ctrl8bit = 0; +- +- for (int col = 0; col < state->cols; col++) { +- if (col % 8 == 0) { +- set_col_tabstop(state, col); +- } else { +- clear_col_tabstop(state, col); +- } +- } +- +- for (int row = 0; row < state->rows; row++) { +- set_lineinfo(state, row, FORCE, DWL_OFF, DHL_OFF); +- } +- +- if (state->callbacks && state->callbacks->initpen) { +- (*state->callbacks->initpen)(state->cbdata); +- } +- +- vterm_state_resetpen(state); +- +- VTermEncoding *default_enc = state->vt->mode.utf8 +- ? vterm_lookup_encoding(ENC_UTF8, 'u') +- : vterm_lookup_encoding(ENC_SINGLE_94, 'B'); +- +- for (int i = 0; i < 4; i++) { +- state->encoding[i].enc = default_enc; +- if (default_enc->init) { +- (*default_enc->init)(default_enc, state->encoding[i].data); +- } +- } +- +- state->gl_set = 0; +- state->gr_set = 1; +- state->gsingle_set = 0; +- +- state->protected_cell = 0; +- +- // Initialise the props +- settermprop_bool(state, VTERM_PROP_CURSORVISIBLE, 1); +- settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1); +- settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BLOCK); +- +- if (hard) { +- state->pos.row = 0; +- state->pos.col = 0; +- state->at_phantom = 0; +- +- VTermRect rect = { 0, state->rows, 0, state->cols }; +- erase(state, rect, 0); +- } +-} +- +-void vterm_state_set_callbacks(VTermState *state, const VTermStateCallbacks *callbacks, void *user) +-{ +- if (callbacks) { +- state->callbacks = callbacks; +- state->cbdata = user; +- +- if (state->callbacks && state->callbacks->initpen) { +- (*state->callbacks->initpen)(state->cbdata); +- } +- } else { +- state->callbacks = NULL; +- state->cbdata = NULL; +- } +-} +- +-void vterm_state_set_unrecognised_fallbacks(VTermState *state, const VTermStateFallbacks *fallbacks, +- void *user) +-{ +- if (fallbacks) { +- state->fallbacks = fallbacks; +- state->fbdata = user; +- } else { +- state->fallbacks = NULL; +- state->fbdata = NULL; +- } +-} +- +-int vterm_state_set_termprop(VTermState *state, VTermProp prop, VTermValue *val) +-{ +- // Only store the new value of the property if usercode said it was happy. This is especially +- // important for altscreen switching +- if (state->callbacks && state->callbacks->settermprop) { +- if (!(*state->callbacks->settermprop)(prop, val, state->cbdata)) { +- return 0; +- } +- } +- +- switch (prop) { +- case VTERM_PROP_TITLE: +- case VTERM_PROP_ICONNAME: +- // we don't store these, just transparently pass through +- return 1; +- case VTERM_PROP_CURSORVISIBLE: +- state->mode.cursor_visible = (unsigned)val->boolean; +- return 1; +- case VTERM_PROP_CURSORBLINK: +- state->mode.cursor_blink = (unsigned)val->boolean; +- return 1; +- case VTERM_PROP_CURSORSHAPE: +- state->mode.cursor_shape = (unsigned)val->number; +- return 1; +- case VTERM_PROP_REVERSE: +- state->mode.screen = (unsigned)val->boolean; +- return 1; +- case VTERM_PROP_ALTSCREEN: +- state->mode.alt_screen = (unsigned)val->boolean; +- state->lineinfo = state->lineinfos[state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY]; +- if (state->mode.alt_screen) { +- VTermRect rect = { +- .start_row = 0, +- .start_col = 0, +- .end_row = state->rows, +- .end_col = state->cols, +- }; +- erase(state, rect, 0); +- } +- return 1; +- case VTERM_PROP_MOUSE: +- state->mouse_flags = 0; +- if (val->number) { +- state->mouse_flags |= MOUSE_WANT_CLICK; +- } +- if (val->number == VTERM_PROP_MOUSE_DRAG) { +- state->mouse_flags |= MOUSE_WANT_DRAG; +- } +- if (val->number == VTERM_PROP_MOUSE_MOVE) { +- state->mouse_flags |= MOUSE_WANT_MOVE; +- } +- return 1; +- case VTERM_PROP_FOCUSREPORT: +- state->mode.report_focus = (unsigned)val->boolean; +- return 1; +- case VTERM_PROP_THEMEUPDATES: +- state->mode.theme_updates = (unsigned)val->boolean; +- return 1; +- case VTERM_PROP_SYNCOUTPUT: +- state->mode.synchronized_output = (unsigned)val->boolean; +- return 1; +- +- case VTERM_N_PROPS: +- return 0; +- } +- +- return 0; +-} +- +-void vterm_state_focus_in(VTermState *state) +-{ +- if (state->mode.report_focus) { +- vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "I"); +- } +-} +- +-void vterm_state_focus_out(VTermState *state) +-{ +- if (state->mode.report_focus) { +- vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "O"); +- } +-} +- +-const VTermLineInfo *vterm_state_get_lineinfo(const VTermState *state, int row) +-{ +- return state->lineinfo + row; +-} +- +-void vterm_state_set_selection_callbacks(VTermState *state, +- const VTermSelectionCallbacks *callbacks, void *user, +- char *buffer, size_t buflen) +-{ +- if (buflen && !buffer) { +- buffer = vterm_allocator_malloc(state->vt, buflen); +- } +- +- state->selection.callbacks = callbacks; +- state->selection.user = user; +- state->selection.buffer = buffer; +- state->selection.buflen = buflen; +-} +diff --git a/src/nvim/vterm/state.h b/src/nvim/vterm/state.h +deleted file mode 100644 +index 924214b7056a97..00000000000000 +--- a/src/nvim/vterm/state.h ++++ /dev/null +@@ -1,5 +0,0 @@ +-#pragma once +- +-#include "nvim/vterm/vterm_defs.h" +- +-#include "vterm/state.h.generated.h" +diff --git a/src/nvim/vterm/vterm.c b/src/nvim/vterm/vterm.c +deleted file mode 100644 +index f05817f78f0f18..00000000000000 +--- a/src/nvim/vterm/vterm.c ++++ /dev/null +@@ -1,337 +0,0 @@ +-#include +-#include +-#include +-#include +- +-#include "auto/config.h" +-#include "nvim/memory.h" +-#include "nvim/vterm/screen.h" +-#include "nvim/vterm/state.h" +-#include "nvim/vterm/vterm.h" +-#include "nvim/vterm/vterm_internal_defs.h" +- +-#include "vterm/vterm.c.generated.h" +- +-// ***************** +-// * API functions * +-// ***************** +- +-static void *default_malloc(size_t size, void *allocdata) +-{ +- void *ptr = xmalloc(size); +- if (ptr) { +- memset(ptr, 0, size); +- } +- return ptr; +-} +- +-static void default_free(void *ptr, void *allocdata) +-{ +- xfree(ptr); +-} +- +-static VTermAllocatorFunctions default_allocator = { +- .malloc = &default_malloc, +- .free = &default_free, +-}; +- +-/// Convenient shortcut for default cases +-VTerm *vterm_new(int rows, int cols) +-{ +- return vterm_build(&(const struct VTermBuilder){ +- .rows = rows, +- .cols = cols, +- }); +-} +- +-// A handy macro for defaulting values out of builder fields +-#define DEFAULT(v, def) ((v) ? (v) : (def)) +- +-VTerm *vterm_build(const struct VTermBuilder *builder) +-{ +- const VTermAllocatorFunctions *allocator = DEFAULT(builder->allocator, &default_allocator); +- +- // Need to bootstrap using the allocator function directly +- VTerm *vt = (*allocator->malloc)(sizeof(VTerm), builder->allocdata); +- +- vt->allocator = allocator; +- vt->allocdata = builder->allocdata; +- +- vt->rows = builder->rows; +- vt->cols = builder->cols; +- +- vt->parser.state = NORMAL; +- +- vt->parser.callbacks = NULL; +- vt->parser.cbdata = NULL; +- +- vt->parser.emit_nul = false; +- +- vt->outfunc = NULL; +- vt->outdata = NULL; +- +- vt->outbuffer_len = DEFAULT(builder->outbuffer_len, 4096); +- vt->outbuffer_cur = 0; +- vt->outbuffer = vterm_allocator_malloc(vt, vt->outbuffer_len); +- +- vt->tmpbuffer_len = DEFAULT(builder->tmpbuffer_len, 4096); +- vt->tmpbuffer = vterm_allocator_malloc(vt, vt->tmpbuffer_len); +- +- return vt; +-} +- +-void vterm_free(VTerm *vt) +-{ +- if (vt->screen) { +- vterm_screen_free(vt->screen); +- } +- +- if (vt->state) { +- vterm_state_free(vt->state); +- } +- +- vterm_allocator_free(vt, vt->outbuffer); +- vterm_allocator_free(vt, vt->tmpbuffer); +- +- vterm_allocator_free(vt, vt); +-} +- +-void *vterm_allocator_malloc(VTerm *vt, size_t size) +-{ +- return (*vt->allocator->malloc)(size, vt->allocdata); +-} +- +-void vterm_allocator_free(VTerm *vt, void *ptr) +-{ +- (*vt->allocator->free)(ptr, vt->allocdata); +-} +- +-void vterm_get_size(const VTerm *vt, int *rowsp, int *colsp) +-{ +- if (rowsp) { +- *rowsp = vt->rows; +- } +- if (colsp) { +- *colsp = vt->cols; +- } +-} +- +-void vterm_set_size(VTerm *vt, int rows, int cols) +-{ +- if (rows < 1 || cols < 1) { +- return; +- } +- +- vt->rows = rows; +- vt->cols = cols; +- +- if (vt->parser.callbacks && vt->parser.callbacks->resize) { +- (*vt->parser.callbacks->resize)(rows, cols, vt->parser.cbdata); +- } +-} +- +-void vterm_set_utf8(VTerm *vt, int is_utf8) +-{ +- vt->mode.utf8 = (unsigned)is_utf8; +-} +- +-void vterm_output_set_callback(VTerm *vt, VTermOutputCallback *func, void *user) +-{ +- vt->outfunc = func; +- vt->outdata = user; +-} +- +-void vterm_push_output_bytes(VTerm *vt, const char *bytes, size_t len) +-{ +- if (vt->outfunc) { +- (vt->outfunc)(bytes, len, vt->outdata); +- return; +- } +- +- if (len > vt->outbuffer_len - vt->outbuffer_cur) { +- return; +- } +- +- memcpy(vt->outbuffer + vt->outbuffer_cur, bytes, len); +- vt->outbuffer_cur += len; +-} +- +-void vterm_push_output_sprintf(VTerm *vt, const char *format, ...) +- FUNC_ATTR_PRINTF(2, 3) +-{ +- va_list args; +- va_start(args, format); +- size_t len = (size_t)vsnprintf(vt->tmpbuffer, vt->tmpbuffer_len, format, args); +- vterm_push_output_bytes(vt, vt->tmpbuffer, len); +- va_end(args); +-} +- +-void vterm_push_output_sprintf_ctrl(VTerm *vt, uint8_t ctrl, const char *fmt, ...) +- FUNC_ATTR_PRINTF(3, 4) +-{ +- size_t cur; +- +- if (ctrl >= 0x80 && !vt->mode.ctrl8bit) { +- cur = (size_t)snprintf(vt->tmpbuffer, vt->tmpbuffer_len, ESC_S "%c", ctrl - 0x40); +- } else { +- cur = (size_t)snprintf(vt->tmpbuffer, vt->tmpbuffer_len, "%c", ctrl); +- } +- +- if (cur >= vt->tmpbuffer_len) { +- return; +- } +- +- va_list args; +- va_start(args, fmt); +- cur += (size_t)vsnprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, fmt, args); +- va_end(args); +- +- if (cur >= vt->tmpbuffer_len) { +- return; +- } +- +- vterm_push_output_bytes(vt, vt->tmpbuffer, cur); +-} +- +-void vterm_push_output_sprintf_str(VTerm *vt, uint8_t ctrl, bool term, const char *fmt, ...) +- FUNC_ATTR_PRINTF(4, 5) +-{ +- size_t cur = 0; +- +- if (ctrl) { +- if (ctrl >= 0x80 && !vt->mode.ctrl8bit) { +- cur = (size_t)snprintf(vt->tmpbuffer, vt->tmpbuffer_len, ESC_S "%c", ctrl - 0x40); +- } else { +- cur = (size_t)snprintf(vt->tmpbuffer, vt->tmpbuffer_len, "%c", ctrl); +- } +- +- if (cur >= vt->tmpbuffer_len) { +- return; +- } +- } +- +- va_list args; +- va_start(args, fmt); +- cur += (size_t)vsnprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, fmt, args); +- va_end(args); +- +- if (cur >= vt->tmpbuffer_len) { +- return; +- } +- +- if (term) { +- cur += (size_t)snprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, +- vt->mode.ctrl8bit ? "\x9C" : ESC_S "\\"); // ST +- +- if (cur >= vt->tmpbuffer_len) { +- return; +- } +- } +- +- vterm_push_output_bytes(vt, vt->tmpbuffer, cur); +-} +- +-VTermValueType vterm_get_attr_type(VTermAttr attr) +-{ +- switch (attr) { +- case VTERM_ATTR_BOLD: +- return VTERM_VALUETYPE_BOOL; +- case VTERM_ATTR_UNDERLINE: +- return VTERM_VALUETYPE_INT; +- case VTERM_ATTR_ITALIC: +- return VTERM_VALUETYPE_BOOL; +- case VTERM_ATTR_BLINK: +- return VTERM_VALUETYPE_BOOL; +- case VTERM_ATTR_REVERSE: +- return VTERM_VALUETYPE_BOOL; +- case VTERM_ATTR_CONCEAL: +- return VTERM_VALUETYPE_BOOL; +- case VTERM_ATTR_STRIKE: +- return VTERM_VALUETYPE_BOOL; +- case VTERM_ATTR_FONT: +- return VTERM_VALUETYPE_INT; +- case VTERM_ATTR_FOREGROUND: +- return VTERM_VALUETYPE_COLOR; +- case VTERM_ATTR_BACKGROUND: +- return VTERM_VALUETYPE_COLOR; +- case VTERM_ATTR_SMALL: +- return VTERM_VALUETYPE_BOOL; +- case VTERM_ATTR_BASELINE: +- return VTERM_VALUETYPE_INT; +- case VTERM_ATTR_URI: +- return VTERM_VALUETYPE_INT; +- case VTERM_ATTR_DIM: +- return VTERM_VALUETYPE_BOOL; +- case VTERM_ATTR_OVERLINE: +- return VTERM_VALUETYPE_BOOL; +- +- case VTERM_N_ATTRS: +- return 0; +- } +- return 0; // UNREACHABLE +-} +- +-void vterm_scroll_rect(VTermRect rect, int downward, int rightward, +- int (*moverect)(VTermRect src, VTermRect dest, void *user), +- int (*eraserect)(VTermRect rect, int selective, void *user), void *user) +-{ +- VTermRect src; +- VTermRect dest; +- +- if (abs(downward) >= rect.end_row - rect.start_row +- || abs(rightward) >= rect.end_col - rect.start_col) { +- // Scroll more than area; just erase the lot +- (*eraserect)(rect, 0, user); +- return; +- } +- +- if (rightward >= 0) { +- // rect: [XXX................] +- // src: [----------------] +- // dest: [----------------] +- dest.start_col = rect.start_col; +- dest.end_col = rect.end_col - rightward; +- src.start_col = rect.start_col + rightward; +- src.end_col = rect.end_col; +- } else { +- // rect: [................XXX] +- // src: [----------------] +- // dest: [----------------] +- int leftward = -rightward; +- dest.start_col = rect.start_col + leftward; +- dest.end_col = rect.end_col; +- src.start_col = rect.start_col; +- src.end_col = rect.end_col - leftward; +- } +- +- if (downward >= 0) { +- dest.start_row = rect.start_row; +- dest.end_row = rect.end_row - downward; +- src.start_row = rect.start_row + downward; +- src.end_row = rect.end_row; +- } else { +- int upward = -downward; +- dest.start_row = rect.start_row + upward; +- dest.end_row = rect.end_row; +- src.start_row = rect.start_row; +- src.end_row = rect.end_row - upward; +- } +- +- if (moverect) { +- (*moverect)(dest, src, user); +- } +- +- if (downward > 0) { +- rect.start_row = rect.end_row - downward; +- } else if (downward < 0) { +- rect.end_row = rect.start_row - downward; +- } +- +- if (rightward > 0) { +- rect.start_col = rect.end_col - rightward; +- } else if (rightward < 0) { +- rect.end_col = rect.start_col - rightward; +- } +- +- (*eraserect)(rect, 0, user); +-} +diff --git a/src/nvim/vterm/vterm.h b/src/nvim/vterm/vterm.h +deleted file mode 100644 +index be5f42e4cde65a..00000000000000 +--- a/src/nvim/vterm/vterm.h ++++ /dev/null +@@ -1,159 +0,0 @@ +-#pragma once +- +-#include +-#include +-#include +-#include +- +-#include "nvim/macros_defs.h" +-#include "nvim/types_defs.h" +-#include "nvim/vterm/vterm_defs.h" +-#include "nvim/vterm/vterm_keycodes_defs.h" +- +-#include "vterm/vterm.h.generated.h" +- +-#define VTERM_VERSION_MAJOR 0 +-#define VTERM_VERSION_MINOR 3 +- +-// move a rect +-static inline void vterm_rect_move(VTermRect *rect, int row_delta, int col_delta) +-{ +- rect->start_row += row_delta; rect->end_row += row_delta; +- rect->start_col += col_delta; rect->end_col += col_delta; +-} +- +-// Bit-field describing the content of the tagged union `VTermColor`. +-typedef enum { +- // If the lower bit of `type` is not set, the colour is 24-bit RGB. +- VTERM_COLOR_RGB = 0x00, +- +- // The colour is an index into a palette of 256 colours. +- VTERM_COLOR_INDEXED = 0x01, +- +- // Mask that can be used to extract the RGB/Indexed bit. +- VTERM_COLOR_TYPE_MASK = 0x01, +- +- // If set, indicates that this colour should be the default foreground color, i.e. there was no +- // SGR request for another colour. When rendering this colour it is possible to ignore "idx" and +- // just use a colour that is not in the palette. +- VTERM_COLOR_DEFAULT_FG = 0x02, +- +- // If set, indicates that this colour should be the default background color, i.e. there was no +- // SGR request for another colour. A common option when rendering this colour is to not render a +- // background at all, for example by rendering the window transparently at this spot. +- VTERM_COLOR_DEFAULT_BG = 0x04, +- +- // Mask that can be used to extract the default foreground/background bit. +- VTERM_COLOR_DEFAULT_MASK = 0x06, +-} VTermColorType; +- +-// Returns true if the VTERM_COLOR_RGB `type` flag is set, indicating that the given VTermColor +-// instance is an indexed colour. +-#define VTERM_COLOR_IS_INDEXED(col) \ +- (((col)->type & VTERM_COLOR_TYPE_MASK) == VTERM_COLOR_INDEXED) +- +-// Returns true if the VTERM_COLOR_INDEXED `type` flag is set, indicating that the given VTermColor +-// instance is an rgb colour. +-#define VTERM_COLOR_IS_RGB(col) \ +- (((col)->type & VTERM_COLOR_TYPE_MASK) == VTERM_COLOR_RGB) +- +-// Returns true if the VTERM_COLOR_DEFAULT_FG `type` flag is set, indicating that the given +-// VTermColor instance corresponds to the default foreground color. +-#define VTERM_COLOR_IS_DEFAULT_FG(col) \ +- (!!((col)->type & VTERM_COLOR_DEFAULT_FG)) +- +-// Returns true if the VTERM_COLOR_DEFAULT_BG `type` flag is set, indicating that the given +-// VTermColor instance corresponds to the default background color. +-#define VTERM_COLOR_IS_DEFAULT_BG(col) \ +- (!!((col)->type & VTERM_COLOR_DEFAULT_BG)) +- +-// Constructs a new VTermColor instance representing the given RGB values. +-static inline void vterm_color_rgb(VTermColor *col, uint8_t red, uint8_t green, uint8_t blue) +-{ +- col->type = VTERM_COLOR_RGB; +- col->rgb.red = red; +- col->rgb.green = green; +- col->rgb.blue = blue; +-} +- +-// Construct a new VTermColor instance representing an indexed color with the given index. +-static inline void vterm_color_indexed(VTermColor *col, uint8_t idx) +-{ +- col->type = VTERM_COLOR_INDEXED; +- col->indexed.idx = idx; +-} +- +-// ------------ +-// Parser layer +-// ------------ +- +-/// Flag to indicate non-final subparameters in a single CSI parameter. +-/// Consider +-/// CSI 1;2:3:4;5a +-/// 1 4 and 5 are final. +-/// 2 and 3 are non-final and will have this bit set +-/// +-/// Don't confuse this with the final byte of the CSI escape; 'a' in this case. +-#define CSI_ARG_FLAG_MORE (1U << 31) +-#define CSI_ARG_MASK (~(1U << 31)) +- +-#define CSI_ARG_HAS_MORE(a) ((a)& CSI_ARG_FLAG_MORE) +-#define CSI_ARG(a) ((a)& CSI_ARG_MASK) +- +-// Can't use -1 to indicate a missing argument; use this instead +-#define CSI_ARG_MISSING ((1UL<<31) - 1) +- +-#define CSI_ARG_IS_MISSING(a) (CSI_ARG(a) == CSI_ARG_MISSING) +-#define CSI_ARG_OR(a, def) (CSI_ARG(a) == CSI_ARG_MISSING ? (def) : CSI_ARG(a)) +-#define CSI_ARG_COUNT(a) (CSI_ARG(a) == CSI_ARG_MISSING || CSI_ARG(a) == 0 ? 1 : CSI_ARG(a)) +- +-enum { +- VTERM_UNDERLINE_OFF, +- VTERM_UNDERLINE_SINGLE, +- VTERM_UNDERLINE_DOUBLE, +- VTERM_UNDERLINE_CURLY, +-}; +- +-enum { +- VTERM_BASELINE_NORMAL, +- VTERM_BASELINE_RAISE, +- VTERM_BASELINE_LOWER, +-}; +- +-// Back-compat alias for the brief time it was in 0.3-RC1 +-#define vterm_screen_set_reflow vterm_screen_enable_reflow +- +-void vterm_scroll_rect(VTermRect rect, int downward, int rightward, +- int (*moverect)(VTermRect src, VTermRect dest, void *user), +- int (*eraserect)(VTermRect rect, int selective, void *user), void *user); +- +-struct VTermScreen { +- VTerm *vt; +- VTermState *state; +- +- const VTermScreenCallbacks *callbacks; +- void *cbdata; +- +- VTermDamageSize damage_merge; +- // start_row == -1 => no damage +- VTermRect damaged; +- VTermRect pending_scrollrect; +- int pending_scroll_downward, pending_scroll_rightward; +- +- int rows; +- int cols; +- +- unsigned global_reverse : 1; +- unsigned reflow : 1; +- +- // Primary and Altscreen. buffers[1] is lazily allocated as needed +- ScreenCell *buffers[2]; +- +- // buffer will == buffers[0] or buffers[1], depending on altscreen +- ScreenCell *buffer; +- +- // buffer for a single screen row used in scrollback storage callbacks +- VTermScreenCell *sb_buffer; +- +- ScreenPen pen; +-}; +diff --git a/src/nvim/vterm/vterm_defs.h b/src/nvim/vterm/vterm_defs.h +deleted file mode 100644 +index 7d6dbc3df7c637..00000000000000 +--- a/src/nvim/vterm/vterm_defs.h ++++ /dev/null +@@ -1,337 +0,0 @@ +-#pragma once +-#include +-#include +-#include +- +-#include "nvim/types_defs.h" +- +-typedef struct VTerm VTerm; +-typedef struct VTermState VTermState; +-typedef struct VTermScreen VTermScreen; +- +-typedef struct { +- int row; +- int col; +-} VTermPos; +- +-// some small utility functions; we can just keep these static here +- +-typedef struct { +- int start_row; +- int end_row; +- int start_col; +- int end_col; +-} VTermRect; +- +-// Tagged union storing either an RGB color or an index into a colour palette. In order to convert +-// indexed colours to RGB, you may use the vterm_state_convert_color_to_rgb() or +-// vterm_screen_convert_color_to_rgb() functions which lookup the RGB colour from the palette +-// maintained by a VTermState or VTermScreen instance. +-typedef union { +- // Tag indicating which union member is actually valid. This variable coincides with the `type` +- // member of the `rgb` and the `indexed` struct in memory. Please use the `VTERM_COLOR_IS_*` test +- // macros to check whether a particular type flag is set. +- uint8_t type; +- +- // Valid if `VTERM_COLOR_IS_RGB(type)` is true. Holds the RGB colour values. +- struct { +- // Same as the top-level `type` member stored in VTermColor. +- uint8_t type; +- +- // The actual 8-bit red, green, blue colour values. +- uint8_t red, green, blue; +- } rgb; +- +- // If `VTERM_COLOR_IS_INDEXED(type)` is true, this member holds the index into the colour palette. +- struct { +- // Same as the top-level `type` member stored in VTermColor. +- uint8_t type; +- +- // Index into the colour map. +- uint8_t idx; +- } indexed; +-} VTermColor; +- +-typedef struct { +- unsigned bold : 1; +- unsigned underline : 2; +- unsigned italic : 1; +- unsigned blink : 1; +- unsigned reverse : 1; +- unsigned conceal : 1; +- unsigned strike : 1; +- unsigned font : 4; // 0 to 9 +- unsigned dwl : 1; // On a DECDWL or DECDHL line +- unsigned dhl : 2; // On a DECDHL line (1=top 2=bottom) +- unsigned small : 1; +- unsigned baseline : 2; +- unsigned dim : 1; +- unsigned overline : 1; +-} VTermScreenCellAttrs; +- +-typedef struct { +- schar_T schar; +- char width; +- VTermScreenCellAttrs attrs; +- VTermColor fg, bg; +- int uri; +-} VTermScreenCell; +- +-typedef enum { +- // VTERM_PROP_NONE = 0 +- VTERM_PROP_CURSORVISIBLE = 1, // bool +- VTERM_PROP_CURSORBLINK, // bool +- VTERM_PROP_ALTSCREEN, // bool +- VTERM_PROP_TITLE, // string +- VTERM_PROP_ICONNAME, // string +- VTERM_PROP_REVERSE, // bool +- VTERM_PROP_CURSORSHAPE, // number +- VTERM_PROP_MOUSE, // number +- VTERM_PROP_FOCUSREPORT, // bool +- VTERM_PROP_THEMEUPDATES, // bool +- VTERM_PROP_SYNCOUTPUT, // bool +- +- VTERM_N_PROPS, +-} VTermProp; +- +-typedef enum { +- VTERM_TERMINATOR_BEL, // \x07 +- VTERM_TERMINATOR_ST, // \x1b\x5c +-} VTermTerminator; +- +-typedef struct { +- const char *str; +- size_t len : 30; +- bool initial : 1; +- bool final : 1; +- VTermTerminator terminator; +-} VTermStringFragment; +- +-typedef union { +- int boolean; +- int number; +- VTermStringFragment string; +- VTermColor color; +-} VTermValue; +- +-typedef struct { +- int (*damage)(VTermRect rect, void *user); +- int (*moverect)(VTermRect dest, VTermRect src, void *user); +- int (*movecursor)(VTermPos pos, VTermPos oldpos, int visible, void *user); +- int (*settermprop)(VTermProp prop, VTermValue *val, void *user); +- int (*bell)(void *user); +- int (*resize)(int rows, int cols, void *user); +- int (*theme)(bool *dark, void *user); +- int (*sb_pushline)(int cols, const VTermScreenCell *cells, void *user); +- int (*sb_popline)(int cols, VTermScreenCell *cells, void *user); +- int (*sb_clear)(void *user); +-} VTermScreenCallbacks; +- +-typedef struct { +- int (*control)(uint8_t control, void *user); +- int (*csi)(const char *leader, const long args[], int argcount, const char *intermed, +- char command, void *user); +- int (*osc)(int command, VTermStringFragment frag, void *user); +- int (*dcs)(const char *command, size_t commandlen, VTermStringFragment frag, void *user); +- int (*apc)(VTermStringFragment frag, void *user); +- int (*pm)(VTermStringFragment frag, void *user); +- int (*sos)(VTermStringFragment frag, void *user); +-} VTermStateFallbacks; +- +-typedef enum { +- VTERM_DAMAGE_CELL, // every cell +- VTERM_DAMAGE_ROW, // entire rows +- VTERM_DAMAGE_SCREEN, // entire screen +- VTERM_DAMAGE_SCROLL, // entire screen + scrollrect +- +- VTERM_N_DAMAGES, +-} VTermDamageSize; +- +-typedef enum { +- VTERM_ATTR_BOLD_MASK = 1 << 0, +- VTERM_ATTR_UNDERLINE_MASK = 1 << 1, +- VTERM_ATTR_ITALIC_MASK = 1 << 2, +- VTERM_ATTR_BLINK_MASK = 1 << 3, +- VTERM_ATTR_REVERSE_MASK = 1 << 4, +- VTERM_ATTR_STRIKE_MASK = 1 << 5, +- VTERM_ATTR_FONT_MASK = 1 << 6, +- VTERM_ATTR_FOREGROUND_MASK = 1 << 7, +- VTERM_ATTR_BACKGROUND_MASK = 1 << 8, +- VTERM_ATTR_CONCEAL_MASK = 1 << 9, +- VTERM_ATTR_SMALL_MASK = 1 << 10, +- VTERM_ATTR_BASELINE_MASK = 1 << 11, +- VTERM_ATTR_URI_MASK = 1 << 12, +- VTERM_ATTR_DIM_MASK = 1 << 13, +- VTERM_ATTR_OVERLINE_MASK = 1 << 14, +- +- VTERM_ALL_ATTRS_MASK = (1 << 15) - 1, +-} VTermAttrMask; +- +-typedef enum { +- // VTERM_VALUETYPE_NONE = 0 +- VTERM_VALUETYPE_BOOL = 1, +- VTERM_VALUETYPE_INT, +- VTERM_VALUETYPE_STRING, +- VTERM_VALUETYPE_COLOR, +- +- VTERM_N_VALUETYPES, +-} VTermValueType; +- +-typedef enum { +- // VTERM_ATTR_NONE = 0 +- VTERM_ATTR_BOLD = 1, // bool: 1, 22 +- VTERM_ATTR_UNDERLINE, // number: 4, 21, 24 +- VTERM_ATTR_ITALIC, // bool: 3, 23 +- VTERM_ATTR_BLINK, // bool: 5, 25 +- VTERM_ATTR_REVERSE, // bool: 7, 27 +- VTERM_ATTR_CONCEAL, // bool: 8, 28 +- VTERM_ATTR_STRIKE, // bool: 9, 29 +- VTERM_ATTR_FONT, // number: 10-19 +- VTERM_ATTR_FOREGROUND, // color: 30-39 90-97 +- VTERM_ATTR_BACKGROUND, // color: 40-49 100-107 +- VTERM_ATTR_SMALL, // bool: 73, 74, 75 +- VTERM_ATTR_BASELINE, // number: 73, 74, 75 +- VTERM_ATTR_URI, // number +- VTERM_ATTR_DIM, // bool: 2, 22 +- VTERM_ATTR_OVERLINE, // bool: 53, 55 +- +- VTERM_N_ATTRS, +-} VTermAttr; +- +-enum { +- VTERM_PROP_CURSORSHAPE_BLOCK = 1, +- VTERM_PROP_CURSORSHAPE_UNDERLINE, +- VTERM_PROP_CURSORSHAPE_BAR_LEFT, +- +- VTERM_N_PROP_CURSORSHAPES, +-}; +- +-enum { +- VTERM_PROP_MOUSE_NONE = 0, +- VTERM_PROP_MOUSE_CLICK, +- VTERM_PROP_MOUSE_DRAG, +- VTERM_PROP_MOUSE_MOVE, +- +- VTERM_N_PROP_MOUSES, +-}; +- +-typedef enum { +- VTERM_SELECTION_CLIPBOARD = (1<<0), +- VTERM_SELECTION_PRIMARY = (1<<1), +- VTERM_SELECTION_SECONDARY = (1<<2), +- VTERM_SELECTION_SELECT = (1<<3), +- VTERM_SELECTION_CUT0 = (1<<4), // also CUT1 .. CUT7 by bitshifting +-} VTermSelectionMask; +- +-typedef struct { +- schar_T schar; +- int width; +- unsigned protected_cell:1; // DECSCA-protected against DECSEL/DECSED +- unsigned dwl:1; // DECDWL or DECDHL double-width line +- unsigned dhl:2; // DECDHL double-height line (1=top 2=bottom) +-} VTermGlyphInfo; +- +-typedef struct { +- unsigned doublewidth:1; // DECDWL or DECDHL line +- unsigned doubleheight:2; // DECDHL line (1=top 2=bottom) +- unsigned continuation:1; // Line is a flow continuation of the previous +-} VTermLineInfo; +- +-// Copies of VTermState fields that the 'resize' callback might have reason to edit. 'resize' +-// callback gets total control of these fields and may free-and-reallocate them if required. They +-// will be copied back from the struct after the callback has returned. +-typedef struct { +- VTermPos pos; // current cursor position +- VTermLineInfo *lineinfos[2]; // [1] may be NULL +-} VTermStateFields; +- +-typedef struct { +- // libvterm relies on this memory to be zeroed out before it is returned by the allocator. +- void *(*malloc)(size_t size, void *allocdata); +- void (*free)(void *ptr, void *allocdata); +-} VTermAllocatorFunctions; +- +-// Setting output callback will override the buffer logic +-typedef void VTermOutputCallback(const char *s, size_t len, void *user); +- +-struct VTermBuilder { +- int ver; // currently unused but reserved for some sort of ABI version flag +- +- int rows, cols; +- +- const VTermAllocatorFunctions *allocator; +- void *allocdata; +- +- // Override default sizes for various structures +- size_t outbuffer_len; // default: 4096 +- size_t tmpbuffer_len; // default: 4096 +-}; +- +-typedef struct { +- int (*putglyph)(VTermGlyphInfo *info, VTermPos pos, void *user); +- int (*movecursor)(VTermPos pos, VTermPos oldpos, int visible, void *user); +- int (*scrollrect)(VTermRect rect, int downward, int rightward, void *user); +- int (*moverect)(VTermRect dest, VTermRect src, void *user); +- int (*erase)(VTermRect rect, int selective, void *user); +- int (*initpen)(void *user); +- int (*setpenattr)(VTermAttr attr, VTermValue *val, void *user); +- int (*settermprop)(VTermProp prop, VTermValue *val, void *user); +- int (*bell)(void *user); +- int (*resize)(int rows, int cols, VTermStateFields *fields, void *user); +- int (*theme)(bool *dark, void *user); +- int (*setlineinfo)(int row, const VTermLineInfo *newinfo, const VTermLineInfo *oldinfo, +- void *user); +- int (*sb_clear)(void *user); +-} VTermStateCallbacks; +- +-typedef struct { +- int (*set)(VTermSelectionMask mask, VTermStringFragment frag, void *user); +- int (*query)(VTermSelectionMask mask, void *user); +-} VTermSelectionCallbacks; +- +-typedef struct { +- int (*text)(const char *bytes, size_t len, void *user); +- int (*control)(uint8_t control, void *user); +- int (*escape)(const char *bytes, size_t len, void *user); +- int (*csi)(const char *leader, const long args[], int argcount, const char *intermed, +- char command, void *user); +- int (*osc)(int command, VTermStringFragment frag, void *user); +- int (*dcs)(const char *command, size_t commandlen, VTermStringFragment frag, void *user); +- int (*apc)(VTermStringFragment frag, void *user); +- int (*pm)(VTermStringFragment frag, void *user); +- int (*sos)(VTermStringFragment frag, void *user); +- int (*resize)(int rows, int cols, void *user); +-} VTermParserCallbacks; +- +-// State of the pen at some moment in time, also used in a cell +-typedef struct { +- // After the bitfield +- VTermColor fg, bg; +- +- // Opaque ID that maps to a URI in a set +- int uri; +- +- unsigned bold : 1; +- unsigned underline : 2; +- unsigned italic : 1; +- unsigned blink : 1; +- unsigned reverse : 1; +- unsigned conceal : 1; +- unsigned strike : 1; +- unsigned font : 4; // 0 to 9 +- unsigned small : 1; +- unsigned baseline : 2; +- unsigned dim : 1; +- unsigned overline : 1; +- +- // Extra state storage that isn't strictly pen-related +- unsigned protected_cell : 1; +- unsigned dwl : 1; // on a DECDWL or DECDHL line +- unsigned dhl : 2; // on a DECDHL line (1=top 2=bottom) +-} ScreenPen; +- +-// Internal representation of a screen cell +-typedef struct { +- schar_T schar; +- ScreenPen pen; +-} ScreenCell; +diff --git a/src/nvim/vterm/vterm_internal_defs.h b/src/nvim/vterm/vterm_internal_defs.h +deleted file mode 100644 +index ef9c82c327821b..00000000000000 +--- a/src/nvim/vterm/vterm_internal_defs.h ++++ /dev/null +@@ -1,295 +0,0 @@ +-#pragma once +- +-#include +- +-#include "nvim/mbyte_defs.h" +-#include "nvim/vterm/vterm_defs.h" +- +-#ifdef DEBUG +-# define DEBUG_LOG(...) fprintf(stderr, __VA_ARGS__) +-#else +-# define DEBUG_LOG(...) +-#endif +- +-#define ESC_S "\x1b" +- +-#define INTERMED_MAX 16 +- +-#define CSI_ARGS_MAX 32 +-#define CSI_LEADER_MAX 16 +- +-#define BUFIDX_PRIMARY 0 +-#define BUFIDX_ALTSCREEN 1 +- +-#define KEY_ENCODING_DISAMBIGUATE 0x1 +-#define KEY_ENCODING_REPORT_EVENTS 0x2 +-#define KEY_ENCODING_REPORT_ALTERNATE 0x4 +-#define KEY_ENCODING_REPORT_ALL_KEYS 0x8 +-#define KEY_ENCODING_REPORT_ASSOCIATED 0x10 +- +-typedef struct VTermEncoding VTermEncoding; +-typedef struct VTermKeyEncodingFlags VTermKeyEncodingFlags; +- +-typedef struct { +- VTermEncoding *enc; +- +- // This size should be increased if required by other stateful encodings +- char data[4 * sizeof(uint32_t)]; +-} VTermEncodingInstance; +- +-struct VTermPen { +- VTermColor fg; +- VTermColor bg; +- int uri; +- unsigned bold:1; +- unsigned underline:2; +- unsigned italic:1; +- unsigned blink:1; +- unsigned reverse:1; +- unsigned conceal:1; +- unsigned strike:1; +- unsigned font:4; // To store 0-9 +- unsigned small:1; +- unsigned baseline:2; +- unsigned dim:1; +- unsigned overline:1; +-}; +- +-// https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement +-struct VTermKeyEncodingFlags { +- bool disambiguate:1; +- bool report_events:1; +- bool report_alternate:1; +- bool report_all_keys:1; +- bool report_associated:1; +-}; +- +-struct VTermKeyEncodingStack { +- VTermKeyEncodingFlags items[16]; +- uint8_t size; ///< Number of items in the stack. This is at least 1 and at +- ///< most the length of the "items" array. +-}; +- +-struct VTermState { +- VTerm *vt; +- +- const VTermStateCallbacks *callbacks; +- void *cbdata; +- +- const VTermStateFallbacks *fallbacks; +- void *fbdata; +- +- int rows; +- int cols; +- +- // Current cursor position +- VTermPos pos; +- +- int at_phantom; // True if we're on the "81st" phantom column to defer a wraparound +- +- int scrollregion_top; +- int scrollregion_bottom; // -1 means unbounded +-#define SCROLLREGION_BOTTOM(state) ((state)->scrollregion_bottom > \ +- -1 ? (state)->scrollregion_bottom : (state)->rows) +- int scrollregion_left; +-#define SCROLLREGION_LEFT(state) ((state)->mode.leftrightmargin ? (state)->scrollregion_left : 0) +- int scrollregion_right; // -1 means unbounded +-#define SCROLLREGION_RIGHT(state) ((state)->mode.leftrightmargin \ +- && (state)->scrollregion_right > \ +- -1 ? (state)->scrollregion_right : (state)->cols) +- +- // Bitvector of tab stops +- uint8_t *tabstops; +- +- // Primary and Altscreen; lineinfos[1] is lazily allocated as needed +- VTermLineInfo *lineinfos[2]; +- +- // lineinfo will == lineinfos[0] or lineinfos[1], depending on altscreen +- VTermLineInfo *lineinfo; +-#define ROWWIDTH(state, \ +- row) ((state)->lineinfo[(row)].doublewidth ? ((state)->cols / 2) : (state)->cols) +-#define THISROWWIDTH(state) ROWWIDTH(state, (state)->pos.row) +- +- // Mouse state +- int mouse_col, mouse_row; +- int mouse_buttons; +- int mouse_flags; +-#define MOUSE_WANT_CLICK 0x01 +-#define MOUSE_WANT_DRAG 0x02 +-#define MOUSE_WANT_MOVE 0x04 +- +- enum { MOUSE_X10, MOUSE_UTF8, MOUSE_SGR, MOUSE_RXVT, } mouse_protocol; +- +-// Last glyph output, for Unicode recombining purposes +- char grapheme_buf[MAX_SCHAR_SIZE]; +- size_t grapheme_len; +- uint32_t grapheme_last; // last added UTF-32 char +- GraphemeState grapheme_state; +- int combine_width; // The width of the glyph above +- VTermPos combine_pos; // Position before movement +- +- struct { +- unsigned keypad:1; +- unsigned cursor:1; +- unsigned autowrap:1; +- unsigned insert:1; +- unsigned newline:1; +- unsigned cursor_visible:1; +- unsigned cursor_blink:1; +- unsigned cursor_shape:2; +- unsigned alt_screen:1; +- unsigned origin:1; +- unsigned screen:1; +- unsigned leftrightmargin:1; +- unsigned bracketpaste:1; +- unsigned report_focus:1; +- unsigned theme_updates:1; +- unsigned synchronized_output:1; +- } mode; +- +- VTermEncodingInstance encoding[4], encoding_utf8; +- int gl_set, gr_set, gsingle_set; +- +- struct VTermPen pen; +- +- VTermColor default_fg; +- VTermColor default_bg; +- VTermColor colors[16]; // Store the 8 ANSI and the 8 ANSI high-brights only +- +- int bold_is_highbright; +- +- unsigned protected_cell : 1; +- +-// Saved state under DEC mode 1048/1049 +- struct { +- VTermPos pos; +- struct VTermPen pen; +- +- struct { +- unsigned cursor_visible:1; +- unsigned cursor_blink:1; +- unsigned cursor_shape:2; +- } mode; +- } saved; +- +-// Temporary state for DECRQSS parsing +- union { +- char decrqss[4]; +- struct { +- uint16_t mask; +- enum { +- SELECTION_INITIAL, +- SELECTION_SELECTED, +- SELECTION_QUERY, +- SELECTION_SET_INITIAL, +- SELECTION_SET, +- SELECTION_INVALID, +- } state : 8; +- uint32_t recvpartial; +- uint32_t sendpartial; +- } selection; +- } tmp; +- +- struct { +- const VTermSelectionCallbacks *callbacks; +- void *user; +- char *buffer; +- size_t buflen; +- } selection; +- +- // Maintain two stacks, one for primary screen and one for altscreen +- struct VTermKeyEncodingStack key_encoding_stacks[2]; +-}; +- +-struct VTerm { +- const VTermAllocatorFunctions *allocator; +- void *allocdata; +- +- int rows; +- int cols; +- +- struct { +- unsigned utf8:1; +- unsigned ctrl8bit:1; +- } mode; +- +- struct { +- enum VTermParserState { +- NORMAL, +- CSI_LEADER, +- CSI_ARGS, +- CSI_INTERMED, +- DCS_COMMAND, +- // below here are the "string states" +- OSC_COMMAND, +- OSC, +- DCS_VTERM, +- APC, +- PM, +- SOS, +- } state; +- +- bool in_esc : 1; +- +- int intermedlen; +- char intermed[INTERMED_MAX]; +- +- union { +- struct { +- int leaderlen; +- char leader[CSI_LEADER_MAX]; +- +- int argi; +- long args[CSI_ARGS_MAX]; +- } csi; +- struct { +- int command; +- } osc; +- struct { +- int commandlen; +- char command[CSI_LEADER_MAX]; +- } dcs; +- } v; +- +- const VTermParserCallbacks *callbacks; +- void *cbdata; +- +- bool string_initial; +- +- bool emit_nul; +- } parser; +- +- // len == malloc()ed size; cur == number of valid bytes +- +- VTermOutputCallback *outfunc; +- void *outdata; +- +- char *outbuffer; +- size_t outbuffer_len; +- size_t outbuffer_cur; +- +- char *tmpbuffer; +- size_t tmpbuffer_len; +- +- VTermState *state; +- VTermScreen *screen; +-}; +- +-struct VTermEncoding { +- void (*init)(VTermEncoding *enc, void *data); +- void (*decode)(VTermEncoding *enc, void *data, uint32_t cp[], int *cpi, int cplen, +- const char bytes[], size_t *pos, size_t len); +-}; +- +-typedef enum { +- ENC_UTF8, +- ENC_SINGLE_94, +-} VTermEncodingType; +- +-enum { +- C1_SS3 = 0x8f, +- C1_DCS = 0x90, +- C1_CSI = 0x9b, +- C1_ST = 0x9c, +- C1_OSC = 0x9d, +-}; +diff --git a/src/nvim/vterm/vterm_keycodes_defs.h b/src/nvim/vterm/vterm_keycodes_defs.h +deleted file mode 100644 +index 70db05af5429a2..00000000000000 +--- a/src/nvim/vterm/vterm_keycodes_defs.h ++++ /dev/null +@@ -1,58 +0,0 @@ +-#pragma once +- +-typedef enum { +- VTERM_MOD_NONE = 0x00, +- VTERM_MOD_SHIFT = 0x01, +- VTERM_MOD_ALT = 0x02, +- VTERM_MOD_CTRL = 0x04, +- +- VTERM_ALL_MODS_MASK = 0x07, +-} VTermModifier; +- +-typedef enum { +- VTERM_KEY_NONE, +- +- VTERM_KEY_ENTER, +- VTERM_KEY_TAB, +- VTERM_KEY_BACKSPACE, +- VTERM_KEY_ESCAPE, +- +- VTERM_KEY_UP, +- VTERM_KEY_DOWN, +- VTERM_KEY_LEFT, +- VTERM_KEY_RIGHT, +- +- VTERM_KEY_INS, +- VTERM_KEY_DEL, +- VTERM_KEY_HOME, +- VTERM_KEY_END, +- VTERM_KEY_PAGEUP, +- VTERM_KEY_PAGEDOWN, +- +- VTERM_KEY_FUNCTION_0 = 256, +- VTERM_KEY_FUNCTION_MAX = VTERM_KEY_FUNCTION_0 + 255, +- +- VTERM_KEY_KP_0, +- VTERM_KEY_KP_1, +- VTERM_KEY_KP_2, +- VTERM_KEY_KP_3, +- VTERM_KEY_KP_4, +- VTERM_KEY_KP_5, +- VTERM_KEY_KP_6, +- VTERM_KEY_KP_7, +- VTERM_KEY_KP_8, +- VTERM_KEY_KP_9, +- VTERM_KEY_KP_MULT, +- VTERM_KEY_KP_PLUS, +- VTERM_KEY_KP_COMMA, +- VTERM_KEY_KP_MINUS, +- VTERM_KEY_KP_PERIOD, +- VTERM_KEY_KP_DIVIDE, +- VTERM_KEY_KP_ENTER, +- VTERM_KEY_KP_EQUAL, +- +- VTERM_KEY_MAX, // Must be last +- VTERM_N_KEYS = VTERM_KEY_MAX, +-} VTermKey; +- +-#define VTERM_KEY_FUNCTION(n) (VTERM_KEY_FUNCTION_0 + (n)) +diff --git a/test/cmakeconfig/paths.lua.in b/test/cmakeconfig/paths.lua.in +index bfc1f47426c22e..b9ba1662c51687 100644 +--- a/test/cmakeconfig/paths.lua.in ++++ b/test/cmakeconfig/paths.lua.in +@@ -9,7 +9,6 @@ M.apple_sysroot = "${CMAKE_OSX_SYSROOT}" + M.translations_enabled = "${ENABLE_TRANSLATIONS}" == "ON" + M.is_asan = "${ENABLE_ASAN_UBSAN}" == "ON" + M.is_zig_build = false +-M.vterm_test_file = "${VTERM_TEST_FILE}" + M.test_build_dir = "${CMAKE_BINARY_DIR}" + M.test_source_path = "${CMAKE_SOURCE_DIR}" + M.test_lua_prg = "${LUA_PRG}" +diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua +index 73a9aea87df39c..cb4c609487292b 100644 +--- a/test/functional/api/vim_spec.lua ++++ b/test/functional/api/vim_spec.lua +@@ -4304,16 +4304,16 @@ describe('API', function() + before_each(function() + screen = Screen.new(100, 35) + screen:add_extra_attr_ids { +- [100] = { background = tonumber('0xffff40'), bg_indexed = true }, ++ [100] = { background = tonumber('0xe7c547'), bg_indexed = true }, + [101] = { + background = Screen.colors.LightMagenta, +- foreground = tonumber('0x00e000'), ++ foreground = tonumber('0xb5bd68'), + fg_indexed = true, + }, + [102] = { background = Screen.colors.LightMagenta, reverse = true }, + [103] = { background = Screen.colors.LightMagenta, bold = true, reverse = true }, +- [104] = { fg_indexed = true, foreground = tonumber('0xe00000') }, +- [105] = { fg_indexed = true, foreground = tonumber('0xe0e000') }, ++ [104] = { fg_indexed = true, foreground = tonumber('0xcc6666') }, ++ [105] = { fg_indexed = true, foreground = tonumber('0xf0c674') }, + } + end) + +diff --git a/test/functional/autocmd/focus_spec.lua b/test/functional/autocmd/focus_spec.lua +index 964a411b6a52fa..ca0a14bab2e63c 100644 +--- a/test/functional/autocmd/focus_spec.lua ++++ b/test/functional/autocmd/focus_spec.lua +@@ -43,7 +43,7 @@ describe('autoread TUI FocusGained/FocusLost', function() + + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + | + {5:-- TERMINAL --} | +@@ -51,7 +51,7 @@ describe('autoread TUI FocusGained/FocusLost', function() + n.feed(':edit ' .. path .. '') + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:xtest-foo }| + :edit xtest-foo | + {5:-- TERMINAL --} | +diff --git a/test/functional/terminal/altscreen_spec.lua b/test/functional/terminal/altscreen_spec.lua +index 4453cc1480d9c9..ed0b78d1c90d83 100644 +--- a/test/functional/terminal/altscreen_spec.lua ++++ b/test/functional/terminal/altscreen_spec.lua +@@ -143,9 +143,10 @@ describe(':terminal altscreen', function() + local function wait_removal() + screen:try_resize(screen._width, screen._height - 2) + screen:expect([[ +- |*2 ++ | + rows: 4, cols: 50 | + ^ | ++ | + {5:-- TERMINAL --} | + ]]) + end +@@ -154,10 +155,9 @@ describe(':terminal altscreen', function() + wait_removal() + feed('4k') + screen:expect([[ +- ^ | +- |*2 +- rows: 4, cols: 50 | +- | ++ line2 | ++ ^line3 | ++ |*3 + ]]) + eq(9, api.nvim_buf_line_count(0)) + end) +@@ -169,18 +169,12 @@ describe(':terminal altscreen', function() + end) + + it('restore buffer state', function() +- screen:expect(t.is_os('win') and [[ ++ screen:expect([[ + line6 | + line7 | + line8 | + ^ | + {5:-- TERMINAL --} | +- ]] or [[ +- line5 | +- line6 | +- line7 | +- ^line8 | +- {5:-- TERMINAL --} | + ]]) + end) + end) +diff --git a/test/functional/terminal/api_spec.lua b/test/functional/terminal/api_spec.lua +index 5187aefe461928..b6465897fac265 100644 +--- a/test/functional/terminal/api_spec.lua ++++ b/test/functional/terminal/api_spec.lua +@@ -34,7 +34,7 @@ describe('api', function() + it('qa! RPC request during insert-mode', function() + screen:expect([[ + ^ | +- {100:~ }|*4 ++ {100:~} |*4 + | + {5:-- TERMINAL --} | + ]]) +@@ -45,7 +45,7 @@ describe('api', function() + -- Wait for socket creation. + screen:expect([[ + ^ | +- {100:~ }|*4 ++ {100:~} |*4 + ]] .. socket_name .. [[ | + {5:-- TERMINAL --} | + ]]) +@@ -57,7 +57,7 @@ describe('api', function() + -- Wait for stdin to be processed. + screen:expect([[ + [tui] insert-mode^ | +- {100:~ }|*4 ++ {100:~} |*4 + {5:-- INSERT --} | + {5:-- TERMINAL --} | + ]]) +@@ -73,7 +73,7 @@ describe('api', function() + [socket 1] this is more t | + han 25 columns | + [socket 2] input^ | +- {100:~ } | ++ {100:~} | + {5:-- INSERT --} | + {5:-- TERMINAL --} | + ]]) +diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua +index 4ec34f17ff0d07..70f8c89ffc9332 100644 +--- a/test/functional/terminal/buffer_spec.lua ++++ b/test/functional/terminal/buffer_spec.lua +@@ -582,9 +582,9 @@ describe(':terminal buffer', function() + {5:-- TERMINAL --} | + ]]) + api.nvim_input_mouse('right', 'press', '', 0, 0, 25) +- screen:expect({ any = vim.pesc('"!!^') }) ++ screen:expect({ any = vim.pesc('"!!') }) + api.nvim_input_mouse('right', 'release', '', 0, 0, 25) +- screen:expect({ any = vim.pesc('#!!^') }) ++ screen:expect({ any = vim.pesc('#!!') }) + vim.uv.kill(pid, 'sigstop') + local s1 = [[ + rows: 6, cols: 25 │rows: 6, cols: 25 | +@@ -615,14 +615,14 @@ describe(':terminal buffer', function() + rows: 6, cols: 25 │rows: 6, cols: 25 | + mouse enabled │mouse enabled | + │ |*3 +- #!! │ #!!^ | ++ #!! │#!!^ | + {5:-- TERMINAL --} | + ]]) + -- Mouse is forwarded after process is resumed. + api.nvim_input_mouse('right', 'press', '', 0, 0, 28) +- screen:expect({ any = vim.pesc('"$!^') }) ++ screen:expect({ any = vim.pesc('"$!') }) + api.nvim_input_mouse('right', 'release', '', 0, 0, 28) +- screen:expect({ any = vim.pesc('#$!^') }) ++ screen:expect({ any = vim.pesc('#$!') }) + end + + it('resumed by an external signal', function() +@@ -645,6 +645,7 @@ describe(':terminal buffer', function() + + local screen = Screen.new(50, 7) + screen:add_extra_attr_ids({ ++ Blank = { background = Screen.colors.NvimLightGrey2 }, + [100] = { + foreground = Screen.colors.NvimDarkGrey2, + background = Screen.colors.NvimLightGrey2, +@@ -661,14 +662,13 @@ describe(':terminal buffer', function() + command('set shell=fish termguicolors') + command(('terminal %s -u NONE -i NONE'):format(fn.shellescape(nvim_prog))) + command('startinsert') +- local s0 = [[ +- {100:^ }| +- {101:~ }|*3 ++ screen:expect([[ ++ {Blank:^ }| ++ {101:~}{Blank: }|*3 + {102:[No Name] 0,0-1 All}| +- {100: }| ++ {Blank: }| + {5:-- TERMINAL --} | +- ]] +- screen:expect(s0) ++ ]]) + feed('') + screen:expect([[ + |*5 +@@ -676,7 +676,13 @@ describe(':terminal buffer', function() + {5:-- TERMINAL --} | + ]]) + feed('') +- screen:expect(s0) ++ screen:expect([[ ++ {Blank:^ }| ++ {101:~}{Blank: }|*3 ++ {102:[No Name] 0,0-1 All}| ++ {100: }| ++ {5:-- TERMINAL --} | ++ ]]) + end) + + it('term_close() use-after-free #4393', function() +@@ -703,6 +709,23 @@ describe(':terminal buffer', function() + eq(termbuf, eval('g:termbuf')) + end) + ++ it('emits event if autocommand is created before sequence terminates', function() ++ command('autocmd! nvim.terminal TermRequest') ++ local term = api.nvim_open_term(0, {}) ++ api.nvim_chan_send(term, '\027]777;part') ++ ++ exec_lua([[ ++ vim.api.nvim_create_autocmd('TermRequest', { ++ callback = function(ev) ++ _G.termrequest_sequence = ev.data.sequence ++ end, ++ }) ++ ]]) ++ api.nvim_chan_send(term, 'ial\027\\') ++ ++ eq('\027]777;partial', exec_lua('return _G.termrequest_sequence')) ++ end) ++ + it('emits events for APC', function() + local term = api.nvim_open_term(0, {}) + +@@ -726,18 +749,18 @@ describe(':terminal buffer', function() + }) + vim.api.nvim_create_autocmd('TermRequest', { + callback = function(ev) +- if ev.data.sequence == '\027]11;?' then +- table.insert(_G.input, '\027]11;rgb:0000/0000/0000\027\\') ++ if ev.data.sequence == '\027]777;?' then ++ table.insert(_G.input, '\027]777;ok\027\\') + end + end + }) + return term + ]]) +- api.nvim_chan_send(term, '\027]11;?\007\027[5n\027]11;?\007\027[5n') ++ api.nvim_chan_send(term, '\027]777;?\007\027[5n\027]777;?\007\027[5n') + eq({ +- '\027]11;rgb:0000/0000/0000\027\\', ++ '\027]777;ok\027\\', + '\027[0n', +- '\027]11;rgb:0000/0000/0000\027\\', ++ '\027]777;ok\027\\', + '\027[0n', + }, exec_lua('return _G.input')) + end) +@@ -803,7 +826,7 @@ describe(':terminal buffer', function() + world!^ | + {5:-- TERMINAL --} | + ]]) +- eq({ 22, 6 }, exec_lua('return _G.cursor')) ++ eq({ 21, 6 }, exec_lua('return _G.cursor')) + + api.nvim_chan_send(term, '\nHello\027]133;D\027\\\nworld!\n') + screen:expect([[ +@@ -815,7 +838,7 @@ describe(':terminal buffer', function() + ^ | + {5:-- TERMINAL --} | + ]]) +- eq({ 23, 5 }, exec_lua('return _G.cursor')) ++ eq({ 22, 5 }, exec_lua('return _G.cursor')) + + api.nvim_chan_send(term, 'Hello\027]133;D\027\\\nworld!' .. ('\n'):rep(6)) + screen:expect([[ +@@ -829,7 +852,7 @@ describe(':terminal buffer', function() + eq({ 25, 5 }, exec_lua('return _G.cursor')) + + api.nvim_set_option_value('scrollback', 10, {}) +- eq(19, api.nvim_buf_line_count(0)) ++ eq(32, api.nvim_buf_line_count(0)) + + api.nvim_chan_send(term, 'Hello\nworld!\027]133;D\027\\') + screen:expect([[ +@@ -840,7 +863,7 @@ describe(':terminal buffer', function() + world!^ | + {5:-- TERMINAL --} | + ]]) +- eq({ 19, 6 }, exec_lua('return _G.cursor')) ++ eq({ 32, 6 }, exec_lua('return _G.cursor')) + + api.nvim_chan_send(term, '\nHello\027]133;D\027\\\nworld!\n') + screen:expect([[ +@@ -852,7 +875,7 @@ describe(':terminal buffer', function() + ^ | + {5:-- TERMINAL --} | + ]]) +- eq({ 17, 5 }, exec_lua('return _G.cursor')) ++ eq({ 33, 5 }, exec_lua('return _G.cursor')) + + api.nvim_chan_send(term, 'Hello\027]133;D\027\\\nworld!' .. ('\n'):rep(6)) + screen:expect([[ +@@ -863,7 +886,7 @@ describe(':terminal buffer', function() + ^ | + {5:-- TERMINAL --} | + ]]) +- eq({ 12, 5 }, exec_lua('return _G.cursor')) ++ eq({ 36, 5 }, exec_lua('return _G.cursor')) + + api.nvim_chan_send(term, 'Hello\027]133;D\027\\\nworld!' .. ('\n'):rep(8)) + screen:expect([[ +@@ -872,7 +895,7 @@ describe(':terminal buffer', function() + ^ | + {5:-- TERMINAL --} | + ]]) +- eq({ 10, 5 }, exec_lua('return _G.cursor')) ++ eq({ 43, 5 }, exec_lua('return _G.cursor')) + + api.nvim_chan_send(term, 'Hello\027]133;D\027\\\nworld!' .. ('\n'):rep(20)) + screen:expect([[ +@@ -880,7 +903,7 @@ describe(':terminal buffer', function() + ^ | + {5:-- TERMINAL --} | + ]]) +- eq({ -2, 5 }, exec_lua('return _G.cursor')) ++ eq({ 52, 5 }, exec_lua('return _G.cursor')) + end) + + it('does not cause hang in vim.wait() #32753', function() +@@ -937,6 +960,27 @@ describe(':terminal buffer', function() + end) + end) + ++ it('reports color scheme queries', function() ++ local function assert_color_scheme(background, response) ++ command('enew!') ++ command('set background=' .. background) ++ local term = exec_lua(function() ++ _G.input_data = '' ++ return vim.api.nvim_open_term(0, { ++ on_input = function(_, _, _, data) ++ _G.input_data = _G.input_data .. data ++ end, ++ }) ++ end) ++ ++ api.nvim_chan_send(term, '\027[?996n') ++ eq(response, exec_lua('return _G.input_data')) ++ end ++ ++ assert_color_scheme('dark', '\027[?997;1n') ++ assert_color_scheme('light', '\027[?997;2n') ++ end) ++ + it('no heap-buffer-overflow when using jobstart("echo",{term=true}) #3161', function() + local testfilename = 'Xtestfile-functional-terminal-buffers_spec' + write_file(testfilename, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaa') +@@ -1030,7 +1074,13 @@ describe(':terminal buffer', function() + {5:-- TERMINAL --} | + ]]):format(count - 5, count - 4, count - 3, count - 2, count - 1)) + local lines = api.nvim_buf_get_lines(0, 0, -1, true) +- local start = math.max(count + 1 - scrollback - 6, 0) ++ local start = tonumber(lines[1]:match('^(%d+): TEST$')) ++ assert(start) ++ if scrollback < count then ++ eq(true, start > 0) ++ else ++ eq(0, start) ++ end + for i = start, count - 1 do + eq(('%d: TEST'):format(i), lines[i - start + 1]) + end +@@ -1100,13 +1150,13 @@ describe(':terminal buffer', function() + local screen = Screen.new(50, 7) + feed 'i' + local chan = api.nvim_open_term(0, {}) +- api.nvim_chan_send(chan, '\239\187\191') -- '\xef\xbb\xbf' ++ api.nvim_chan_send(chan, '\239\191\191') -- U+FFFF + screen:expect([[ +- {18:}^ | ++ {18:}^ | + |*5 + {5:-- TERMINAL --} | + ]]) +- eq('\239\187\191', api.nvim_get_current_line()) ++ eq('\239\191\191', api.nvim_get_current_line()) + end) + + it("handles bell respecting 'belloff' and 'visualbell'", function() +@@ -1538,6 +1588,22 @@ describe('terminal input', function() + eq('aaa\0bbb', exec_lua([[return _G.input_data]])) + end) + ++ it('sends normal-mode keypad keys as generated text', function() ++ feed('') ++ feed('') ++ eq('013579./*-+,=', exec_lua([[return _G.input_data]])) ++ end) ++ ++ it('sends super-modified keys', function() ++ feed('') ++ eq('\027[1;9D', exec_lua([[return _G.input_data]])) ++ ++ api.nvim_chan_send(chan, '\027[>1u') ++ poke_eventloop() ++ feed('') ++ eq('\027[1;9D\027[119;9u', exec_lua([[return _G.input_data]])) ++ end) ++ + it('unknown special keys are not sent', function() + feed('aaabbb') + eq('aaabbb', exec_lua([[return _G.input_data]])) +@@ -1590,7 +1656,7 @@ describe('terminal input', function() + }) + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] 0,0-1 All}| + | + {5:-- TERMINAL --} | +@@ -1685,7 +1751,7 @@ describe('terminal input', function() + feed(key) + screen:expect(([[ + | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] 0,0-1 All}| + %s^ {MATCH: *}| + {5:-- TERMINAL --} | +diff --git a/test/functional/terminal/clipboard_spec.lua b/test/functional/terminal/clipboard_spec.lua +index b6ff9797cd3bc6..42ac22f718935b 100644 +--- a/test/functional/terminal/clipboard_spec.lua ++++ b/test/functional/terminal/clipboard_spec.lua +@@ -6,6 +6,7 @@ local eq = t.eq + local retry = t.retry + + local clear = n.clear ++local api = n.api + local fn = n.fn + local testprg = n.testprg + local exec_lua = n.exec_lua +@@ -84,4 +85,27 @@ describe(':terminal', function() + eq({ '', 'foo', '', 'bar', '' }, exec_lua([[ return vim.g.clipboard_lines ]])) + end) + end) ++ ++ it('emits TermRequest events', function() ++ local text = 'Hello from OSC 52' ++ local encoded = exec_lua('return vim.base64.encode(...)', text) ++ local expected = string.format('\027]52;;%s', encoded) ++ ++ exec_lua([[ ++ vim.g.termrequest_sequence = nil ++ vim.api.nvim_create_autocmd('TermRequest', { ++ callback = function(ev) ++ vim.g.termrequest_sequence = ev.data.sequence ++ end, ++ }) ++ ]]) ++ ++ local term = api.nvim_open_term(0, {}) ++ api.nvim_chan_send(term, string.format('%s\027\\', expected)) ++ ++ retry(nil, 1000, function() ++ eq(text, exec_lua([[ return vim.g.clipboard_data ]])) ++ eq(expected, eval('g:termrequest_sequence')) ++ end) ++ end) + end) +diff --git a/test/functional/terminal/cursor_spec.lua b/test/functional/terminal/cursor_spec.lua +index 63bd5ff0ac24c7..33a2c121304ba9 100644 +--- a/test/functional/terminal/cursor_spec.lua ++++ b/test/functional/terminal/cursor_spec.lua +@@ -45,6 +45,28 @@ describe(':terminal cursor', function() + ]]) + end) + ++ it("uses the t: block of 'guicursor' as the terminal default cursor", function() ++ command('set guicursor=t:hor20-blinkon500-blinkoff500') ++ ++ tt.feed_data('\027[6 q') ++ screen:expect({ ++ condition = function() ++ eq('vertical', screen._mode_info[terminal_mode_idx].cursor_shape) ++ eq(0, screen._mode_info[terminal_mode_idx].blinkon) ++ eq(0, screen._mode_info[terminal_mode_idx].blinkoff) ++ end, ++ }) ++ ++ tt.feed_data('\027[0 q') ++ screen:expect({ ++ condition = function() ++ eq('horizontal', screen._mode_info[terminal_mode_idx].cursor_shape) ++ eq(500, screen._mode_info[terminal_mode_idx].blinkon) ++ eq(500, screen._mode_info[terminal_mode_idx].blinkoff) ++ end, ++ }) ++ end) ++ + it('is highlighted when not focused', function() + feed('') + screen:expect([[ +diff --git a/test/functional/terminal/edit_spec.lua b/test/functional/terminal/edit_spec.lua +index d9652cdcff1c99..84f81e090bce58 100644 +--- a/test/functional/terminal/edit_spec.lua ++++ b/test/functional/terminal/edit_spec.lua +@@ -44,8 +44,7 @@ describe(':edit term://*', function() + + local bufcontents = {} + local winheight = api.nvim_win_get_height(0) +- local buf_cont_start = rep - sb - winheight + 1 +- for i = buf_cont_start, (rep - 1) do ++ for i = 0, (rep - 1) do + bufcontents[#bufcontents + 1] = ('%d: foobar'):format(i) + end + bufcontents[#bufcontents + 1] = '' +@@ -59,6 +58,7 @@ describe(':edit term://*', function() + + exp_screen = exp_screen .. (' '):rep(columns) .. '|\n' + scr:expect(exp_screen) ++ eq(sb, api.nvim_get_option_value('scrollback', { buf = 0 })) + eq(bufcontents, api.nvim_buf_get_lines(0, 0, -1, true)) + end) + end) +diff --git a/test/functional/terminal/highlight_spec.lua b/test/functional/terminal/highlight_spec.lua +index 9df2777d1f0d3f..8fa91adfbe0409 100644 +--- a/test/functional/terminal/highlight_spec.lua ++++ b/test/functional/terminal/highlight_spec.lua +@@ -129,20 +129,26 @@ end) + it(':terminal highlight has lower precedence than editor #9964', function() + clear() + local screen = Screen.new(30, 4, { rgb = true }) ++ local child_blank_attr = is_os('win') and 'N_child' or 'N_child_blank' ++ local child_blank_sep = is_os('win') and '' or '}{N_child_blank:' + screen:set_default_attr_ids({ + -- "Normal" highlight emitted by the child nvim process. + N_child = { +- foreground = tonumber('0x4040ff'), +- background = tonumber('0xffff40'), ++ foreground = tonumber('0x7aa6da'), ++ background = tonumber('0xe7c547'), + fg_indexed = true, + bg_indexed = true, + }, ++ N_child_blank = { ++ background = tonumber('0xe7c547'), ++ bg_indexed = true, ++ }, + -- "Search" highlight in the parent nvim process. + S = { background = Screen.colors.Green, italic = true, foreground = Screen.colors.Red }, + -- "Question" highlight in the parent nvim process. + -- note: bg is indexed as it comes from the (cterm) child, while fg isn't as it comes from (rgb) parent + Q = { +- background = tonumber('0xffff40'), ++ background = tonumber('0xe7c547'), + bold = true, + foreground = Screen.colors.SeaGreen4, + bg_indexed = true, +@@ -167,27 +173,27 @@ it(':terminal highlight has lower precedence than editor #9964', function() + VIMRUNTIME = os.getenv('VIMRUNTIME'), + }, + }) +- screen:expect([[ +- {N_child:^child nvim }| +- {N_child:line 2 }| +- {N_child: }| ++ screen:expect(([[ ++ {N_child:^child nvim%s }| ++ {N_child:line 2%s }| ++ {%s: }| + | +- ]]) ++ ]]):format(child_blank_sep, child_blank_sep, child_blank_attr)) + command('hi Search gui=italic guifg=Red guibg=Green cterm=italic ctermfg=Red ctermbg=Green') + feed('/nvim') +- screen:expect([[ +- {N_child:child }{S:^nvim}{N_child: }| +- {N_child:line 2 }| +- {N_child: }| ++ screen:expect(([[ ++ {N_child:child }{S:^nvim}{%s: }| ++ {N_child:line 2%s }| ++ {%s: }| + /nvim | +- ]]) ++ ]]):format(child_blank_attr, child_blank_sep, child_blank_attr)) + command('syntax keyword Question line') +- screen:expect([[ +- {N_child:child }{S:^nvim}{N_child: }| +- {Q:line}{N_child: 2 }| +- {N_child: }| ++ screen:expect(([[ ++ {N_child:child }{S:^nvim}{%s: }| ++ {Q:line}{N_child: 2%s }| ++ {%s: }| + /nvim | +- ]]) ++ ]]):format(child_blank_attr, child_blank_sep, child_blank_attr)) + end) + + it('CursorLine and CursorColumn work in :terminal buffer in Normal mode', function() +@@ -304,8 +310,10 @@ describe(':terminal highlight forwarding', function() + screen:set_rgb_cterm(true) + screen:set_default_attr_ids({ + [1] = { { bold = true }, { bold = true } }, +- [2] = { { fg_indexed = true, foreground = tonumber('0xe0e000') }, { foreground = 3 } }, ++ [2] = { { fg_indexed = true, foreground = tonumber('0xf0c674') }, { foreground = 3 } }, + [3] = { { foreground = tonumber('0xff8000') }, {} }, ++ [4] = { { undercurl = true, special = tonumber('0xf08f68') }, { undercurl = true } }, ++ [5] = { { undercurl = true }, { undercurl = true } }, + }) + command(("enew | call jobstart(['%s'], {'term':v:true})"):format(testprg('tty-test'))) + feed('i') +@@ -317,6 +325,21 @@ describe(':terminal highlight forwarding', function() + ]]) + end) + ++ it('will handle colored underline', function() ++ tt.set_undercurl() ++ tt.feed_termcode('[58:2::240:143:104m') ++ tt.feed_data('color') ++ tt.feed_termcode('[59m') ++ tt.feed_data('plain') ++ tt.clear_attrs() ++ screen:expect([[ ++ tty ready | ++ {4:color}{5:plain}^ | ++ |*4 ++ {1:-- TERMINAL --} | ++ ]]) ++ end) ++ + it('will handle cterm and rgb attributes', function() + skip(is_os('win')) + tt.set_fg(3) +diff --git a/test/functional/terminal/mouse_spec.lua b/test/functional/terminal/mouse_spec.lua +index 3aac88f73c3d35..63e3ccfedaa734 100644 +--- a/test/functional/terminal/mouse_spec.lua ++++ b/test/functional/terminal/mouse_spec.lua +@@ -116,45 +116,13 @@ describe(':terminal mouse', function() + it('will forward mouse press, drag and release to the program', function() + skip(is_os('win')) + feed('<1,2>') +- screen:expect([[ +- line27 | +- line28 | +- line29 | +- line30 | +- mouse enabled | +- "#^ | +- {5:-- TERMINAL --} | +- ]]) ++ screen:expect({ any = vim.pesc('"#') }) + feed('<2,2>') +- screen:expect([[ +- line27 | +- line28 | +- line29 | +- line30 | +- mouse enabled | +- @##^ | +- {5:-- TERMINAL --} | +- ]]) ++ screen:expect({ any = vim.pesc('@##') }) + feed('<3,2>') +- screen:expect([[ +- line27 | +- line28 | +- line29 | +- line30 | +- mouse enabled | +- @$#^ | +- {5:-- TERMINAL --} | +- ]]) ++ screen:expect({ any = vim.pesc('@$#') }) + feed('<3,2>') +- screen:expect([[ +- line27 | +- line28 | +- line29 | +- line30 | +- mouse enabled | +- #$#^ | +- {5:-- TERMINAL --} | +- ]]) ++ screen:expect({ any = vim.pesc('#$#') }) + end) + + it('will forward mouse scroll to the program', function() +@@ -174,67 +142,27 @@ describe(':terminal mouse', function() + it('dragging and scrolling do not interfere with each other', function() + skip(is_os('win')) + feed('<1,2>') +- screen:expect([[ +- line27 | +- line28 | +- line29 | +- line30 | +- mouse enabled | +- "#^ | +- {5:-- TERMINAL --} | +- ]]) ++ screen:expect({ any = vim.pesc('"#') }) + feed('<1,2>') +- screen:expect([[ +- line27 | +- line28 | +- line29 | +- line30 | +- mouse enabled | +- `"#^ | +- {5:-- TERMINAL --} | +- ]]) ++ screen:expect({ any = vim.pesc('`"#') }) + feed('<2,2>') +- screen:expect([[ +- line27 | +- line28 | +- line29 | +- line30 | +- mouse enabled | +- @##^ | +- {5:-- TERMINAL --} | +- ]]) ++ screen:expect({ any = vim.pesc('@##') }) + feed('<2,2>') +- screen:expect([[ +- line27 | +- line28 | +- line29 | +- line30 | +- mouse enabled | +- `##^ | +- {5:-- TERMINAL --} | +- ]]) ++ screen:expect({ any = vim.pesc('`##') }) + feed('<2,2>') +- screen:expect([[ +- line27 | +- line28 | +- line29 | +- line30 | +- mouse enabled | +- ###^ | +- {5:-- TERMINAL --} | +- ]]) ++ screen:expect({ any = vim.pesc('###') }) + end) + + it('will forward mouse clicks to the program with the correct even if set nu', function() + skip(is_os('win')) + command('set number') + screen:expect([[ +- {121: 11 }line28 | +- {121: 12 }line29 | +- {121: 13 }line30 | +- {121: 14 }mouse enabled | +- {121: 15 }rows: 6, cols: 46 | +- {121: 16 }^ | ++ {121: 29 }line28 | ++ {121: 30 }line29 | ++ {121: 31 }line30 | ++ {121: 32 }mouse enabled | ++ {121: 33 }rows: 6, cols: 46 | ++ {121: 34 }^ | + {5:-- TERMINAL --} | + ]]) + -- When the display area such as a number is clicked, it returns to the +@@ -242,24 +170,24 @@ describe(':terminal mouse', function() + feed('<3,0>') + eq('nt', eval('mode(1)')) + screen:expect([[ +- {121: 11 }^line28 | +- {121: 12 }line29 | +- {121: 13 }line30 | +- {121: 14 }mouse enabled | +- {121: 15 }rows: 6, cols: 46 | +- {121: 16 } | ++ {121: 29 }^line28 | ++ {121: 30 }line29 | ++ {121: 31 }line30 | ++ {121: 32 }mouse enabled | ++ {121: 33 }rows: 6, cols: 46 | ++ {121: 34 } | + | + ]]) + -- If click on the coordinate (0,1) of the region of the terminal + -- (i.e. the coordinate (4,1) of vim), 'CSI !"' is sent to the terminal. + feed('i<4,1>') + screen:expect([[ +- {121: 11 }line28 | +- {121: 12 }line29 | +- {121: 13 }line30 | +- {121: 14 }mouse enabled | +- {121: 15 }rows: 6, cols: 46 | +- {121: 16 } !"^ | ++ {121: 29 }line28 | ++ {121: 30 }line29 | ++ {121: 31 }line30 | ++ {121: 32 }mouse enabled | ++ {121: 33 }rows: 6, cols: 46 | ++ {121: 34 } !"^ | + {5:-- TERMINAL --} | + ]]) + end) +@@ -298,30 +226,31 @@ describe(':terminal mouse', function() + + it('will lose focus if right separator is clicked', function() + command('rightbelow vnew | wincmd p | startinsert') ++ screen:expect({ any = 'rows: 5, cols: 24' }) ++ tt.feed_data('\027[2J\027[Hmouse enabled\027[5;1H') + screen:expect([[ +- line29 │ | +- line30 │{100:~ }| +- mouse enabled │{100:~ }| +- rows: 5, cols: 24 │{100:~ }| ++ mouse enabled │ | ++ │{100:~ }| ++ │{100:~ }| ++ │{100:~ }| + ^ │{100:~ }| + ========== ========== | + {5:-- TERMINAL --} | + ]]) + feed('<24,0>') + screen:expect([[ +- line29 │ | +- line30 │{100:~ }| +- mouse enabled │{100:~ }| +- rows: 5, cols: 24 │{100:~ }| ++ mouse enabled │ | ++ │{100:~ }| ++ │{100:~ }| ++ │{100:~ }| + ^ │{100:~ }| + ========== ========== | + | + ]]) + feed('<23,0>') + screen:expect([[ +- line30 │ | +- mouse enabled │{100:~ }| +- rows: 5, cols: 24 │{100:~ }| ++ │ | ++ │{100:~ }|*2 + rows: 5, cols: 23 │{100:~ }| + ^ │{100:~ }| + ========== ========== | +@@ -415,24 +344,24 @@ describe(':terminal mouse', function() + screen:expect({ any = vim.pesc('rows: 11, cols: 58') }) + + api.nvim_input_mouse('right', 'press', '', 2, 0, 0) +- screen:expect({ any = vim.pesc('"!!^') }) ++ screen:expect({ any = vim.pesc('"!!') }) + api.nvim_input_mouse('right', 'release', '', 2, 0, 0) +- screen:expect({ any = vim.pesc('#!!^') }) ++ screen:expect({ any = vim.pesc('#!!') }) + + api.nvim_input_mouse('right', 'press', '', 2, 10, 0) +- screen:expect({ any = vim.pesc('"!+^') }) ++ screen:expect({ any = vim.pesc('"!+') }) + api.nvim_input_mouse('right', 'release', '', 2, 10, 0) +- screen:expect({ any = vim.pesc('#!+^') }) ++ screen:expect({ any = vim.pesc('#!+') }) + + api.nvim_input_mouse('right', 'press', '', 2, 0, 57) +- screen:expect({ any = vim.pesc('"Z!^') }) ++ screen:expect({ any = vim.pesc('"Z!') }) + api.nvim_input_mouse('right', 'release', '', 2, 0, 57) +- screen:expect({ any = vim.pesc('#Z!^') }) ++ screen:expect({ any = vim.pesc('#Z!') }) + + api.nvim_input_mouse('right', 'press', '', 2, 10, 57) +- screen:expect({ any = vim.pesc('"Z+^') }) ++ screen:expect({ any = vim.pesc('"Z+') }) + api.nvim_input_mouse('right', 'release', '', 2, 10, 57) +- screen:expect({ any = vim.pesc('#Z+^') }) ++ screen:expect({ any = vim.pesc('#Z+') }) + + command('setlocal winbar=WINBAR') + screen:expect({ any = vim.pesc(('{5:WINBAR%s}'):format((' '):rep(52))) }) +@@ -451,79 +380,83 @@ describe(':terminal mouse', function() + eq('t', api.nvim_get_mode().mode) + + api.nvim_input_mouse('right', 'press', '', 2, 1, 0) +- screen:expect({ any = vim.pesc('"!!^') }) ++ screen:expect({ any = vim.pesc('"!!') }) + api.nvim_input_mouse('right', 'release', '', 2, 1, 0) +- screen:expect({ any = vim.pesc('#!!^') }) ++ screen:expect({ any = vim.pesc('#!!') }) + + api.nvim_input_mouse('right', 'press', '', 2, 10, 0) +- screen:expect({ any = vim.pesc('"!*^') }) ++ screen:expect({ any = vim.pesc('"!*') }) + api.nvim_input_mouse('right', 'release', '', 2, 10, 0) +- screen:expect({ any = vim.pesc('#!*^') }) ++ screen:expect({ any = vim.pesc('#!*') }) + + api.nvim_input_mouse('right', 'press', '', 2, 1, 57) +- screen:expect({ any = vim.pesc('"Z!^') }) ++ screen:expect({ any = vim.pesc('"Z!') }) + api.nvim_input_mouse('right', 'release', '', 2, 1, 57) +- screen:expect({ any = vim.pesc('#Z!^') }) ++ screen:expect({ any = vim.pesc('#Z!') }) + + api.nvim_input_mouse('right', 'press', '', 2, 10, 57) +- screen:expect({ any = vim.pesc('"Z*^') }) ++ screen:expect({ any = vim.pesc('"Z*') }) + api.nvim_input_mouse('right', 'release', '', 2, 10, 57) +- screen:expect({ any = vim.pesc('#Z*^') }) ++ screen:expect({ any = vim.pesc('#Z*') }) + end) + end) + + describe('with a split window and other buffer', function() + before_each(function() + feed(':vsp') +- screen:expect([[ +- line28 │line28 | +- line29 │line29 | +- line30 │line30 | +- rows: 5, cols: 25 │rows: 5, cols: 25 | +- ^ │ | ++ screen:expect({ any = 'rows: 5, cols: 25 │rows: 5, cols: 25' }) ++ local term_chan = eval('b:terminal_job_id') ++ tt.feed_data( ++ '\027[2J\027[Hterm line 1\nterm line 2\nterm line 3\nterm line 4\nterm line 5\027[5;12H' ++ ) ++ screen:expect([[ ++ term line 1 │term line 1 | ++ term line 2 │term line 2 | ++ term line 3 │term line 3 | ++ term line 4 │term line 4 | ++ ^term line 5 │term line 5 | + ========== ========== | + :vsp | + ]]) + feed(':enew | set number') +- screen:expect([[ +- {121: 1 }^ │line29 | +- {100:~ }│line30 | +- {100:~ }│rows: 5, cols: 25 | +- {100:~ }│rows: 5, cols: 24 | +- {100:~ }│ | +- ========== ========== | +- :enew | set number | +- ]]) ++ -- Synchronize on the debounced resize before overwriting and asserting ++ -- the terminal contents below. ++ screen:expect({ any = 'rows: 5, cols' }) + feed('30iline\n') +- screen:expect([[ +- {121: 27 }line │line29 | +- {121: 28 }line │line30 | +- {121: 29 }line │rows: 5, cols: 25 | +- {121: 30 }line │rows: 5, cols: 24 | +- {121: 31 }^ │ | ++ api.nvim_chan_send( ++ term_chan, ++ '\027[2J\027[Hterm line 1\nterm line 2\nterm line 3\nterm line 4\nterm line 5\027[5;12H' ++ ) ++ screen:expect([[ ++ {121: 27 }line │term line 1 | ++ {121: 28 }line │term line 2 | ++ {121: 29 }line │term line 3 | ++ {121: 30 }line │term line 4 | ++ {121: 31 }^ │term line 5 | + ========== ========== | + | + ]]) + feed('li') + screen:expect([[ +- {121: 27 }line │line29 | +- {121: 28 }line │line30 | +- {121: 29 }line │rows: 5, cols: 25 | +- {121: 30 }line │rows: 5, cols: 24 | +- {121: 31 } │^ | ++ {121: 27 }line │term line 1 | ++ {121: 28 }line │term line 2 | ++ {121: 29 }line │term line 3 | ++ {121: 30 }line │term line 4 | ++ {121: 31 } │term line 5^ | + ========== ========== | + {5:-- TERMINAL --} | + ]]) +- + -- enabling mouse won't affect interaction with other windows + tt.enable_mouse() +- tt.feed_data('mouse enabled\n') +- screen:expect([[ +- {121: 27 }line │line30 | +- {121: 28 }line │rows: 5, cols: 25 | +- {121: 29 }line │rows: 5, cols: 24 | +- {121: 30 }line │mouse enabled | +- {121: 31 } │^ | ++ tt.feed_data( ++ '\027[2J\027[Hterm line 1\nterm line 2\nterm line 3\nterm line 4\nmouse enabled\027[5;1H' ++ ) ++ screen:expect([[ ++ {121: 27 }line │term line 1 | ++ {121: 28 }line │term line 2 | ++ {121: 29 }line │term line 3 | ++ {121: 30 }line │term line 4 | ++ {121: 31 } │^mouse enabled | + ========== ========== | + {5:-- TERMINAL --} | + ]]) +@@ -532,32 +465,32 @@ describe(':terminal mouse', function() + it("scrolling another window keeps focus and respects 'mousescroll'", function() + feed('<4,0><4,0>') + screen:expect([[ +- {121: 21 }line │line30 | +- {121: 22 }line │rows: 5, cols: 25 | +- {121: 23 }line │rows: 5, cols: 24 | +- {121: 24 }line │mouse enabled | +- {121: 25 }line │^ | ++ {121: 21 }line │term line 1 | ++ {121: 22 }line │term line 2 | ++ {121: 23 }line │term line 3 | ++ {121: 24 }line │term line 4 | ++ {121: 25 }line │^mouse enabled | + ========== ========== | + {5:-- TERMINAL --} | + ]]) + feed('<4,0>') + screen:expect([[ +- {121: 26 }line │line30 | +- {121: 27 }line │rows: 5, cols: 25 | +- {121: 28 }line │rows: 5, cols: 24 | +- {121: 29 }line │mouse enabled | +- {121: 30 }line │^ | ++ {121: 26 }line │term line 1 | ++ {121: 27 }line │term line 2 | ++ {121: 28 }line │term line 3 | ++ {121: 29 }line │term line 4 | ++ {121: 30 }line │^mouse enabled | + ========== ========== | + {5:-- TERMINAL --} | + ]]) + command('set mousescroll=ver:10') + feed('<0,0>') + screen:expect([[ +- {121: 16 }line │line30 | +- {121: 17 }line │rows: 5, cols: 25 | +- {121: 18 }line │rows: 5, cols: 24 | +- {121: 19 }line │mouse enabled | +- {121: 20 }line │^ | ++ {121: 16 }line │term line 1 | ++ {121: 17 }line │term line 2 | ++ {121: 18 }line │term line 3 | ++ {121: 19 }line │term line 4 | ++ {121: 20 }line │^mouse enabled | + ========== ========== | + {5:-- TERMINAL --} | + ]]) +@@ -568,32 +501,32 @@ describe(':terminal mouse', function() + command('setlocal nowrap') + feed('0gg3ly$4pwi') + screen:expect([[ +- {121: 1 }linelinelinelineline │line30 | +- {121: 2 }linelinelinelineline │rows: 5, cols: 25 | +- {121: 3 }linelinelinelineline │rows: 5, cols: 24 | +- {121: 4 }linelinelinelineline │mouse enabled | +- {121: 5 }linelinelinelineline │^ | ++ {121: 1 }linelinelinelineline │term line 1 | ++ {121: 2 }linelinelinelineline │term line 2 | ++ {121: 3 }linelinelinelineline │term line 3 | ++ {121: 4 }linelinelinelineline │term line 4 | ++ {121: 5 }linelinelinelineline │^mouse enabled | + ========== ========== | + {5:-- TERMINAL --} | + ]]) + feed('<4,0>') + screen:expect([[ +- {121: 1 }nelinelineline │line30 | +- {121: 2 }nelinelineline │rows: 5, cols: 25 | +- {121: 3 }nelinelineline │rows: 5, cols: 24 | +- {121: 4 }nelinelineline │mouse enabled | +- {121: 5 }nelinelineline │^ | ++ {121: 1 }nelinelineline │term line 1 | ++ {121: 2 }nelinelineline │term line 2 | ++ {121: 3 }nelinelineline │term line 3 | ++ {121: 4 }nelinelineline │term line 4 | ++ {121: 5 }nelinelineline │^mouse enabled | + ========== ========== | + {5:-- TERMINAL --} | + ]]) + command('set mousescroll=hor:4') + feed('<4,0>') + screen:expect([[ +- {121: 1 }nelinelinelineline │line30 | +- {121: 2 }nelinelinelineline │rows: 5, cols: 25 | +- {121: 3 }nelinelinelineline │rows: 5, cols: 24 | +- {121: 4 }nelinelinelineline │mouse enabled | +- {121: 5 }nelinelinelineline │^ | ++ {121: 1 }nelinelinelineline │term line 1 | ++ {121: 2 }nelinelinelineline │term line 2 | ++ {121: 3 }nelinelinelineline │term line 3 | ++ {121: 4 }nelinelinelineline │term line 4 | ++ {121: 5 }nelinelinelineline │^mouse enabled | + ========== ========== | + {5:-- TERMINAL --} | + ]]) +@@ -602,11 +535,11 @@ describe(':terminal mouse', function() + it('will lose focus if another window is clicked', function() + feed('<5,1>') + screen:expect([[ +- {121: 27 }line │line30 | +- {121: 28 }l^ine │rows: 5, cols: 25 | +- {121: 29 }line │rows: 5, cols: 24 | +- {121: 30 }line │mouse enabled | +- {121: 31 } │ | ++ {121: 27 }line │term line 1 | ++ {121: 28 }l^ine │term line 2 | ++ {121: 29 }line │term line 3 | ++ {121: 30 }line │term line 4 | ++ {121: 31 } │mouse enabled | + ========== ========== | + | + ]]) +@@ -616,19 +549,19 @@ describe(':terminal mouse', function() + api.nvim_set_option_value('hidden', true, {}) + feed('') + screen:expect([[ +- {121: 27 }line │line30 | +- {121: 28 }line │rows: 5, cols: 25 | +- {121: 29 }line │rows: 5, cols: 24 | +- {121: 30 }line │mouse enabled | +- {121: 31 }^ │ | ++ {121: 27 }line │term line 1 | ++ {121: 28 }line │term line 2 | ++ {121: 29 }line │term line 3 | ++ {121: 30 }line │term line 4 | ++ {121: 31 }^ │mouse enabled | + ========== ========== | + | + ]]) + feed(':bn') + screen:expect([[ +- rows: 5, cols: 25 │rows: 5, cols: 25 | +- rows: 5, cols: 24 │rows: 5, cols: 24 | +- mouse enabled │mouse enabled | ++ term line 2 │term line 2 | ++ term line 3 │term line 3 | ++ term line 4 │term line 4 | + rows: 5, cols: 25 │rows: 5, cols: 25 | + ^ │ | + ========== ========== | +@@ -636,8 +569,8 @@ describe(':terminal mouse', function() + ]]) + feed(':bn') + screen:expect([[ +- {121: 27 }line │rows: 5, cols: 24 | +- {121: 28 }line │mouse enabled | ++ {121: 27 }line │term line 3 | ++ {121: 28 }line │term line 4 | + {121: 29 }line │rows: 5, cols: 25 | + {121: 30 }line │rows: 5, cols: 24 | + {121: 31 }^ │ | +diff --git a/test/functional/terminal/scrollback_spec.lua b/test/functional/terminal/scrollback_spec.lua +index 45f6ce4e9a1295..4da7d4fd18660a 100644 +--- a/test/functional/terminal/scrollback_spec.lua ++++ b/test/functional/terminal/scrollback_spec.lua +@@ -106,7 +106,7 @@ local function test_terminal_scrollback(hide_curbuf) + restore_terminal_mode = nil + end) + +- describe('when the limit is exceeded', function() ++ describe('when Ghostty retains more than the requested limit', function() + before_each(function() + feed_lines('line', 1, 30) + screen:expect([[ +@@ -118,18 +118,18 @@ local function test_terminal_scrollback(hide_curbuf) + ^ | + {5:-- TERMINAL --} | + ]]) +- eq(16, api.nvim_buf_line_count(0)) ++ eq(32, api.nvim_buf_line_count(0)) + end) + +- it('will delete extra lines at the top', function() ++ it('keeps the retained lines available', function() + feed('gg') + screen:expect([[ +- ^line16 | +- line17 | +- line18 | +- line19 | +- line20 | +- line21 | ++ ^tty ready | ++ line1 | ++ line2 | ++ line3 | ++ line4 | ++ line5 | + | + ]]) + end) +@@ -137,9 +137,9 @@ local function test_terminal_scrollback(hide_curbuf) + describe('and cursor on non-last row in screen', function() + before_each(function() + feed([[M$]]) +- fn.setpos("'m", { 0, 13, 4, 0 }) ++ fn.setpos("'m", { 0, 29, 4, 0 }) + local ns = api.nvim_create_namespace('test') +- api.nvim_buf_set_extmark(0, ns, 12, 0, { end_col = 6, hl_group = 'ErrorMsg' }) ++ api.nvim_buf_set_extmark(0, ns, 28, 0, { end_col = 6, hl_group = 'ErrorMsg' }) + screen:expect([[ + line26 | + line27 | +@@ -161,38 +161,38 @@ local function test_terminal_scrollback(hide_curbuf) + new_line1 | + | + ]]) +- eq({ 0, 7, 4, 0 }, fn.getpos("'m")) +- eq({ 0, 7, 6, 0 }, fn.getpos('.')) ++ eq({ 0, 29, 4, 0 }, fn.getpos("'m")) ++ eq({ 0, 29, 6, 0 }, fn.getpos('.')) + end) + + it("outputting more than 'scrollback' lines", function() + feed_lines('new_line', 1, 11) + screen:expect([[ ++ line26 | + line27 | + {101:line2^8} | + line29 | + line30 | + new_line1 | +- new_line2 | + | + ]]) +- eq({ 0, 2, 4, 0 }, fn.getpos("'m")) +- eq({ 0, 2, 6, 0 }, fn.getpos('.')) ++ eq({ 0, 29, 4, 0 }, fn.getpos("'m")) ++ eq({ 0, 29, 6, 0 }, fn.getpos('.')) + end) + + it('outputting more lines than whole buffer', function() + feed_lines('new_line', 1, 20) + screen:expect([[ +- ^new_line6 | +- new_line7 | +- new_line8 | +- new_line9 | +- new_line10 | +- new_line11 | ++ line26 | ++ line27 | ++ {101:line2^8} | ++ line29 | ++ line30 | ++ new_line1 | + | + ]]) +- eq({ 0, 0, 0, 0 }, fn.getpos("'m")) +- eq({ 0, 1, 1, 0 }, fn.getpos('.')) ++ eq({ 0, 29, 4, 0 }, fn.getpos("'m")) ++ eq({ 0, 29, 6, 0 }, fn.getpos('.')) + end) + + it('clearing scrollback with ED 3', function() +@@ -243,12 +243,12 @@ local function test_terminal_scrollback(hide_curbuf) + end) + end) + +- describe('and cursor on scrollback row #12651', function() ++ describe('and cursor on a scrollback row', function() + before_each(function() + feed([[Hk$]]) +- fn.setpos("'m", { 0, 10, 4, 0 }) ++ fn.setpos("'m", { 0, 26, 4, 0 }) + local ns = api.nvim_create_namespace('test') +- api.nvim_buf_set_extmark(0, ns, 9, 0, { end_col = 6, hl_group = 'ErrorMsg' }) ++ api.nvim_buf_set_extmark(0, ns, 25, 0, { end_col = 6, hl_group = 'ErrorMsg' }) + screen:expect([[ + {101:line2^5} | + line26 | +@@ -263,23 +263,15 @@ local function test_terminal_scrollback(hide_curbuf) + it("outputting fewer than 'scrollback' lines", function() + feed_lines('new_line', 1, 6) + screen:expect_unchanged(hide_curbuf) +- eq({ 0, 4, 4, 0 }, fn.getpos("'m")) +- eq({ 0, 4, 6, 0 }, fn.getpos('.')) ++ eq({ 0, 26, 4, 0 }, fn.getpos("'m")) ++ eq({ 0, 26, 6, 0 }, fn.getpos('.')) + end) + + it("outputting more than 'scrollback' lines", function() + feed_lines('new_line', 1, 11) +- screen:expect([[ +- ^line27 | +- line28 | +- line29 | +- line30 | +- new_line1 | +- new_line2 | +- | +- ]]) +- eq({ 0, 0, 0, 0 }, fn.getpos("'m")) +- eq({ 0, 1, 1, 0 }, fn.getpos('.')) ++ screen:expect_unchanged(hide_curbuf) ++ eq({ 0, 26, 4, 0 }, fn.getpos("'m")) ++ eq({ 0, 26, 6, 0 }, fn.getpos('.')) + end) + end) + +@@ -302,7 +294,7 @@ local function test_terminal_scrollback(hide_curbuf) + ^ | + {5:-- TERMINAL --} | + ]]) +- eq(17, api.nvim_buf_line_count(0)) ++ eq(33, api.nvim_buf_line_count(0)) + try_resize(screen._width, screen._height - 2) + screen:expect([[ + line26 | +@@ -315,7 +307,7 @@ local function test_terminal_scrollback(hide_curbuf) + ^ | + {5:-- TERMINAL --} | + ]]) +- eq(18, api.nvim_buf_line_count(0)) ++ eq(34, api.nvim_buf_line_count(0)) + try_resize(screen._width, screen._height - 3) + screen:expect([[ + line30 | +@@ -325,7 +317,7 @@ local function test_terminal_scrollback(hide_curbuf) + ^ | + {5:-- TERMINAL --} | + ]]) +- eq(15, api.nvim_buf_line_count(0)) ++ eq(35, api.nvim_buf_line_count(0)) + try_resize(screen._width, screen._height + 3) + screen:expect([[ + line28 | +@@ -338,7 +330,7 @@ local function test_terminal_scrollback(hide_curbuf) + ^ | + {5:-- TERMINAL --} | + ]]) +- eq(16, api.nvim_buf_line_count(0)) ++ eq(36, api.nvim_buf_line_count(0)) + feed([[8]]) + screen:expect([[ + line20 | +@@ -380,7 +372,7 @@ local function test_terminal_scrollback(hide_curbuf) + ]]) + end) + +- it("outputting more than 'scrollback' lines in Normal mode", function() ++ it("keeps retained lines after outputting more than 'scrollback' in Normal mode", function() + feed([[]]) + feed_lines('new_line', 1, 11) + screen:expect([[ +@@ -394,15 +386,15 @@ local function test_terminal_scrollback(hide_curbuf) + ]]) + feed('gg') + screen:expect([[ +- ^line1 | ++ ^tty ready | ++ line1 | + {101:line2} | + line3 | + line4 | + new_line1 | +- new_line2 | + | + ]]) +- eq({ 0, 2, 4, 0 }, fn.getpos("'m")) ++ eq({ 0, 3, 4, 0 }, fn.getpos("'m")) + feed('G') + feed_lines('new_line', 12, 31) + screen:expect([[ +@@ -416,15 +408,15 @@ local function test_terminal_scrollback(hide_curbuf) + ]]) + feed('gg') + screen:expect([[ +- ^new_line17 | +- new_line18 | +- new_line19 | +- new_line20 | +- new_line21 | +- new_line22 | ++ ^tty ready | ++ line1 | ++ {101:line2} | ++ line3 | ++ line4 | ++ new_line1 | + | + ]]) +- eq({ 0, 0, 0, 0 }, fn.getpos("'m")) ++ eq({ 0, 3, 4, 0 }, fn.getpos("'m")) + end) + + it('clearing scrollback with ED 3', function() +@@ -575,14 +567,14 @@ local function test_terminal_scrollback(hide_curbuf) + describe('and height decreased by 1', function() + local function will_hide_top_line() + feed([[]]) +- try_resize(screen._width - 2, screen._height - 1) ++ try_resize(screen._width, screen._height - 1) + screen:expect([[ +- {101:line2} | +- line3 | +- line4 | +- rows: 5, cols: 28 | +- ^ | +- | ++ {101:line2} | ++ line3 | ++ line4 | ++ rows: 5, cols: 30 | ++ ^ | ++ | + ]]) + eq({ 0, 3, 4, 0 }, fn.getpos("'m")) + end +@@ -592,31 +584,35 @@ local function test_terminal_scrollback(hide_curbuf) + describe('and then decreased by 2', function() + before_each(function() + will_hide_top_line() +- try_resize(screen._width - 2, screen._height - 2) ++ try_resize(screen._width, screen._height - 2) ++ screen:expect({ any = 'rows: 3, cols: 30' }) + end) + + it('will hide the top 3 lines', function() +- screen:expect([[ +- rows: 5, cols: 28 | +- rows: 3, cols: 26 | +- ^ | +- | +- ]]) ++ screen:expect({ ++ grid = [[ ++ rows: 5, cols: 30 | ++ rows: 3, cols: 30 | ++ ^ | ++ | ++ ]], ++ unchanged = true, ++ }) + eq(8, api.nvim_buf_line_count(0)) + eq({ 0, 3, 4, 0 }, fn.getpos("'m")) + feed('3k') + screen:expect([[ +- ^line4 | +- rows: 5, cols: 28 | +- rows: 3, cols: 26 | +- | ++ ^line4 | ++ rows: 5, cols: 30 | ++ rows: 3, cols: 30 | ++ | + ]]) + feed('gg') + screen:expect([[ +- ^tty ready | +- line1 | +- {101:line2} | +- | ++ ^tty ready | ++ line1 | ++ {101:line2} | ++ | + ]]) + end) + end) +@@ -781,19 +777,19 @@ local function test_terminal_scrollback(hide_curbuf) + + it('will show all lines and leave a blank one at the end', function() + screen:expect([[ +- tty ready | +- line1 | +- {101:line2} | +- line3 | +- line4 | +- rows: 3, cols: 30 | +- rows: 4, cols: 30 | +- rows: 7, cols: 30 | +- rows: 11, cols: 30 | +- ^ | +- | +- {5:-- TERMINAL --} | +- ]]) ++ tty ready | ++ line1 | ++ {101:line2} | ++ line3 | ++ line4 | ++ rows: 3, cols: 30 | ++ rows: 4, cols: 30 | ++ rows: 7, cols: 30 | ++ rows: 11, cols: 30 | ++ ^ | ++ | ++ {5:-- TERMINAL --} | ++ ]]) + -- since there's an empty line after the cursor, the buffer line + -- count equals the terminal screen height + eq(11, api.nvim_buf_line_count(0)) +@@ -804,7 +800,7 @@ local function test_terminal_scrollback(hide_curbuf) + end) + end) + +- it('reducing &scrollback deletes extra lines immediately', function() ++ it('reducing &scrollback keeps Ghostty retained lines available', function() + feed_lines('line', 1, 30) + screen:expect([[ + line26 | +@@ -815,14 +811,20 @@ local function test_terminal_scrollback(hide_curbuf) + ^ | + {5:-- TERMINAL --} | + ]]) +- local term_height = 6 -- Actual terminal screen height, not the scrollback +- -- Initial ++ local term_height = 6 -- Actual terminal screen height, not the scrollback. + local scrollback = api.nvim_get_option_value('scrollback', { buf = buf }) +- eq(scrollback + term_height, fn.line('$')) +- eq(scrollback + term_height, fn.line('.')) +- n.fn.setpos("'m", { 0, scrollback + 1, 4, 0 }) ++ local line_count = fn.line('$') ++ eq(true, line_count > scrollback + term_height) ++ eq(line_count, fn.line('.')) ++ n.fn.setpos("'m", { 0, line_count - term_height + 1, 4, 0 }) + local ns = api.nvim_create_namespace('test') +- api.nvim_buf_set_extmark(0, ns, scrollback, 0, { end_col = 6, hl_group = 'ErrorMsg' }) ++ api.nvim_buf_set_extmark( ++ 0, ++ ns, ++ line_count - term_height, ++ 0, ++ { end_col = 6, hl_group = 'ErrorMsg' } ++ ) + screen:expect([[ + {101:line26} | + line27 | +@@ -832,15 +834,14 @@ local function test_terminal_scrollback(hide_curbuf) + ^ | + {5:-- TERMINAL --} | + ]]) +- -- Reduction + scrollback = scrollback - 2 + may_hide_curbuf() + api.nvim_set_option_value('scrollback', scrollback, { buf = buf }) + may_restore_curbuf() +- eq(scrollback + term_height, fn.line('$')) +- eq(scrollback + term_height, fn.line('.')) ++ eq(line_count, fn.line('$')) ++ eq(line_count, fn.line('.')) + screen:expect_unchanged(hide_curbuf) +- eq({ 0, scrollback + 1, 4, 0 }, n.fn.getpos("'m")) ++ eq({ 0, line_count - term_height + 1, 4, 0 }, n.fn.getpos("'m")) + end) + end + +@@ -890,15 +891,7 @@ describe("'scrollback' option", function() + api.nvim_set_option_value('shell', string.format('"%s" INTERACT', testprg('shell-test')), {}) + end + +- local function expect_lines(expected, epsilon) +- local ep = epsilon and epsilon or 0 +- local actual = eval("line('$')") +- if expected > actual + ep and expected < actual - ep then +- error('expected (+/- ' .. ep .. '): ' .. expected .. ', actual: ' .. tostring(actual)) +- end +- end +- +- it('set to 0 behaves as 1', function() ++ it('set to 0 behaves as the maximum in a terminal buffer', function() + local screen + if is_os('win') then + screen = tt.setup_screen(nil, { 'cmd.exe' }, 30) +@@ -907,14 +900,15 @@ describe("'scrollback' option", function() + end + + api.nvim_set_option_value('scrollback', 0, {}) ++ eq(1000000, api.nvim_get_option_value('scrollback', {})) + feed_data(('%s REP 31 line%s'):format(testprg('shell-test'), is_os('win') and '\r' or '\n')) + screen:expect { any = '30: line ' } + retry(nil, nil, function() +- expect_lines(7) ++ t.matches('0: line', table.concat(api.nvim_buf_get_lines(0, 0, -1, true), '\n')) + end) + end) + +- it('deletes lines (only) if necessary', function() ++ it('treats the line limit as best-effort', function() + local screen + if is_os('win') then + command([[let $PROMPT='$$']]) +@@ -933,16 +927,17 @@ describe("'scrollback' option", function() + screen:expect { any = '30: line ' } + + retry(nil, nil, function() +- expect_lines(33, 2) ++ t.matches('0: line', table.concat(api.nvim_buf_get_lines(0, 0, -1, true), '\n')) + end) ++ local retained_lines = eval("line('$')") + api.nvim_set_option_value('scrollback', 10, {}) + poke_eventloop() + retry(nil, nil, function() +- expect_lines(16) ++ eq(retained_lines, eval("line('$')")) + end) + api.nvim_set_option_value('scrollback', 10000, {}) + retry(nil, nil, function() +- expect_lines(16) ++ eq(retained_lines, eval("line('$')")) + end) + -- Terminal job data is received asynchronously, may happen before the + -- 'scrollback' option is synchronized with the internal sb_buffer. +@@ -970,13 +965,64 @@ describe("'scrollback' option", function() + {5:-- TERMINAL --} | + ]]) + end +- expect_lines(58) ++ eq(true, eval("line('$')") > retained_lines) + + -- Verify off-screen state + eq((is_os('win') and '36: line' or '35: line'), eval("getline(line('w0') - 1)->trim(' ', 2)")) + eq((is_os('win') and '27: line' or '26: line'), eval("getline(line('w0') - 10)->trim(' ', 2)")) + end) + ++ it("increases Ghostty's best-effort scrollback line limit", function() ++ Screen.new(80, 24) ++ ++ local buf = api.nvim_get_current_buf() ++ local chan = api.nvim_open_term(buf, {}) ++ api.nvim_set_option_value('scrollback', 10, { buf = buf }) ++ ++ local function send_lines(prefix) ++ local lines = {} ++ for i = 1, 500 do ++ lines[i] = prefix .. i ++ end ++ api.nvim_chan_send(chan, table.concat(lines, '\r\n') .. '\r\n') ++ retry(nil, 3000, function() ++ eq({ prefix .. '500', '' }, api.nvim_buf_get_lines(buf, -3, -1, true)) ++ end) ++ end ++ ++ send_lines('old') ++ local low_count = api.nvim_buf_line_count(buf) ++ ++ api.nvim_set_option_value('scrollback', 2000, { buf = buf }) ++ send_lines('new') ++ eq(true, api.nvim_buf_line_count(buf) > low_count + 250) ++ end) ++ ++ it(':setglobal does not change the current terminal limit', function() ++ Screen.new(80, 24) ++ ++ local buf = api.nvim_get_current_buf() ++ local chan = api.nvim_open_term(buf, {}) ++ api.nvim_set_option_value('scrollback', 2000, { buf = buf }) ++ ++ local lines = {} ++ for i = 1, 500 do ++ lines[i] = 'line' .. i ++ end ++ api.nvim_chan_send(chan, table.concat(lines, '\r\n') .. '\r\n') ++ retry(nil, 3000, function() ++ eq({ 'line500', '' }, api.nvim_buf_get_lines(buf, -3, -1, true)) ++ end) ++ local line_count = api.nvim_buf_line_count(buf) ++ ++ command('setglobal scrollback=10') ++ eq(2000, api.nvim_get_option_value('scrollback', { buf = buf })) ++ api.nvim_chan_send(chan, 'after\r\n') ++ retry(nil, 3000, function() ++ eq(line_count + 1, api.nvim_buf_line_count(buf)) ++ end) ++ end) ++ + it('defaults to 10000 in :terminal buffers', function() + set_fake_shell() + command('terminal') +@@ -1006,9 +1052,7 @@ describe("'scrollback' option", function() + + -- _Local_ scrollback=-1 in :terminal forces the _maximum_. + command('setlocal scrollback=-1') +- retry(nil, nil, function() -- Fixup happens on refresh, not immediately. +- eq(1000000, api.nvim_get_option_value('scrollback', {})) +- end) ++ eq(1000000, api.nvim_get_option_value('scrollback', {})) + + -- _Local_ scrollback=-1 during TermOpen forces the maximum. #9605 + command('setglobal scrollback=-1') +@@ -1131,6 +1175,77 @@ describe('pending scrollback line handling', function() + end) + end) + ++describe('wrapped lines in scrollback are reflowed on resize', function() ++ local screen --- @type test.functional.ui.screen ++ local buf --- @type integer ++ local chan --- @type integer ++ ++ local long_line = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWX' ++ local narrow_lines = { ++ 'abcdefghijklmnopqrst', ++ 'uvwxyzABCDEFGHIJKLMN', ++ 'OPQRSTUVWX', ++ 'line1', ++ 'line2', ++ 'line3', ++ 'line4', ++ 'line5', ++ 'line6', ++ 'line7', ++ 'line8', ++ '', ++ } ++ local wide_lines = { ++ 'abcdefghijklmnopqrstuvwxyzABCD', ++ 'EFGHIJKLMNOPQRSTUVWX', ++ 'line1', ++ 'line2', ++ 'line3', ++ 'line4', ++ 'line5', ++ 'line6', ++ 'line7', ++ 'line8', ++ '', ++ } ++ ++ local function expect_buffer_lines(lines) ++ retry(nil, 1000, function() ++ eq(lines, api.nvim_buf_get_lines(buf, 0, -1, true)) ++ end) ++ end ++ ++ before_each(function() ++ clear() ++ screen = Screen.new(20, 6) ++ buf = api.nvim_get_current_buf() ++ chan = exec_lua(function() ++ local chan_id = vim.api.nvim_open_term(0, {}) ++ vim.bo.scrollback = 50 ++ return chan_id ++ end) ++ ++ local data = long_line .. '\n' ++ for i = 1, 8 do ++ data = data .. ('line%d\n'):format(i) ++ end ++ api.nvim_chan_send(chan, data) ++ expect_buffer_lines(narrow_lines) ++ end) ++ ++ it('reflows scrollback when the terminal grows wider', function() ++ screen:try_resize(30, screen._height) ++ expect_buffer_lines(wide_lines) ++ end) ++ ++ it('reflows scrollback back to the previous wrapping when narrowed again', function() ++ screen:try_resize(30, screen._height) ++ expect_buffer_lines(wide_lines) ++ screen:try_resize(20, screen._height) ++ expect_buffer_lines(narrow_lines) ++ end) ++end) ++ + describe('scrollback is correct', function() + local screen --- @type test.functional.ui.screen + local buf --- @type integer +@@ -1147,6 +1262,7 @@ describe('scrollback is correct', function() + foreground = Screen.colors.White, + background = Screen.colors.DarkGreen, + }, ++ [103] = { foreground = tonumber('0xcc6666'), fg_indexed = true }, + }) + api.nvim_buf_set_lines(0, 0, -1, true, { '\027[31mTEST\027[0m 0' }) + feed('yy99pG$98kg') +@@ -1176,10 +1292,10 @@ describe('scrollback is correct', function() + local function check_common() + feed('lG') + screen:expect([[ +- │{100:TEST} 96 | +- {1:~ }│{100:TEST} 97 | +- {1:~ }│{100:TEST} 98 | +- {1:~ }│{100:TEST} 99 | ++ │{103:TEST} 96 | ++ {1:~ }│{103:TEST} 97 | ++ {1:~ }│{103:TEST} 98 | ++ {1:~ }│{103:TEST} 99 | + {1:~ }│^ | + {2:[No Name] }{102:[Scratch] [-] }| + 99 lines changed | +@@ -1192,11 +1308,11 @@ describe('scrollback is correct', function() + vim.api.nvim_win_set_cursor(win, { 3, 0 }) + end) + screen:expect([[ +- ^ │{100:TEST} 0 | +- {1:~ }│{100:TEST} 1 | +- {1:~ }│{100:TEST} 2 | +- {1:~ }│{100:TEST} 3 | +- {1:~ }│{100:TEST} 4 | ++ ^ │{103:TEST} 0 | ++ {1:~ }│{103:TEST} 1 | ++ {1:~ }│{103:TEST} 2 | ++ {1:~ }│{103:TEST} 3 | ++ {1:~ }│{103:TEST} 4 | + {3:[No Name] }{101:[Scratch] [-] }| + 99 lines changed | + ]]) +@@ -1212,16 +1328,16 @@ describe('scrollback is correct', function() + vim.api.nvim_win_set_cursor(win, { 3, 3 }) + end) + screen:expect([[ +- ^ │{100:TEST} 86 | +- {1:~ }│{100:TEST} 87 | +- {1:~ }│{100:TEST} 88 | +- {1:~ }│{100:TEST} 89 | +- {1:~ }│{100:TEST} 90 | ++ ^ │{103:TEST} 0 | ++ {1:~ }│{103:TEST} 1 | ++ {1:~ }│{103:TEST} 2 | ++ {1:~ }│{103:TEST} 3 | ++ {1:~ }│{103:TEST} 4 | + {3:[No Name] }{101:[Scratch] [-] }| + 99 lines changed | + ]]) +- eq({ 1, 0 }, api.nvim_win_get_cursor(win)) +- check_buffer_lines(86, 99) ++ eq({ 3, 0 }, api.nvim_win_get_cursor(win)) ++ check_buffer_lines(0, 99) + check_common() + end) + +@@ -1235,13 +1351,13 @@ describe('scrollback is correct', function() + vim.cmd('$') + end) + screen:expect([[ +- │{100:TEST} 88 | +- {1:~ }│{100:TEST} 89 | +- {1:~ }│{100:TEST} 90 | +- {1:~ }│{100:TEST} 91 | +- {1:~ }│{100:TEST} 92 | +- {1:~ }│{100:TEST} 93 | +- {1:~ }│{100:TEST} 94 | ++ │{103:TEST} 88 | ++ {1:~ }│{103:TEST} 89 | ++ {1:~ }│{103:TEST} 90 | ++ {1:~ }│{103:TEST} 91 | ++ {1:~ }│{103:TEST} 92 | ++ {1:~ }│{103:TEST} 93 | ++ {1:~ }│{103:TEST} 94 | + {1:~ }│^ | + {2:[No Name] }{102:[Scratch] [-] }| + | +@@ -1263,15 +1379,15 @@ describe('scrollback is correct', function() + ) + assert(#perms == 2 + 6 + 6) + local screen_final = [[ +- │{100:TEST} 91 | +- {1:~ }│{100:TEST} 92 | +- {1:~ }│{100:TEST} 93 | +- {1:~ }│{100:TEST} 94 | +- {1:~ }│{100:TEST} 95 | +- {1:~ }│{100:TEST} 96 | +- {1:~ }│{100:TEST} 97 | +- {1:~ }│{100:TEST} 98 | +- {1:~ }│{100:TEST} 99 | ++ │{103:TEST} 91 | ++ {1:~ }│{103:TEST} 92 | ++ {1:~ }│{103:TEST} 93 | ++ {1:~ }│{103:TEST} 94 | ++ {1:~ }│{103:TEST} 95 | ++ {1:~ }│{103:TEST} 96 | ++ {1:~ }│{103:TEST} 97 | ++ {1:~ }│{103:TEST} 98 | ++ {1:~ }│{103:TEST} 99 | + {1:~ }│^ | + {2:[No Name] }{102:[Scratch] [-] }| + | +@@ -1292,22 +1408,22 @@ describe('scrollback is correct', function() + end) + end + +- describe('with full scrollback,', function() ++ describe('with history beyond the requested scrollback,', function() + before_each(function() + api.nvim_set_option_value('scrollback', 6, { buf = buf }) +- check_buffer_lines(82, 94) ++ check_buffer_lines(0, 94) + end) + + it('output first', function() + command(send_cmd .. ' | resize +2') + screen:expect(screen_final) +- check_buffer_lines(87, 99) ++ check_buffer_lines(0, 99) + end) + + it('resize first', function() + command('resize +2 | ' .. send_cmd) + screen:expect(screen_final) +- check_buffer_lines(85, 99) ++ check_buffer_lines(0, 99) + end) + end) + end) +@@ -1321,11 +1437,11 @@ describe('scrollback is correct', function() + ) + assert(#perms == 2 + 6 + 6) + local screen_final = [[ +- │{100:TEST} 95 | +- {1:~ }│{100:TEST} 96 | +- {1:~ }│{100:TEST} 97 | +- {1:~ }│{100:TEST} 98 | +- {1:~ }│{100:TEST} 99 | ++ │{103:TEST} 95 | ++ {1:~ }│{103:TEST} 96 | ++ {1:~ }│{103:TEST} 97 | ++ {1:~ }│{103:TEST} 98 | ++ {1:~ }│{103:TEST} 99 | + {1:~ }│^ | + {2:[No Name] }{102:[Scratch] [-] }| + | +@@ -1347,10 +1463,10 @@ describe('scrollback is correct', function() + end + end) + +- describe("decreases by more than 'scrollback'", function() ++ describe("decreases by more than the requested 'scrollback'", function() + before_each(function() + api.nvim_set_option_value('scrollback', 4, { buf = buf }) +- check_buffer_lines(84, 94) ++ check_buffer_lines(0, 94) + end) + + local perms = { +@@ -1361,7 +1477,7 @@ describe('scrollback is correct', function() + { 'resize +6', 'resize -12', send_cmd }, + } + local screen_final = [[ +- │{100:TEST} 99 | ++ │{103:TEST} 99 | + {1:~ }│^ | + {2:[No Name] }{102:[Scratch] [-] }| + | +@@ -1378,7 +1494,7 @@ describe('scrollback is correct', function() + end + end) + screen:expect(screen_final) +- check_buffer_lines(95, 99) ++ check_buffer_lines(0, 99) + end) + end + end) +diff --git a/test/functional/terminal/synchronized_output_spec.lua b/test/functional/terminal/synchronized_output_spec.lua +index c0a9a6467f7ef6..b6bc1f5c0afa62 100644 +--- a/test/functional/terminal/synchronized_output_spec.lua ++++ b/test/functional/terminal/synchronized_output_spec.lua +@@ -5,6 +5,7 @@ local Screen = require('test.functional.ui.screen') + local describe, it, before_each = t.describe, t.it, t.before_each + local clear = n.clear + local api = n.api ++local exec_lua = n.exec_lua + local eq = t.eq + local poke_eventloop = n.poke_eventloop + local assert_alive = n.assert_alive +@@ -157,4 +158,24 @@ describe(':terminal synchronized output (mode 2026)', function() + | + ]]) + end) ++ ++ it('flushes a synchronized update completed in one write', function() ++ api.nvim_chan_send(chan, 'before\r\n') ++ screen:expect([[ ++ ^before | ++ |*5 ++ | ++ ]]) ++ ++ exec_lua(function(chan_, buf_, input) ++ vim.api.nvim_chan_send(chan_, input) ++ vim.schedule(function() ++ vim.g.sync_output_line = vim.api.nvim_buf_get_lines(buf_, 1, 2, true)[1] ++ end) ++ end, chan, buf, CSI .. '?2026hcoalesced' .. CSI .. '?2026l') ++ t.retry(nil, nil, function() ++ eq(true, exec_lua('return vim.g.sync_output_line ~= nil')) ++ end) ++ eq('coalesced', exec_lua('return vim.g.sync_output_line')) ++ end) + end) +diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua +index 3087268e944f19..cd098d9f028be4 100644 +--- a/test/functional/terminal/tui_spec.lua ++++ b/test/functional/terminal/tui_spec.lua +@@ -132,8 +132,6 @@ describe('TUI', function() + '--cmd', + nvim_set .. ' laststatus=2 background=dark', + }, { env = env_notermguicolors }) +- tt.override_screen_expect_for_conpty(screen) +- + feed_data('iZZZSENTINEL') + + -- Leave insert mode and run a job that writes directly to CON, blocking +@@ -142,7 +140,7 @@ describe('TUI', function() + + screen:expect([[ + ZZZSENTINE^L | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -173,7 +171,6 @@ describe('TUI :detach', function() + '--cmd', + nvim_set .. ' laststatus=2 background=dark', + }, { env = env_notermguicolors, cols = opts.cols }) +- tt.override_screen_expect_for_conpty(screen) + end + + it('does not stop server', function() +@@ -182,7 +179,7 @@ describe('TUI :detach', function() + tt.feed_data('iHello, World') + screen:expect([[ + Hello, World^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -233,7 +230,7 @@ describe('TUI :detach', function() + + screen_reattached:expect([[ + We did it, pooky^. | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -309,7 +306,7 @@ describe('TUI :detach', function() + tt.feed_data('\013') + screen:expect([[ + Hello from detach^! | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -579,7 +576,7 @@ describe('TUI :restart', function() + + local s0 = [[ + ^ | +- {1:~}{18: }|*3 ++ {1:~} |*3 + {3:[No Name] }| + | + {5:-- TERMINAL --} | +@@ -607,7 +604,7 @@ describe('TUI :restart', function() + local s1 = [[ + | + ^Hello1 | +- {1:~}{18: }|*2 ++ {1:~} |*2 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -634,7 +631,7 @@ describe('TUI :restart', function() + | + Hello2 | + ^World2 | +- {1:~}{18: }| ++ {1:~} | + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -664,7 +661,7 @@ describe('TUI :restart', function() + tt.feed_data(cmd .. ' +echo\013') + screen:expect([[ + | +- {1:~}{18: }| ++ {1:~} | + {3: }| + {9:E5201: Restart failed: +cmd did not quit server: e}| + {9:cho} | +@@ -746,7 +743,7 @@ describe('TUI :restart', function() + screen:try_resize(60, 6) + screen:expect([[ + ^ | +- {1:~}{18: }|*2 ++ {1:~} |*2 + {3:[No Name] }| + | + {5:-- TERMINAL --} | +@@ -756,7 +753,7 @@ describe('TUI :restart', function() + tt.feed_data(':restart! echo "restarted"\013') + screen:expect([[ + ^ | +- {1:~}{18: }|*2 ++ {1:~} |*2 + {3:[No Name] }| + restarted | + {5:-- TERMINAL --} | +@@ -1055,7 +1052,7 @@ describe('TUI', function() + }, { env = env_notermguicolors }) + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + | + {5:-- TERMINAL --} | +@@ -1130,7 +1127,6 @@ describe('TUI', function() + end) + + it('accepts resize while pager is active', function() +- tt.override_screen_expect_for_conpty(screen) + child_session:request( + 'nvim_exec2', + [[ +@@ -1227,7 +1223,7 @@ describe('TUI', function() + feed_data('\003') + screen:expect([[ + ^ | +- {100:~ }|*6 ++ {100:~} |*6 + {3:[No Name] }| + | + {5:-- TERMINAL --} | +@@ -1240,7 +1236,7 @@ describe('TUI', function() + abc | + test1 | + test2^ | +- {100:~ }| ++ {100:~} | + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -1250,7 +1246,7 @@ describe('TUI', function() + abc | + test1 | + test^2 | +- {100:~ }| ++ {100:~} | + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -1290,7 +1286,7 @@ describe('TUI', function() + feed_data('i\022\027j') + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -1323,7 +1319,7 @@ describe('TUI', function() + expect_child_buf_lines({ 'ESCsemicolonCtrlEscSuperEscESC' }) + screen:expect([[ + ESCsemicolonCtrlEscSuperEscES^C | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -1339,7 +1335,7 @@ describe('TUI', function() + feed_data('i\022\027\000') + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -1354,7 +1350,7 @@ describe('TUI', function() + feed_data('i') + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -1367,7 +1363,7 @@ describe('TUI', function() + feed_data('\130') + screen:expect([[ + ⌂^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -1382,7 +1378,7 @@ describe('TUI', function() + feed_data('5u') + screen:expect([[ + ⌂^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + {5:-- ^X mode (^]^D^E^F^I^K^L^N^O^P^Rs^U^V^Y)} | + {5:-- TERMINAL --} | +@@ -1397,7 +1393,7 @@ describe('TUI', function() + vim.uv.sleep(225) + screen:expect([[ + ^⌂ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -1411,7 +1407,7 @@ describe('TUI', function() + feed_data('\022\013') -- ctrl+m + screen:expect([[ + {104:^G^V^M}^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -1420,7 +1416,7 @@ describe('TUI', function() + feed_data('\031') + screen:expect([[ + {104:^G^V^M}!!!^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -1442,7 +1438,7 @@ describe('TUI', function() + feed_data('\031') + screen:expect([[ + ^ | +- {100:~ }| ++ {100:~} | + {3:[No Name] }| + interact $ | + |*2 +@@ -1747,9 +1743,9 @@ describe('TUI', function() + end + screen:expect([[ + ^popup menu test | +- {100:~ }{105: foo }{100: }| +- {100:~ }{105: bar }{100: }| +- {100:~ }{105: baz }{100: }| ++ {100:~} {105: foo } | ++ {100:~} {105: bar } | ++ {100:~} {105: baz } | + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -1767,9 +1763,9 @@ describe('TUI', function() + end + screen:expect([[ + ^popup menu test | +- {100:~ }{106: foo }{100: }| +- {100:~ }{105: bar }{100: }| +- {100:~ }{105: baz }{100: }| ++ {100:~} {106: foo } | ++ {100:~} {105: bar } | ++ {100:~} {105: baz } | + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -1781,9 +1777,9 @@ describe('TUI', function() + end + screen:expect([[ + ^popup menu test | +- {100:~ }{105: foo }{100: }| +- {100:~ }{105: bar }{100: }| +- {100:~ }{106: baz }{100: }| ++ {100:~} {105: foo } | ++ {100:~} {105: bar } | ++ {100:~} {106: baz } | + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -1795,9 +1791,9 @@ describe('TUI', function() + end + screen:expect([[ + ^popup menu test | +- {100:~ }{105: foo }{100: }| +- {100:~ }{106: bar }{100: }| +- {100:~ }{105: baz }{100: }| ++ {100:~} {105: foo } | ++ {100:~} {106: bar } | ++ {100:~} {105: baz } | + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -1809,7 +1805,7 @@ describe('TUI', function() + end + screen:expect([[ + ^popup menu test | +- {100:~ }|*3 ++ {100:~} {100: } |*3 + {3:[No Name] [+] }| + :let g:menustr = 'bar' | + {5:-- TERMINAL --} | +@@ -1827,8 +1823,8 @@ describe('TUI', function() + end + screen:expect([[ + ^popup menu test | +- {100:~ }|*2 +- {100:~ }{105: foo }{100: }| ++ {100:~} {100: } |*2 ++ {100:~} {100: } {105: foo } | + {3:[No Name] [+] }{105: bar }{3: }| + :let g:menustr = 'bar' {105: baz } | + {5:-- TERMINAL --} | +@@ -1840,8 +1836,8 @@ describe('TUI', function() + end + screen:expect([[ + ^popup menu test | +- {100:~ }|*2 +- {100:~ }{105: foo }{100: }| ++ {100:~} {100: } |*2 ++ {100:~} {100: } {105: foo } | + {3:[No Name] [+] }{105: bar }{3: }| + :let g:menustr = 'bar' {106: baz } | + {5:-- TERMINAL --} | +@@ -1853,7 +1849,8 @@ describe('TUI', function() + end + screen:expect([[ + ^popup menu test | +- {100:~ }|*3 ++ {100:~} {100: } |*2 ++ {100:~} {100: } {100: } | + {3:[No Name] [+] }| + :let g:menustr = 'baz' | + {5:-- TERMINAL --} | +@@ -1892,7 +1889,7 @@ describe('TUI', function() + screen:expect([[ + 0123456789./*-+ | + =^ | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -1901,7 +1898,7 @@ describe('TUI', function() + screen:expect([[ + 0123456789./*-+ | + ^= | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -1910,7 +1907,7 @@ describe('TUI', function() + screen:expect([[ + 0123456789./*-+ | + =^ | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -1919,7 +1916,7 @@ describe('TUI', function() + screen:expect([[ + 0^123456789./*-+ | + = | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -1928,7 +1925,7 @@ describe('TUI', function() + screen:expect([[ + 0123456789./*-+ | + =^ | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -1937,7 +1934,7 @@ describe('TUI', function() + screen:expect([[ + 0123456789./*-+ | + =^ | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + {5:-- REPLACE --} | + {5:-- TERMINAL --} | +@@ -1946,7 +1943,7 @@ describe('TUI', function() + screen:expect([[ + 0123456789./*-+ | + ^= | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -1955,7 +1952,7 @@ describe('TUI', function() + screen:expect([[ + ^0123456789./*-+ | + = | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -1964,7 +1961,7 @@ describe('TUI', function() + screen:expect([[ + 0123456789^./*-+ | + = | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -1973,7 +1970,7 @@ describe('TUI', function() + screen:expect([[ + 0123456789^/*-+ | + = | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -1982,7 +1979,7 @@ describe('TUI', function() + screen:expect([[ + ^0123456789/*-+ | + = | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -1991,7 +1988,7 @@ describe('TUI', function() + screen:expect([[ + 0123456789/*-^+ | + = | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -2008,7 +2005,7 @@ describe('TUI', function() + screen:expect([[ + {107: + [No Name] + [No Name] }{5: [No Name] }{2: }{107:X}| + ^ | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] }| + | + {5:-- TERMINAL --} | +@@ -2018,7 +2015,7 @@ describe('TUI', function() + {107: + [No Name] }{5: + [No Name] }{107: [No Name] }{2: }{107:X}| + 0123456789/*-^+ | + = | +- {100:~ }| ++ {100:~} | + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -2027,7 +2024,7 @@ describe('TUI', function() + screen:expect([[ + {107: + [No Name] + [No Name] }{5: [No Name] }{2: }{107:X}| + ^ | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] }| + | + {5:-- TERMINAL --} | +@@ -2048,7 +2045,7 @@ describe('TUI', function() + screen:expect([[ + | + ^ | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -2060,7 +2057,7 @@ describe('TUI', function() + feed_data('i""\027i\027[200~') + screen:expect([[ + "^" | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -2069,7 +2066,7 @@ describe('TUI', function() + expect_child_buf_lines({ '"pasted from terminal"' }) + screen:expect([[ + "pasted from terminal^" | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -2081,7 +2078,7 @@ describe('TUI', function() + wait_for_mode('n') + screen:expect([[ + "pasted from termina^l" | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -2092,7 +2089,7 @@ describe('TUI', function() + screen:expect([[ + "pasted from terminapasted from terminalpasted fro| + m termina^ll" | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -2135,7 +2132,7 @@ describe('TUI', function() + screen:expect([[ + thisjust paste it^™3 is here | + | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -2191,7 +2188,7 @@ describe('TUI', function() + line 1 | + ESC:{104:^[} / CR: | + ^x | +- {100:~ }| ++ {100:~} | + {3:[No Name] [+] 3,1 All}| + | + {5:-- TERMINAL --} | +@@ -2242,7 +2239,7 @@ describe('TUI', function() + screen:expect([[ + foo | + | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + :"^" | + {5:-- TERMINAL --} | +@@ -2256,7 +2253,7 @@ describe('TUI', function() + screen:expect([[ + foo | + | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + :"line 1^" | + {5:-- TERMINAL --} | +@@ -2268,7 +2265,7 @@ describe('TUI', function() + screen:expect([[ + foo |*2 + ^ | +- {100:~ }| ++ {100:~} | + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -2315,7 +2312,7 @@ describe('TUI', function() + screen:expect([[ + foo | + ^ | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -2325,7 +2322,7 @@ describe('TUI', function() + screen:expect([[ + foo | + ^ | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + {101:paste: Lua: [string ""]:4: fake fail} | + {5:-- TERMINAL --} | +@@ -2344,7 +2341,7 @@ describe('TUI', function() + screen:expect([[ + foo |*2 + ^ | +- {100:~ }| ++ {100:~} | + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -2354,7 +2351,7 @@ describe('TUI', function() + screen:expect([[ + foo |*2 + typed input..^. | +- {100:~ }| ++ {100:~} | + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -2418,7 +2415,6 @@ describe('TUI', function() + end) + + it("paste: 'nomodifiable' buffer", function() +- tt.override_screen_expect_for_conpty(screen) + child_exec_lua([[ + vim.bo.modifiable = false + -- Truncate the error message to hide the line number +@@ -2427,7 +2423,7 @@ describe('TUI', function() + feed_data('\027[200~fail 1\nfail 2\n\027[201~') + screen:expect([[ + | +- {100:~ }| ++ {100:~} | + {3: }| + {101:paste: Lua: Vim:E21: Cannot make changes, 'modifia}| + {101:ble' is off} | +@@ -2460,7 +2456,7 @@ describe('TUI', function() + screen:expect([[ + zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz| + zzzzzzzzzzzzzz end^ | +- {100:~ }|*2 ++ {100:~} |*2 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -2476,7 +2472,7 @@ describe('TUI', function() + feed_data('\027[200~' .. expected .. '\027[201~') + screen:expect([[ + | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + :<^ | + {5:-- TERMINAL --} | +@@ -2552,7 +2548,7 @@ describe('TUI', function() + feed_data('\027[201~') + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -2568,7 +2564,7 @@ describe('TUI', function() + feed_data('00~pasted from terminal\027[201~') + screen:expect([[ + pasted from terminal^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -2584,7 +2580,7 @@ describe('TUI', function() + feed_data('1~') + screen:expect([[ + pasted from terminal^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -2606,7 +2602,7 @@ describe('TUI', function() + feed_data('\027[200~pasted') -- phase 1 + screen:expect([[ + pasted^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -2614,7 +2610,7 @@ describe('TUI', function() + feed_data(' from terminal') -- phase 2 + screen:expect([[ + pasted from terminal^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -2629,7 +2625,6 @@ describe('TUI', function() + end) + + it('allows termguicolors to be set at runtime', function() +- tt.override_screen_expect_for_conpty(screen) + screen:set_option('rgb', true) + feed_data(':hi SpecialKey ctermfg=3 guifg=SeaGreen\n') + feed_data('i') +@@ -2638,7 +2633,7 @@ describe('TUI', function() + feed_data(':set termguicolors?\n') + screen:expect([[ + {110:^^G} | +- {111:~ }|*3 ++ {111:~} |*3 + {3:[No Name] [+] }| + notermguicolors | + {5:-- TERMINAL --} | +@@ -2647,7 +2642,7 @@ describe('TUI', function() + feed_data(':set termguicolors\n') + screen:expect([[ + {113:^^G} | +- {1:~}{18: }|*3 ++ {1:~} |*3 + {3:[No Name] [+] }| + :set termguicolors | + {5:-- TERMINAL --} | +@@ -2656,7 +2651,7 @@ describe('TUI', function() + feed_data(':set notermguicolors\n') + screen:expect([[ + {110:^^G} | +- {111:~ }|*3 ++ {111:~} |*3 + {3:[No Name] [+] }| + :set notermguicolors | + {5:-- TERMINAL --} | +@@ -2673,15 +2668,15 @@ describe('TUI', function() + { bold = true }, + }, + [3] = { { bold = true }, { bold = true } }, +- [4] = { { fg_indexed = true, foreground = tonumber('0xe0e000') }, { foreground = 3 } }, ++ [4] = { { fg_indexed = true, foreground = tonumber('0xf0c674') }, { foreground = 3 } }, + [5] = { { foreground = tonumber('0xff8000') }, {} }, + [6] = { + { + fg_indexed = true, + bg_indexed = true, + bold = true, +- background = tonumber('0x66ff99'), +- foreground = Screen.colors.Black, ++ background = tonumber('0x87ffaf'), ++ foreground = tonumber('0x1d1f21'), + }, + { bold = true, background = 121, foreground = 0 }, + }, +@@ -2689,11 +2684,18 @@ describe('TUI', function() + { + fg_indexed = true, + bg_indexed = true, +- background = tonumber('0x66ff99'), +- foreground = Screen.colors.Black, ++ background = tonumber('0x87ffaf'), ++ foreground = tonumber('0x1d1f21'), + }, + { background = 121, foreground = 0 }, + }, ++ [8] = { ++ { ++ bg_indexed = true, ++ background = tonumber('0x87ffaf'), ++ }, ++ { background = 121 }, ++ }, + }) + + child_exec_lua('vim.o.statusline="^^^^^^^"') +@@ -2723,19 +2725,18 @@ describe('TUI', function() + ^tty ready | + {4:text}colortext | + |*2 +- {6:^^^^^^^}{7: }| ++ {6:^^^^^^^}{8: }| + :set notermguicolors | + {3:-- TERMINAL --} | + ]]) + end) + +- -- Note: libvterm doesn't support colored underline or undercurl. + it('supports undercurl and underdouble when run in :terminal', function() + child_session:request('nvim_set_hl', 0, 'Visual', { undercurl = true }) + feed_data('ifoobar\027V') + screen:expect([[ + {114:fooba}^r | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + {5:-- VISUAL LINE --} | + {5:-- TERMINAL --} | +@@ -2743,7 +2744,7 @@ describe('TUI', function() + child_session:request('nvim_set_hl', 0, 'Visual', { underdouble = true }) + screen:expect([[ + {115:fooba}^r | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + {5:-- VISUAL LINE --} | + {5:-- TERMINAL --} | +@@ -2818,7 +2819,6 @@ describe('TUI', function() + end) + + it('allows grid to assume wider ambiwidth chars than host terminal', function() +- tt.override_screen_expect_for_conpty(screen) + child_session:request( + 'nvim_buf_set_lines', + 0, +@@ -2863,7 +2863,6 @@ describe('TUI', function() + end) + + it('allows grid to assume wider non-ambiwidth chars than host terminal', function() +- tt.override_screen_expect_for_conpty(screen) + child_session:request( + 'nvim_buf_set_lines', + 0, +@@ -2930,7 +2929,7 @@ describe('TUI', function() + {107:ꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨ}|*310 + {107:ꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨ℃ }| + b | +- {100:~ }|*17 ++ {100:~} |*17 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -3008,7 +3007,7 @@ describe('TUI', function() + feed_data ':set visualbell\n' + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + :set visualbell | + {5:-- TERMINAL --} | +@@ -3020,7 +3019,7 @@ describe('TUI', function() + feed_data 'i' + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -3088,7 +3087,7 @@ describe('TUI', function() + ) + screen:expect([[ + ^ | +- {1:~}{18: }|*3 ++ {1:~} |*3 + {3:[No Name] }| + | + {5:-- TERMINAL --} | +@@ -3096,35 +3095,13 @@ describe('TUI', function() + feed_data('i') + screen:expect([[ + ^ | +- {1:~}{18: }|*3 ++ {1:~} |*3 + {3:[No Name] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | + ]]) + end) + +- it('redraws on SIGWINCH even if terminal size is unchanged #23411', function() +- -- On Windows, SIGWINCH cannot be sent as a signal with uv_kill(), while +- -- SIGWINCH handlers are only called on terminal resize. +- t.skip(is_os('win'), 'N/A for Windows') +- child_session:request('nvim_echo', { { 'foo' } }, false, {}) +- screen:expect([[ +- ^ | +- {100:~ }|*3 +- {3:[No Name] }| +- foo | +- {5:-- TERMINAL --} | +- ]]) +- exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigwinch')]]) +- screen:expect([[ +- ^ | +- {100:~ }|*3 +- {3:[No Name] }| +- | +- {5:-- TERMINAL --} | +- ]]) +- end) +- + it('supports hiding cursor', function() + child_session:request( + 'nvim_command', +@@ -3133,7 +3110,7 @@ describe('TUI', function() + feed_data(':call jobwait([g:id])\n') + screen:expect([[ + | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + :call jobwait([g:id]) | + {5:-- TERMINAL --} | +@@ -3141,7 +3118,7 @@ describe('TUI', function() + feed_data('\003') + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + Type :qa and press to exit Nvim | + {5:-- TERMINAL --} | +@@ -3153,7 +3130,7 @@ describe('TUI', function() + feed_data('/foo') + screen:expect([[ + {2:foo} | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + /foo^ | + {5:-- TERMINAL --} | +@@ -3162,7 +3139,7 @@ describe('TUI', function() + feed_data('b') + screen:expect([[ + foo | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + /foob^ | + {5:-- TERMINAL --} | +@@ -3171,7 +3148,7 @@ describe('TUI', function() + feed_data('a') + screen:expect([[ + foo | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + /fooba^ | + {5:-- TERMINAL --} | +@@ -3331,7 +3308,7 @@ describe('TUI', function() + feed_data(':restart! lua ={ vim.uv.guess_handle(0), vim.uv.guess_handle(1) }\r') + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + { "tty", "tty" } | + {5:-- TERMINAL --} | +@@ -3348,8 +3325,9 @@ describe('TUI', function() + it('resize at startup #17285 #15044 #11330', function() + local screen = Screen.new(50, 10) + screen:add_extra_attr_ids({ +- [100] = { foreground = tonumber('0x4040ff'), fg_indexed = true }, ++ [100] = { foreground = tonumber('0x7aa6da'), fg_indexed = true }, + [101] = { foreground = Screen.colors.Gray100, background = Screen.colors.DarkGreen }, ++ [102] = { foreground = tonumber('0x4040ff'), fg_indexed = true }, + }) + fn.jobstart({ + nvim_prog, +@@ -3374,7 +3352,7 @@ describe('TUI', function() + ]]) + screen:expect(([[ + ^ │ | +- {1:~ }│{100:~ }|*6 ++ {1:~ }│{100:~} |*6 + {1:~ }│ | + {3:new }{101:{MATCH:<.*%s} [-] }| + | +@@ -3390,12 +3368,26 @@ describe('TUI', function() + -- so bg is dark. + local fg = is_os('win') and Screen.colors.NvimLightGrey2 or Screen.colors.NvimDarkGrey2 + local bg = is_os('win') and Screen.colors.NvimDarkGrey2 or Screen.colors.NvimLightGrey2 +- screen:add_extra_attr_ids({ ++ local extra_attr_ids = { ++ BgOnly = { ++ background = bg, ++ }, + [100] = { + foreground = fg, + background = bg, + }, +- }) ++ } ++ if is_os('win') then ++ extra_attr_ids.WinTilde = { ++ foreground = Screen.colors.NvimDarkGrey4, ++ background = bg, ++ } ++ extra_attr_ids.WinStatus = { ++ foreground = fg, ++ background = Screen.colors.NvimDarkGrey4, ++ } ++ end ++ screen:add_extra_attr_ids(extra_attr_ids) + fn.jobstart({ + nvim_prog, + '--clean', +@@ -3410,12 +3402,22 @@ describe('TUI', function() + env = { VIMRUNTIME = os.getenv('VIMRUNTIME') }, + }) + if guicolors == 'termguicolors' then +- screen:expect([[ +- {100:^ }| +- {100: }|*7 +- {100:foo }| +- | +- ]]) ++ if is_os('win') then ++ screen:expect([[ ++ {100:^ }| ++ {WinTilde:~ }|*6 ++ {WinStatus:[No Name] 0,0-1 All}| ++ {100:foo }| ++ | ++ ]]) ++ else ++ screen:expect([[ ++ {BgOnly:^ }| ++ {BgOnly: }|*7 ++ {100:foo}{BgOnly: }| ++ | ++ ]]) ++ end + else + screen:expect([[ + ^ | +@@ -3517,7 +3519,7 @@ describe('TUI', function() + }, { env = env_notermguicolors }) + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + | + {5:-- TERMINAL --} | +@@ -3526,7 +3528,7 @@ describe('TUI', function() + command([[call chansend(b:terminal_job_id, "\")]]) + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + | + {5:-- TERMINAL --} | +@@ -3685,7 +3687,7 @@ describe('TUI UIEnter/UILeave', function() + }, { env = env_notermguicolors }) + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + | + {5:-- TERMINAL --} | +@@ -3693,7 +3695,7 @@ describe('TUI UIEnter/UILeave', function() + feed_data(':echo g:evs\n') + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + ['VimEnter', 'UIEnter'] | + {5:-- TERMINAL --} | +@@ -3723,7 +3725,7 @@ describe('TUI FocusGained/FocusLost', function() + + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + | + {5:-- TERMINAL --} | +@@ -3745,7 +3747,7 @@ describe('TUI FocusGained/FocusLost', function() + feed_data('\027[I') + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + gained | + {5:-- TERMINAL --} | +@@ -3754,7 +3756,7 @@ describe('TUI FocusGained/FocusLost', function() + feed_data('\027[O') + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + lost | + {5:-- TERMINAL --} | +@@ -3767,7 +3769,7 @@ describe('TUI FocusGained/FocusLost', function() + feed_data('i') + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + :set noshowmode | + {5:-- TERMINAL --} | +@@ -3776,7 +3778,7 @@ describe('TUI FocusGained/FocusLost', function() + feed_data('\027[I') + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + gained | + {5:-- TERMINAL --} | +@@ -3784,7 +3786,7 @@ describe('TUI FocusGained/FocusLost', function() + feed_data('\027[O') + screen:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + lost | + {5:-- TERMINAL --} | +@@ -3799,7 +3801,7 @@ describe('TUI FocusGained/FocusLost', function() + feed_data('\027[I') + screen:expect([[ + | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + :^ | + {5:-- TERMINAL --} | +@@ -3877,7 +3879,6 @@ describe('TUI FocusGained/FocusLost', function() + end) + + it('in hit-enter prompt', function() +- tt.override_screen_expect_for_conpty(screen) + feed_data(":echom 'msg1'|echom 'msg2'|echom 'msg3'|echom 'msg4'|echom 'msg5'\n") + screen:expect([[ + msg1 | +@@ -3919,11 +3920,11 @@ describe("TUI 't_Co' (terminal colors)", function() + + local tline --[[@type string]] + if maxcolors == 8 then +- tline = '{112:~ }' ++ tline = '{112:~} ' + elseif maxcolors == 16 then + tline = '~ ' + else +- tline = '{100:~ }' ++ tline = '{100:~} ' + end + + screen:expect(string.format( +@@ -4159,7 +4160,7 @@ describe("TUI 't_Co' (terminal colors)", function() + -- others: + + -- TODO(blueyed): this is made pending, since it causes failure + later hang +- -- when using non-compatible libvterm (#9494/#10179). ++ -- with some terminal implementations (#9494/#10179). + pending('TERM=interix uses 8 colors', function() + assert_term_colors('interix', nil, 8) + end) +@@ -4257,7 +4258,7 @@ describe('TUI', function() + feed_data('Gitext') + screen:expect([[ + text^ | +- {100:~ }|*4 ++ {100:~} |*4 + {5:-- INSERT --} | + {5:-- TERMINAL --} | + ]]) +@@ -4313,7 +4314,7 @@ describe('TUI', function() + + screen:expect([[ + ^ | +- {100:~ }|*4 ++ {100:~} |*4 + | + {5:-- TERMINAL --} | + ]]) +@@ -4434,14 +4435,20 @@ describe('TUI', function() + pending('N/A: missing LuaJIT FFI') + end + +- -- Change vterm's DA1 response so that it doesn't include 52 ++ -- Change Ghostty's DA1 response so that it doesn't include 52. + exec_lua(function() + local ffi = require('ffi') + ffi.cdef [[ +- extern char vterm_primary_device_attr[] ++ extern int terminal_ghostty_da_clipboard; + ]] + +- ffi.copy(ffi.C.vterm_primary_device_attr, '61;22') ++ ffi.C.terminal_ghostty_da_clipboard = 0 ++ end) ++ finally(function() ++ exec_lua(function() ++ local ffi = require('ffi') ++ ffi.C.terminal_ghostty_da_clipboard = 1 ++ end) + end) + + exec_lua([[ +@@ -4618,7 +4625,9 @@ describe('TUI bg color', function() + end) + end) + +- it('queries the terminal for background color', function() ++ it('answers the terminal background color query and emits TermRequest', function() ++ command('highlight clear Normal') ++ command('set background=light') + exec_lua([[ + vim.api.nvim_create_autocmd('TermRequest', { + callback = function(ev) +@@ -4630,16 +4639,22 @@ describe('TUI bg color', function() + end, + }) + ]]) +- tt.setup_child_nvim({ ++ local child_server = new_pipename() ++ local screen = tt.setup_child_nvim({ + '--clean', ++ '--listen', ++ child_server, + '--cmd', + 'colorscheme vim', + '--cmd', + 'set noswapfile', + }) +- retry(nil, 1000, function() +- eq(true, eval("get(g:, 'oscrequest', v:false)")) ++ screen:expect({ any = '%[No Name%]' }) ++ local child_session = n.connect(child_server) ++ retry(nil, nil, function() ++ eq({ true, 'light' }, { child_session:request('nvim_eval', '&background') }) + end) ++ eq(true, eval("get(g:, 'oscrequest', v:false)")) + end) + + it('does not trigger OptionSet from automatic background processing', function() +@@ -4805,7 +4820,7 @@ describe('TUI client', function() + }, { env = env_notermguicolors }) + screen_server:expect([[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + | + {5:-- TERMINAL --} | +@@ -4814,7 +4829,7 @@ describe('TUI client', function() + feed_data('iHello, World') + screen_server:expect([[ + Hello, World^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + {5:-- INSERT --} | + {5:-- TERMINAL --} | +@@ -4822,7 +4837,7 @@ describe('TUI client', function() + feed_data('\027') + local s0 = [[ + Hello, Worl^d | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +@@ -4869,7 +4884,7 @@ describe('TUI client', function() + feed_data(':restart! +qall!\n') + local screen_restarted = [[ + ^ | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] }| + | + {5:-- TERMINAL --} | +@@ -4884,7 +4899,7 @@ describe('TUI client', function() + feed_data(':restart! vsplit\r') + screen_restarted = [[ + ^ │ | +- {100:~ }│{100:~ }|*3 ++ {100:~ }│{100:~} |*3 + {3:[No Name] }{2:[No Name] }| + | + {5:-- TERMINAL --} | +@@ -4928,7 +4943,7 @@ describe('TUI client', function() + }, { env = env_notermguicolors }) + screen_client:expect([[ + Halloj^! | +- {100:~ }|*4 ++ {100:~} |*4 + | + {5:-- TERMINAL --} | + ]]) +@@ -4980,7 +4995,7 @@ describe('TUI client', function() + feed_data(':restart! +qall!\n') + screen_client:expect([[ + ^ | +- {100:~ }|*4 ++ {100:~} |*4 + | + {5:-- TERMINAL --} | + ]]) +@@ -5103,7 +5118,7 @@ describe('TUI client', function() + + local screen_normal = [[ + Hello, Worl^d | +- {100:~ }|*3 ++ {100:~} |*3 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | +diff --git a/test/functional/terminal/window_spec.lua b/test/functional/terminal/window_spec.lua +index 0421fc073745ed..61a504d8f1ea84 100644 +--- a/test/functional/terminal/window_spec.lua ++++ b/test/functional/terminal/window_spec.lua +@@ -918,12 +918,11 @@ describe(':terminal with multigrid', function() + [2:--------------------------------------------------]|*6 + [3:--------------------------------------------------]| + ## grid 2 +- tty ready | + rows: 10, cols: 20 | + rows: 3, cols: 70 | + rows: 6, cols: 50 | + ^ | +- | ++ |*2 + ## grid 3 + {5:-- TERMINAL --} | + ]]) +diff --git a/test/functional/terminal/window_split_tab_spec.lua b/test/functional/terminal/window_split_tab_spec.lua +index 8fa76f0eb5eee1..fa7151eaa6244f 100644 +--- a/test/functional/terminal/window_split_tab_spec.lua ++++ b/test/functional/terminal/window_split_tab_spec.lua +@@ -96,22 +96,13 @@ describe(':terminal', function() + return + end + ++ -- SIGWINCH delivery is asynchronous, so the child may print one or more ++ -- intermediate size reports. Wait for the final size instead of asserting ++ -- the whole resize transcript. + screen:try_resize(w1, h1) +- screen:expect([[ +- tty ready | +- rows: 7, cols: 47 | +- |*4 +- ^ | +- | +- ]]) ++ screen:expect { any = 'rows: 7, cols: 47' } + screen:try_resize(w2, h2) +- screen:expect([[ +- tty ready | +- rows: 7, cols: 47 | +- rows: 4, cols: 41 | +- ^ | +- | +- ]]) ++ screen:expect { any = 'rows: 4, cols: 41' } + end) + + it('stays in terminal mode with wincmd', function() +diff --git a/test/functional/testterm.lua b/test/functional/testterm.lua +index d672a92a6a8a4c..e73e427f6c0841 100644 +--- a/test/functional/testterm.lua ++++ b/test/functional/testterm.lua +@@ -50,7 +50,7 @@ function M.make_lua_executor(session) + end + + -- some helpers for controlling the terminal. the codes were taken from +--- infocmp xterm-256color which is less what libvterm understands ++-- infocmp xterm-256color. + -- civis/cnorm + function M.hide_cursor() + M.feed_termcode('[?25l') +@@ -134,8 +134,8 @@ function M.setup_screen(extra_rows, cmd, cols, env, screen_opts) + [107] = { underline = true }, + [108] = { background = 248, foreground = Screen.colors.Black }, + [109] = { bold = true, background = 121, foreground = Screen.colors.Grey0 }, +- [110] = { fg_indexed = true, foreground = tonumber('0xe0e000') }, +- [111] = { fg_indexed = true, foreground = tonumber('0x4040ff') }, ++ [110] = { fg_indexed = true, foreground = tonumber('0xf0c674') }, ++ [111] = { fg_indexed = true, foreground = tonumber('0x7aa6da') }, + [112] = { foreground = 4 }, + [113] = { foreground = Screen.colors.SeaGreen4 }, + [114] = { undercurl = true }, +@@ -148,6 +148,7 @@ function M.setup_screen(extra_rows, cmd, cols, env, screen_opts) + [121] = { foreground = 130 }, + [122] = { background = 46 }, + [123] = { foreground = 2 }, ++ [124] = { fg_indexed = true, foreground = tonumber('0x4040ff') }, + }) + + api.nvim_command('enew') +@@ -207,7 +208,11 @@ function M.setup_child_nvim(args, opts) + env.VIMRUNTIME = env.VIMRUNTIME or os.getenv('VIMRUNTIME') + env.NVIM_TEST = env.NVIM_TEST or os.getenv('NVIM_TEST') + +- return M.setup_screen(opts.extra_rows, argv, opts.cols, env) ++ local screen = M.setup_screen(opts.extra_rows, argv, opts.cols, env) ++ if t.is_os('win') then ++ M.override_screen_expect_for_conpty(screen) ++ end ++ return screen + end + + --- FIXME: On Windows spaces at the end of a screen line may have wrong attrs. +@@ -215,14 +220,17 @@ end + --- + --- @param screen test.functional.ui.screen + function M.override_screen_expect_for_conpty(screen) +- if not t.is_os('win') then +- return +- end + local orig_screen_expect = screen.expect + function screen.expect(self, expected, attr_ids, ...) + if type(expected) == 'string' then +- expected = expected:gsub(' *%} +%|\n', '{MATCH: *}}{MATCH: *}|\n') +- expected = expected:gsub('%}%^ +%|\n', '{MATCH:[ ^]*}}{MATCH:[ ^]*}|\n') ++ -- Ignore attribute groups that contain only blank cells. ConPTY may ++ -- merge them into either adjacent group, so retaining the group itself ++ -- makes the patterns below require a boundary that may not exist. ++ expected = expected:gsub('(\n[ \t]*){[^{}\n]-: +}(%|%*?%d*)', '%1{MATCH:[^|]*}%2') ++ expected = expected:gsub('{[^{}\n]-: +}', ' ') ++ expected = expected:gsub('%}%^ +', '{MATCH:[^|]*%%^[^|]*}') ++ expected = expected:gsub(' +%} *', '{MATCH: *}}{MATCH:[^|]*}') ++ expected = expected:gsub('%} +', '{MATCH: *}}{MATCH:[^|]*}') + end + orig_screen_expect(self, expected, attr_ids, ...) + end +diff --git a/test/functional/ui/hlstate_spec.lua b/test/functional/ui/hlstate_spec.lua +index f387c16a640ccf..722a25d9a409d6 100644 +--- a/test/functional/ui/hlstate_spec.lua ++++ b/test/functional/ui/hlstate_spec.lua +@@ -213,14 +213,14 @@ describe('ext_hlstate detailed highlights', function() + + screen:set_default_attr_ids({ + [1] = { {}, { { hi_name = 'TermCursorNC', ui_name = 'TermCursorNC', kind = 'ui' } } }, +- [2] = { { foreground = tonumber('0x00ccff'), fg_indexed = true }, { { kind = 'term' } } }, ++ [2] = { { foreground = tonumber('0x00d7ff'), fg_indexed = true }, { { kind = 'term' } } }, + [3] = { +- { bold = true, foreground = tonumber('0x00ccff'), fg_indexed = true }, ++ { bold = true, foreground = tonumber('0x00d7ff'), fg_indexed = true }, + { + { kind = 'term' }, + }, + }, +- [4] = { { foreground = tonumber('0x00ccff'), fg_indexed = true }, { 2, 1 } }, ++ [4] = { { foreground = tonumber('0x00d7ff'), fg_indexed = true }, { 2, 1 } }, + [5] = { { foreground = tonumber('0x40ffff'), fg_indexed = true }, { { kind = 'term' } } }, + [6] = { { foreground = tonumber('0x40ffff'), fg_indexed = true }, { 5, 1 } }, + [7] = { {}, { { hi_name = 'MsgArea', ui_name = 'MsgArea', kind = 'ui' } } }, +diff --git a/test/functional/ui/messages_spec.lua b/test/functional/ui/messages_spec.lua +index 654475e8f3d744..5cc31caaf0b929 100644 +--- a/test/functional/ui/messages_spec.lua ++++ b/test/functional/ui/messages_spec.lua +@@ -3048,7 +3048,7 @@ describe('pager', function() + {4:-- More --}^ | + ]]) + +- -- responds to resize, but text is not reflown ++ -- responds to resize, but text is not reflowed + screen:try_resize(45, 5) + screen:expect([[ + {2:adipisicing elit, sed do eiusmod te} | +@@ -3059,7 +3059,7 @@ describe('pager', function() + ]]) + + -- can create empty space, as the command hasn't output the text below yet. +- -- text is not reflown; existing lines get cut ++ -- text is not reflowed; existing lines get cut + screen:try_resize(30, 12) + screen:expect([[ + :lua error(_G.x) | +diff --git a/test/functional/ui/output_spec.lua b/test/functional/ui/output_spec.lua +index 896e77389750d5..cc33ceba2e1be0 100644 +--- a/test/functional/ui/output_spec.lua ++++ b/test/functional/ui/output_spec.lua +@@ -36,7 +36,7 @@ describe('shell command :!', function() + }) + screen:expect([[ + ^ | +- {100:~ }|*4 ++ {100:~} |*4 + | + {5:-- TERMINAL --} | + ]]) +@@ -49,7 +49,7 @@ describe('shell command :!', function() + tt.feed_data(':!printf foo; sleep 200\n') + screen:expect([[ + | +- {100:~ }|*2 ++ {100:~} |*2 + {3: }| + :!printf foo; sleep 200 | + foo | +diff --git a/test/unit/fixtures/vterm_test.c b/test/unit/fixtures/vterm_test.c +deleted file mode 100644 +index a3b40d65da2931..00000000000000 +--- a/test/unit/fixtures/vterm_test.c ++++ /dev/null +@@ -1,813 +0,0 @@ +-#include +-#include +- +-#include "nvim/grid.h" +-#include "nvim/mbyte.h" +-#include "nvim/vterm/pen.h" +-#include "nvim/vterm/screen.h" +-#include "nvim/vterm/vterm_internal_defs.h" +-#include "vterm_test.h" +- +-int parser_text(const char bytes[], size_t len, void *user) +-{ +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- fprintf(f, "text "); +- size_t i; +- for (i = 0; i < len; i++) { +- unsigned char b = (unsigned char)bytes[i]; +- if (b < 0x20 || b == 0x7f || (b >= 0x80 && b < 0xa0)) { +- break; +- } +- fprintf(f, i ? ",%x" : "%x", b); +- } +- fprintf(f, "\n"); +- fclose(f); +- +- return (int)i; +-} +- +-static void printchars(const char *s, size_t len, FILE *f) +-{ +- while (len--) { +- fprintf(f, "%c", (s++)[0]); +- } +-} +- +-int parser_csi(const char *leader, const long args[], int argcount, const char *intermed, +- char command, void *user) +-{ +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- fprintf(f, "csi %02x", command); +- +- if (leader && leader[0]) { +- fprintf(f, " L="); +- for (int i = 0; leader[i]; i++) { +- fprintf(f, "%02x", leader[i]); +- } +- } +- +- for (int i = 0; i < argcount; i++) { +- char sep = i ? ',' : ' '; +- +- if (args[i] == CSI_ARG_MISSING) { +- fprintf(f, "%c*", sep); +- } else { +- fprintf(f, "%c%ld%s", sep, CSI_ARG(args[i]), CSI_ARG_HAS_MORE(args[i]) ? "+" : ""); +- } +- } +- +- if (intermed && intermed[0]) { +- fprintf(f, " I="); +- for (int i = 0; intermed[i]; i++) { +- fprintf(f, "%02x", intermed[i]); +- } +- } +- +- fprintf(f, "\n"); +- +- fclose(f); +- +- return 1; +-} +- +-int parser_osc(int command, VTermStringFragment frag, void *user) +-{ +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- fprintf(f, "osc "); +- +- if (frag.initial) { +- if (command == -1) { +- fprintf(f, "["); +- } else { +- fprintf(f, "[%d;", command); +- } +- } +- +- printchars(frag.str, frag.len, f); +- +- if (frag.final) { +- fprintf(f, "]"); +- } +- +- fprintf(f, "\n"); +- fclose(f); +- +- return 1; +-} +- +-int parser_dcs(const char *command, size_t commandlen, VTermStringFragment frag, void *user) +-{ +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- fprintf(f, "dcs "); +- +- if (frag.initial) { +- fprintf(f, "["); +- for (size_t i = 0; i < commandlen; i++) { +- fprintf(f, "%c", command[i]); +- } +- } +- +- printchars(frag.str, frag.len, f); +- +- if (frag.final) { +- fprintf(f, "]"); +- } +- +- fprintf(f, "\n"); +- fclose(f); +- +- return 1; +-} +- +-int parser_apc(VTermStringFragment frag, void *user) +-{ +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- fprintf(f, "apc "); +- +- if (frag.initial) { +- fprintf(f, "["); +- } +- +- printchars(frag.str, frag.len, f); +- +- if (frag.final) { +- fprintf(f, "]"); +- } +- +- fprintf(f, "\n"); +- fclose(f); +- +- return 1; +-} +- +-int parser_pm(VTermStringFragment frag, void *user) +-{ +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- fprintf(f, "pm "); +- +- if (frag.initial) { +- fprintf(f, "["); +- } +- +- printchars(frag.str, frag.len, f); +- +- if (frag.final) { +- fprintf(f, "]"); +- } +- +- fprintf(f, "\n"); +- fclose(f); +- +- return 1; +-} +- +-int parser_sos(VTermStringFragment frag, void *user) +-{ +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- fprintf(f, "sos "); +- +- if (frag.initial) { +- fprintf(f, "["); +- } +- +- printchars(frag.str, frag.len, f); +- +- if (frag.final) { +- fprintf(f, "]"); +- } +- +- fprintf(f, "\n"); +- fclose(f); +- +- return 1; +-} +- +-int selection_set(VTermSelectionMask mask, VTermStringFragment frag, void *user) +-{ +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- fprintf(f, "selection-set mask=%04X ", mask); +- if (frag.initial) { +- fprintf(f, "["); +- } +- printchars(frag.str, frag.len, f); +- if (frag.final) { +- fprintf(f, "]"); +- } +- fprintf(f, "\n"); +- +- fclose(f); +- return 1; +-} +- +-int selection_query(VTermSelectionMask mask, void *user) +-{ +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- fprintf(f, "selection-query mask=%04X\n", mask); +- +- fclose(f); +- return 1; +-} +- +-static void print_schar(FILE *f, schar_T schar) +-{ +- char buf[MAX_SCHAR_SIZE]; +- schar_get(buf, schar); +- StrCharInfo ci = utf_ptr2StrCharInfo(buf); +- bool did = false; +- while (*ci.ptr != 0) { +- if (did) { +- fprintf(f, ","); +- } +- +- if (ci.chr.len == 1 && ci.chr.value >= 0x80) { +- fprintf(f, "??%x", ci.chr.value); +- } else { +- fprintf(f, "%x", ci.chr.value); +- } +- did = true; +- ci = utf_ptr2StrCharInfo(ci.ptr + ci.chr.len); +- } +-} +- +-bool want_state_putglyph; +-int state_putglyph(VTermGlyphInfo *info, VTermPos pos, void *user) +-{ +- if (!want_state_putglyph) { +- return 1; +- } +- +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- fprintf(f, "putglyph "); +- print_schar(f, info->schar); +- fprintf(f, " %d %d,%d", info->width, pos.row, pos.col); +- if (info->protected_cell) { +- fprintf(f, " prot"); +- } +- if (info->dwl) { +- fprintf(f, " dwl"); +- } +- if (info->dhl) { +- fprintf(f, " dhl-%s", info->dhl == 1 ? "top" : info->dhl == 2 ? "bottom" : "?"); +- } +- fprintf(f, "\n"); +- +- fclose(f); +- +- return 1; +-} +- +-bool want_state_movecursor; +-VTermPos state_pos; +-int state_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user) +-{ +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- state_pos = pos; +- +- if (want_state_movecursor) { +- fprintf(f, "movecursor %d,%d\n", pos.row, pos.col); +- } +- +- fclose(f); +- return 1; +-} +- +-bool want_state_scrollrect; +-int state_scrollrect(VTermRect rect, int downward, int rightward, void *user) +-{ +- if (!want_state_scrollrect) { +- return 0; +- } +- +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- +- fprintf(f, "scrollrect %d..%d,%d..%d => %+d,%+d\n", +- rect.start_row, rect.end_row, rect.start_col, rect.end_col, +- downward, rightward); +- +- fclose(f); +- return 1; +-} +- +-bool want_state_moverect; +-int state_moverect(VTermRect dest, VTermRect src, void *user) +-{ +- if (!want_state_moverect) { +- return 0; +- } +- +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- fprintf(f, "moverect %d..%d,%d..%d -> %d..%d,%d..%d\n", +- src.start_row, src.end_row, src.start_col, src.end_col, +- dest.start_row, dest.end_row, dest.start_col, dest.end_col); +- +- fclose(f); +- return 1; +-} +- +-void print_color(const VTermColor *col) +-{ +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- if (VTERM_COLOR_IS_RGB(col)) { +- fprintf(f, "rgb(%d,%d,%d", col->rgb.red, col->rgb.green, col->rgb.blue); +- } else if (VTERM_COLOR_IS_INDEXED(col)) { +- fprintf(f, "idx(%d", col->indexed.idx); +- } else { +- fprintf(f, "invalid(%d", col->type); +- } +- if (VTERM_COLOR_IS_DEFAULT_FG(col)) { +- fprintf(f, ",is_default_fg"); +- } +- if (VTERM_COLOR_IS_DEFAULT_BG(col)) { +- fprintf(f, ",is_default_bg"); +- } +- fprintf(f, ")"); +- fclose(f); +-} +- +-static VTermValueType vterm_get_prop_type(VTermProp prop) +-{ +- switch (prop) { +- case VTERM_PROP_CURSORVISIBLE: +- return VTERM_VALUETYPE_BOOL; +- case VTERM_PROP_CURSORBLINK: +- return VTERM_VALUETYPE_BOOL; +- case VTERM_PROP_ALTSCREEN: +- return VTERM_VALUETYPE_BOOL; +- case VTERM_PROP_TITLE: +- return VTERM_VALUETYPE_STRING; +- case VTERM_PROP_ICONNAME: +- return VTERM_VALUETYPE_STRING; +- case VTERM_PROP_REVERSE: +- return VTERM_VALUETYPE_BOOL; +- case VTERM_PROP_CURSORSHAPE: +- return VTERM_VALUETYPE_INT; +- case VTERM_PROP_MOUSE: +- return VTERM_VALUETYPE_INT; +- case VTERM_PROP_FOCUSREPORT: +- return VTERM_VALUETYPE_BOOL; +- case VTERM_PROP_THEMEUPDATES: +- return VTERM_VALUETYPE_BOOL; +- case VTERM_PROP_SYNCOUTPUT: +- return VTERM_VALUETYPE_BOOL; +- +- case VTERM_N_PROPS: +- return 0; +- } +- return 0; // UNREACHABLE +-} +- +-bool want_state_settermprop; +-int state_settermprop(VTermProp prop, VTermValue *val, void *user) +-{ +- if (!want_state_settermprop) { +- return 1; +- } +- +- int errcode = 0; +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- +- VTermValueType type = vterm_get_prop_type(prop); +- switch (type) { +- case VTERM_VALUETYPE_BOOL: +- fprintf(f, "settermprop %d %s\n", prop, val->boolean ? "true" : "false"); +- errcode = 1; +- goto end; +- case VTERM_VALUETYPE_INT: +- fprintf(f, "settermprop %d %d\n", prop, val->number); +- errcode = 1; +- goto end; +- case VTERM_VALUETYPE_STRING: +- fprintf(f, "settermprop %d %s\"%.*s\"%s\n", prop, +- val->string.initial ? "[" : "", (int)val->string.len, val->string.str, +- val->string.final ? "]" : ""); +- errcode = 0; +- goto end; +- case VTERM_VALUETYPE_COLOR: +- fprintf(f, "settermprop %d ", prop); +- print_color(&val->color); +- fprintf(f, "\n"); +- errcode = 1; +- goto end; +- case VTERM_N_VALUETYPES: +- goto end; +- } +- +-end: +- fclose(f); +- return errcode; +-} +- +-bool want_state_erase; +-int state_erase(VTermRect rect, int selective, void *user) +-{ +- if (!want_state_erase) { +- return 1; +- } +- +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- +- fprintf(f, "erase %d..%d,%d..%d%s\n", +- rect.start_row, rect.end_row, rect.start_col, rect.end_col, +- selective ? " selective" : ""); +- +- fclose(f); +- return 1; +-} +- +-struct { +- int bold; +- int underline; +- int italic; +- int blink; +- int reverse; +- int conceal; +- int strike; +- int font; +- int small; +- int baseline; +- int dim; +- int overline; +- VTermColor foreground; +- VTermColor background; +-} state_pen; +- +-int state_setpenattr(VTermAttr attr, VTermValue *val, void *user) +-{ +- switch (attr) { +- case VTERM_ATTR_BOLD: +- state_pen.bold = val->boolean; +- break; +- case VTERM_ATTR_UNDERLINE: +- state_pen.underline = val->number; +- break; +- case VTERM_ATTR_ITALIC: +- state_pen.italic = val->boolean; +- break; +- case VTERM_ATTR_BLINK: +- state_pen.blink = val->boolean; +- break; +- case VTERM_ATTR_REVERSE: +- state_pen.reverse = val->boolean; +- break; +- case VTERM_ATTR_CONCEAL: +- state_pen.conceal = val->boolean; +- break; +- case VTERM_ATTR_STRIKE: +- state_pen.strike = val->boolean; +- break; +- case VTERM_ATTR_FONT: +- state_pen.font = val->number; +- break; +- case VTERM_ATTR_SMALL: +- state_pen.small = val->boolean; +- break; +- case VTERM_ATTR_BASELINE: +- state_pen.baseline = val->number; +- break; +- case VTERM_ATTR_DIM: +- state_pen.dim = val->boolean; +- break; +- case VTERM_ATTR_OVERLINE: +- state_pen.overline = val->boolean; +- break; +- case VTERM_ATTR_FOREGROUND: +- state_pen.foreground = val->color; +- break; +- case VTERM_ATTR_BACKGROUND: +- state_pen.background = val->color; +- break; +- +- case VTERM_N_ATTRS: +- return 0; +- default: +- break; +- } +- +- return 1; +-} +- +-bool want_state_scrollback; +-int state_sb_clear(void *user) +-{ +- if (!want_state_scrollback) { +- return 1; +- } +- +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- fprintf(f, "sb_clear\n"); +- fclose(f); +- +- return 0; +-} +- +-bool want_screen_scrollback; +-int screen_sb_pushline(int cols, const VTermScreenCell *cells, void *user) +-{ +- if (!want_screen_scrollback) { +- return 1; +- } +- +- int eol = cols; +- while (eol && !cells[eol - 1].schar) { +- eol--; +- } +- +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- fprintf(f, "sb_pushline %d =", cols); +- for (int c = 0; c < eol; c++) { +- fprintf(f, " "); +- print_schar(f, cells[c].schar); +- } +- fprintf(f, "\n"); +- +- fclose(f); +- +- return 1; +-} +- +-int screen_sb_popline(int cols, VTermScreenCell *cells, void *user) +-{ +- if (!want_screen_scrollback) { +- return 0; +- } +- +- // All lines of scrollback contain "ABCDE" +- for (int col = 0; col < cols; col++) { +- if (col < 5) { +- cells[col].schar = schar_from_ascii((uint32_t)('A' + col)); +- } else { +- cells[col].schar = 0; +- } +- +- cells[col].width = 1; +- } +- +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- fprintf(f, "sb_popline %d\n", cols); +- fclose(f); +- return 1; +-} +- +-int screen_sb_clear(void *user) +-{ +- if (!want_screen_scrollback) { +- return 1; +- } +- +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- fprintf(f, "sb_clear\n"); +- fclose(f); +- return 0; +-} +- +-void term_output(const char *s, size_t len, void *user) +-{ +- FILE *f = fopen(VTERM_TEST_FILE, "a"); +- fprintf(f, "output "); +- for (size_t i = 0; i < len; i++) { +- fprintf(f, "%x%s", (unsigned char)s[i], i < len - 1 ? "," : "\n"); +- } +- fclose(f); +-} +- +-int vterm_state_get_penattr(const VTermState *state, VTermAttr attr, VTermValue *val) +-{ +- switch (attr) { +- case VTERM_ATTR_BOLD: +- val->boolean = state->pen.bold; +- return 1; +- +- case VTERM_ATTR_UNDERLINE: +- val->number = state->pen.underline; +- return 1; +- +- case VTERM_ATTR_ITALIC: +- val->boolean = state->pen.italic; +- return 1; +- +- case VTERM_ATTR_BLINK: +- val->boolean = state->pen.blink; +- return 1; +- +- case VTERM_ATTR_REVERSE: +- val->boolean = state->pen.reverse; +- return 1; +- +- case VTERM_ATTR_CONCEAL: +- val->boolean = state->pen.conceal; +- return 1; +- +- case VTERM_ATTR_STRIKE: +- val->boolean = state->pen.strike; +- return 1; +- +- case VTERM_ATTR_FONT: +- val->number = state->pen.font; +- return 1; +- +- case VTERM_ATTR_FOREGROUND: +- val->color = state->pen.fg; +- return 1; +- +- case VTERM_ATTR_BACKGROUND: +- val->color = state->pen.bg; +- return 1; +- +- case VTERM_ATTR_SMALL: +- val->boolean = state->pen.small; +- return 1; +- +- case VTERM_ATTR_BASELINE: +- val->number = state->pen.baseline; +- return 1; +- +- case VTERM_ATTR_URI: +- val->number = state->pen.uri; +- return 1; +- +- case VTERM_ATTR_DIM: +- val->boolean = state->pen.dim; +- return 1; +- +- case VTERM_ATTR_OVERLINE: +- val->boolean = state->pen.overline; +- return 1; +- +- case VTERM_N_ATTRS: +- return 0; +- } +- +- return 0; +-} +- +-static int attrs_differ(VTermAttrMask attrs, ScreenCell *a, ScreenCell *b) +-{ +- if ((attrs & VTERM_ATTR_BOLD_MASK) && (a->pen.bold != b->pen.bold)) { +- return 1; +- } +- if ((attrs & VTERM_ATTR_UNDERLINE_MASK) && (a->pen.underline != b->pen.underline)) { +- return 1; +- } +- if ((attrs & VTERM_ATTR_ITALIC_MASK) && (a->pen.italic != b->pen.italic)) { +- return 1; +- } +- if ((attrs & VTERM_ATTR_BLINK_MASK) && (a->pen.blink != b->pen.blink)) { +- return 1; +- } +- if ((attrs & VTERM_ATTR_REVERSE_MASK) && (a->pen.reverse != b->pen.reverse)) { +- return 1; +- } +- if ((attrs & VTERM_ATTR_CONCEAL_MASK) && (a->pen.conceal != b->pen.conceal)) { +- return 1; +- } +- if ((attrs & VTERM_ATTR_STRIKE_MASK) && (a->pen.strike != b->pen.strike)) { +- return 1; +- } +- if ((attrs & VTERM_ATTR_FONT_MASK) && (a->pen.font != b->pen.font)) { +- return 1; +- } +- if ((attrs & VTERM_ATTR_FOREGROUND_MASK) && !vterm_color_is_equal(&a->pen.fg, &b->pen.fg)) { +- return 1; +- } +- if ((attrs & VTERM_ATTR_BACKGROUND_MASK) && !vterm_color_is_equal(&a->pen.bg, &b->pen.bg)) { +- return 1; +- } +- if ((attrs & VTERM_ATTR_SMALL_MASK) && (a->pen.small != b->pen.small)) { +- return 1; +- } +- if ((attrs & VTERM_ATTR_BASELINE_MASK) && (a->pen.baseline != b->pen.baseline)) { +- return 1; +- } +- if ((attrs & VTERM_ATTR_URI_MASK) && (a->pen.uri != b->pen.uri)) { +- return 1; +- } +- if ((attrs & VTERM_ATTR_DIM_MASK) && (a->pen.dim != b->pen.dim)) { +- return 1; +- } +- if ((attrs & VTERM_ATTR_OVERLINE_MASK) && (a->pen.overline != b->pen.overline)) { +- return 1; +- } +- +- return 0; +-} +- +-int vterm_screen_get_attrs_extent(const VTermScreen *screen, VTermRect *extent, VTermPos pos, +- VTermAttrMask attrs) +-{ +- ScreenCell *target = getcell(screen, pos.row, pos.col); +- +- // TODO(vterm): bounds check +- extent->start_row = pos.row; +- extent->end_row = pos.row + 1; +- +- if (extent->start_col < 0) { +- extent->start_col = 0; +- } +- if (extent->end_col < 0) { +- extent->end_col = screen->cols; +- } +- +- int col; +- +- for (col = pos.col - 1; col >= extent->start_col; col--) { +- if (attrs_differ(attrs, target, getcell(screen, pos.row, col))) { +- break; +- } +- } +- extent->start_col = col + 1; +- +- for (col = pos.col + 1; col < extent->end_col; col++) { +- if (attrs_differ(attrs, target, getcell(screen, pos.row, col))) { +- break; +- } +- } +- extent->end_col = col - 1; +- +- return 1; +-} +- +-/// Does not NUL-terminate the buffer +-size_t vterm_screen_get_text(const VTermScreen *screen, char *buffer, size_t len, +- const VTermRect rect) +-{ +- size_t outpos = 0; +- int padding = 0; +- +-#define PUT(bytes, thislen) \ +- if (true) { \ +- if (buffer && outpos + thislen <= len) \ +- memcpy((char *)buffer + outpos, bytes, thislen); \ +- outpos += thislen; \ +- } \ +- +- for (int row = rect.start_row; row < rect.end_row; row++) { +- for (int col = rect.start_col; col < rect.end_col; col++) { +- ScreenCell *cell = getcell(screen, row, col); +- +- if (cell->schar == 0) { +- // Erased cell, might need a space +- padding++; +- } else if (cell->schar == (uint32_t)-1) { +- // Gap behind a double-width char, do nothing +- } else { +- while (padding) { +- PUT(" ", 1); +- padding--; +- } +- char buf[MAX_SCHAR_SIZE + 1]; +- size_t thislen = schar_get(buf, cell->schar); +- PUT(buf, thislen); +- } +- } +- +- if (row < rect.end_row - 1) { +- PUT("\n", 1); +- padding = 0; +- } +- } +- +- return outpos; +-} +- +-int vterm_screen_is_eol(const VTermScreen *screen, VTermPos pos) +-{ +- // This cell is EOL if this and every cell to the right is black +- for (; pos.col < screen->cols; pos.col++) { +- ScreenCell *cell = getcell(screen, pos.row, pos.col); +- if (cell->schar != 0) { +- return 0; +- } +- } +- +- return 1; +-} +- +-void vterm_state_get_cursorpos(const VTermState *state, VTermPos *cursorpos) +-{ +- *cursorpos = state->pos; +-} +- +-void vterm_state_set_bold_highbright(VTermState *state, int bold_is_highbright) +-{ +- state->bold_is_highbright = bold_is_highbright; +-} +- +-/// Compares two colours. Returns true if the colors are equal, false otherwise. +-int vterm_color_is_equal(const VTermColor *a, const VTermColor *b) +-{ +- // First make sure that the two colours are of the same type (RGB/Indexed) +- if (a->type != b->type) { +- return false; +- } +- +- // Depending on the type inspect the corresponding members +- if (VTERM_COLOR_IS_INDEXED(a)) { +- return a->indexed.idx == b->indexed.idx; +- } else if (VTERM_COLOR_IS_RGB(a)) { +- return (a->rgb.red == b->rgb.red) +- && (a->rgb.green == b->rgb.green) +- && (a->rgb.blue == b->rgb.blue); +- } +- +- return 0; +-} +diff --git a/test/unit/fixtures/vterm_test.h b/test/unit/fixtures/vterm_test.h +deleted file mode 100644 +index ef6463af6d4c77..00000000000000 +--- a/test/unit/fixtures/vterm_test.h ++++ /dev/null +@@ -1,45 +0,0 @@ +-#include +-#include +- +-#include "nvim/macros_defs.h" +-#include "nvim/vterm/vterm.h" +- +-EXTERN VTermPos state_pos; +-EXTERN bool want_state_putglyph INIT (=false); +-EXTERN bool want_state_movecursor INIT(= false); +-EXTERN bool want_state_erase INIT(= false); +-EXTERN bool want_state_scrollrect INIT(= false); +-EXTERN bool want_state_moverect INIT(= false); +-EXTERN bool want_state_settermprop INIT(= false); +-EXTERN bool want_state_scrollback INIT(= false); +-EXTERN bool want_screen_scrollback INIT(= false); +-int parser_text(const char bytes[], size_t len, void *user); +-int parser_csi(const char *leader, const long args[], int argcount, const char *intermed, +- char command, void *user); +-int parser_osc(int command, VTermStringFragment frag, void *user); +-int parser_dcs(const char *command, size_t commandlen, VTermStringFragment frag, void *user); +-int parser_apc(VTermStringFragment frag, void *user); +-int parser_pm(VTermStringFragment frag, void *user); +-int parser_sos(VTermStringFragment frag, void *user); +-int selection_set(VTermSelectionMask mask, VTermStringFragment frag, void *user); +-int selection_query(VTermSelectionMask mask, void *user); +-int state_putglyph(VTermGlyphInfo *info, VTermPos pos, void *user); +-int state_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user); +-int state_scrollrect(VTermRect rect, int downward, int rightward, void *user); +-int state_moverect(VTermRect dest, VTermRect src, void *user); +-int state_settermprop(VTermProp prop, VTermValue *val, void *user); +-int state_erase(VTermRect rect, int selective, void *user); +-int state_setpenattr(VTermAttr attr, VTermValue *val, void *user); +-int state_sb_clear(void *user); +-void print_color(const VTermColor *col); +-int screen_sb_pushline(int cols, const VTermScreenCell *cells, void *user); +-int screen_sb_popline(int cols, VTermScreenCell *cells, void *user); +-int screen_sb_clear(void *user); +-void term_output(const char *s, size_t len, void *user); +-int vterm_state_get_penattr(const VTermState *state, VTermAttr attr, VTermValue *val); +-int vterm_screen_get_attrs_extent(const VTermScreen *screen, VTermRect *extent, VTermPos pos, VTermAttrMask attrs); +-size_t vterm_screen_get_text(const VTermScreen *screen, char *buffer, size_t len, VTermRect rect); +-int vterm_screen_is_eol(const VTermScreen *screen, VTermPos pos); +-void vterm_state_get_cursorpos(const VTermState *state, VTermPos *cursorpos); +-void vterm_state_set_bold_highbright(VTermState *state, int bold_is_highbright); +-int vterm_color_is_equal(const VTermColor *a, const VTermColor *b); +diff --git a/test/unit/testutil.lua b/test/unit/testutil.lua +index 99d37f45252725..95e560e37c8721 100644 +--- a/test/unit/testutil.lua ++++ b/test/unit/testutil.lua +@@ -171,13 +171,6 @@ local function filter_complex_blocks(body) + -- Remove GCC's extension keyword which is just used to disable warnings. + line = string.gsub(line, '__extension__', '') + +- -- HACK: remove bitfields from specific structs as luajit can't seem to handle them. +- if line:find('struct VTermState') then +- line = string.gsub(line, 'state : 8;', 'state;') +- end +- if line:find('VTermStringFragment') then +- line = string.gsub(line, 'size_t.*len : 30;', 'size_t len;') +- end + result[#result + 1] = line + end + end +diff --git a/test/unit/vterm_spec.lua b/test/unit/vterm_spec.lua +deleted file mode 100644 +index da118d33b4cdf8..00000000000000 +--- a/test/unit/vterm_spec.lua ++++ /dev/null +@@ -1,3958 +0,0 @@ +-local t = require('test.unit.testutil') +-local describe, before_each, pending = t.describe, t.before_each, t.pending +-local itp = t.gen_itp(t.it) +-local bit = require('bit') +- +---- @class vterm +---- @field ENC_UTF8 integer +---- @field VTERM_ATTR_BLINK integer +---- @field VTERM_ATTR_BOLD integer +---- @field VTERM_ATTR_FONT integer +---- @field VTERM_ATTR_ITALIC integer +---- @field VTERM_ATTR_REVERSE integer +---- @field VTERM_ATTR_UNDERLINE integer +---- @field VTERM_BASELINE_RAISE integer +---- @field VTERM_KEY_ENTER integer +---- @field VTERM_KEY_FUNCTION_0 integer +---- @field VTERM_KEY_KP_0 integer +---- @field VTERM_KEY_NONE integer +---- @field VTERM_KEY_TAB integer +---- @field VTERM_KEY_UP integer +---- @field VTERM_KEY_BACKSPACE integer +---- @field VTERM_KEY_ESCAPE integer +---- @field VTERM_KEY_DEL integer +---- @field VTERM_MOD_ALT integer +---- @field VTERM_MOD_CTRL integer +---- @field VTERM_MOD_SHIFT integer +---- @field parser_apc function +---- @field parser_csi function +---- @field parser_dcs function +---- @field parser_osc function +---- @field parser_pm function +---- @field parser_sos function +---- @field parser_text function +---- @field print_color function +---- @field schar_get fun(any, any):integer +---- @field screen_sb_clear function +---- @field screen_sb_popline function +---- @field screen_sb_pushline function +---- @field selection_query function +---- @field selection_set function +---- @field state_erase function +---- @field state_movecursor function +---- @field state_moverect function +---- @field state_pos function +---- @field state_putglyph function +---- @field state_sb_clear function +---- @field state_scrollrect function +---- @field state_setpenattr function +---- @field state_settermprop function +---- @field term_output function +---- @field utf_ptr2char fun(any):integer +---- @field utf_ptr2len fun(any):integer +---- @field vterm_input_write function +---- @field vterm_keyboard_end_paste function +---- @field vterm_keyboard_key function +---- @field vterm_keyboard_start_paste function +---- @field vterm_keyboard_unichar function +---- @field vterm_lookup_encoding fun(any, any):any +---- @field vterm_mouse_button function +---- @field vterm_mouse_move function +---- @field vterm_new fun(any, any):any +---- @field vterm_obtain_screen fun(any):any +---- @field vterm_obtain_state fun(any): any +---- @field vterm_output_set_callback function +---- @field vterm_parser_set_callbacks fun(any, any, any):any +---- @field vterm_screen_convert_color_to_rgb function +---- @field vterm_screen_enable_altscreen function +---- @field vterm_screen_enable_reflow function +---- @field vterm_screen_get_attrs_extent function +---- @field vterm_screen_get_cell function +---- @field vterm_screen_get_text fun(any, any, any, any):any +---- @field vterm_screen_is_eol fun(any, any):any +---- @field vterm_screen_reset function +---- @field vterm_screen_set_callbacks function +---- @field vterm_set_size function +---- @field vterm_set_utf8 fun(any, any, any):any +---- @field vterm_state_focus_in function +---- @field vterm_state_focus_out function +---- @field vterm_state_get_cursorpos fun(any, any) +---- @field vterm_state_get_lineinfo fun(any, any):any +---- @field vterm_state_get_penattr function +---- @field vterm_state_reset function +---- @field vterm_state_set_bold_highbright function +---- @field vterm_state_set_callbacks function +---- @field vterm_state_set_selection_callbacks function +---- @field vterm_state_set_unrecognised_fallbacks function +-local vterm = t.cimport( +- './src/nvim/grid.h', +- './src/nvim/mbyte.h', +- './src/nvim/vterm/encoding.h', +- './src/nvim/vterm/keyboard.h', +- './src/nvim/vterm/mouse.h', +- './src/nvim/vterm/parser.h', +- './src/nvim/vterm/pen.h', +- './src/nvim/vterm/screen.h', +- './src/nvim/vterm/state.h', +- './src/nvim/vterm/vterm.h', +- './src/nvim/vterm/vterm_internal_defs.h', +- './test/unit/fixtures/vterm_test.h' +-) +- +--- libvterm stores pointers to these callback structs instead of copying them. +--- Keep the cdata alive for as long as the owning handle exists, and let the +--- weak-key table release everything once the handle itself becomes unreachable. +-local callback_structs = setmetatable({}, { __mode = 'k' }) +- +----@param owner any +----@param ... any +-local function keep_callback_structs(owner, ...) +- local refs = callback_structs[owner] +- if refs == nil then +- refs = {} +- callback_structs[owner] = refs +- end +- +- for i = 1, select('#', ...) do +- local ref = select(i, ...) +- if ref ~= nil then +- refs[#refs + 1] = ref +- end +- end +-end +- +---- @return string +-local function read_rm() +- local f = assert(io.open(t.paths.vterm_test_file, 'rb')) +- local text = f:read('*a') +- f:close() +- vim.fs.rm(t.paths.vterm_test_file, { force = true }) +- return text +-end +- +-local function append(str) +- local f = assert(io.open(t.paths.vterm_test_file, 'a')) +- f:write(str) +- f:close() +- return 1 +-end +- +-local function parser_control(control) +- return append(string.format('control %02x\n', control)) +-end +- +-local function parser_escape(bytes) +- return append(string.format('escape %s\n', t.ffi.string(bytes))) +-end +- +-local function wantparser(vt) +- assert(vt) +- +- local parser_cbs = t.ffi.new('VTermParserCallbacks') +- parser_cbs['text'] = vterm.parser_text +- parser_cbs['control'] = parser_control +- parser_cbs['escape'] = parser_escape +- parser_cbs['csi'] = vterm.parser_csi +- parser_cbs['osc'] = vterm.parser_osc +- parser_cbs['dcs'] = vterm.parser_dcs +- parser_cbs['apc'] = vterm.parser_apc +- parser_cbs['pm'] = vterm.parser_pm +- parser_cbs['sos'] = vterm.parser_sos +- +- vterm.vterm_parser_set_callbacks(vt, parser_cbs, nil) +- keep_callback_structs(vt, parser_cbs) +-end +- +---- @return any +-local function init() +- local vt = vterm.vterm_new(25, 80) +- vterm.vterm_output_set_callback(vt, vterm.term_output, nil) +- vterm.vterm_set_utf8(vt, true) +- return vt +-end +- +-local function state_setlineinfo() +- return 1 +-end +- +---- @return any +-local function wantstate(vt, opts) +- opts = opts or {} +- assert(vt) +- local state = vterm.vterm_obtain_state(vt) +- +- local state_cbs = t.ffi.new('VTermStateCallbacks') +- state_cbs['putglyph'] = vterm.state_putglyph +- state_cbs['movecursor'] = vterm.state_movecursor +- state_cbs['scrollrect'] = vterm.state_scrollrect +- state_cbs['moverect'] = vterm.state_moverect +- state_cbs['erase'] = vterm.state_erase +- state_cbs['setpenattr'] = vterm.state_setpenattr +- state_cbs['settermprop'] = vterm.state_settermprop +- state_cbs['setlineinfo'] = state_setlineinfo +- state_cbs['sb_clear'] = vterm.state_sb_clear +- +- local selection_cbs = t.ffi.new('VTermSelectionCallbacks') +- selection_cbs['set'] = vterm.selection_set +- selection_cbs['query'] = vterm.selection_query +- +- vterm.vterm_state_set_callbacks(state, state_cbs, nil) +- +- -- In some tests we want to check the behaviour of overflowing the buffer, so make it nicely small +- vterm.vterm_state_set_selection_callbacks(state, selection_cbs, nil, nil, 16) +- vterm.vterm_state_set_bold_highbright(state, 1) +- vterm.vterm_state_reset(state, 1) +- +- local fallback_cbs +- if opts.f then +- fallback_cbs = t.ffi.new('VTermStateFallbacks') +- fallback_cbs['control'] = parser_control +- fallback_cbs['csi'] = vterm.parser_csi +- fallback_cbs['osc'] = vterm.parser_osc +- fallback_cbs['dcs'] = vterm.parser_dcs +- fallback_cbs['apc'] = vterm.parser_apc +- fallback_cbs['pm'] = vterm.parser_pm +- fallback_cbs['sos'] = vterm.parser_sos +- end +- +- vterm.want_state_scrollback = opts.b or false +- vterm.want_state_erase = opts.e or false +- vterm.vterm_state_set_unrecognised_fallbacks(state, fallback_cbs, nil) +- vterm.want_state_putglyph = opts.g or false +- vterm.want_state_moverect = opts.m or false +- vterm.want_state_settermprop = opts.p or false +- vterm.want_state_scrollrect = opts.s or false +- +- keep_callback_structs(state, state_cbs, selection_cbs, fallback_cbs) +- return state +-end +- +---- @return any +-local function wantscreen(vt, opts) +- opts = opts or {} +- local screen = vterm.vterm_obtain_screen(vt) +- local screen_cbs = t.ffi.new('VTermScreenCallbacks') +- +- -- TODO(dundargoc): fix +- -- screen_cbs['damage'] = vterm.screen_damage +- screen_cbs['moverect'] = vterm.state_moverect +- screen_cbs['movecursor'] = vterm.state_movecursor +- screen_cbs['settermprop'] = vterm.state_settermprop +- screen_cbs['sb_pushline'] = vterm.screen_sb_pushline +- screen_cbs['sb_popline'] = vterm.screen_sb_popline +- screen_cbs['sb_clear'] = vterm.screen_sb_clear +- +- vterm.vterm_screen_set_callbacks(screen, screen_cbs, nil) +- +- if opts.a then +- vterm.vterm_screen_enable_altscreen(screen, 1) +- end +- vterm.want_screen_scrollback = opts.b or false +- vterm.want_state_movecursor = opts.c or false +- -- TODO(dundargoc): fix +- -- vterm.want_screen_damage = opts.d or opts.D or false +- -- vterm.want_screen_cells = opts.D or false +- vterm.want_state_moverect = opts.m or false +- vterm.want_state_settermprop = opts.p or false +- if opts.r then +- vterm.vterm_screen_enable_reflow(screen, true) +- end +- +- keep_callback_structs(screen, screen_cbs) +- return screen +-end +- +-local function reset(state, screen) +- if state then +- vterm.vterm_state_reset(state, 1) +- vterm.vterm_state_get_cursorpos(state, vterm.state_pos) +- end +- if screen then +- vterm.vterm_screen_reset(screen, 1) +- end +-end +- +-local function push(input, vt) +- vterm.vterm_input_write(vt, input, string.len(input)) +-end +- +--- vterm_input_write() can synchronously invoke the Lua callbacks installed +--- above. LuaJIT must not JIT-compile an FFI call that re-enters Lua, or the +--- test can panic with "bad callback". +-if jit then +- jit.off(push, true) +-end +- +-local function expect(expected) +- local actual = read_rm() +- t.eq(expected .. '\n', actual) +-end +- +-local function expect_output(expected_preformat) +- local actual = read_rm() +- local expected = 'output ' +- +- for c in string.gmatch(expected_preformat, '.') do +- if expected ~= 'output ' then +- expected = expected .. ',' +- end +- expected = string.format('%s%x', expected, string.byte(c)) +- end +- +- t.eq(expected .. '\n', actual) +-end +- +-local function cursor(row, col, state) +- local pos = t.ffi.new('VTermPos') --- @type {row: integer, col: integer} +- vterm.vterm_state_get_cursorpos(state, pos) +- t.eq(row, pos.row) +- t.eq(col, pos.col) +-end +- +-local function lineinfo(row, expected, state) +- local info = vterm.vterm_state_get_lineinfo(state, row) +- local dwl = info.doublewidth == 1 +- local dhl = info.doubleheight == 1 +- local cont = info.continuation == 1 +- +- t.eq(dwl, expected.dwl or false) +- t.eq(dhl, expected.dhl or false) +- t.eq(cont, expected.cont or false) +-end +- +-local function pen(attribute, expected, state) +- local is_bool = +- { bold = true, italic = true, blink = true, reverse = true, dim = true, overline = true } +- local vterm_attribute = { +- bold = vterm.VTERM_ATTR_BOLD, +- underline = vterm.VTERM_ATTR_UNDERLINE, +- italic = vterm.VTERM_ATTR_ITALIC, +- blink = vterm.VTERM_ATTR_BLINK, +- reverse = vterm.VTERM_ATTR_REVERSE, +- font = vterm.VTERM_ATTR_FONT, +- dim = vterm.VTERM_ATTR_DIM, +- overline = vterm.VTERM_ATTR_OVERLINE, +- } +- +- local val = t.ffi.new('VTermValue') --- @type {boolean: integer} +- vterm.vterm_state_get_penattr(state, vterm_attribute[attribute], val) +- local actual = val.boolean --- @type integer|boolean +- if is_bool[attribute] then +- actual = val.boolean == 1 +- end +- t.eq(expected, actual) +-end +- +-local function resize(rows, cols, vt) +- vterm.vterm_set_size(vt, rows, cols) +-end +- +-local function screen_chars(start_row, start_col, end_row, end_col, expected, screen) +- local rect = t.ffi.new('VTermRect') +- rect['start_row'] = start_row +- rect['start_col'] = start_col +- rect['end_row'] = end_row +- rect['end_col'] = end_col +- +- local len = vterm.vterm_screen_get_text(screen, nil, 0, rect) +- +- local text = t.ffi.new('unsigned char[?]', len) +- vterm.vterm_screen_get_text(screen, text, len, rect) +- +- local actual = t.ffi.string(text, len) +- t.eq(expected, actual) +-end +- +-local function screen_text(start_row, start_col, end_row, end_col, expected, screen) +- local rect = t.ffi.new('VTermRect') +- rect['start_row'] = start_row +- rect['start_col'] = start_col +- rect['end_row'] = end_row +- rect['end_col'] = end_col +- +- local len = vterm.vterm_screen_get_text(screen, nil, 0, rect) +- +- local text = t.ffi.new('unsigned char[?]', len) +- vterm.vterm_screen_get_text(screen, text, len, rect) +- +- local actual = '' +- for i = 0, tonumber(len) - 1 do +- actual = string.format('%s%02x,', actual, text[i]) +- end +- actual = actual:sub(1, -2) +- +- t.eq(expected, actual) +-end +- +---- @param row integer +-local function screen_row(row, expected, screen, end_col) +- local rect = t.ffi.new('VTermRect') +- rect['start_row'] = row +- rect['start_col'] = 0 +- rect['end_row'] = row + 1 +- rect['end_col'] = end_col or 80 +- +- local len = vterm.vterm_screen_get_text(screen, nil, 0, rect) +- +- local text = t.ffi.new('unsigned char[?]', len) +- vterm.vterm_screen_get_text(screen, text, len, rect) +- +- t.eq(expected, t.ffi.string(text, len)) +-end +- +-local function screen_cell(row, col, expected, screen) +- local pos = t.ffi.new('VTermPos') +- pos['row'] = row +- pos['col'] = col +- +- local cell = t.ffi.new('VTermScreenCell') ---@type any +- vterm.vterm_screen_get_cell(screen, pos, cell) +- +- local buf = t.ffi.new('unsigned char[32]') +- vterm.schar_get(buf, cell.schar) +- +- local actual = '{' +- local i = 0 +- while buf[i] > 0 do +- local char = vterm.utf_ptr2char(buf + i) +- local charlen = vterm.utf_ptr2len(buf + i) +- if i > 0 then +- actual = actual .. ',' +- end +- local invalid = char >= 128 and charlen == 1 +- actual = string.format('%s%s%02x', actual, invalid and '?' or '', char) +- i = i + charlen +- end +- actual = string.format('%s} width=%d attrs={', actual, cell['width']) +- actual = actual .. (cell['attrs'].bold ~= 0 and 'B' or '') +- actual = actual .. (cell['attrs'].dim ~= 0 and 'D' or '') +- actual = actual +- .. (cell['attrs'].underline ~= 0 and string.format('U%d', cell['attrs'].underline) or '') +- actual = actual .. (cell['attrs'].italic ~= 0 and 'I' or '') +- actual = actual .. (cell['attrs'].blink ~= 0 and 'K' or '') +- actual = actual .. (cell['attrs'].reverse ~= 0 and 'R' or '') +- actual = actual .. (cell['attrs'].overline ~= 0 and 'O' or '') +- actual = actual .. (cell['attrs'].font ~= 0 and string.format('F%d', cell['attrs'].font) or '') +- actual = actual .. (cell['attrs'].small ~= 0 and 'S' or '') +- if cell['attrs'].baseline ~= 0 then +- actual = actual .. (cell['attrs'].baseline == vterm.VTERM_BASELINE_RAISE and '^' or '_') +- end +- actual = actual .. '} ' +- +- actual = actual .. (cell['attrs'].dwl ~= 0 and 'dwl ' or '') +- if cell['attrs'].dhl ~= 0 then +- actual = actual .. string.format('dhl-%s ', cell['attrs'].dhl == 2 and 'bottom' or 'top') +- end +- +- actual = string.format('%sfg=', actual) +- vterm.vterm_screen_convert_color_to_rgb(screen, cell['fg']) +- vterm.print_color(cell['fg']) +- +- actual = actual .. read_rm() +- actual = actual .. ' bg=' +- +- vterm.vterm_screen_convert_color_to_rgb(screen, cell['bg']) +- vterm.print_color(cell['bg']) +- +- actual = actual .. read_rm() +- +- t.eq(expected, actual) +-end +- +-local function screen_eol(row, col, expected, screen) +- local pos = t.ffi.new('VTermPos') +- pos['row'] = row +- pos['col'] = col +- +- local is_eol = vterm.vterm_screen_is_eol(screen, pos) +- t.eq(expected, is_eol) +-end +- +-local function screen_attrs_extent(row, col, expected, screen) +- local pos = t.ffi.new('VTermPos') +- pos['row'] = row +- pos['col'] = col +- +- local rect = t.ffi.new('VTermRect') +- rect['start_col'] = 0 +- rect['end_col'] = -1 +- vterm.vterm_screen_get_attrs_extent(screen, rect, pos, 1) +- +- local actual = string.format( +- '%d,%d-%d,%d', +- rect['start_row'], +- rect['start_col'], +- rect['end_row'], +- rect['end_col'] +- ) +- +- t.eq(expected, actual) +-end +- +-local function wantencoding() +- local encoding = t.ffi.new('VTermEncodingInstance') +- encoding['enc'] = vterm.vterm_lookup_encoding(vterm.ENC_UTF8, string.byte('u')) +- if encoding.enc.init then +- encoding.enc.init(encoding.enc, encoding['data']) +- end +- return encoding +-end +- +-local function encin(input, encoding) +- local len = string.len(input) +- +- local cp = t.ffi.new('uint32_t[?]', len) +- local cpi = t.ffi.new('int[1]') +- local pos = t.ffi.new('size_t[1]', 0) +- +- encoding.enc.decode(encoding.enc, encoding.data, cp, cpi, len, input, pos, len) +- +- local f = assert(io.open(t.paths.vterm_test_file, 'w')) +- if tonumber(cpi[0]) > 0 then +- f:write('encout ') +- for i = 0, cpi[0] - 1 do +- if i == 0 then +- f:write(string.format('%x', cp[i])) +- else +- f:write(string.format(',%x', cp[i])) +- end +- end +- f:write('\n') +- end +- f:close() +-end +- +-local function strpe_modifiers(input_mod) +- local mod = t.ffi.new('VTermModifier') ---@type any +- if input_mod.C then +- mod = bit.bor(mod, vterm.VTERM_MOD_CTRL) +- end +- if input_mod.S then +- mod = bit.bor(mod, vterm.VTERM_MOD_SHIFT) +- end +- if input_mod.A then +- mod = bit.bor(mod, vterm.VTERM_MOD_ALT) +- end +- return mod +-end +- +-local function strp_key(input_key) +- if input_key == 'up' then +- return vterm.VTERM_KEY_UP +- end +- +- if input_key == 'tab' then +- return vterm.VTERM_KEY_TAB +- end +- +- if input_key == 'enter' then +- return vterm.VTERM_KEY_ENTER +- end +- +- if input_key == 'bs' then +- return vterm.VTERM_KEY_BACKSPACE +- end +- +- if input_key == 'del' then +- return vterm.VTERM_KEY_DEL +- end +- +- if input_key == 'esc' then +- return vterm.VTERM_KEY_ESCAPE +- end +- +- if input_key == 'f1' then +- return vterm.VTERM_KEY_FUNCTION_0 + 1 +- end +- +- if input_key == 'kp0' then +- return vterm.VTERM_KEY_KP_0 +- end +- +- return vterm.VTERM_KEY_NONE +-end +- +-local function mousemove(row, col, vt, input_mod) +- input_mod = input_mod or {} +- local mod = strpe_modifiers(input_mod) +- vterm.vterm_mouse_move(vt, row, col, mod) +-end +- +-local function mousebtn(press, button, vt, input_mod) +- input_mod = input_mod or {} +- local mod = strpe_modifiers(input_mod) +- local flag = press == 'd' or press == 'D' +- vterm.vterm_mouse_button(vt, button, flag, mod) +-end +- +-local function inchar(c, vt, input_mod) +- input_mod = input_mod or {} +- local mod = strpe_modifiers(input_mod) +- vterm.vterm_keyboard_unichar(vt, c, mod) +-end +- +-local function inkey(input_key, vt, input_mod) +- input_mod = input_mod or {} +- local mod = strpe_modifiers(input_mod) +- local key = strp_key(input_key) +- vterm.vterm_keyboard_key(vt, key, mod) +-end +- +-before_each(function() +- vim.fs.rm(t.paths.vterm_test_file, { force = true }) +-end) +- +-describe('vterm', function() +- itp('02parser', function() +- local vt = init() +- vterm.vterm_set_utf8(vt, false) +- wantparser(vt) +- +- -- Basic text +- push('hello', vt) +- expect('text 68,65,6c,6c,6f') +- +- -- C0 +- push('\x03', vt) +- expect('control 03') +- push('\x1f', vt) +- expect('control 1f') +- +- -- C1 8bit +- push('\x83', vt) +- expect('control 83') +- push('\x99', vt) +- expect('control 99') +- +- -- C1 7bit +- push('\x1b\x43', vt) +- expect('control 83') +- push('\x1b\x59', vt) +- expect('control 99') +- +- -- High bytes +- push('\xa0\xcc\xfe', vt) +- expect('text a0,cc,fe') +- +- -- Mixed +- push('1\n2', vt) +- expect('text 31\ncontrol 0a\ntext 32') +- +- -- Escape +- push('\x1b=', vt) +- expect('escape =') +- +- -- Escape 2-byte +- push('\x1b(X', vt) +- expect('escape (X') +- +- -- Split write Escape +- push('\x1b(', vt) +- push('Y', vt) +- expect('escape (Y') +- +- -- Escape cancels Escape, starts another +- push('\x1b(\x1b)Z', vt) +- expect('escape )Z') +- +- -- CAN cancels Escape, returns to normal mode +- push('\x1b(\x18AB', vt) +- expect('text 41,42') +- +- -- C0 in Escape interrupts and continues +- push('\x1b(\nX', vt) +- expect('control 0a\nescape (X') +- +- -- CSI 0 args +- push('\x1b[a', vt) +- expect('csi 61 *') +- +- -- CSI 1 arg +- push('\x1b[9b', vt) +- expect('csi 62 9') +- +- -- CSI 2 args +- push('\x1b[3;4c', vt) +- expect('csi 63 3,4') +- +- -- CSI 1 arg 1 sub +- push('\x1b[1:2c', vt) +- expect('csi 63 1+,2') +- +- -- CSI many digits +- push('\x1b[678d', vt) +- expect('csi 64 678') +- +- -- CSI leading zero +- push('\x1b[007e', vt) +- expect('csi 65 7') +- +- -- CSI qmark +- push('\x1b[?2;7f', vt) +- expect('csi 66 L=3f 2,7') +- +- -- CSI greater +- push('\x1b[>c', vt) +- expect('csi 63 L=3e *') +- +- -- CSI SP +- push('\x1b[12 q', vt) +- expect('csi 71 12 I=20') +- +- -- Mixed CSI +- push('A\x1b[8mB', vt) +- expect('text 41\ncsi 6d 8\ntext 42') +- +- -- Split write +- push('\x1b', vt) +- push('[a', vt) +- expect('csi 61 *') +- push('foo\x1b[', vt) +- expect('text 66,6f,6f') +- push('4b', vt) +- expect('csi 62 4') +- push('\x1b[12;', vt) +- push('3c', vt) +- expect('csi 63 12,3') +- +- -- Escape cancels CSI, starts Escape +- push('\x1b[123\x1b9', vt) +- expect('escape 9') +- +- -- CAN cancels CSI, returns to normal mode +- push('\x1b[12\x18AB', vt) +- expect('text 41,42') +- +- -- C0 in Escape interrupts and continues +- push('\x1b(\nX', vt) +- expect('control 0a\nescape (X') +- +- -- OSC BEL +- push('\x1b]1;Hello\x07', vt) +- expect('osc [1;Hello]') +- +- -- OSC ST (7bit) +- push('\x1b]1;Hello\x1b\\', vt) +- expect('osc [1;Hello]') +- +- -- OSC ST (8bit) +- push('\x9d1;Hello\x9c', vt) +- expect('osc [1;Hello]') +- +- -- OSC in parts +- push('\x1b]52;abc', vt) +- expect('osc [52;abc') +- push('def', vt) +- expect('osc def') +- push('ghi\x1b\\', vt) +- expect('osc ghi]') +- +- -- OSC BEL without semicolon +- push('\x1b]1234\x07', vt) +- expect('osc [1234;]') +- +- -- OSC ST without semicolon +- push('\x1b]1234\x1b\\', vt) +- expect('osc [1234;]') +- +- -- Escape cancels OSC, starts Escape +- push('\x1b]Something\x1b9', vt) +- expect('escape 9') +- +- -- CAN cancels OSC, returns to normal mode +- push('\x1b]12\x18AB', vt) +- expect('text 41,42') +- +- -- C0 in OSC interrupts and continues +- push('\x1b]2;\nBye\x07', vt) +- expect('osc [2;\ncontrol 0a\nosc Bye]') +- +- -- DCS BEL +- push('\x1bPHello\x07', vt) +- expect('dcs [Hello]') +- +- -- DCS ST (7bit) +- push('\x1bPHello\x1b\\', vt) +- expect('dcs [Hello]') +- +- -- DCS ST (8bit) +- push('\x90Hello\x9c', vt) +- expect('dcs [Hello]') +- +- -- Split write of 7bit ST +- push('\x1bPABC\x1b', vt) +- expect('dcs [ABC') +- push('\\', vt) +- expect('dcs ]') +- +- -- Escape cancels DCS, starts Escape +- push('\x1bPSomething\x1b9', vt) +- expect('escape 9') +- +- -- CAN cancels DCS, returns to normal mode +- push('\x1bP12\x18AB', vt) +- expect('text 41,42') +- +- -- C0 in OSC interrupts and continues +- push('\x1bPBy\ne\x07', vt) +- expect('dcs [By\ncontrol 0a\ndcs e]') +- +- -- APC BEL +- push('\x1b_Hello\x07', vt) +- expect('apc [Hello]') +- +- -- APC ST (7bit) +- push('\x1b_Hello\x1b\\', vt) +- expect('apc [Hello]') +- +- -- APC ST (8bit) +- push('\x9fHello\x9c', vt) +- expect('apc [Hello]') +- +- -- PM BEL +- push('\x1b^Hello\x07', vt) +- expect('pm [Hello]') +- +- -- PM ST (7bit) +- push('\x1b^Hello\x1b\\', vt) +- expect('pm [Hello]') +- +- -- PM ST (8bit) +- push('\x9eHello\x9c', vt) +- expect('pm [Hello]') +- +- -- SOS BEL +- push('\x1bXHello\x07', vt) +- expect('sos [Hello]') +- +- -- SOS ST (7bit) +- push('\x1bXHello\x1b\\', vt) +- expect('sos [Hello]') +- +- -- SOS ST (8bit) +- push('\x98Hello\x9c', vt) +- expect('sos [Hello]') +- +- push('\x1bXABC\x01DEF\x1b\\', vt) +- expect('sos [ABC\x01DEF]') +- push('\x1bXABC\x99DEF\x1b\\', vt) +- expect('sos [ABC\x99DEF]') +- +- -- NUL ignored +- push('\x00', vt) +- +- -- NUL ignored within CSI +- push('\x1b[12\x003m', vt) +- expect('csi 6d 123') +- +- -- DEL ignored +- push('\x7f', vt) +- +- -- DEL ignored within CSI +- push('\x1b[12\x7f3m', vt) +- expect('csi 6d 123') +- +- -- DEL inside text" +- push('AB\x7fC', vt) +- expect('text 41,42\ntext 43') +- end) +- +- itp('03encoding_utf8', function() +- local encoding = wantencoding() +- +- -- Low +- encin('123', encoding) +- expect('encout 31,32,33') +- +- -- We want to prove the UTF-8 parser correctly handles all the sequences. +- -- Easy way to do this is to check it does low/high boundary cases, as that +- -- leaves only two for each sequence length +- -- +- -- These ranges are therefore: +- -- +- -- Two bytes: +- -- U+0080 = 000 10000000 => 00010 000000 +- -- => 11000010 10000000 = C2 80 +- -- U+07FF = 111 11111111 => 11111 111111 +- -- => 11011111 10111111 = DF BF +- -- +- -- Three bytes: +- -- U+0800 = 00001000 00000000 => 0000 100000 000000 +- -- => 11100000 10100000 10000000 = E0 A0 80 +- -- U+FFFD = 11111111 11111101 => 1111 111111 111101 +- -- => 11101111 10111111 10111101 = EF BF BD +- -- (We avoid U+FFFE and U+FFFF as they're invalid codepoints) +- -- +- -- Four bytes: +- -- U+10000 = 00001 00000000 00000000 => 000 010000 000000 000000 +- -- => 11110000 10010000 10000000 10000000 = F0 90 80 80 +- -- U+1FFFFF = 11111 11111111 11111111 => 111 111111 111111 111111 +- -- => 11110111 10111111 10111111 10111111 = F7 BF BF BF +- +- -- 2 byte +- encin('\xC2\x80\xDF\xBF', encoding) +- expect('encout 80,7ff') +- +- -- 3 byte +- encin('\xE0\xA0\x80\xEF\xBF\xBD', encoding) +- expect('encout 800,fffd') +- +- -- 4 byte +- encin('\xF0\x90\x80\x80\xF7\xBF\xBF\xBF', encoding) +- expect('encout 10000,1fffff') +- +- -- Next up, we check some invalid sequences +- -- + Early termination (back to low bytes too soon) +- -- + Early restart (another sequence introduction before the previous one was finished) +- +- -- Early termination +- encin('\xC2!', encoding) +- expect('encout fffd,21') +- +- encin('\xE0!\xE0\xA0!', encoding) +- expect('encout fffd,21,fffd,21') +- +- encin('\xF0!\xF0\x90!\xF0\x90\x80!', encoding) +- expect('encout fffd,21,fffd,21,fffd,21') +- +- -- Early restart +- encin('\xC2\xC2\x90', encoding) +- expect('encout fffd,90') +- +- encin('\xE0\xC2\x90\xE0\xA0\xC2\x90', encoding) +- expect('encout fffd,90,fffd,90') +- +- encin('\xF0\xC2\x90\xF0\x90\xC2\x90\xF0\x90\x80\xC2\x90', encoding) +- expect('encout fffd,90,fffd,90,fffd,90') +- +- -- Test the overlong sequences by giving an overlong encoding of U+0000 and +- -- an encoding of the highest codepoint still too short +- -- +- -- Two bytes: +- -- U+0000 = C0 80 +- -- U+007F = 000 01111111 => 00001 111111 => +- -- => 11000001 10111111 => C1 BF +- -- +- -- Three bytes: +- -- U+0000 = E0 80 80 +- -- U+07FF = 00000111 11111111 => 0000 011111 111111 +- -- => 11100000 10011111 10111111 = E0 9F BF +- -- +- -- Four bytes: +- -- U+0000 = F0 80 80 80 +- -- U+FFFF = 11111111 11111111 => 000 001111 111111 111111 +- -- => 11110000 10001111 10111111 10111111 = F0 8F BF BF +- +- -- Overlong +- encin('\xC0\x80\xC1\xBF', encoding) +- expect('encout fffd,fffd') +- +- encin('\xE0\x80\x80\xE0\x9F\xBF', encoding) +- expect('encout fffd,fffd') +- +- encin('\xF0\x80\x80\x80\xF0\x8F\xBF\xBF', encoding) +- expect('encout fffd,fffd') +- +- -- UTF-16 surrogates U+D800 and U+DFFF +- -- UTF-16 Surrogates +- encin('\xED\xA0\x80\xED\xBF\xBF', encoding) +- expect('encout fffd,fffd') +- +- -- Split write +- encin('\xC2', encoding) +- encin('\xA0', encoding) +- expect('encout a0') +- +- encin('\xE0', encoding) +- encin('\xA0\x80', encoding) +- expect('encout 800') +- encin('\xE0\xA0', encoding) +- encin('\x80', encoding) +- expect('encout 800') +- +- encin('\xF0', encoding) +- encin('\x90\x80\x80', encoding) +- expect('encout 10000') +- encin('\xF0\x90', encoding) +- encin('\x80\x80', encoding) +- expect('encout 10000') +- encin('\xF0\x90\x80', encoding) +- encin('\x80', encoding) +- expect('encout 10000') +- end) +- +- itp('10state_putglyph', function() +- local vt = init() +- local state = wantstate(vt, { g = true }) +- +- -- Low +- reset(state, nil) +- push('ABC', vt) +- expect('putglyph 41 1 0,0\nputglyph 42 1 0,1\nputglyph 43 1 0,2') +- +- -- UTF-8 1 char +- -- U+00C1 = 0xC3 0x81 name: LATIN CAPITAL LETTER A WITH ACUTE +- -- U+00E9 = 0xC3 0xA9 name: LATIN SMALL LETTER E WITH ACUTE +- reset(state, nil) +- push('\xC3\x81\xC3\xA9', vt) +- expect('putglyph c1 1 0,0\nputglyph e9 1 0,1') +- +- -- UTF-8 split writes +- reset(state, nil) +- push('\xC3', vt) +- push('\x81', vt) +- expect('putglyph c1 1 0,0') +- +- -- UTF-8 wide char +- -- U+FF10 = EF BC 90 name: FULLWIDTH DIGIT ZERO +- reset(state, nil) +- push('\xEF\xBC\x90 ', vt) +- expect('putglyph ff10 2 0,0\nputglyph 20 1 0,2') +- +- -- UTF-8 emoji wide char +- -- U+1F600 = F0 9F 98 80 name: GRINNING FACE +- reset(state, nil) +- push('\xF0\x9F\x98\x80 ', vt) +- expect('putglyph 1f600 2 0,0\nputglyph 20 1 0,2') +- +- -- UTF-8 combining chars +- -- U+0301 = CC 81 name: COMBINING ACUTE +- reset(state, nil) +- push('e\xCC\x81Z', vt) +- expect('putglyph 65,301 1 0,0\nputglyph 5a 1 0,1') +- +- -- Combining across buffers +- reset(state, nil) +- push('e', vt) +- expect('putglyph 65 1 0,0') +- push('\xCC\x81Z', vt) +- expect('putglyph 65,301 1 0,0\nputglyph 5a 1 0,1') +- +- -- Spare combining chars get truncated +- reset(state, nil) +- push('e' .. string.rep('\xCC\x81', 20), vt) +- expect('putglyph 65,301,301,301,301,301,301,301,301,301,301,301,301,301,301 1 0,0') -- and nothing more +- +- -- Combining across buffers multiple times +- reset(state, nil) +- push('e', vt) +- expect('putglyph 65 1 0,0') +- push('\xCC\x81', vt) +- expect('putglyph 65,301 1 0,0') +- push('\xCC\x82', vt) +- expect('putglyph 65,301,302 1 0,0') +- +- -- Combining across buffers at right edge +- reset(state, nil) +- push('\x1b[5;80H', vt) +- push('e', vt) +- expect('putglyph 65 1 4,79') +- push('\xCC\x81', vt) +- expect('putglyph 65,301 1 4,79') +- push('\xCC\x82Z', vt) +- expect('putglyph 65,301,302 1 4,79\nputglyph 5a 1 5,0') +- +- -- Combining regional indicators +- reset(state, nil) +- push('\x1b[5;77H', vt) +- push('🇦', vt) +- expect('putglyph 1f1e6 2 4,76') +- push('🇩', vt) +- expect('putglyph 1f1e6,1f1e9 2 4,76') +- push('🇱', vt) +- expect('putglyph 1f1f1 2 4,78') +- push('🇮', vt) +- expect('putglyph 1f1f1,1f1ee 2 4,78') +- push('🇲', vt) +- expect('putglyph 1f1f2 2 5,0') +- push('🇨', vt) +- expect('putglyph 1f1f2,1f1e8 2 5,0') +- +- -- emoji with ZWJ and variant selectors, as one chunk +- reset(state, nil) +- push('🏳️‍🌈🏳️‍⚧️🏴‍☠️', vt) +- expect([[putglyph 1f3f3,fe0f,200d,1f308 2 0,0 +-putglyph 1f3f3,fe0f,200d,26a7,fe0f 2 0,2 +-putglyph 1f3f4,200d,2620,fe0f 2 0,4]]) +- +- -- emoji, one code point at a time +- reset(state, nil) +- push('🏳', vt) +- expect('putglyph 1f3f3 2 0,0') +- push('\xef\xb8\x8f', vt) +- expect('putglyph 1f3f3,fe0f 2 0,0') +- push('\xe2\x80\x8d', vt) +- expect('putglyph 1f3f3,fe0f,200d 2 0,0') +- push('🌈', vt) +- expect('putglyph 1f3f3,fe0f,200d,1f308 2 0,0') +- +- -- modifier can change width +- push('❤', vt) +- expect('putglyph 2764 1 0,2') +- push('\xef\xb8\x8f', vt) +- expect('putglyph 2764,fe0f 2 0,2') +- +- -- also works batched +- push('❤️', vt) +- expect('putglyph 2764,fe0f 2 0,4') +- +- -- DECSCA protected +- reset(state, nil) +- push('A\x1b[1"qB\x1b[2"qC', vt) +- expect('putglyph 41 1 0,0\nputglyph 42 1 0,1 prot\nputglyph 43 1 0,2') +- end) +- +- itp('11state_movecursor', function() +- local vt = init() +- local state = wantstate(vt) +- +- -- Implicit +- push('ABC', vt) +- cursor(0, 3, state) +- +- -- Backspace +- push('\b', vt) +- cursor(0, 2, state) +- -- Horizontal Tab +- push('\t', vt) +- cursor(0, 8, state) +- -- Carriage Return +- push('\r', vt) +- cursor(0, 0, state) +- -- Linefeed +- push('\n', vt) +- cursor(1, 0, state) +- +- -- Backspace bounded by lefthand edge +- push('\x1b[4;2H', vt) +- cursor(3, 1, state) +- push('\b', vt) +- cursor(3, 0, state) +- push('\b', vt) +- cursor(3, 0, state) +- +- -- Backspace cancels phantom +- push('\x1b[4;80H', vt) +- cursor(3, 79, state) +- push('X', vt) +- cursor(3, 79, state) +- push('\b', vt) +- cursor(3, 78, state) +- +- -- HT bounded by righthand edge +- push('\x1b[1;78H', vt) +- cursor(0, 77, state) +- push('\t', vt) +- cursor(0, 79, state) +- push('\t', vt) +- cursor(0, 79, state) +- +- reset(state, nil) +- +- -- Index +- push('ABC\x1bD', vt) +- cursor(1, 3, state) +- -- Reverse Index +- push('\x1bM', vt) +- cursor(0, 3, state) +- -- Newline +- push('\x1bE', vt) +- cursor(1, 0, state) +- +- reset(state, nil) +- +- -- Cursor Forward +- push('\x1b[B', vt) +- cursor(1, 0, state) +- push('\x1b[3B', vt) +- cursor(4, 0, state) +- push('\x1b[0B', vt) +- cursor(5, 0, state) +- +- -- Cursor Down +- push('\x1b[C', vt) +- cursor(5, 1, state) +- push('\x1b[3C', vt) +- cursor(5, 4, state) +- push('\x1b[0C', vt) +- cursor(5, 5, state) +- +- -- Cursor Up +- push('\x1b[A', vt) +- cursor(4, 5, state) +- push('\x1b[3A', vt) +- cursor(1, 5, state) +- push('\x1b[0A', vt) +- cursor(0, 5, state) +- +- -- Cursor Backward +- push('\x1b[D', vt) +- cursor(0, 4, state) +- push('\x1b[3D', vt) +- cursor(0, 1, state) +- push('\x1b[0D', vt) +- cursor(0, 0, state) +- +- -- Cursor Next Line +- push(' ', vt) +- cursor(0, 3, state) +- push('\x1b[E', vt) +- cursor(1, 0, state) +- push(' ', vt) +- cursor(1, 3, state) +- push('\x1b[2E', vt) +- cursor(3, 0, state) +- push('\x1b[0E', vt) +- cursor(4, 0, state) +- +- -- Cursor Previous Line +- push(' ', vt) +- cursor(4, 3, state) +- push('\x1b[F', vt) +- cursor(3, 0, state) +- push(' ', vt) +- cursor(3, 3, state) +- push('\x1b[2F', vt) +- cursor(1, 0, state) +- push('\x1b[0F', vt) +- cursor(0, 0, state) +- +- -- Cursor Horizontal Absolute +- push('\n', vt) +- cursor(1, 0, state) +- push('\x1b[20G', vt) +- cursor(1, 19, state) +- push('\x1b[G', vt) +- cursor(1, 0, state) +- +- -- Cursor Position +- push('\x1b[10;5H', vt) +- cursor(9, 4, state) +- push('\x1b[8H', vt) +- cursor(7, 0, state) +- push('\x1b[H', vt) +- cursor(0, 0, state) +- +- -- Cursor Position cancels phantom +- push('\x1b[10;78H', vt) +- cursor(9, 77, state) +- push('ABC', vt) +- cursor(9, 79, state) +- push('\x1b[10;80H', vt) +- push('C', vt) +- cursor(9, 79, state) +- push('X', vt) +- cursor(10, 1, state) +- +- reset(state, nil) +- +- -- Bounds Checking +- push('\x1b[A', vt) +- cursor(0, 0, state) +- push('\x1b[D', vt) +- cursor(0, 0, state) +- push('\x1b[25;80H', vt) +- cursor(24, 79, state) +- push('\x1b[B', vt) +- cursor(24, 79, state) +- push('\x1b[C', vt) +- cursor(24, 79, state) +- push('\x1b[E', vt) +- cursor(24, 0, state) +- push('\x1b[H', vt) +- cursor(0, 0, state) +- push('\x1b[F', vt) +- cursor(0, 0, state) +- push('\x1b[999G', vt) +- cursor(0, 79, state) +- push('\x1b[99;99H', vt) +- cursor(24, 79, state) +- +- reset(state, nil) +- +- -- Horizontal Position Absolute +- push('\x1b[5`', vt) +- cursor(0, 4, state) +- +- -- Horizontal Position Relative +- push('\x1b[3a', vt) +- cursor(0, 7, state) +- +- -- Horizontal Position Backward +- push('\x1b[3j', vt) +- cursor(0, 4, state) +- +- -- Horizontal and Vertical Position +- push('\x1b[3;3f', vt) +- cursor(2, 2, state) +- +- -- Vertical Position Absolute +- push('\x1b[5d', vt) +- cursor(4, 2, state) +- +- -- Vertical Position Relative +- push('\x1b[2e', vt) +- cursor(6, 2, state) +- +- -- Vertical Position Backward +- push('\x1b[2k', vt) +- cursor(4, 2, state) +- +- reset(state, nil) +- +- -- Horizontal Tab +- push('\t', vt) +- cursor(0, 8, state) +- push(' ', vt) +- cursor(0, 11, state) +- push('\t', vt) +- cursor(0, 16, state) +- push(' ', vt) +- cursor(0, 23, state) +- push('\t', vt) +- cursor(0, 24, state) +- push(' ', vt) +- cursor(0, 32, state) +- push('\t', vt) +- cursor(0, 40, state) +- +- -- Cursor Horizontal Tab +- push('\x1b[I', vt) +- cursor(0, 48, state) +- push('\x1b[2I', vt) +- cursor(0, 64, state) +- +- -- Cursor Backward Tab +- push('\x1b[Z', vt) +- cursor(0, 56, state) +- push('\x1b[2Z', vt) +- cursor(0, 40, state) +- end) +- +- itp('12state_scroll', function() +- local vt = init() +- local state = wantstate(vt, { s = true }) +- +- -- Linefeed +- push(string.rep('\n', 24), vt) +- cursor(24, 0, state) +- push('\n', vt) +- expect('scrollrect 0..25,0..80 => +1,+0') +- cursor(24, 0, state) +- +- reset(state, nil) +- +- -- Index +- push('\x1b[25H', vt) +- push('\x1bD', vt) +- expect('scrollrect 0..25,0..80 => +1,+0') +- +- reset(state, nil) +- +- -- Reverse Index +- push('\x1bM', vt) +- expect('scrollrect 0..25,0..80 => -1,+0') +- +- reset(state, nil) +- +- -- Linefeed in DECSTBM +- push('\x1b[1;10r', vt) +- cursor(0, 0, state) +- push(string.rep('\n', 9), vt) +- cursor(9, 0, state) +- push('\n', vt) +- expect('scrollrect 0..10,0..80 => +1,+0') +- cursor(9, 0, state) +- +- -- Linefeed outside DECSTBM +- push('\x1b[20H', vt) +- cursor(19, 0, state) +- push('\n', vt) +- cursor(20, 0, state) +- +- -- Index in DECSTBM +- push('\x1b[9;10r', vt) +- push('\x1b[10H', vt) +- push('\x1bM', vt) +- cursor(8, 0, state) +- push('\x1bM', vt) +- expect('scrollrect 8..10,0..80 => -1,+0') +- +- -- Reverse Index in DECSTBM +- push('\x1b[25H', vt) +- cursor(24, 0, state) +- push('\n', vt) +- -- no scrollrect +- cursor(24, 0, state) +- +- -- Linefeed in DECSTBM+DECSLRM +- push('\x1b[?69h', vt) +- push('\x1b[3;10r\x1b[10;40s', vt) +- push('\x1b[10;10H\n', vt) +- expect('scrollrect 2..10,9..40 => +1,+0') +- +- -- IND/RI in DECSTBM+DECSLRM +- push('\x1bD', vt) +- expect('scrollrect 2..10,9..40 => +1,+0') +- push('\x1b[3;10H\x1bM', vt) +- expect('scrollrect 2..10,9..40 => -1,+0') +- +- -- DECRQSS on DECSTBM +- push('\x1bP$qr\x1b\\', vt) +- expect_output('\x1bP1$r3;10r\x1b\\') +- +- -- DECRQSS on DECSLRM +- push('\x1bP$qs\x1b\\', vt) +- expect_output('\x1bP1$r10;40s\x1b\\') +- +- -- Setting invalid DECSLRM with !DECVSSM is still rejected +- push('\x1b[?69l\x1b[;0s\x1b[?69h', vt) +- +- reset(state, nil) +- +- -- Scroll Down +- push('\x1b[S', vt) +- expect('scrollrect 0..25,0..80 => +1,+0') +- cursor(0, 0, state) +- push('\x1b[2S', vt) +- expect('scrollrect 0..25,0..80 => +2,+0') +- cursor(0, 0, state) +- push('\x1b[100S', vt) +- expect('scrollrect 0..25,0..80 => +25,+0') +- +- -- Scroll Up +- push('\x1b[T', vt) +- expect('scrollrect 0..25,0..80 => -1,+0') +- cursor(0, 0, state) +- push('\x1b[2T', vt) +- expect('scrollrect 0..25,0..80 => -2,+0') +- cursor(0, 0, state) +- push('\x1b[100T', vt) +- expect('scrollrect 0..25,0..80 => -25,+0') +- +- -- SD/SU in DECSTBM +- push('\x1b[5;20r', vt) +- push('\x1b[S', vt) +- expect('scrollrect 4..20,0..80 => +1,+0') +- push('\x1b[T', vt) +- expect('scrollrect 4..20,0..80 => -1,+0') +- +- reset(state, nil) +- +- -- SD/SU in DECSTBM+DECSLRM +- push('\x1b[?69h', vt) +- push('\x1b[3;10r\x1b[10;40s', vt) +- cursor(0, 0, state) +- push('\x1b[3;10H', vt) +- cursor(2, 9, state) +- push('\x1b[S', vt) +- expect('scrollrect 2..10,9..40 => +1,+0') +- push('\x1b[?69l', vt) +- push('\x1b[S', vt) +- expect('scrollrect 2..10,0..80 => +1,+0') +- +- -- Invalid boundaries +- reset(state, nil) +- +- push('\x1b[100;105r\x1bD', vt) +- push('\x1b[5;2r\x1bD', vt) +- +- reset(state, nil) +- state = wantstate(vt, { m = true, e = true }) +- +- -- Scroll Down move+erase emulation +- push('\x1b[S', vt) +- expect('moverect 1..25,0..80 -> 0..24,0..80\nerase 24..25,0..80') +- cursor(0, 0, state) +- push('\x1b[2S', vt) +- expect('moverect 2..25,0..80 -> 0..23,0..80\nerase 23..25,0..80') +- cursor(0, 0, state) +- +- -- Scroll Up move+erase emulation +- push('\x1b[T', vt) +- expect('moverect 0..24,0..80 -> 1..25,0..80\nerase 0..1,0..80') +- cursor(0, 0, state) +- push('\x1b[2T', vt) +- expect('moverect 0..23,0..80 -> 2..25,0..80\nerase 0..2,0..80') +- cursor(0, 0, state) +- +- -- DECSTBM resets cursor position +- push('\x1b[5;5H', vt) +- cursor(4, 4, state) +- push('\x1b[r', vt) +- cursor(0, 0, state) +- end) +- +- itp('13state_edit', function() +- local vt = init() +- local state = wantstate(vt, { s = true, e = true, b = true }) +- +- -- ICH +- reset(state, nil) +- expect('erase 0..25,0..80') +- cursor(0, 0, state) +- push('ACD', vt) +- push('\x1b[2D', vt) +- cursor(0, 1, state) +- push('\x1b[@', vt) +- expect('scrollrect 0..1,1..80 => +0,-1') +- cursor(0, 1, state) +- push('B', vt) +- cursor(0, 2, state) +- push('\x1b[3@', vt) +- expect('scrollrect 0..1,2..80 => +0,-3') +- +- -- ICH with DECSLRM +- push('\x1b[?69h', vt) +- push('\x1b[;50s', vt) +- push('\x1b[20G\x1b[@', vt) +- expect('scrollrect 0..1,19..50 => +0,-1') +- +- -- ICH outside DECSLRM +- push('\x1b[70G\x1b[@', vt) +- -- nothing happens +- +- -- DCH +- reset(state, nil) +- expect('erase 0..25,0..80') +- cursor(0, 0, state) +- push('ABBC', vt) +- push('\x1b[3D', vt) +- cursor(0, 1, state) +- push('\x1b[P', vt) +- expect('scrollrect 0..1,1..80 => +0,+1') +- cursor(0, 1, state) +- push('\x1b[3P', vt) +- expect('scrollrect 0..1,1..80 => +0,+3') +- cursor(0, 1, state) +- +- -- DCH with DECSLRM +- push('\x1b[?69h', vt) +- push('\x1b[;50s', vt) +- push('\x1b[20G\x1b[P', vt) +- expect('scrollrect 0..1,19..50 => +0,+1') +- +- -- DCH outside DECSLRM +- push('\x1b[70G\x1b[P', vt) +- -- nothing happens +- +- -- ECH +- reset(state, nil) +- expect('erase 0..25,0..80') +- cursor(0, 0, state) +- push('ABC', vt) +- push('\x1b[2D', vt) +- cursor(0, 1, state) +- push('\x1b[X', vt) +- expect('erase 0..1,1..2') +- cursor(0, 1, state) +- push('\x1b[3X', vt) +- expect('erase 0..1,1..4') +- cursor(0, 1, state) +- -- ECH more columns than there are should be bounded +- push('\x1b[100X', vt) +- expect('erase 0..1,1..80') +- +- -- IL +- reset(state, nil) +- expect('erase 0..25,0..80') +- cursor(0, 0, state) +- push('A\r\nC', vt) +- cursor(1, 1, state) +- push('\x1b[L', vt) +- expect('scrollrect 1..25,0..80 => -1,+0') +- -- TODO(libvterm): ECMA-48 says we should move to line home, but neither xterm nor xfce4-terminal do this +- cursor(1, 1, state) +- push('\rB', vt) +- cursor(1, 1, state) +- push('\x1b[3L', vt) +- expect('scrollrect 1..25,0..80 => -3,+0') +- +- -- IL with DECSTBM +- push('\x1b[5;15r', vt) +- push('\x1b[5H\x1b[L', vt) +- expect('scrollrect 4..15,0..80 => -1,+0') +- +- -- IL outside DECSTBM +- push('\x1b[20H\x1b[L', vt) +- -- nothing happens +- +- -- IL with DECSTBM+DECSLRM +- push('\x1b[?69h', vt) +- push('\x1b[10;50s', vt) +- push('\x1b[5;10H\x1b[L', vt) +- expect('scrollrect 4..15,9..50 => -1,+0') +- +- -- DL +- reset(state, nil) +- expect('erase 0..25,0..80') +- cursor(0, 0, state) +- push('A\r\nB\r\nB\r\nC', vt) +- cursor(3, 1, state) +- push('\x1b[2H', vt) +- cursor(1, 0, state) +- push('\x1b[M', vt) +- expect('scrollrect 1..25,0..80 => +1,+0') +- cursor(1, 0, state) +- push('\x1b[3M', vt) +- expect('scrollrect 1..25,0..80 => +3,+0') +- cursor(1, 0, state) +- +- -- DL with DECSTBM +- push('\x1b[5;15r', vt) +- push('\x1b[5H\x1b[M', vt) +- expect('scrollrect 4..15,0..80 => +1,+0') +- +- -- DL outside DECSTBM +- push('\x1b[20H\x1b[M', vt) +- -- nothing happens +- +- -- DL with DECSTBM+DECSLRM +- push('\x1b[?69h', vt) +- push('\x1b[10;50s', vt) +- push('\x1b[5;10H\x1b[M', vt) +- expect('scrollrect 4..15,9..50 => +1,+0') +- +- -- DECIC +- reset(state, nil) +- expect('erase 0..25,0..80') +- push("\x1b[20G\x1b[5'}", vt) +- expect('scrollrect 0..25,19..80 => +0,-5') +- +- -- DECIC with DECSTBM+DECSLRM +- push('\x1b[?69h', vt) +- push('\x1b[4;20r\x1b[20;60s', vt) +- push("\x1b[4;20H\x1b[3'}", vt) +- expect('scrollrect 3..20,19..60 => +0,-3') +- +- -- DECIC outside DECSLRM +- push("\x1b[70G\x1b['}", vt) +- -- nothing happens +- +- -- DECDC +- reset(state, nil) +- expect('erase 0..25,0..80') +- push("\x1b[20G\x1b[5'~", vt) +- expect('scrollrect 0..25,19..80 => +0,+5') +- +- -- DECDC with DECSTBM+DECSLRM +- push('\x1b[?69h', vt) +- push('\x1b[4;20r\x1b[20;60s', vt) +- push("\x1b[4;20H\x1b[3'~", vt) +- expect('scrollrect 3..20,19..60 => +0,+3') +- +- -- DECDC outside DECSLRM +- push("\x1b[70G\x1b['~", vt) +- -- nothing happens +- +- -- EL 0 +- reset(state, nil) +- expect('erase 0..25,0..80') +- cursor(0, 0, state) +- push('ABCDE', vt) +- push('\x1b[3D', vt) +- cursor(0, 2, state) +- push('\x1b[0K', vt) +- expect('erase 0..1,2..80') +- cursor(0, 2, state) +- +- -- EL 1 +- reset(state, nil) +- expect('erase 0..25,0..80') +- cursor(0, 0, state) +- push('ABCDE', vt) +- push('\x1b[3D', vt) +- cursor(0, 2, state) +- push('\x1b[1K', vt) +- expect('erase 0..1,0..3') +- cursor(0, 2, state) +- +- -- EL 2 +- reset(state, nil) +- expect('erase 0..25,0..80') +- cursor(0, 0, state) +- push('ABCDE', vt) +- push('\x1b[3D', vt) +- cursor(0, 2, state) +- push('\x1b[2K', vt) +- expect('erase 0..1,0..80') +- cursor(0, 2, state) +- +- -- SEL +- reset(state, nil) +- expect('erase 0..25,0..80') +- cursor(0, 0, state) +- push('\x1b[11G', vt) +- cursor(0, 10, state) +- push('\x1b[?0K', vt) +- expect('erase 0..1,10..80 selective') +- cursor(0, 10, state) +- push('\x1b[?1K', vt) +- expect('erase 0..1,0..11 selective') +- cursor(0, 10, state) +- push('\x1b[?2K', vt) +- expect('erase 0..1,0..80 selective') +- cursor(0, 10, state) +- +- -- ED 0 +- reset(state, nil) +- expect('erase 0..25,0..80') +- cursor(0, 0, state) +- push('\x1b[2;2H', vt) +- cursor(1, 1, state) +- push('\x1b[0J', vt) +- expect('erase 1..2,1..80\nerase 2..25,0..80') +- cursor(1, 1, state) +- +- -- ED 1 +- reset(state, nil) +- expect('erase 0..25,0..80') +- cursor(0, 0, state) +- push('\x1b[2;2H', vt) +- cursor(1, 1, state) +- push('\x1b[1J', vt) +- expect('erase 0..1,0..80\nerase 1..2,0..2') +- cursor(1, 1, state) +- +- -- ED 2 +- reset(state, nil) +- expect('erase 0..25,0..80') +- cursor(0, 0, state) +- push('\x1b[2;2H', vt) +- cursor(1, 1, state) +- push('\x1b[2J', vt) +- expect('erase 0..25,0..80') +- cursor(1, 1, state) +- +- -- ED 3 +- push('\x1b[3J', vt) +- expect('sb_clear') +- +- -- SED +- reset(state, nil) +- expect('erase 0..25,0..80') +- push('\x1b[5;5H', vt) +- cursor(4, 4, state) +- push('\x1b[?0J', vt) +- expect('erase 4..5,4..80 selective\nerase 5..25,0..80 selective') +- cursor(4, 4, state) +- push('\x1b[?1J', vt) +- expect('erase 0..4,0..80 selective\nerase 4..5,0..5 selective') +- cursor(4, 4, state) +- push('\x1b[?2J', vt) +- expect('erase 0..25,0..80 selective') +- cursor(4, 4, state) +- +- -- DECRQSS on DECSCA +- push('\x1b[2"q', vt) +- push('\x1bP$q"q\x1b\\', vt) +- expect_output('\x1bP1$r2"q\x1b\\') +- +- state = wantstate(vt, { m = true, e = true, b = true }) +- expect('erase 0..25,0..80') -- TODO(dundargoc): strange, this should not be needed according to the original code +- +- -- ICH move+erase emuation +- reset(state, nil) +- expect('erase 0..25,0..80') +- cursor(0, 0, state) +- push('ACD', vt) +- push('\x1b[2D', vt) +- cursor(0, 1, state) +- push('\x1b[@', vt) +- expect('moverect 0..1,1..79 -> 0..1,2..80\nerase 0..1,1..2') +- cursor(0, 1, state) +- push('B', vt) +- cursor(0, 2, state) +- push('\x1b[3@', vt) +- expect('moverect 0..1,2..77 -> 0..1,5..80\nerase 0..1,2..5') +- +- -- DCH move+erase emulation +- reset(state, nil) +- expect('erase 0..25,0..80') +- cursor(0, 0, state) +- push('ABBC', vt) +- push('\x1b[3D', vt) +- cursor(0, 1, state) +- push('\x1b[P', vt) +- expect('moverect 0..1,2..80 -> 0..1,1..79\nerase 0..1,79..80') +- cursor(0, 1, state) +- push('\x1b[3P', vt) +- expect('moverect 0..1,4..80 -> 0..1,1..77\nerase 0..1,77..80') +- cursor(0, 1, state) +- end) +- +- itp('14state_encoding', function() +- local vt = init() +- vterm.vterm_set_utf8(vt, false) +- local state = wantstate(vt, { g = true }) +- +- -- Default +- reset(state, nil) +- push('#', vt) +- expect('putglyph 23 1 0,0') +- +- -- Designate G0=DEC drawing +- reset(state, nil) +- push('\x1b(0', vt) +- push('a', vt) +- expect('putglyph 2592 1 0,0') +- +- -- Designate G1 + LS1 +- reset(state, nil) +- push('\x1b)0', vt) +- push('a', vt) +- expect('putglyph 61 1 0,0') +- push('\x0e', vt) +- push('a', vt) +- expect('putglyph 2592 1 0,1') +- -- LS0 +- push('\x0f', vt) +- push('a', vt) +- expect('putglyph 61 1 0,2') +- +- -- Designate G2 + LS2 +- push('\x1b*0', vt) +- push('a', vt) +- expect('putglyph 61 1 0,3') +- push('\x1bn', vt) +- push('a', vt) +- expect('putglyph 2592 1 0,4') +- push('\x0f', vt) +- push('a', vt) +- expect('putglyph 61 1 0,5') +- +- -- Designate G3 + LS3 +- push('\x1b+0', vt) +- push('a', vt) +- expect('putglyph 61 1 0,6') +- push('\x1bo', vt) +- push('a', vt) +- expect('putglyph 2592 1 0,7') +- push('\x0f', vt) +- push('a', vt) +- expect('putglyph 61 1 0,8') +- +- -- SS2 +- push('a\x8eaa', vt) +- expect('putglyph 61 1 0,9\nputglyph 2592 1 0,10\nputglyph 61 1 0,11') +- +- -- SS3 +- push('a\x8faa', vt) +- expect('putglyph 61 1 0,12\nputglyph 2592 1 0,13\nputglyph 61 1 0,14') +- +- -- LS1R +- reset(state, nil) +- push('\x1b~', vt) +- push('\xe1', vt) +- expect('putglyph 61 1 0,0') +- push('\x1b)0', vt) +- push('\xe1', vt) +- expect('putglyph 2592 1 0,1') +- +- -- LS2R +- reset(state, nil) +- push('\x1b}', vt) +- push('\xe1', vt) +- expect('putglyph 61 1 0,0') +- push('\x1b*0', vt) +- push('\xe1', vt) +- expect('putglyph 2592 1 0,1') +- +- -- LS3R +- reset(state, nil) +- push('\x1b|', vt) +- push('\xe1', vt) +- expect('putglyph 61 1 0,0') +- push('\x1b+0', vt) +- push('\xe1', vt) +- expect('putglyph 2592 1 0,1') +- +- vterm.vterm_set_utf8(vt, true) +- +- -- Mixed US-ASCII and UTF-8 +- -- U+0108 == c4 88 +- reset(state, nil) +- push('\x1b(B', vt) +- push('AB\xc4\x88D', vt) +- expect('putglyph 41 1 0,0\nputglyph 42 1 0,1\nputglyph 108 1 0,2\nputglyph 44 1 0,3') +- +- -- Split UTF-8 after US-ASCII +- reset(state, nil) +- push('AB\xc4', vt) +- expect('putglyph 41 1 0,0\nputglyph 42 1 0,1') +- push('\x88D', vt) +- expect('putglyph 108 1 0,2\nputglyph 44 1 0,3') +- end) +- +- itp('15state_mode', function() +- local vt = init() +- local state = wantstate(vt, { g = true, m = true, e = true }) +- +- -- Insert/Replace Mode +- reset(state, nil) +- expect('erase 0..25,0..80') +- cursor(0, 0, state) +- push('AC\x1b[DB', vt) +- expect('putglyph 41 1 0,0\nputglyph 43 1 0,1\nputglyph 42 1 0,1') +- push('\x1b[4h', vt) +- push('\x1b[G', vt) +- push('AC\x1b[DB', vt) +- expect( +- 'moverect 0..1,0..79 -> 0..1,1..80\nerase 0..1,0..1\nputglyph 41 1 0,0\nmoverect 0..1,1..79 -> 0..1,2..80\nerase 0..1,1..2\nputglyph 43 1 0,1\nmoverect 0..1,1..79 -> 0..1,2..80\nerase 0..1,1..2\nputglyph 42 1 0,1' +- ) +- +- -- Insert mode only happens once for UTF-8 combining +- push('e', vt) +- expect('moverect 0..1,2..79 -> 0..1,3..80\nerase 0..1,2..3\nputglyph 65 1 0,2') +- push('\xCC\x81', vt) +- expect('putglyph 65,301 1 0,2') +- +- -- Newline/Linefeed mode +- reset(state, nil) +- expect('erase 0..25,0..80') +- cursor(0, 0, state) +- push('\x1b[5G\n', vt) +- cursor(1, 4, state) +- push('\x1b[20h', vt) +- push('\x1b[5G\n', vt) +- cursor(2, 0, state) +- +- -- DEC origin mode +- reset(state, nil) +- expect('erase 0..25,0..80') +- cursor(0, 0, state) +- push('\x1b[5;15r', vt) +- push('\x1b[H', vt) +- cursor(0, 0, state) +- push('\x1b[3;3H', vt) +- cursor(2, 2, state) +- push('\x1b[?6h', vt) +- push('\x1b[H', vt) +- cursor(4, 0, state) +- push('\x1b[3;3H', vt) +- cursor(6, 2, state) +- +- -- DECRQM on DECOM +- push('\x1b[?6h', vt) +- push('\x1b[?6$p', vt) +- expect_output('\x1b[?6;1$y') +- push('\x1b[?6l', vt) +- push('\x1b[?6$p', vt) +- expect_output('\x1b[?6;2$y') +- +- -- Origin mode with DECSLRM +- push('\x1b[?6h', vt) +- push('\x1b[?69h', vt) +- push('\x1b[20;60s', vt) +- push('\x1b[H', vt) +- cursor(4, 19, state) +- +- push('\x1b[?69l', vt) +- +- -- Origin mode bounds cursor to scrolling region +- push('\x1b[H', vt) +- push('\x1b[10A', vt) +- cursor(4, 0, state) +- push('\x1b[20B', vt) +- cursor(14, 0, state) +- +- -- Origin mode without scroll region +- push('\x1b[?6l', vt) +- push('\x1b[r\x1b[?6h', vt) +- cursor(0, 0, state) +- end) +- +- itp('16state_resize', function() +- local vt = init() +- local state = wantstate(vt, { g = true }) +- +- -- Placement +- reset(state, nil) +- push('AB\x1b[79GCDE', vt) +- expect( +- 'putglyph 41 1 0,0\nputglyph 42 1 0,1\nputglyph 43 1 0,78\nputglyph 44 1 0,79\nputglyph 45 1 1,0' +- ) +- +- -- Resize +- reset(state, nil) +- resize(27, 85, vt) +- push('AB\x1b[79GCDE', vt) +- expect( +- 'putglyph 41 1 0,0\nputglyph 42 1 0,1\nputglyph 43 1 0,78\nputglyph 44 1 0,79\nputglyph 45 1 0,80' +- ) +- cursor(0, 81, state) +- +- -- Resize without reset +- resize(28, 90, vt) +- cursor(0, 81, state) +- push('FGHI', vt) +- expect('putglyph 46 1 0,81\nputglyph 47 1 0,82\nputglyph 48 1 0,83\nputglyph 49 1 0,84') +- cursor(0, 85, state) +- +- -- Resize shrink moves cursor +- resize(25, 80, vt) +- cursor(0, 79, state) +- +- -- Resize grow doesn't cancel phantom +- reset(state, nil) +- push('\x1b[79GAB', vt) +- expect('putglyph 41 1 0,78\nputglyph 42 1 0,79') +- cursor(0, 79, state) +- resize(30, 100, vt) +- cursor(0, 80, state) +- push('C', vt) +- expect('putglyph 43 1 0,80') +- cursor(0, 81, state) +- end) +- +- itp('17state_mouse', function() +- local vt = init() +- local state = wantstate(vt, { p = true }) +- +- -- DECRQM on with mouse off +- push('\x1b[?1000$p', vt) +- expect_output('\x1b[?1000;2$y') +- push('\x1b[?1002$p', vt) +- expect_output('\x1b[?1002;2$y') +- push('\x1b[?1003$p', vt) +- expect_output('\x1b[?1003;2$y') +- +- -- Mouse in simple button report mode +- reset(state, nil) +- expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') +- push('\x1b[?1000h', vt) +- expect('settermprop 8 1') +- +- -- Press 1 +- mousemove(0, 0, vt) +- mousebtn('d', 1, vt) +- expect_output('\x1b[M\x20\x21\x21') +- +- -- Release 1 +- mousebtn('u', 1, vt) +- expect_output('\x1b[M\x23\x21\x21') +- +- -- Ctrl-Press 1 +- mousebtn('d', 1, vt, { C = true }) +- expect_output('\x1b[M\x30\x21\x21') +- mousebtn('u', 1, vt, { C = true }) +- expect_output('\x1b[M\x33\x21\x21') +- +- -- Button 2 +- mousebtn('d', 2, vt) +- expect_output('\x1b[M\x21\x21\x21') +- mousebtn('u', 2, vt) +- expect_output('\x1b[M\x23\x21\x21') +- +- -- Position +- mousemove(10, 20, vt) +- mousebtn('d', 1, vt) +- expect_output('\x1b[M\x20\x35\x2b') +- +- mousebtn('u', 1, vt) +- expect_output('\x1b[M\x23\x35\x2b') +- mousemove(10, 21, vt) +- -- no output +- +- -- Wheel events +- mousebtn('d', 4, vt) +- expect_output('\x1b[M\x60\x36\x2b') +- mousebtn('d', 4, vt) +- expect_output('\x1b[M\x60\x36\x2b') +- mousebtn('d', 5, vt) +- expect_output('\x1b[M\x61\x36\x2b') +- mousebtn('d', 6, vt) +- expect_output('\x1b[M\x62\x36\x2b') +- mousebtn('d', 7, vt) +- expect_output('\x1b[M\x63\x36\x2b') +- +- -- DECRQM on mouse button mode +- push('\x1b[?1000$p', vt) +- expect_output('\x1b[?1000;1$y') +- push('\x1b[?1002$p', vt) +- expect_output('\x1b[?1002;2$y') +- push('\x1b[?1003$p', vt) +- expect_output('\x1b[?1003;2$y') +- +- -- Drag events +- reset(state, nil) +- expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') +- push('\x1b[?1002h', vt) +- expect('settermprop 8 2') +- +- mousemove(5, 5, vt) +- mousebtn('d', 1, vt) +- expect_output('\x1b[M\x20\x26\x26') +- mousemove(5, 6, vt) +- expect_output('\x1b[M\x40\x27\x26') +- mousemove(6, 6, vt) +- expect_output('\x1b[M\x40\x27\x27') +- mousemove(6, 6, vt) +- -- no output +- mousebtn('u', 1, vt) +- expect_output('\x1b[M\x23\x27\x27') +- mousemove(6, 7, vt) +- -- no output +- +- -- DECRQM on mouse drag mode +- push('\x1b[?1000$p', vt) +- expect_output('\x1b[?1000;2$y') +- push('\x1b[?1002$p', vt) +- expect_output('\x1b[?1002;1$y') +- push('\x1b[?1003$p', vt) +- expect_output('\x1b[?1003;2$y') +- +- -- Non-drag motion events +- push('\x1b[?1003h', vt) +- expect('settermprop 8 3') +- +- mousemove(6, 8, vt) +- expect_output('\x1b[M\x43\x29\x27') +- +- -- DECRQM on mouse motion mode +- push('\x1b[?1000$p', vt) +- expect_output('\x1b[?1000;2$y') +- push('\x1b[?1002$p', vt) +- expect_output('\x1b[?1002;2$y') +- push('\x1b[?1003$p', vt) +- expect_output('\x1b[?1003;1$y') +- +- -- Bounds checking +- mousemove(300, 300, vt) +- expect_output('\x1b[M\x43\xff\xff') +- mousebtn('d', 1, vt) +- expect_output('\x1b[M\x20\xff\xff') +- mousebtn('u', 1, vt) +- expect_output('\x1b[M\x23\xff\xff') +- +- -- DECRQM on standard encoding mode +- push('\x1b[?1005$p', vt) +- expect_output('\x1b[?1005;2$y') +- push('\x1b[?1006$p', vt) +- expect_output('\x1b[?1006;2$y') +- push('\x1b[?1015$p', vt) +- expect_output('\x1b[?1015;2$y') +- +- -- UTF-8 extended encoding mode +- -- 300 + 32 + 1 = 333 = U+014d = \xc5\x8d +- push('\x1b[?1005h', vt) +- mousebtn('d', 1, vt) +- expect_output('\x1b[M\x20\xc5\x8d\xc5\x8d') +- mousebtn('u', 1, vt) +- expect_output('\x1b[M\x23\xc5\x8d\xc5\x8d') +- +- -- DECRQM on UTF-8 extended encoding mode +- push('\x1b[?1005$p', vt) +- expect_output('\x1b[?1005;1$y') +- push('\x1b[?1006$p', vt) +- expect_output('\x1b[?1006;2$y') +- push('\x1b[?1015$p', vt) +- expect_output('\x1b[?1015;2$y') +- +- -- SGR extended encoding mode +- push('\x1b[?1006h', vt) +- mousebtn('d', 1, vt) +- expect_output('\x1b[<0;301;301M') +- mousebtn('u', 1, vt) +- expect_output('\x1b[<0;301;301m') +- +- -- Button 8 on SGR extended encoding mode +- mousebtn('d', 8, vt) +- expect_output('\x1b[<128;301;301M') +- mousebtn('u', 8, vt) +- expect_output('\x1b[<128;301;301m') +- +- -- Button 9 on SGR extended encoding mode +- mousebtn('d', 9, vt) +- expect_output('\x1b[<129;301;301M') +- mousebtn('u', 9, vt) +- expect_output('\x1b[<129;301;301m') +- +- -- DECRQM on SGR extended encoding mode +- push('\x1b[?1005$p', vt) +- expect_output('\x1b[?1005;2$y') +- push('\x1b[?1006$p', vt) +- expect_output('\x1b[?1006;1$y') +- push('\x1b[?1015$p', vt) +- expect_output('\x1b[?1015;2$y') +- +- -- rxvt extended encoding mode +- push('\x1b[?1015h', vt) +- mousebtn('d', 1, vt) +- expect_output('\x1b[0;301;301M') +- mousebtn('u', 1, vt) +- expect_output('\x1b[3;301;301M') +- +- -- Button 8 on rxvt extended encoding mode +- mousebtn('d', 8, vt) +- expect_output('\x1b[128;301;301M') +- mousebtn('u', 8, vt) +- expect_output('\x1b[3;301;301M') +- +- -- Button 9 on rxvt extended encoding mode +- mousebtn('d', 9, vt) +- expect_output('\x1b[129;301;301M') +- mousebtn('u', 9, vt) +- expect_output('\x1b[3;301;301M') +- +- -- DECRQM on rxvt extended encoding mode +- push('\x1b[?1005$p', vt) +- expect_output('\x1b[?1005;2$y') +- push('\x1b[?1006$p', vt) +- expect_output('\x1b[?1006;2$y') +- push('\x1b[?1015$p', vt) +- expect_output('\x1b[?1015;1$y') +- +- -- Mouse disabled reports nothing +- reset(state, nil) +- expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') +- mousemove(0, 0, vt) +- mousebtn('d', 1, vt) +- mousebtn('u', 1, vt) +- +- -- DECSM can set multiple modes at once +- push('\x1b[?1002;1006h', vt) +- expect('settermprop 8 2') +- mousebtn('d', 1, vt) +- expect_output('\x1b[<0;1;1M') +- end) +- +- itp('18state_termprops', function() +- local vt = init() +- local state = wantstate(vt, { p = true }) +- +- reset(state, nil) +- expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') +- +- -- Cursor visibility +- push('\x1b[?25h', vt) +- expect('settermprop 1 true') +- push('\x1b[?25$p', vt) +- expect_output('\x1b[?25;1$y') +- push('\x1b[?25l', vt) +- expect('settermprop 1 false') +- push('\x1b[?25$p', vt) +- expect_output('\x1b[?25;2$y') +- +- -- Cursor blink +- push('\x1b[?12h', vt) +- expect('settermprop 2 true') +- push('\x1b[?12$p', vt) +- expect_output('\x1b[?12;1$y') +- push('\x1b[?12l', vt) +- expect('settermprop 2 false') +- push('\x1b[?12$p', vt) +- expect_output('\x1b[?12;2$y') +- +- -- Cursor shape +- push('\x1b[3 q', vt) +- expect('settermprop 2 true\nsettermprop 7 2') +- +- -- Title +- push('\x1b]2;Here is my title\a', vt) +- expect('settermprop 4 ["Here is my title"]') +- +- -- Title split write +- push('\x1b]2;Here is', vt) +- expect('settermprop 4 ["Here is"') +- push(' another title\a', vt) +- expect('settermprop 4 " another title"]') +- end) +- +- itp('20state_wrapping', function() +- local vt = init() +- local state = wantstate(vt, { g = true, m = true }) +- +- -- 79th Column +- push('\x1b[75G', vt) +- push(string.rep('A', 5), vt) +- expect( +- 'putglyph 41 1 0,74\nputglyph 41 1 0,75\nputglyph 41 1 0,76\nputglyph 41 1 0,77\nputglyph 41 1 0,78' +- ) +- cursor(0, 79, state) +- +- -- 80th Column Phantom +- push('A', vt) +- expect('putglyph 41 1 0,79') +- cursor(0, 79, state) +- +- -- Line Wraparound +- push('B', vt) +- expect('putglyph 42 1 1,0') +- cursor(1, 1, state) +- +- -- Line Wraparound during combined write +- push('\x1b[78G', vt) +- push('BBBCC', vt) +- expect( +- 'putglyph 42 1 1,77\nputglyph 42 1 1,78\nputglyph 42 1 1,79\nputglyph 43 1 2,0\nputglyph 43 1 2,1' +- ) +- cursor(2, 2, state) +- +- -- DEC Auto Wrap Mode +- reset(state, nil) +- push('\x1b[?7l', vt) +- push('\x1b[75G', vt) +- push(string.rep('D', 6), vt) +- expect( +- 'putglyph 44 1 0,74\nputglyph 44 1 0,75\nputglyph 44 1 0,76\nputglyph 44 1 0,77\nputglyph 44 1 0,78\nputglyph 44 1 0,79' +- ) +- cursor(0, 79, state) +- push('D', vt) +- expect('putglyph 44 1 0,79') +- cursor(0, 79, state) +- push('\x1b[?7h', vt) +- +- -- 80th column causes linefeed on wraparound +- push('\x1b[25;78HABC', vt) +- expect('putglyph 41 1 24,77\nputglyph 42 1 24,78\nputglyph 43 1 24,79') +- cursor(24, 79, state) +- push('D', vt) +- expect('moverect 1..25,0..80 -> 0..24,0..80\nputglyph 44 1 24,0') +- +- -- 80th column phantom linefeed phantom cancelled by explicit cursor move +- push('\x1b[25;78HABC', vt) +- expect('putglyph 41 1 24,77\nputglyph 42 1 24,78\nputglyph 43 1 24,79') +- cursor(24, 79, state) +- push('\x1b[25;1HD', vt) +- expect('putglyph 44 1 24,0') +- end) +- +- itp('21state_tabstops', function() +- local vt = init() +- local state = wantstate(vt, { g = true }) +- +- -- Initial +- reset(state, nil) +- push('\tX', vt) +- expect('putglyph 58 1 0,8') +- push('\tX', vt) +- expect('putglyph 58 1 0,16') +- cursor(0, 17, state) +- +- -- HTS +- push('\x1b[5G\x1bH', vt) +- push('\x1b[G\tX', vt) +- expect('putglyph 58 1 0,4') +- cursor(0, 5, state) +- +- -- TBC 0 +- push('\x1b[9G\x1b[g', vt) +- push('\x1b[G\tX\tX', vt) +- expect('putglyph 58 1 0,4\nputglyph 58 1 0,16') +- cursor(0, 17, state) +- +- -- TBC 3 +- push('\x1b[3g\x1b[50G\x1bH\x1b[G', vt) +- cursor(0, 0, state) +- push('\tX', vt) +- expect('putglyph 58 1 0,49') +- cursor(0, 50, state) +- +- -- Tabstops after resize +- reset(state, nil) +- resize(30, 100, vt) +- -- Should be 100/8 = 12 tabstops +- push('\tX', vt) +- expect('putglyph 58 1 0,8') +- push('\tX', vt) +- expect('putglyph 58 1 0,16') +- push('\tX', vt) +- expect('putglyph 58 1 0,24') +- push('\tX', vt) +- expect('putglyph 58 1 0,32') +- push('\tX', vt) +- expect('putglyph 58 1 0,40') +- push('\tX', vt) +- expect('putglyph 58 1 0,48') +- push('\tX', vt) +- expect('putglyph 58 1 0,56') +- push('\tX', vt) +- expect('putglyph 58 1 0,64') +- push('\tX', vt) +- expect('putglyph 58 1 0,72') +- push('\tX', vt) +- expect('putglyph 58 1 0,80') +- push('\tX', vt) +- expect('putglyph 58 1 0,88') +- push('\tX', vt) +- expect('putglyph 58 1 0,96') +- cursor(0, 97, state) +- end) +- +- itp('22state_save', function() +- local vt = init() +- local state = wantstate(vt, { p = true }) +- +- reset(state, nil) +- expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') +- +- -- Set up state +- push('\x1b[2;2H', vt) +- cursor(1, 1, state) +- push('\x1b[1m', vt) +- pen('bold', true, state) +- +- -- Save +- push('\x1b[?1048h', vt) +- +- -- Change state +- push('\x1b[5;5H', vt) +- cursor(4, 4, state) +- push('\x1b[4 q', vt) +- expect('settermprop 2 false\nsettermprop 7 2') +- push('\x1b[22;4m', vt) +- pen('bold', false, state) +- pen('underline', 1, state) +- +- -- Restore +- push('\x1b[?1048l', vt) +- expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') +- cursor(1, 1, state) +- pen('bold', true, state) +- pen('underline', 0, state) +- +- -- Save/restore using DECSC/DECRC +- push('\x1b[2;2H\x1b7', vt) +- cursor(1, 1, state) +- +- push('\x1b[5;5H', vt) +- cursor(4, 4, state) +- push('\x1b8', vt) +- expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') +- cursor(1, 1, state) +- +- -- Save twice, restore twice happens on both edge transitions +- push('\x1b[2;10H\x1b[?1048h\x1b[6;10H\x1b[?1048h', vt) +- push('\x1b[H', vt) +- cursor(0, 0, state) +- push('\x1b[?1048l', vt) +- expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') +- cursor(5, 9, state) +- push('\x1b[H', vt) +- cursor(0, 0, state) +- push('\x1b[?1048l', vt) +- expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') +- cursor(5, 9, state) +- end) +- +- itp('25state_input', function() +- local vt = init() +- local state = wantstate(vt) +- +- -- Disambiguate escape codes enabled +- push('\x1b[>1u', vt) +- +- -- Unmodified ASCII +- inchar(0x41, vt) +- expect_output('A') +- inchar(0x61, vt) +- expect_output('a') +- +- -- Ctrl modifier on ASCII letters +- inchar(0x41, vt, { C = true }) +- expect_output('\x1b[97;6u') +- inchar(0x61, vt, { C = true }) +- expect_output('\x1b[97;5u') +- +- -- Alt modifier on ASCII letters +- inchar(0x41, vt, { A = true }) +- expect_output('\x1b[97;4u') +- inchar(0x61, vt, { A = true }) +- expect_output('\x1b[97;3u') +- +- -- Ctrl-Alt modifier on ASCII letters +- inchar(0x41, vt, { C = true, A = true }) +- expect_output('\x1b[97;8u') +- inchar(0x61, vt, { C = true, A = true }) +- expect_output('\x1b[97;7u') +- +- -- Ctrl-I is disambiguated +- inchar(0x49, vt) +- expect_output('I') +- inchar(0x69, vt) +- expect_output('i') +- inchar(0x49, vt, { C = true }) +- expect_output('\x1b[105;6u') +- inchar(0x69, vt, { C = true }) +- expect_output('\x1b[105;5u') +- inchar(0x49, vt, { A = true }) +- expect_output('\x1b[105;4u') +- inchar(0x69, vt, { A = true }) +- expect_output('\x1b[105;3u') +- inchar(0x49, vt, { A = true, C = true }) +- expect_output('\x1b[105;8u') +- inchar(0x69, vt, { A = true, C = true }) +- expect_output('\x1b[105;7u') +- +- -- Ctrl+Digits +- for i = 0, 9 do +- local c = 0x30 + i +- inchar(c, vt) +- expect_output(tostring(i)) +- inchar(c, vt, { C = true }) +- expect_output(string.format('\x1b[%d;5u', c)) +- inchar(c, vt, { C = true, S = true }) +- expect_output(string.format('\x1b[%d;6u', c)) +- inchar(c, vt, { C = true, A = true }) +- expect_output(string.format('\x1b[%d;7u', c)) +- inchar(c, vt, { C = true, A = true, S = true }) +- expect_output(string.format('\x1b[%d;8u', c)) +- end +- +- -- Special handling of Space +- inchar(0x20, vt) +- expect_output(' ') +- inchar(0x20, vt, { S = true }) +- expect_output('\x1b[32;2u') +- inchar(0x20, vt, { C = true }) +- expect_output('\x1b[32;5u') +- inchar(0x20, vt, { C = true, S = true }) +- expect_output('\x1b[32;6u') +- inchar(0x20, vt, { A = true }) +- expect_output('\x1b[32;3u') +- inchar(0x20, vt, { S = true, A = true }) +- expect_output('\x1b[32;4u') +- inchar(0x20, vt, { C = true, A = true }) +- expect_output('\x1b[32;7u') +- inchar(0x20, vt, { S = true, C = true, A = true }) +- expect_output('\x1b[32;8u') +- +- -- Cursor keys in reset (cursor) mode +- inkey('up', vt) +- expect_output('\x1b[A') +- inkey('up', vt, { S = true }) +- expect_output('\x1b[1;2A') +- inkey('up', vt, { C = true }) +- expect_output('\x1b[1;5A') +- inkey('up', vt, { S = true, C = true }) +- expect_output('\x1b[1;6A') +- inkey('up', vt, { A = true }) +- expect_output('\x1b[1;3A') +- inkey('up', vt, { S = true, A = true }) +- expect_output('\x1b[1;4A') +- inkey('up', vt, { C = true, A = true }) +- expect_output('\x1b[1;7A') +- inkey('up', vt, { S = true, C = true, A = true }) +- expect_output('\x1b[1;8A') +- +- -- Cursor keys in application mode +- push('\x1b[?1h', vt) +- -- Plain "Up" should be SS3 A now +- inkey('up', vt) +- expect_output('\x1bOA') +- -- Modified keys should still use CSI +- inkey('up', vt, { S = true }) +- expect_output('\x1b[1;2A') +- inkey('up', vt, { C = true }) +- expect_output('\x1b[1;5A') +- +- -- Tab +- inkey('tab', vt) +- expect_output('\x09') +- inkey('tab', vt, { S = true }) +- expect_output('\x1b[9;2u') +- inkey('tab', vt, { C = true }) +- expect_output('\x1b[9;5u') +- inkey('tab', vt, { A = true }) +- expect_output('\x1b[9;3u') +- inkey('tab', vt, { C = true, A = true }) +- expect_output('\x1b[9;7u') +- +- -- Backspace +- inkey('bs', vt) +- expect_output('\x7f') +- inkey('bs', vt, { S = true }) +- expect_output('\x1b[127;2u') +- inkey('bs', vt, { C = true }) +- expect_output('\x1b[127;5u') +- inkey('bs', vt, { A = true }) +- expect_output('\x1b[127;3u') +- inkey('bs', vt, { C = true, A = true }) +- expect_output('\x1b[127;7u') +- +- -- DEL +- inkey('del', vt) +- expect_output('\x1b[3~') +- inkey('del', vt, { S = true }) +- expect_output('\x1b[3;2~') +- inkey('del', vt, { C = true }) +- expect_output('\x1b[3;5~') +- inkey('del', vt, { A = true }) +- expect_output('\x1b[3;3~') +- inkey('del', vt, { C = true, A = true }) +- expect_output('\x1b[3;7~') +- +- -- ESC +- inkey('esc', vt) +- expect_output('\x1b[27;1u') +- inkey('esc', vt, { S = true }) +- expect_output('\x1b[27;2u') +- inkey('esc', vt, { C = true }) +- expect_output('\x1b[27;5u') +- inkey('esc', vt, { A = true }) +- expect_output('\x1b[27;3u') +- inkey('esc', vt, { C = true, A = true }) +- expect_output('\x1b[27;7u') +- +- -- Enter in linefeed mode +- inkey('enter', vt) +- expect_output('\x0d') +- inkey('enter', vt, { S = true }) +- expect_output('\x1b[13;2u') +- inkey('enter', vt, { C = true }) +- expect_output('\x1b[13;5u') +- inkey('enter', vt, { A = true }) +- expect_output('\x1b[13;3u') +- inkey('enter', vt, { C = true, A = true }) +- expect_output('\x1b[13;7u') +- +- -- Enter in newline mode +- push('\x1b[20h', vt) +- inkey('enter', vt) +- expect_output('\x0d\x0a') +- +- -- Unmodified F1 is SS3 P +- inkey('f1', vt) +- expect_output('\x1bOP') +- +- -- Modified F1 is CSI P +- inkey('f1', vt, { S = true }) +- expect_output('\x1b[1;2P') +- inkey('f1', vt, { A = true }) +- expect_output('\x1b[1;3P') +- inkey('f1', vt, { C = true }) +- expect_output('\x1b[1;5P') +- +- -- Keypad in DECKPNM +- inkey('kp0', vt) +- expect_output('\x1b[57399;1u') +- +- -- Keypad in DECKPAM +- push('\x1b=', vt) +- inkey('kp0', vt) +- expect_output('\x1bOp') +- +- -- Bracketed paste mode off +- vterm.vterm_keyboard_start_paste(vt) +- vterm.vterm_keyboard_end_paste(vt) +- +- -- Bracketed paste mode on +- push('\x1b[?2004h', vt) +- vterm.vterm_keyboard_start_paste(vt) +- expect_output('\x1b[200~') +- vterm.vterm_keyboard_end_paste(vt) +- expect_output('\x1b[201~') +- +- -- Focus reporting disabled +- vterm.vterm_state_focus_in(state) +- vterm.vterm_state_focus_out(state) +- +- -- Focus reporting enabled +- state = wantstate(vt, { p = true }) +- push('\x1b[?1004h', vt) +- expect('settermprop 9 true') +- vterm.vterm_state_focus_in(state) +- expect_output('\x1b[I') +- vterm.vterm_state_focus_out(state) +- expect_output('\x1b[O') +- +- -- Synchronized output mode 2026 +- push('\x1b[?2026h', vt) +- expect('settermprop 11 true') +- push('\x1b[?2026l', vt) +- expect('settermprop 11 false') +- +- -- DECRQM on synchronized output +- push('\x1b[?2026h', vt) +- expect('settermprop 11 true') +- push('\x1b[?2026$p', vt) +- expect_output('\x1b[?2026;1$y') +- push('\x1b[?2026l', vt) +- expect('settermprop 11 false') +- push('\x1b[?2026$p', vt) +- expect_output('\x1b[?2026;2$y') +- +- -- Disambiguate escape codes disabled +- push('\x1b[q', vt) +- expect_output('\x1bP>|libvterm(0.3)\x1b\\') +- +- -- DSR +- reset(state, nil) +- push('\x1b[5n', vt) +- expect_output('\x1b[0n') +- +- -- CPR +- push('\x1b[6n', vt) +- expect_output('\x1b[1;1R') +- push('\x1b[10;10H\x1b[6n', vt) +- expect_output('\x1b[10;10R') +- +- -- DECCPR +- push('\x1b[?6n', vt) +- expect_output('\x1b[?10;10R') +- +- -- DECRQSS on DECSCUSR +- push('\x1b[3 q', vt) +- push('\x1bP$q q\x1b\\', vt) +- expect_output('\x1bP1$r3 q\x1b\\') +- +- -- DECRQSS on SGR +- push('\x1b[1;5;7m', vt) +- push('\x1bP$qm\x1b\\', vt) +- expect_output('\x1bP1$r1;5;7m\x1b\\') +- +- -- DECRQSS on SGR ANSI colours +- push('\x1b[0;31;42m', vt) +- push('\x1bP$qm\x1b\\', vt) +- expect_output('\x1bP1$r31;42m\x1b\\') +- +- -- DECRQSS on SGR ANSI hi-bright colours +- push('\x1b[0;93;104m', vt) +- push('\x1bP$qm\x1b\\', vt) +- expect_output('\x1bP1$r93;104m\x1b\\') +- +- -- DECRQSS on SGR 256-palette colours +- push('\x1b[0;38:5:56;48:5:78m', vt) +- push('\x1bP$qm\x1b\\', vt) +- expect_output('\x1bP1$r38:5:56;48:5:78m\x1b\\') +- +- -- DECRQSS on SGR RGB8 colours +- push('\x1b[0;38:2:24:68:112;48:2:13:57:101m', vt) +- push('\x1bP$qm\x1b\\', vt) +- expect_output('\x1bP1$r38:2:24:68:112;48:2:13:57:101m\x1b\\') +- +- -- S8C1T on DSR +- push('\x1b G', vt) +- push('\x1b[5n', vt) +- expect_output('\x9b0n') +- push('\x1b F', vt) +- end) +- +- itp('27state_reset', function() +- local vt = init() +- local state = wantstate(vt) +- +- reset(state, nil) +- +- -- RIS homes cursor +- push('\x1b[5;5H', vt) +- cursor(4, 4, state) +- state = wantstate(vt, { m = true }) +- push('\x1bc', vt) +- cursor(0, 0, state) +- wantstate(vt) +- +- -- RIS cancels scrolling region +- push('\x1b[5;10r', vt) +- wantstate(vt, { s = true }) +- push('\x1bc\x1b[25H\n', vt) +- expect('scrollrect 0..25,0..80 => +1,+0') +- wantstate(vt) +- +- -- RIS erases screen +- push('ABCDE', vt) +- state = wantstate(vt, { e = true }) +- push('\x1bc', vt) +- expect('erase 0..25,0..80') +- wantstate(vt) +- +- -- RIS clears tabstops +- push('\x1b[5G\x1bH\x1b[G\t', vt) +- cursor(0, 4, state) +- push('\x1bc\t', vt) +- cursor(0, 8, state) +- end) +- +- itp('28state_dbl_wh', function() +- local vt = init() +- local state = wantstate(vt, { g = true }) +- +- -- Single Width, Single Height +- reset(state, nil) +- push('\x1b#5', vt) +- push('Hello', vt) +- expect( +- 'putglyph 48 1 0,0\nputglyph 65 1 0,1\nputglyph 6c 1 0,2\nputglyph 6c 1 0,3\nputglyph 6f 1 0,4' +- ) +- +- -- Double Width, Single Height +- reset(state, nil) +- push('\x1b#6', vt) +- push('Hello', vt) +- expect( +- 'putglyph 48 1 0,0 dwl\nputglyph 65 1 0,1 dwl\nputglyph 6c 1 0,2 dwl\nputglyph 6c 1 0,3 dwl\nputglyph 6f 1 0,4 dwl' +- ) +- cursor(0, 5, state) +- push('\x1b[40GAB', vt) +- expect('putglyph 41 1 0,39 dwl\nputglyph 42 1 1,0') +- cursor(1, 1, state) +- +- -- Double Height +- reset(state, nil) +- push('\x1b#3', vt) +- push('Hello', vt) +- expect( +- 'putglyph 48 1 0,0 dwl dhl-top\nputglyph 65 1 0,1 dwl dhl-top\nputglyph 6c 1 0,2 dwl dhl-top\nputglyph 6c 1 0,3 dwl dhl-top\nputglyph 6f 1 0,4 dwl dhl-top' +- ) +- cursor(0, 5, state) +- push('\r\n\x1b#4', vt) +- push('Hello', vt) +- expect( +- 'putglyph 48 1 1,0 dwl dhl-bottom\nputglyph 65 1 1,1 dwl dhl-bottom\nputglyph 6c 1 1,2 dwl dhl-bottom\nputglyph 6c 1 1,3 dwl dhl-bottom\nputglyph 6f 1 1,4 dwl dhl-bottom' +- ) +- cursor(1, 5, state) +- +- -- Double Width scrolling +- reset(state, nil) +- push('\x1b[20H\x1b#6ABC', vt) +- expect('putglyph 41 1 19,0 dwl\nputglyph 42 1 19,1 dwl\nputglyph 43 1 19,2 dwl') +- push('\x1b[25H\n', vt) +- push('\x1b[19;4HDE', vt) +- expect('putglyph 44 1 18,3 dwl\nputglyph 45 1 18,4 dwl') +- push('\x1b[H\x1bM', vt) +- push('\x1b[20;6HFG', vt) +- expect('putglyph 46 1 19,5 dwl\nputglyph 47 1 19,6 dwl') +- end) +- +- itp('29state_fallback', function() +- local vt = init() +- local state = wantstate(vt, { f = true }) +- reset(state, nil) +- +- -- Unrecognised control +- push('\x03', vt) +- expect('control 03') +- +- -- Unrecognised CSI +- push('\x1b[?15;2z', vt) +- expect('csi 7a L=3f 15,2') +- +- -- Unrecognised OSC +- push('\x1b]27;Something\x1b\\', vt) +- expect('osc [27;Something]') +- +- -- Unrecognised DCS +- push('\x1bPz123\x1b\\', vt) +- expect('dcs [z123]') +- +- -- Unrecognised APC +- push('\x1b_z123\x1b\\', vt) +- expect('apc [z123]') +- +- -- Unrecognised PM +- push('\x1b^z123\x1b\\', vt) +- expect('pm [z123]') +- +- -- Unrecognised SOS +- push('\x1bXz123\x1b\\', vt) +- expect('sos [z123]') +- end) +- +- itp('30state_pen', function() +- local vt = init() +- local state = wantstate(vt) +- +- -- Reset +- push('\x1b[m', vt) +- pen('bold', false, state) +- pen('underline', 0, state) +- pen('italic', false, state) +- pen('blink', false, state) +- pen('reverse', false, state) +- pen('font', 0, state) +- -- TODO(dundargoc): fix +- -- ?pen foreground = rgb(240,240,240,is_default_fg) +- -- ?pen background = rgb(0,0,0,is_default_bg) +- +- -- Bold +- push('\x1b[1m', vt) +- pen('bold', true, state) +- push('\x1b[22m', vt) +- pen('bold', false, state) +- push('\x1b[1m\x1b[m', vt) +- pen('bold', false, state) +- +- -- Underline +- push('\x1b[4m', vt) +- pen('underline', 1, state) +- push('\x1b[21m', vt) +- pen('underline', 2, state) +- push('\x1b[24m', vt) +- pen('underline', 0, state) +- push('\x1b[4m\x1b[4:0m', vt) +- pen('underline', 0, state) +- push('\x1b[4:1m', vt) +- pen('underline', 1, state) +- push('\x1b[4:2m', vt) +- pen('underline', 2, state) +- push('\x1b[4:3m', vt) +- pen('underline', 3, state) +- push('\x1b[4m\x1b[m', vt) +- pen('underline', 0, state) +- +- -- Italic +- push('\x1b[3m', vt) +- pen('italic', true, state) +- push('\x1b[23m', vt) +- pen('italic', false, state) +- push('\x1b[3m\x1b[m', vt) +- pen('italic', false, state) +- +- -- Blink +- push('\x1b[5m', vt) +- pen('blink', true, state) +- push('\x1b[25m', vt) +- pen('blink', false, state) +- push('\x1b[5m\x1b[m', vt) +- pen('blink', false, state) +- +- -- Reverse +- push('\x1b[7m', vt) +- pen('reverse', true, state) +- push('\x1b[27m', vt) +- pen('reverse', false, state) +- push('\x1b[7m\x1b[m', vt) +- pen('reverse', false, state) +- +- -- Font Selection +- push('\x1b[11m', vt) +- pen('font', 1, state) +- push('\x1b[19m', vt) +- pen('font', 9, state) +- push('\x1b[10m', vt) +- pen('font', 0, state) +- push('\x1b[11m\x1b[m', vt) +- pen('font', 0, state) +- +- -- Dim +- push('\x1b[2m', vt) +- pen('dim', true, state) +- push('\x1b[22m', vt) +- pen('dim', false, state) +- push('\x1b[2m\x1b[m', vt) +- pen('dim', false, state) +- +- -- Bold and Dim interaction (SGR 22 turns off both) +- push('\x1b[1;2m', vt) +- pen('bold', true, state) +- pen('dim', true, state) +- push('\x1b[22m', vt) +- pen('bold', false, state) +- pen('dim', false, state) +- +- -- Overline +- push('\x1b[53m', vt) +- pen('overline', true, state) +- push('\x1b[55m', vt) +- pen('overline', false, state) +- push('\x1b[53m\x1b[m', vt) +- pen('overline', false, state) +- +- -- TODO(dundargoc): fix +- -- Foreground +- -- push "\x1b[31m" +- -- ?pen foreground = idx(1) +- -- push "\x1b[32m" +- -- ?pen foreground = idx(2) +- -- push "\x1b[34m" +- -- ?pen foreground = idx(4) +- -- push "\x1b[91m" +- -- ?pen foreground = idx(9) +- -- push "\x1b[38:2:10:20:30m" +- -- ?pen foreground = rgb(10,20,30) +- -- push "\x1b[38:5:1m" +- -- ?pen foreground = idx(1) +- -- push "\x1b[39m" +- -- ?pen foreground = rgb(240,240,240,is_default_fg) +- -- +- -- Background +- -- push "\x1b[41m" +- -- ?pen background = idx(1) +- -- push "\x1b[42m" +- -- ?pen background = idx(2) +- -- push "\x1b[44m" +- -- ?pen background = idx(4) +- -- push "\x1b[101m" +- -- ?pen background = idx(9) +- -- push "\x1b[48:2:10:20:30m" +- -- ?pen background = rgb(10,20,30) +- -- push "\x1b[48:5:1m" +- -- ?pen background = idx(1) +- -- push "\x1b[49m" +- -- ?pen background = rgb(0,0,0,is_default_bg) +- -- +- -- Bold+ANSI colour == highbright +- -- push "\x1b[m\x1b[1;37m" +- -- ?pen bold = on +- -- ?pen foreground = idx(15) +- -- push "\x1b[m\x1b[37;1m" +- -- ?pen bold = on +- -- ?pen foreground = idx(15) +- -- +- -- Super/Subscript +- -- push "\x1b[73m" +- -- ?pen small = on +- -- ?pen baseline = raise +- -- push "\x1b[74m" +- -- ?pen small = on +- -- ?pen baseline = lower +- -- push "\x1b[75m" +- -- ?pen small = off +- -- ?pen baseline = normal +- -- +- -- DECSTR resets pen attributes +- -- push "\x1b[1;4m" +- -- ?pen bold = on +- -- ?pen underline = 1 +- -- push "\x1b[!p" +- -- ?pen bold = off +- -- ?pen underline = 0 +- end) +- +- itp('31state_rep', function() +- local vt = init() +- local state = wantstate(vt, { g = true }) +- +- -- REP no argument +- reset(state, nil) +- push('a\x1b[b', vt) +- expect('putglyph 61 1 0,0\nputglyph 61 1 0,1') +- +- -- REP zero (zero should be interpreted as one) +- reset(state, nil) +- push('a\x1b[0b', vt) +- expect('putglyph 61 1 0,0\nputglyph 61 1 0,1') +- +- -- REP lowercase a times two +- reset(state, nil) +- push('a\x1b[2b', vt) +- expect('putglyph 61 1 0,0\nputglyph 61 1 0,1\nputglyph 61 1 0,2') +- +- -- REP with UTF-8 1 char +- -- U+00E9 = C3 A9 name: LATIN SMALL LETTER E WITH ACUTE +- reset(state, nil) +- push('\xC3\xA9\x1b[b', vt) +- expect('putglyph e9 1 0,0\nputglyph e9 1 0,1') +- +- -- REP with UTF-8 wide char +- -- U+00E9 = C3 A9 name: LATIN SMALL LETTER E WITH ACUTE +- reset(state, nil) +- push('\xEF\xBC\x90\x1b[b', vt) +- expect('putglyph ff10 2 0,0\nputglyph ff10 2 0,2') +- +- -- REP with UTF-8 combining character +- reset(state, nil) +- push('e\xCC\x81\x1b[b', vt) +- expect('putglyph 65,301 1 0,0\nputglyph 65,301 1 0,1') +- +- -- REP till end of line +- reset(state, nil) +- push('a\x1b[1000bb', vt) +- expect( +- 'putglyph 61 1 0,0\nputglyph 61 1 0,1\nputglyph 61 1 0,2\nputglyph 61 1 0,3\nputglyph 61 1 0,4\nputglyph 61 1 0,5\nputglyph 61 1 0,6\nputglyph 61 1 0,7\nputglyph 61 1 0,8\nputglyph 61 1 0,9\nputglyph 61 1 0,10\nputglyph 61 1 0,11\nputglyph 61 1 0,12\nputglyph 61 1 0,13\nputglyph 61 1 0,14\nputglyph 61 1 0,15\nputglyph 61 1 0,16\nputglyph 61 1 0,17\nputglyph 61 1 0,18\nputglyph 61 1 0,19\nputglyph 61 1 0,20\nputglyph 61 1 0,21\nputglyph 61 1 0,22\nputglyph 61 1 0,23\nputglyph 61 1 0,24\nputglyph 61 1 0,25\nputglyph 61 1 0,26\nputglyph 61 1 0,27\nputglyph 61 1 0,28\nputglyph 61 1 0,29\nputglyph 61 1 0,30\nputglyph 61 1 0,31\nputglyph 61 1 0,32\nputglyph 61 1 0,33\nputglyph 61 1 0,34\nputglyph 61 1 0,35\nputglyph 61 1 0,36\nputglyph 61 1 0,37\nputglyph 61 1 0,38\nputglyph 61 1 0,39\nputglyph 61 1 0,40\nputglyph 61 1 0,41\nputglyph 61 1 0,42\nputglyph 61 1 0,43\nputglyph 61 1 0,44\nputglyph 61 1 0,45\nputglyph 61 1 0,46\nputglyph 61 1 0,47\nputglyph 61 1 0,48\nputglyph 61 1 0,49\nputglyph 61 1 0,50\nputglyph 61 1 0,51\nputglyph 61 1 0,52\nputglyph 61 1 0,53\nputglyph 61 1 0,54\nputglyph 61 1 0,55\nputglyph 61 1 0,56\nputglyph 61 1 0,57\nputglyph 61 1 0,58\nputglyph 61 1 0,59\nputglyph 61 1 0,60\nputglyph 61 1 0,61\nputglyph 61 1 0,62\nputglyph 61 1 0,63\nputglyph 61 1 0,64\nputglyph 61 1 0,65\nputglyph 61 1 0,66\nputglyph 61 1 0,67\nputglyph 61 1 0,68\nputglyph 61 1 0,69\nputglyph 61 1 0,70\nputglyph 61 1 0,71\nputglyph 61 1 0,72\nputglyph 61 1 0,73\nputglyph 61 1 0,74\nputglyph 61 1 0,75\nputglyph 61 1 0,76\nputglyph 61 1 0,77\nputglyph 61 1 0,78\nputglyph 61 1 0,79\nputglyph 62 1 1,0' +- ) +- end) +- +- itp('32state_flow', function() +- local vt = init() +- local state = wantstate(vt) +- +- -- Many of these test cases inspired by +- -- https://blueprints.launchpad.net/libvterm/+spec/reflow-cases +- +- -- Spillover text marks continuation on second line +- reset(state, nil) +- push(string.rep('A', 100), vt) +- push('\r\n', vt) +- lineinfo(0, {}, state) +- lineinfo(1, { cont = true }, state) +- +- -- CRLF in column 80 does not mark continuation +- reset(state, nil) +- push(string.rep('B', 80), vt) +- push('\r\n', vt) +- push(string.rep('B', 20), vt) +- push('\r\n', vt) +- lineinfo(0, {}, state) +- lineinfo(1, {}, state) +- +- -- EL cancels continuation of following line +- reset(state, nil) +- push(string.rep('D', 100), vt) +- lineinfo(1, { cont = true }, state) +- push('\x1bM\x1b[79G\x1b[K', vt) +- lineinfo(1, {}, state) +- end) +- +- itp('40state_selection', function() +- local vt = init() +- wantstate(vt) +- +- -- Set clipboard; final chunk len 4 +- push('\x1b]52;c;SGVsbG8s\x1b\\', vt) +- expect('selection-set mask=0001 [Hello,]') +- +- -- Set clipboard; final chunk len 3 +- push('\x1b]52;c;SGVsbG8sIHc=\x1b\\', vt) +- expect('selection-set mask=0001 [Hello, w]') +- +- -- Set clipboard; final chunk len 2 +- push('\x1b]52;c;SGVsbG8sIHdvcmxkCg==\x1b\\', vt) +- expect('selection-set mask=0001 [Hello, world\n]') +- +- -- Set clipboard; split between chunks +- push('\x1b]52;c;SGVs', vt) +- expect('selection-set mask=0001 [Hel') +- push('bG8s\x1b\\', vt) +- expect('selection-set mask=0001 lo,]') +- +- -- Set clipboard; split within chunk +- push('\x1b]52;c;SGVsbG', vt) +- expect('selection-set mask=0001 [Hel') +- push('8s\x1b\\', vt) +- expect('selection-set mask=0001 lo,]') +- +- -- Set clipboard; empty first chunk +- push('\x1b]52;c;', vt) +- push('SGVsbG8s\x1b\\', vt) +- expect('selection-set mask=0001 [Hello,]') +- +- -- Set clipboard; empty final chunk +- push('\x1b]52;c;SGVsbG8s', vt) +- expect('selection-set mask=0001 [Hello,') +- push('\x1b\\', vt) +- expect('selection-set mask=0001 ]') +- +- -- Set clipboard; longer than buffer +- push('\x1b]52;c;' .. string.rep('LS0t', 10) .. '\x1b\\', vt) +- expect('selection-set mask=0001 [---------------\nselection-set mask=0001 ---------------]') +- +- -- Clear clipboard +- push('\x1b]52;c;\x1b\\', vt) +- expect('selection-set mask=0001 []') +- +- -- Set invalid data clears and ignores +- push('\x1b]52;c;SGVs*SGVsbG8s\x1b\\', vt) +- expect('selection-set mask=0001 []') +- +- -- Query clipboard +- push('\x1b]52;c;?\x1b\\', vt) +- expect('selection-query mask=0001') +- +- -- TODO(dundargoc): fix +- -- Send clipboard; final chunk len 4 +- -- SELECTION 1 ["Hello,"] +- -- output "\x1b]52;c;" +- -- output "SGVsbG8s" +- -- output "\x1b\\" +- -- +- -- Send clipboard; final chunk len 3 +- -- SELECTION 1 ["Hello, w"] +- -- output "\x1b]52;c;" +- -- output "SGVsbG8s" +- -- output "IHc=\x1b\\" +- -- +- -- Send clipboard; final chunk len 2 +- -- SELECTION 1 ["Hello, world\n"] +- -- output "\x1b]52;c;" +- -- output "SGVsbG8sIHdvcmxk" +- -- output "Cg==\x1b\\" +- -- +- -- Send clipboard; split between chunks +- -- SELECTION 1 ["Hel" +- -- output "\x1b]52;c;" +- -- output "SGVs" +- -- SELECTION 1 "lo,"] +- -- output "bG8s" +- -- output "\x1b\\" +- -- +- -- Send clipboard; split within chunk +- -- SELECTION 1 ["Hello" +- -- output "\x1b]52;c;" +- -- output "SGVs" +- -- SELECTION 1 ","] +- -- output "bG8s" +- -- output "\x1b\\" +- end) +- +- itp('60screen_ascii', function() +- local vt = init() +- local screen = wantscreen(vt, { a = true, c = true }) +- +- -- Get +- reset(nil, screen) +- push('ABC', vt) +- expect('movecursor 0,3') +- screen_chars(0, 0, 1, 3, 'ABC', screen) +- screen_chars(0, 0, 1, 80, 'ABC', screen) +- screen_text(0, 0, 1, 3, '41,42,43', screen) +- screen_text(0, 0, 1, 80, '41,42,43', screen) +- screen_cell(0, 0, '{41} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- screen_cell(0, 1, '{42} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- screen_cell(0, 2, '{43} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- screen_row(0, 'ABC', screen) +- screen_eol(0, 0, 0, screen) +- screen_eol(0, 2, 0, screen) +- screen_eol(0, 3, 1, screen) +- push('\x1b[H', vt) +- expect('movecursor 0,0') +- screen_row(0, 'ABC', screen) +- screen_text(0, 0, 1, 80, '41,42,43', screen) +- push('E', vt) +- expect('movecursor 0,1') +- screen_row(0, 'EBC', screen) +- screen_text(0, 0, 1, 80, '45,42,43', screen) +- +- screen = wantscreen(vt, { a = true }) +- +- -- Erase +- reset(nil, screen) +- push('ABCDE\x1b[H\x1b[K', vt) +- -- TODO(dundargoc): fix +- -- screen_row(0, '', screen) +- screen_text(0, 0, 1, 80, '', screen) +- +- -- Copycell +- reset(nil, screen) +- push('ABC\x1b[H\x1b[@', vt) +- push('1', vt) +- screen_row(0, '1ABC', screen) +- +- reset(nil, screen) +- push('ABC\x1b[H\x1b[P', vt) +- screen_chars(0, 0, 1, 1, 'B', screen) +- screen_chars(0, 1, 1, 2, 'C', screen) +- screen_chars(0, 0, 1, 80, 'BC', screen) +- +- -- Space padding +- reset(nil, screen) +- push('Hello\x1b[CWorld', vt) +- screen_row(0, 'Hello World', screen) +- screen_text(0, 0, 1, 80, '48,65,6c,6c,6f,20,57,6f,72,6c,64', screen) +- +- -- Linefeed padding +- reset(nil, screen) +- push('Hello\r\nWorld', vt) +- screen_chars(0, 0, 2, 80, 'Hello\nWorld', screen) +- screen_text(0, 0, 2, 80, '48,65,6c,6c,6f,0a,57,6f,72,6c,64', screen) +- +- -- Altscreen +- reset(nil, screen) +- push('P', vt) +- screen_row(0, 'P', screen) +- -- TODO(dundargoc): fix +- -- push('\x1b[?1049h', vt) +- -- screen_row(0, '', screen) +- -- push('\x1b[2K\x1b[HA', vt) +- -- screen_row(0, 'A', screen) +- -- push('\x1b[?1049l', vt) +- -- screen_row(0, 'P', screen) +- end) +- +- itp('61screen_unicode', function() +- local vt = init() +- local screen = wantscreen(vt) +- +- -- Single width UTF-8 +- -- U+00C1 = C3 81 name: LATIN CAPITAL LETTER A WITH ACUTE +- -- U+00E9 = C3 A9 name: LATIN SMALL LETTER E WITH ACUTE +- reset(nil, screen) +- push('\xC3\x81\xC3\xA9', vt) +- screen_row(0, 'Áé', screen) +- screen_text(0, 0, 1, 80, 'c3,81,c3,a9', screen) +- screen_cell(0, 0, '{c1} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- +- -- Wide char +- -- U+FF10 = EF BC 90 name: FULLWIDTH DIGIT ZERO +- reset(nil, screen) +- push('0123\x1b[H', vt) +- push('\xEF\xBC\x90', vt) +- screen_row(0, '023', screen) +- screen_text(0, 0, 1, 80, 'ef,bc,90,32,33', screen) +- screen_cell(0, 0, '{ff10} width=2 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- +- -- Combining char +- -- U+0301 = CC 81 name: COMBINING ACUTE +- reset(nil, screen) +- push('0123\x1b[H', vt) +- push('e\xCC\x81', vt) +- screen_row(0, 'é123', screen) +- screen_text(0, 0, 1, 80, '65,cc,81,31,32,33', screen) +- screen_cell(0, 0, '{65,301} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- +- -- 10 combining accents should not crash +- reset(nil, screen) +- push('e\xCC\x81\xCC\x82\xCC\x83\xCC\x84\xCC\x85\xCC\x86\xCC\x87\xCC\x88\xCC\x89\xCC\x8A', vt) +- screen_cell( +- 0, +- 0, +- '{65,301,302,303,304,305,306,307,308,309,30a} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', +- screen +- ) +- +- -- 40 combining accents in two split writes of 20 should not crash +- reset(nil, screen) +- push( +- 'e\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81', +- vt +- ) +- push( +- '\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81', +- vt +- ) +- screen_cell( +- 0, +- 0, +- '{65,301,301,301,301,301,301,301,301,301,301,301,301,301,301} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', +- screen +- ) +- +- -- Outputting CJK doublewidth in 80th column should wraparound to next line and not crash" +- reset(nil, screen) +- push('\x1b[80G\xEF\xBC\x90', vt) +- screen_cell(0, 79, '{} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- screen_cell(1, 0, '{ff10} width=2 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- +- -- Outputting emoji with ZWJ and variant selectors +- reset(nil, screen) +- push('🏳️‍🌈🏳️‍⚧️🏴‍☠️', vt) +- +- -- stylua: ignore start +- screen_cell(0, 0, '{1f3f3,fe0f,200d,1f308} width=2 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- screen_cell(0, 2, '{1f3f3,fe0f,200d,26a7,fe0f} width=2 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- screen_cell(0, 4, '{1f3f4,200d,2620,fe0f} width=2 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- -- stylua: ignore end +- end) +- +- pending('62screen_damage', function() end) +- +- itp('63screen_resize', function() +- local vt = init() +- local state = wantstate(vt) +- local screen = wantscreen(vt) +- +- -- Resize wider preserves cells +- reset(state, screen) +- resize(25, 80, vt) +- push('AB\r\nCD', vt) +- screen_chars(0, 0, 1, 80, 'AB', screen) +- screen_chars(1, 0, 2, 80, 'CD', screen) +- resize(25, 100, vt) +- screen_chars(0, 0, 1, 100, 'AB', screen) +- screen_chars(1, 0, 2, 100, 'CD', screen) +- +- -- Resize wider allows print in new area +- reset(state, screen) +- resize(25, 80, vt) +- push('AB\x1b[79GCD', vt) +- screen_chars(0, 0, 1, 2, 'AB', screen) +- screen_chars(0, 78, 1, 80, 'CD', screen) +- resize(25, 100, vt) +- screen_chars(0, 0, 1, 2, 'AB', screen) +- screen_chars(0, 78, 1, 80, 'CD', screen) +- push('E', vt) +- screen_chars(0, 78, 1, 81, 'CDE', screen) +- +- -- Resize shorter with blanks just truncates +- reset(state, screen) +- resize(25, 80, vt) +- push('Top\x1b[10HLine 10', vt) +- screen_row(0, 'Top', screen) +- screen_row(9, 'Line 10', screen) +- cursor(9, 7, state) +- resize(20, 80, vt) +- screen_row(0, 'Top', screen) +- screen_row(9, 'Line 10', screen) +- cursor(9, 7, state) +- +- -- Resize shorter with content must scroll +- reset(state, screen) +- resize(25, 80, vt) +- push('Top\x1b[25HLine 25\x1b[15H', vt) +- screen_row(0, 'Top', screen) +- screen_row(24, 'Line 25', screen) +- cursor(14, 0, state) +- screen = wantscreen(vt, { b = true }) +- resize(20, 80, vt) +- expect( +- 'sb_pushline 80 = 54 6f 70\nsb_pushline 80 =\nsb_pushline 80 =\nsb_pushline 80 =\nsb_pushline 80 =' +- ) +- -- TODO(dundargoc): fix or remove +- -- screen_row( 0 , "",screen) +- screen_row(19, 'Line 25', screen) +- cursor(9, 0, state) +- +- -- Resize shorter does not lose line with cursor +- -- See also https://github.com/neovim/libvterm/commit/1b745d29d45623aa8d22a7b9288c7b0e331c7088 +- reset(state, screen) +- wantscreen(vt) +- resize(25, 80, vt) +- screen = wantscreen(vt, { b = true }) +- push('\x1b[24HLine 24\r\nLine 25\r\n', vt) +- expect('sb_pushline 80 =') +- screen_row(23, 'Line 25', screen) +- cursor(24, 0, state) +- resize(24, 80, vt) +- expect('sb_pushline 80 =') +- screen_row(22, 'Line 25', screen) +- cursor(23, 0, state) +- +- -- Resize shorter does not send the cursor to a negative row +- -- See also https://github.com/vim/vim/pull/6141 +- reset(state, screen) +- wantscreen(vt) +- resize(25, 80, vt) +- screen = wantscreen(vt, { b = true }) +- push('\x1b[24HLine 24\r\nLine 25\x1b[H', vt) +- cursor(0, 0, state) +- resize(20, 80, vt) +- expect( +- 'sb_pushline 80 =\nsb_pushline 80 =\nsb_pushline 80 =\nsb_pushline 80 =\nsb_pushline 80 =' +- ) +- cursor(0, 0, state) +- +- -- Resize taller attempts to pop scrollback +- reset(state, screen) +- screen = wantscreen(vt) +- resize(25, 80, vt) +- push('Line 1\x1b[25HBottom\x1b[15H', vt) +- screen_row(0, 'Line 1', screen) +- screen_row(24, 'Bottom', screen) +- cursor(14, 0, state) +- screen = wantscreen(vt, { b = true }) +- resize(30, 80, vt) +- expect('sb_popline 80\nsb_popline 80\nsb_popline 80\nsb_popline 80\nsb_popline 80') +- screen_row(0, 'ABCDE', screen) +- screen_row(5, 'Line 1', screen) +- screen_row(29, 'Bottom', screen) +- cursor(19, 0, state) +- screen = wantscreen(vt) +- +- -- Resize can operate on altscreen +- reset(state, screen) +- screen = wantscreen(vt, { a = true }) +- resize(25, 80, vt) +- push('Main screen\x1b[?1049h\x1b[HAlt screen', vt) +- resize(30, 80, vt) +- screen_row(0, 'Alt screen', screen) +- push('\x1b[?1049l', vt) +- screen_row(0, 'Main screen', screen) +- end) +- +- itp('64screen_pen', function() +- local vt = init() +- local screen = wantscreen(vt) +- +- reset(nil, screen) +- +- -- Plain +- push('A', vt) +- screen_cell(0, 0, '{41} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- +- -- Bold +- push('\x1b[1mB', vt) +- screen_cell(0, 1, '{42} width=1 attrs={B} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- +- -- Italic +- push('\x1b[3mC', vt) +- screen_cell(0, 2, '{43} width=1 attrs={BI} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- +- -- Underline +- push('\x1b[4mD', vt) +- screen_cell(0, 3, '{44} width=1 attrs={BU1I} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- +- -- Reset +- push('\x1b[mE', vt) +- screen_cell(0, 4, '{45} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- +- -- Font +- push('\x1b[11mF\x1b[m', vt) +- screen_cell(0, 5, '{46} width=1 attrs={F1} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- +- -- Foreground +- push('\x1b[31mG\x1b[m', vt) +- screen_cell(0, 6, '{47} width=1 attrs={} fg=rgb(224,0,0) bg=rgb(0,0,0)', screen) +- +- -- Background +- push('\x1b[42mH\x1b[m', vt) +- screen_cell(0, 7, '{48} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,224,0)', screen) +- +- -- Super/subscript +- push('x\x1b[74m0\x1b[73m2\x1b[m', vt) +- screen_cell(0, 8, '{78} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- screen_cell(0, 9, '{30} width=1 attrs={S_} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- screen_cell(0, 10, '{32} width=1 attrs={S^} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- +- -- Dim +- push('\x1b[2mI\x1b[m', vt) +- screen_cell(0, 11, '{49} width=1 attrs={D} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- +- -- Overline +- push('\x1b[53mJ\x1b[m', vt) +- screen_cell(0, 12, '{4a} width=1 attrs={O} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- +- -- EL sets only colours to end of line, not other attrs +- push('\x1b[H\x1b[7;33;44m\x1b[K', vt) +- screen_cell(0, 0, '{} width=1 attrs={} fg=rgb(224,224,0) bg=rgb(0,0,224)', screen) +- screen_cell(0, 79, '{} width=1 attrs={} fg=rgb(224,224,0) bg=rgb(0,0,224)', screen) +- +- -- DECSCNM xors reverse for entire screen +- push('R\x1b[?5h', vt) +- screen_cell(0, 0, '{52} width=1 attrs={} fg=rgb(224,224,0) bg=rgb(0,0,224)', screen) +- screen_cell(1, 0, '{} width=1 attrs={R} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- push('\x1b[?5$p', vt) +- expect_output('\x1b[?5;1$y') +- push('\x1b[?5l', vt) +- screen_cell(0, 0, '{52} width=1 attrs={R} fg=rgb(224,224,0) bg=rgb(0,0,224)', screen) +- screen_cell(1, 0, '{} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- -- TODO(dundargoc): fix +- -- push('\x1b[?5$p') +- -- expect_output('\x1b[?5;2$y') +- +- -- Set default colours +- reset(nil, screen) +- push('ABC\x1b[31mDEF\x1b[m', vt) +- screen_cell(0, 0, '{41} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- screen_cell(0, 3, '{44} width=1 attrs={} fg=rgb(224,0,0) bg=rgb(0,0,0)', screen) +- -- TODO(dundargoc): fix +- -- SETDEFAULTCOL rgb(252,253,254) +- -- ?screen_cell 0,0 = {41} width=1 attrs={} fg=rgb(252,253,254) bg=rgb(0,0,0) +- -- ?screen_cell 0,3 = {44} width=1 attrs={} fg=rgb(224,0,0) bg=rgb(0,0,0) +- -- SETDEFAULTCOL rgb(250,250,250) rgb(10,20,30) +- -- ?screen_cell 0,0 = {41} width=1 attrs={} fg=rgb(250,250,250) bg=rgb(10,20,30) +- -- ?screen_cell 0,3 = {44} width=1 attrs={} fg=rgb(224,0,0) bg=rgb(10,20,30) +- end) +- +- itp('65screen_protect', function() +- local vt = init() +- local screen = wantscreen(vt) +- +- -- Selective erase +- reset(nil, screen) +- push('A\x1b[1"qB\x1b["qC', vt) +- screen_row(0, 'ABC', screen) +- push('\x1b[G\x1b[?J', vt) +- screen_row(0, ' B', screen) +- +- -- Non-selective erase +- reset(nil, screen) +- push('A\x1b[1"qB\x1b["qC', vt) +- screen_row(0, 'ABC', screen) +- -- TODO(dundargoc): fix +- -- push('\x1b[G\x1b[J', vt) +- -- screen_row(0, '', screen) +- end) +- +- itp('66screen_extent', function() +- local vt = init() +- local screen = wantscreen(vt) +- +- -- Bold extent +- reset(nil, screen) +- push('AB\x1b[1mCD\x1b[mE', vt) +- screen_attrs_extent(0, 0, '0,0-1,1', screen) +- screen_attrs_extent(0, 1, '0,0-1,1', screen) +- screen_attrs_extent(0, 2, '0,2-1,3', screen) +- screen_attrs_extent(0, 3, '0,2-1,3', screen) +- screen_attrs_extent(0, 4, '0,4-1,79', screen) +- end) +- +- itp('67screen_dbl_wh', function() +- local vt = init() +- local screen = wantscreen(vt) +- +- reset(nil, screen) +- +- -- Single Width, Single Height +- reset(nil, screen) +- push('\x1b#5', vt) +- push('abcde', vt) +- screen_cell(0, 0, '{61} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- +- -- Double Width, Single Height +- reset(nil, screen) +- push('\x1b#6', vt) +- push('abcde', vt) +- screen_cell(0, 0, '{61} width=1 attrs={} dwl fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- +- -- Double Height +- reset(nil, screen) +- push('\x1b#3', vt) +- push('abcde', vt) +- push('\r\n\x1b#4', vt) +- push('abcde', vt) +- screen_cell(0, 0, '{61} width=1 attrs={} dwl dhl-top fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- screen_cell( +- 1, +- 0, +- '{61} width=1 attrs={} dwl dhl-bottom fg=rgb(240,240,240) bg=rgb(0,0,0)', +- screen +- ) +- +- -- Late change +- reset(nil, screen) +- push('abcde', vt) +- screen_cell(0, 0, '{61} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- push('\x1b#6', vt) +- screen_cell(0, 0, '{61} width=1 attrs={} dwl fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- +- -- DWL doesn't spill over on scroll +- reset(nil, screen) +- push('\x1b[25H\x1b#6Final\r\n', vt) +- screen_cell(23, 0, '{46} width=1 attrs={} dwl fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- screen_cell(24, 0, '{} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen) +- end) +- +- itp('68screen_termprops', function() +- local vt = init() +- local screen = wantscreen(vt, { p = true }) +- +- reset(nil, screen) +- expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1') +- +- -- Cursor visibility +- push('\x1b[?25h', vt) +- expect('settermprop 1 true') +- push('\x1b[?25l', vt) +- expect('settermprop 1 false') +- +- -- Title +- push('\x1b]2;Here is my title\a', vt) +- expect('settermprop 4 ["Here is my title"]') +- end) +- +- itp('69screen_pushline', function() +- local vt = init() +- -- Run these tests on a much smaller default screen, so debug output is nowhere near as noisy +- resize(5, 10, vt) +- local state = wantstate(vt) +- local screen = wantscreen(vt, { r = true }) +- reset(state, screen) +- +- -- Resize wider reflows wide lines +- reset(state, screen) +- push(string.rep('A', 12), vt) +- screen_row(0, 'AAAAAAAAAA', screen, vt.cols) +- screen_row(1, 'AA', screen, vt.cols) +- lineinfo(1, { cont = true }, state) +- cursor(1, 2, state) +- resize(5, 15, vt) +- screen_row(0, 'AAAAAAAAAAAA', screen, vt.cols) +- -- TODO(dundargoc): fix +- -- screen_row(1, '', screen, vt.cols) +- lineinfo(1, {}, state) +- cursor(0, 12, state) +- resize(5, 20, vt) +- screen_row(0, 'AAAAAAAAAAAA', screen, vt.cols) +- -- TODO(dundargoc): fix +- -- screen_row( 1 ,'',screen, vt.cols) +- lineinfo(1, {}, state) +- cursor(0, 12, state) +- +- -- Resize narrower can create continuation lines +- reset(state, screen) +- resize(5, 10, vt) +- push('ABCDEFGHI', vt) +- screen_row(0, 'ABCDEFGHI', screen, vt.cols) +- -- TODO(dundargoc): fix +- -- screen_row( 1 , "",screen, vt.cols) +- lineinfo(1, {}, state) +- cursor(0, 9, state) +- resize(5, 8, vt) +- -- TODO(dundargoc): fix +- -- screen_row( 0 , "ABCDEFGH",screen,vt.cols) +- screen_row(1, 'I', screen, vt.cols) +- lineinfo(1, { cont = true }, state) +- cursor(1, 1, state) +- resize(5, 6, vt) +- screen_row(0, 'ABCDEF', screen, vt.cols) +- screen_row(1, 'GHI', screen, vt.cols) +- lineinfo(1, { cont = true }, state) +- cursor(1, 3, state) +- +- -- Shell wrapped prompt behaviour +- reset(state, screen) +- resize(5, 10, vt) +- push('PROMPT GOES HERE\r\n> \r\n\r\nPROMPT GOES HERE\r\n> ', vt) +- screen_row(0, '> ', screen, vt.cols) +- -- TODO(dundargoc): fix +- -- screen_row( 1 , "",screen,vt.cols) +- screen_row(2, 'PROMPT GOE', screen, vt.cols) +- screen_row(3, 'S HERE', screen, vt.cols) +- lineinfo(3, { cont = true }, state) +- screen_row(4, '> ', screen, vt.cols) +- cursor(4, 2, state) +- resize(5, 11, vt) +- screen_row(0, '> ', screen, vt.cols) +- -- TODO(dundargoc): fix +- -- screen_row( 1 , "",screen,vt.cols) +- screen_row(2, 'PROMPT GOES', screen, vt.cols) +- screen_row(3, ' HERE', screen, vt.cols) +- lineinfo(3, { cont = true }, state) +- screen_row(4, '> ', screen, vt.cols) +- cursor(4, 2, state) +- resize(5, 12, vt) +- screen_row(0, '> ', screen, vt.cols) +- -- TODO(dundargoc): fix +- -- screen_row( 1 , "",screen,vt.cols) +- screen_row(2, 'PROMPT GOES ', screen, vt.cols) +- screen_row(3, 'HERE', screen, vt.cols) +- lineinfo(3, { cont = true }, state) +- screen_row(4, '> ', screen, vt.cols) +- cursor(4, 2, state) +- resize(5, 16, vt) +- screen_row(0, '> ', screen, vt.cols) +- -- TODO(dundargoc): fix +- -- screen_row( 1 , "",screen,vt.cols) +- -- screen_row( 2 , "PROMPT GOES HERE",screen,vt.cols) +- lineinfo(3, {}, state) +- screen_row(3, '> ', screen, vt.cols) +- cursor(3, 2, state) +- +- -- Cursor goes missing +- -- For more context: https://github.com/neovim/neovim/pull/21124 +- reset(state, screen) +- resize(5, 5, vt) +- resize(3, 1, vt) +- push('\x1b[2;1Habc\r\n\x1b[H', vt) +- resize(1, 1, vt) +- cursor(0, 0, state) +- +- reset(state, screen) +- resize(4, 4, vt) +- push('ABCD\r\nX', vt) +- screen_row(0, 'ABCD', screen, vt.cols) +- screen_row(1, 'X', screen, vt.cols) +- cursor(1, 1, state) +- resize(4, 5, vt) +- screen_row(0, 'ABCD', screen, vt.cols) +- screen_row(1, 'X', screen, vt.cols) +- cursor(1, 1, state) +- +- reset(state, screen) +- resize(4, 4, vt) +- push('ABCD\r\nX', vt) +- cursor(1, 1, state) +- resize(3, 4, vt) +- screen_row(0, 'ABCD', screen, vt.cols) +- screen_row(1, 'X', screen, vt.cols) +- cursor(1, 1, state) +- +- resize(5, 10, vt) +- end) +- +- pending('90vttest_01-movement-1', function() end) +- pending('90vttest_01-movement-2', function() end) +- +- itp('90vttest_01-movement-3', function() +- -- Test of cursor-control characters inside ESC sequences +- local vt = init() +- local state = wantstate(vt) +- local screen = wantscreen(vt) +- +- reset(state, screen) +- +- push('A B C D E F G H I', vt) +- push('\x0d\x0a', vt) +- push('A\x1b[2\bCB\x1b[2\bCC\x1b[2\bCD\x1b[2\bCE\x1b[2\bCF\x1b[2\bCG\x1b[2\bCH\x1b[2\bCI', vt) +- push('\x0d\x0a', vt) +- push( +- 'A \x1b[\x0d2CB\x1b[\x0d4CC\x1b[\x0d6CD\x1b[\x0d8CE\x1b[\x0d10CF\x1b[\x0d12CG\x1b[\x0d14CH\x1b[\x0d16CI', +- vt +- ) +- push('\x0d\x0a', vt) +- push( +- 'A \x1b[1\x0bAB \x1b[1\x0bAC \x1b[1\x0bAD \x1b[1\x0bAE \x1b[1\x0bAF \x1b[1\x0bAG \x1b[1\x0bAH \x1b[1\x0bAI \x1b[1\x0bA', +- vt +- ) +- +- -- Output +- +- for i = 0, 2 do +- screen_row(i, 'A B C D E F G H I', screen) +- end +- screen_row(3, 'A B C D E F G H I ', screen) +- +- cursor(3, 18, state) +- end) +- +- itp('90vttest_01-movement-4', function() +- -- Test of leading zeroes in ESC sequences +- local vt = init() +- local screen = wantscreen(vt) +- +- reset(nil, screen) +- +- push('\x1b[00000000004;000000001HT', vt) +- push('\x1b[00000000004;000000002Hh', vt) +- push('\x1b[00000000004;000000003Hi', vt) +- push('\x1b[00000000004;000000004Hs', vt) +- push('\x1b[00000000004;000000005H ', vt) +- push('\x1b[00000000004;000000006Hi', vt) +- push('\x1b[00000000004;000000007Hs', vt) +- push('\x1b[00000000004;000000008H ', vt) +- push('\x1b[00000000004;000000009Ha', vt) +- push('\x1b[00000000004;0000000010H ', vt) +- push('\x1b[00000000004;0000000011Hc', vt) +- push('\x1b[00000000004;0000000012Ho', vt) +- push('\x1b[00000000004;0000000013Hr', vt) +- push('\x1b[00000000004;0000000014Hr', vt) +- push('\x1b[00000000004;0000000015He', vt) +- push('\x1b[00000000004;0000000016Hc', vt) +- push('\x1b[00000000004;0000000017Ht', vt) +- push('\x1b[00000000004;0000000018H ', vt) +- push('\x1b[00000000004;0000000019Hs', vt) +- push('\x1b[00000000004;0000000020He', vt) +- push('\x1b[00000000004;0000000021Hn', vt) +- push('\x1b[00000000004;0000000022Ht', vt) +- push('\x1b[00000000004;0000000023He', vt) +- push('\x1b[00000000004;0000000024Hn', vt) +- push('\x1b[00000000004;0000000025Hc', vt) +- push('\x1b[00000000004;0000000026He', vt) +- +- -- Output +- +- screen_row(3, 'This is a correct sentence', screen) +- end) +- +- pending('90vttest_02-screen-1', function() end) +- pending('90vttest_02-screen-2', function() end) +- +- itp('90vttest_02-screen-3', function() +- -- Origin mode +- local vt = init() +- local screen = wantscreen(vt) +- +- reset(nil, screen) +- +- push('\x1b[?6h', vt) +- push('\x1b[23;24r', vt) +- push('\n', vt) +- push('Bottom', vt) +- push('\x1b[1;1H', vt) +- push('Above', vt) +- +- -- Output +- screen_row(22, 'Above', screen) +- screen_row(23, 'Bottom', screen) +- end) +- +- itp('90vttest_02-screen-4', function() +- -- Origin mode (2) +- local vt = init() +- local screen = wantscreen(vt) +- +- reset(nil, screen) +- +- push('\x1b[?6l', vt) +- push('\x1b[23;24r', vt) +- push('\x1b[24;1H', vt) +- push('Bottom', vt) +- push('\x1b[1;1H', vt) +- push('Top', vt) +- +- -- Output +- screen_row(23, 'Bottom', screen) +- screen_row(0, 'Top', screen) +- end) +- +- itp('Mouse reporting should not break by idempotent DECSM 1002', function() +- -- Regression test for https://bugs.launchpad.net/libvterm/+bug/1640917 +- -- Related: https://github.com/neovim/neovim/issues/5583 +- local vt = init() +- wantstate(vt, {}) +- +- push('\x1b[?1002h', vt) +- mousemove(0, 0, vt) +- mousebtn('d', 1, vt) +- expect_output('\x1b[M\x20\x21\x21') +- mousemove(1, 0, vt) +- expect_output('\x1b[M\x40\x21\x22') +- push('\x1b[?1002h', vt) +- mousemove(2, 0, vt) +- expect_output('\x1b[M\x40\x21\x23') +- end) +-end) diff --git a/pkg/ui/tui.go b/pkg/ui/tui.go index 6e71f36..b9da4ed 100644 --- a/pkg/ui/tui.go +++ b/pkg/ui/tui.go @@ -19,6 +19,7 @@ import ( "github.com/bluekeyes/go-gitdiff/gitdiff" zone "github.com/lrstanley/bubblezone/v2" + "github.com/dlvhdr/diffnav/internal/api" "github.com/dlvhdr/diffnav/pkg/config" "github.com/dlvhdr/diffnav/pkg/dirnode" "github.com/dlvhdr/diffnav/pkg/filenode" @@ -65,7 +66,10 @@ const ( ) type mainModel struct { - input string + apiClient api.API + diff string + prURL string + pr api.PRQuery files []*gitdiff.File fileTree filetree.Model diffViewer diffviewer.Model @@ -97,14 +101,21 @@ type mainModel struct { repoRoot string } -func New(input string, cfg config.Config) mainModel { +type ModelOpts struct { + Input string + PRUrl string +} + +func New(opts ModelOpts, cfg config.Config) mainModel { initialPanel := FileTreePanel if !cfg.UI.ShowFileTree { initialPanel = DiffViewerPanel } m := mainModel{ - input: input, + apiClient: api.New(), + diff: opts.Input, + prURL: opts.PRUrl, isShowingFileTree: cfg.UI.ShowFileTree, activePanel: initialPanel, config: cfg, @@ -148,6 +159,35 @@ func (m mainModel) fetchRepoRoot() tea.Msg { return repoRootMsg(strings.TrimSpace(string(out))) } +type prFetchedMsg struct { + res api.PRQuery + err error +} + +func (m mainModel) fetchPR() tea.Msg { + res, err := m.apiClient.FetchPR(m.prURL) + if err != nil { + return prFetchedMsg{err: err} + } + return prFetchedMsg{res: res} +} + +type ghDiffFetchedMsg struct { + diff string + err error +} + +func (m mainModel) fetchPRDiff() tea.Msg { + res, err := m.apiClient.FetchPRDiff(m.prURL) + if err != nil { + log.Error("failed fetching pr diff", "err", err) + return ghDiffFetchedMsg{err: err} + } + + log.Debug("successfully fetched pr diff", "len", len(res)) + return ghDiffFetchedMsg{diff: res} +} + type watchTickMsg struct{ time.Time } type watchResultMsg struct { @@ -156,10 +196,15 @@ type watchResultMsg struct { } func (m mainModel) Init() tea.Cmd { - cmds := []tea.Cmd{m.fetchFileTree, m.diffViewer.Init(), m.fetchRepoRoot} + cmds := []tea.Cmd{m.diffViewer.Init(), m.fetchRepoRoot} if m.watchEnabled { cmds = append(cmds, m.scheduleWatchTick()) } + if m.prURL != "" { + cmds = append(cmds, m.fetchPR, m.fetchPRDiff) + } else { + cmds = append(cmds, m.parseDiff) + } return tea.Batch(cmds...) } @@ -387,6 +432,13 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.updateMessageVp() } + case prFetchedMsg: + m.pr = msg.res + + case ghDiffFetchedMsg: + m.diff = msg.diff + cmds = append(cmds, m.parseDiff) + case watchTickMsg: if m.watchInFlight { return m, nil @@ -404,17 +456,17 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, m.scheduleWatchTick()) return m, tea.Batch(cmds...) } - if msg.output == m.input { + if msg.output == m.diff { cmds = append(cmds, m.scheduleWatchTick()) return m, tea.Batch(cmds...) } m.pendingCursorPath = m.fileTree.CurrNodePath() m.diffViewer.ClearCache() - m.input = msg.output - cmds = append(cmds, m.fetchFileTree, m.scheduleWatchTick()) + m.diff = msg.output + cmds = append(cmds, m.parseDiff, m.scheduleWatchTick()) return m, tea.Batch(cmds...) - case fileTreeMsg: + case parsedDiffMsg: m.files = msg.files if len(m.files) == 0 && !m.watchEnabled { return m, tea.Quit @@ -631,22 +683,22 @@ func (m mainModel) View() tea.View { return view } -type fileTreeMsg struct { +type parsedDiffMsg struct { files []*gitdiff.File preamble string branch string } -func (m mainModel) fetchFileTree() tea.Msg { +func (m mainModel) parseDiff() tea.Msg { // TODO: handle error - files, preamble, err := gitdiff.Parse(strings.NewReader(m.input + "\n")) + files, preamble, err := gitdiff.Parse(strings.NewReader(m.diff + "\n")) if err != nil { return common.ErrMsg{Err: err} } sortFiles(files) branch := resolveBranch(preamble) - return fileTreeMsg{files: files, preamble: preamble, branch: branch} + return parsedDiffMsg{files: files, preamble: preamble, branch: branch} } // resolveBranch finds branches pointing at the preamble commit. diff --git a/pkg/ui/tui_test.go b/pkg/ui/tui_test.go index 9389623..7505021 100644 --- a/pkg/ui/tui_test.go +++ b/pkg/ui/tui_test.go @@ -192,7 +192,7 @@ func TestInitialActivePanelWhenFileTreeIsHidden(t *testing.T) { t.Fatal(err) } - m := New(string(data), cfg) + m := New(ModelOpts{Input: string(data)}, cfg) if m.activePanel != DiffViewerPanel { t.Fatalf( @@ -220,7 +220,7 @@ func newTestMainModel(t *testing.T) mainModel { t.Fatal(err) } - m := New(string(data), cfg) + m := New(ModelOpts{Input: string(data)}, cfg) m.files = files m.fileTree = m.fileTree.SetFiles(files)