diff --git a/examples/altscreen-toggle/main.go b/examples/altscreen-toggle/main.go index c05599cd3b..327c47766a 100644 --- a/examples/altscreen-toggle/main.go +++ b/examples/altscreen-toggle/main.go @@ -29,7 +29,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "q", "ctrl+c", "esc": m.quitting = true return m, tea.Quit - case " ": + case "space": var cmd tea.Cmd if m.altscreen { cmd = tea.ExitAltScreen diff --git a/examples/autocomplete/main.go b/examples/autocomplete/main.go index 4195a745a0..42d768478b 100644 --- a/examples/autocomplete/main.go +++ b/examples/autocomplete/main.go @@ -21,8 +21,10 @@ func main() { } } -type gotReposSuccessMsg []repo -type gotReposErrMsg error +type ( + gotReposSuccessMsg []repo + gotReposErrMsg error +) type repo struct { Name string `json:"name"` @@ -76,6 +78,7 @@ func (k keymap) ShortHelp() []key.Binding { key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")), } } + func (k keymap) FullHelp() [][]key.Binding { return [][]key.Binding{k.ShortHelp()} } @@ -105,8 +108,8 @@ func (m model) Init() tea.Cmd { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: - switch msg.Type { - case tea.KeyEnter, tea.KeyCtrlC, tea.KeyEsc: + switch msg.String() { + case "enter", "ctrl+c", "esc": return m, tea.Quit } case gotReposSuccessMsg: diff --git a/examples/chat/main.go b/examples/chat/main.go index 4a573d5b65..34f22ef827 100644 --- a/examples/chat/main.go +++ b/examples/chat/main.go @@ -80,11 +80,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: - switch msg.Type { - case tea.KeyCtrlC, tea.KeyEsc: + switch msg.String() { + case "ctrl+c", "esc": fmt.Println(m.textarea.Value()) return m, tea.Quit - case tea.KeyEnter: + case "enter": m.messages = append(m.messages, m.senderStyle.Render("You: ")+m.textarea.Value()) m.viewport.SetContent(strings.Join(m.messages, "\n")) m.textarea.Reset() diff --git a/examples/credit-card-form/main.go b/examples/credit-card-form/main.go index 71b3bf58bf..9618b3d5fb 100644 --- a/examples/credit-card-form/main.go +++ b/examples/credit-card-form/main.go @@ -135,17 +135,17 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: - switch msg.Type { - case tea.KeyEnter: + switch msg.String() { + case "enter": if m.focused == len(m.inputs)-1 { return m, tea.Quit } m.nextInput() - case tea.KeyCtrlC, tea.KeyEsc: + case "ctrl+c", "esc": return m, tea.Quit - case tea.KeyShiftTab, tea.KeyCtrlP: + case "shift+tab", "ctrl+p": m.prevInput() - case tea.KeyTab, tea.KeyCtrlN: + case "tab", "ctrl+n": m.nextInput() } for i := range m.inputs { diff --git a/examples/go.mod b/examples/go.mod index 249aca6aee..d8ac34315d 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -3,8 +3,8 @@ module examples go 1.18 require ( - github.com/charmbracelet/bubbles v0.18.0 - github.com/charmbracelet/bubbletea v0.25.0 + github.com/charmbracelet/bubbles v0.18.1-0.20240422182241-0a4be6cc5dcc + github.com/charmbracelet/bubbletea v0.25.1-0.20240306212323-3df8b37dba50 github.com/charmbracelet/glamour v0.7.0 github.com/charmbracelet/harmonica v0.2.0 github.com/charmbracelet/lipgloss v0.10.0 @@ -21,12 +21,11 @@ require ( github.com/aymanbagabas/go-udiff v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/charmbracelet/x/exp/golden v0.0.0-20240222125807-0344fda748f8 // indirect - github.com/charmbracelet/x/exp/term v0.0.0-20240322170634-ebda89b611f2 // indirect + github.com/charmbracelet/x/exp/term v0.0.0-20240422143423-943add0689d8 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/gorilla/css v1.0.0 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/microcosm-cc/bluemonday v1.0.25 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect @@ -35,14 +34,14 @@ require ( github.com/muesli/termenv v0.15.2 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect + github.com/sahilm/fuzzy v0.1.1 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.5.4 // indirect github.com/yuin/goldmark-emoji v1.0.2 // indirect golang.org/x/net v0.23.0 // indirect - golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect ) replace github.com/charmbracelet/bubbletea => ../ diff --git a/examples/go.sum b/examples/go.sum index 3089b4dc12..0fb8744c83 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -1,3 +1,4 @@ +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264= @@ -11,8 +12,8 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= -github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= +github.com/charmbracelet/bubbles v0.18.1-0.20240422182241-0a4be6cc5dcc h1:FOSO6UV6m9VPgH0HTlACu91VSfcC4yC0JacwkLYPAs0= +github.com/charmbracelet/bubbles v0.18.1-0.20240422182241-0a4be6cc5dcc/go.mod h1:tq6pvF3rPA+RofPhg75WmmNBykFcrlMUuDSVQ/XF2qA= github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng= github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= @@ -23,9 +24,8 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20240222125807-0344fda748f8 h1:kyT+ github.com/charmbracelet/x/exp/golden v0.0.0-20240222125807-0344fda748f8/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/teatest v0.0.0-20240229115032-4b79243a3516 h1:7IZFEUZpEgjlTSd7P1MRRhGXs7t4F6mENeMw17TxnQs= github.com/charmbracelet/x/exp/teatest v0.0.0-20240229115032-4b79243a3516/go.mod h1:SG24wGkG/mix5V2dZLXfQ6Bod43HGvk9CkTDxATwKN4= -github.com/charmbracelet/x/exp/term v0.0.0-20240322170634-ebda89b611f2 h1:53tB77HCI013H68JTiH1kX6rtSJQPB6Cu4AmXj3cCzs= -github.com/charmbracelet/x/exp/term v0.0.0-20240322170634-ebda89b611f2/go.mod h1:madZtB2OVDOG+ZnLruGITVZceYy047W+BLQ1MNQzbWg= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/charmbracelet/x/exp/term v0.0.0-20240422143423-943add0689d8 h1:m/dga5sTNG+jU1cojiv83uAZLgG2jpq8619I4Hb+mmQ= +github.com/charmbracelet/x/exp/term v0.0.0-20240422143423-943add0689d8/go.mod h1:yQqGHmheaQfkqiJWjklPHVAq1dKbk8uGbcoS/lcKCJ0= github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -42,8 +42,6 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= @@ -60,29 +58,27 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= -github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= diff --git a/examples/mouse/main.go b/examples/mouse/main.go index da923ad863..4227218242 100644 --- a/examples/mouse/main.go +++ b/examples/mouse/main.go @@ -31,8 +31,19 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit } - case tea.MouseMsg: - return m, tea.Printf("(X: %d, Y: %d) %s", msg.X, msg.Y, tea.MouseEvent(msg)) + case tea.MouseDownMsg, tea.MouseUpMsg, tea.MouseWheelMsg, tea.MouseMotionMsg: + var x, y int + switch msg := msg.(type) { + case tea.MouseDownMsg: + x, y = msg.X, msg.Y + case tea.MouseUpMsg: + x, y = msg.X, msg.Y + case tea.MouseWheelMsg: + x, y = msg.X, msg.Y + case tea.MouseMotionMsg: + x, y = msg.X, msg.Y + } + return m, tea.Printf("(X: %d, Y: %d) %s", x, y, msg) } return m, nil diff --git a/examples/pipe/main.go b/examples/pipe/main.go index a309566327..2c562b3a19 100644 --- a/examples/pipe/main.go +++ b/examples/pipe/main.go @@ -74,8 +74,8 @@ func (m model) Init() tea.Cmd { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if key, ok := msg.(tea.KeyMsg); ok { - switch key.Type { - case tea.KeyCtrlC, tea.KeyEscape, tea.KeyEnter: + switch key.String() { + case "ctrl+c", "esc", "enter": return m, tea.Quit } } diff --git a/examples/prevent-quit/main.go b/examples/prevent-quit/main.go index 7399d3088b..9904915a03 100644 --- a/examples/prevent-quit/main.go +++ b/examples/prevent-quit/main.go @@ -101,7 +101,7 @@ func (m model) updateTextView(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, m.keymap.quit): m.quitting = true return m, tea.Quit - case msg.Type == tea.KeyRunes: + case msg.Sym == tea.KeyNone && (msg.Mod == 0 || msg.Mod == tea.Shift): m.saveText = "" m.hasChanges = true fallthrough diff --git a/examples/simple/main_test.go b/examples/simple/main_test.go index e2d9f9730a..1da5158a37 100644 --- a/examples/simple/main_test.go +++ b/examples/simple/main_test.go @@ -27,7 +27,7 @@ func TestApp(t *testing.T) { tm.Type("I'm typing things, but it'll be ignored by my program") tm.Send("ignored msg") tm.Send(tea.KeyMsg{ - Type: tea.KeyEnter, + Sym: tea.KeyEnter, }) if err := tm.Quit(); err != nil { @@ -64,7 +64,7 @@ func TestAppInteractive(t *testing.T) { }, teatest.WithDuration(5*time.Second)) tm.Send(tea.KeyMsg{ - Type: tea.KeyEnter, + Sym: tea.KeyEnter, }) if err := tm.Quit(); err != nil { diff --git a/examples/textarea/main.go b/examples/textarea/main.go index 68d1097a72..f884004097 100644 --- a/examples/textarea/main.go +++ b/examples/textarea/main.go @@ -47,12 +47,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: - switch msg.Type { - case tea.KeyEsc: + switch msg.String() { + case "esc": if m.textarea.Focused() { m.textarea.Blur() } - case tea.KeyCtrlC: + case "ctrl+c": return m, tea.Quit default: if !m.textarea.Focused() { diff --git a/examples/textinput/main.go b/examples/textinput/main.go index 7c25344e6e..4e03f82b37 100644 --- a/examples/textinput/main.go +++ b/examples/textinput/main.go @@ -49,8 +49,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: - switch msg.Type { - case tea.KeyEnter, tea.KeyCtrlC, tea.KeyEsc: + switch msg.String() { + case "enter", "ctrl+c", "esc": return m, tea.Quit } diff --git a/go.mod b/go.mod index e1bdacba68..7493e4ac50 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,8 @@ module github.com/charmbracelet/bubbletea go 1.18 require ( - github.com/charmbracelet/x/exp/term v0.0.0-20240322170634-ebda89b611f2 + github.com/charmbracelet/x/exp/term v0.0.0-20240422143423-943add0689d8 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f - github.com/mattn/go-localereader v0.0.1 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 github.com/muesli/cancelreader v0.2.2 golang.org/x/sync v0.7.0 @@ -16,5 +15,5 @@ require ( require ( github.com/mattn/go-runewidth v0.0.15 // indirect github.com/rivo/uniseg v0.4.7 // indirect - golang.org/x/text v0.3.8 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect ) diff --git a/go.sum b/go.sum index 2a80ead905..4c269f595d 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,10 @@ github.com/charmbracelet/x/exp/term v0.0.0-20240322170634-ebda89b611f2 h1:53tB77HCI013H68JTiH1kX6rtSJQPB6Cu4AmXj3cCzs= github.com/charmbracelet/x/exp/term v0.0.0-20240322170634-ebda89b611f2/go.mod h1:madZtB2OVDOG+ZnLruGITVZceYy047W+BLQ1MNQzbWg= +github.com/charmbracelet/x/exp/term v0.0.0-20240422143423-943add0689d8 h1:m/dga5sTNG+jU1cojiv83uAZLgG2jpq8619I4Hb+mmQ= +github.com/charmbracelet/x/exp/term v0.0.0-20240422143423-943add0689d8/go.mod h1:yQqGHmheaQfkqiJWjklPHVAq1dKbk8uGbcoS/lcKCJ0= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= @@ -16,6 +16,9 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -23,6 +26,4 @@ golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/key.go b/key.go index aac07d02fa..570e804127 100644 --- a/key.go +++ b/key.go @@ -1,12 +1,7 @@ package tea import ( - "context" - "fmt" - "io" - "regexp" - "strings" - "unicode/utf8" + "github.com/charmbracelet/x/exp/term/input" ) // KeyMsg contains information about a keypress. KeyMsgs are always sent to @@ -27,12 +22,12 @@ import ( // // Switch on the key type (more foolproof) // switch msg := msg.(type) { // case KeyMsg: -// switch msg.Type { +// switch msg.Sym { // case KeyEnter: // fmt.Println("you pressed enter!") -// case KeyRunes: -// switch string(msg.Runes) { -// case "a": +// default: +// switch msg.Rune { +// case 'a': // fmt.Println("you pressed a!") // } // } @@ -42,667 +37,200 @@ import ( // always safely call Key.Runes[0]. In most cases Key.Runes will only contain // one character, though certain input method editors (most notably Chinese // IMEs) can input multiple runes at once. -type KeyMsg Key - -// String returns a string representation for a key message. It's safe (and -// encouraged) for use in key comparison. -func (k KeyMsg) String() (str string) { - return Key(k).String() -} - -// Key contains information about a keypress. -type Key struct { - Type KeyType - Runes []rune - Alt bool - Paste bool -} - -// String returns a friendly string representation for a key. It's safe (and -// encouraged) for use in key comparison. -// -// k := Key{Type: KeyEnter} -// fmt.Println(k) -// // Output: enter -func (k Key) String() (str string) { - var buf strings.Builder - if k.Alt { - buf.WriteString("alt+") - } - if k.Type == KeyRunes { - if k.Paste { - // Note: bubbles/keys bindings currently do string compares to - // recognize shortcuts. Since pasted text should never activate - // shortcuts, we need to ensure that the binding code doesn't - // match Key events that result from pastes. We achieve this - // here by enclosing pastes in '[...]' so that the string - // comparison in Matches() fails in that case. - buf.WriteByte('[') - } - buf.WriteString(string(k.Runes)) - if k.Paste { - buf.WriteByte(']') - } - return buf.String() - } else if s, ok := keyNames[k.Type]; ok { - buf.WriteString(s) - return buf.String() - } - return "" -} - -// KeyType indicates the key pressed, such as KeyEnter or KeyBreak or KeyCtrlC. -// All other keys will be type KeyRunes. To get the rune value, check the Rune -// method on a Key struct, or use the Key.String() method: -// -// k := Key{Type: KeyRunes, Runes: []rune{'a'}, Alt: true} -// if k.Type == KeyRunes { -// -// fmt.Println(k.Runes) -// // Output: a -// -// fmt.Println(k.String()) -// // Output: alt+a -// -// } -type KeyType int - -func (k KeyType) String() (str string) { - if s, ok := keyNames[k]; ok { - return s - } - return "" -} - -// Control keys. We could do this with an iota, but the values are very -// specific, so we set the values explicitly to avoid any confusion. -// -// See also: -// https://en.wikipedia.org/wiki/C0_and_C1_control_codes -const ( - keyNUL KeyType = 0 // null, \0 - keySOH KeyType = 1 // start of heading - keySTX KeyType = 2 // start of text - keyETX KeyType = 3 // break, ctrl+c - keyEOT KeyType = 4 // end of transmission - keyENQ KeyType = 5 // enquiry - keyACK KeyType = 6 // acknowledge - keyBEL KeyType = 7 // bell, \a - keyBS KeyType = 8 // backspace - keyHT KeyType = 9 // horizontal tabulation, \t - keyLF KeyType = 10 // line feed, \n - keyVT KeyType = 11 // vertical tabulation \v - keyFF KeyType = 12 // form feed \f - keyCR KeyType = 13 // carriage return, \r - keySO KeyType = 14 // shift out - keySI KeyType = 15 // shift in - keyDLE KeyType = 16 // data link escape - keyDC1 KeyType = 17 // device control one - keyDC2 KeyType = 18 // device control two - keyDC3 KeyType = 19 // device control three - keyDC4 KeyType = 20 // device control four - keyNAK KeyType = 21 // negative acknowledge - keySYN KeyType = 22 // synchronous idle - keyETB KeyType = 23 // end of transmission block - keyCAN KeyType = 24 // cancel - keyEM KeyType = 25 // end of medium - keySUB KeyType = 26 // substitution - keyESC KeyType = 27 // escape, \e - keyFS KeyType = 28 // file separator - keyGS KeyType = 29 // group separator - keyRS KeyType = 30 // record separator - keyUS KeyType = 31 // unit separator - keyDEL KeyType = 127 // delete. on most systems this is mapped to backspace, I hear -) - -// Control key aliases. -const ( - KeyNull KeyType = keyNUL - KeyBreak KeyType = keyETX - KeyEnter KeyType = keyCR - KeyBackspace KeyType = keyDEL - KeyTab KeyType = keyHT - KeyEsc KeyType = keyESC - KeyEscape KeyType = keyESC - - KeyCtrlAt KeyType = keyNUL // ctrl+@ - KeyCtrlA KeyType = keySOH - KeyCtrlB KeyType = keySTX - KeyCtrlC KeyType = keyETX - KeyCtrlD KeyType = keyEOT - KeyCtrlE KeyType = keyENQ - KeyCtrlF KeyType = keyACK - KeyCtrlG KeyType = keyBEL - KeyCtrlH KeyType = keyBS - KeyCtrlI KeyType = keyHT - KeyCtrlJ KeyType = keyLF - KeyCtrlK KeyType = keyVT - KeyCtrlL KeyType = keyFF - KeyCtrlM KeyType = keyCR - KeyCtrlN KeyType = keySO - KeyCtrlO KeyType = keySI - KeyCtrlP KeyType = keyDLE - KeyCtrlQ KeyType = keyDC1 - KeyCtrlR KeyType = keyDC2 - KeyCtrlS KeyType = keyDC3 - KeyCtrlT KeyType = keyDC4 - KeyCtrlU KeyType = keyNAK - KeyCtrlV KeyType = keySYN - KeyCtrlW KeyType = keyETB - KeyCtrlX KeyType = keyCAN - KeyCtrlY KeyType = keyEM - KeyCtrlZ KeyType = keySUB - KeyCtrlOpenBracket KeyType = keyESC // ctrl+[ - KeyCtrlBackslash KeyType = keyFS // ctrl+\ - KeyCtrlCloseBracket KeyType = keyGS // ctrl+] - KeyCtrlCaret KeyType = keyRS // ctrl+^ - KeyCtrlUnderscore KeyType = keyUS // ctrl+_ - KeyCtrlQuestionMark KeyType = keyDEL // ctrl+? +type ( + Key = input.Key + KeyMsg = input.KeyDownEvent + KeyDownMsg = input.KeyDownEvent + KeyUpMsg = input.KeyUpEvent ) -// Other keys. +// Modifier keys. const ( - KeyRunes KeyType = -(iota + 1) - KeyUp - KeyDown - KeyRight - KeyLeft - KeyShiftTab - KeyHome - KeyEnd - KeyPgUp - KeyPgDown - KeyCtrlPgUp - KeyCtrlPgDown - KeyDelete - KeyInsert - KeySpace - KeyCtrlUp - KeyCtrlDown - KeyCtrlRight - KeyCtrlLeft - KeyCtrlHome - KeyCtrlEnd - KeyShiftUp - KeyShiftDown - KeyShiftRight - KeyShiftLeft - KeyShiftHome - KeyShiftEnd - KeyCtrlShiftUp - KeyCtrlShiftDown - KeyCtrlShiftLeft - KeyCtrlShiftRight - KeyCtrlShiftHome - KeyCtrlShiftEnd - KeyF1 - KeyF2 - KeyF3 - KeyF4 - KeyF5 - KeyF6 - KeyF7 - KeyF8 - KeyF9 - KeyF10 - KeyF11 - KeyF12 - KeyF13 - KeyF14 - KeyF15 - KeyF16 - KeyF17 - KeyF18 - KeyF19 - KeyF20 -) - -// Mappings for control keys and other special keys to friendly consts. -var keyNames = map[KeyType]string{ - // Control keys. - keyNUL: "ctrl+@", // also ctrl+` (that's ctrl+backtick) - keySOH: "ctrl+a", - keySTX: "ctrl+b", - keyETX: "ctrl+c", - keyEOT: "ctrl+d", - keyENQ: "ctrl+e", - keyACK: "ctrl+f", - keyBEL: "ctrl+g", - keyBS: "ctrl+h", - keyHT: "tab", // also ctrl+i - keyLF: "ctrl+j", - keyVT: "ctrl+k", - keyFF: "ctrl+l", - keyCR: "enter", - keySO: "ctrl+n", - keySI: "ctrl+o", - keyDLE: "ctrl+p", - keyDC1: "ctrl+q", - keyDC2: "ctrl+r", - keyDC3: "ctrl+s", - keyDC4: "ctrl+t", - keyNAK: "ctrl+u", - keySYN: "ctrl+v", - keyETB: "ctrl+w", - keyCAN: "ctrl+x", - keyEM: "ctrl+y", - keySUB: "ctrl+z", - keyESC: "esc", - keyFS: "ctrl+\\", - keyGS: "ctrl+]", - keyRS: "ctrl+^", - keyUS: "ctrl+_", - keyDEL: "backspace", - - // Other keys. - KeyRunes: "runes", - KeyUp: "up", - KeyDown: "down", - KeyRight: "right", - KeySpace: " ", // for backwards compatibility - KeyLeft: "left", - KeyShiftTab: "shift+tab", - KeyHome: "home", - KeyEnd: "end", - KeyCtrlHome: "ctrl+home", - KeyCtrlEnd: "ctrl+end", - KeyShiftHome: "shift+home", - KeyShiftEnd: "shift+end", - KeyCtrlShiftHome: "ctrl+shift+home", - KeyCtrlShiftEnd: "ctrl+shift+end", - KeyPgUp: "pgup", - KeyPgDown: "pgdown", - KeyCtrlPgUp: "ctrl+pgup", - KeyCtrlPgDown: "ctrl+pgdown", - KeyDelete: "delete", - KeyInsert: "insert", - KeyCtrlUp: "ctrl+up", - KeyCtrlDown: "ctrl+down", - KeyCtrlRight: "ctrl+right", - KeyCtrlLeft: "ctrl+left", - KeyShiftUp: "shift+up", - KeyShiftDown: "shift+down", - KeyShiftRight: "shift+right", - KeyShiftLeft: "shift+left", - KeyCtrlShiftUp: "ctrl+shift+up", - KeyCtrlShiftDown: "ctrl+shift+down", - KeyCtrlShiftLeft: "ctrl+shift+left", - KeyCtrlShiftRight: "ctrl+shift+right", - KeyF1: "f1", - KeyF2: "f2", - KeyF3: "f3", - KeyF4: "f4", - KeyF5: "f5", - KeyF6: "f6", - KeyF7: "f7", - KeyF8: "f8", - KeyF9: "f9", - KeyF10: "f10", - KeyF11: "f11", - KeyF12: "f12", - KeyF13: "f13", - KeyF14: "f14", - KeyF15: "f15", - KeyF16: "f16", - KeyF17: "f17", - KeyF18: "f18", - KeyF19: "f19", - KeyF20: "f20", -} - -// Sequence mappings. -var sequences = map[string]Key{ - // Arrow keys - "\x1b[A": {Type: KeyUp}, - "\x1b[B": {Type: KeyDown}, - "\x1b[C": {Type: KeyRight}, - "\x1b[D": {Type: KeyLeft}, - "\x1b[1;2A": {Type: KeyShiftUp}, - "\x1b[1;2B": {Type: KeyShiftDown}, - "\x1b[1;2C": {Type: KeyShiftRight}, - "\x1b[1;2D": {Type: KeyShiftLeft}, - "\x1b[OA": {Type: KeyShiftUp}, // DECCKM - "\x1b[OB": {Type: KeyShiftDown}, // DECCKM - "\x1b[OC": {Type: KeyShiftRight}, // DECCKM - "\x1b[OD": {Type: KeyShiftLeft}, // DECCKM - "\x1b[a": {Type: KeyShiftUp}, // urxvt - "\x1b[b": {Type: KeyShiftDown}, // urxvt - "\x1b[c": {Type: KeyShiftRight}, // urxvt - "\x1b[d": {Type: KeyShiftLeft}, // urxvt - "\x1b[1;3A": {Type: KeyUp, Alt: true}, - "\x1b[1;3B": {Type: KeyDown, Alt: true}, - "\x1b[1;3C": {Type: KeyRight, Alt: true}, - "\x1b[1;3D": {Type: KeyLeft, Alt: true}, - - "\x1b[1;4A": {Type: KeyShiftUp, Alt: true}, - "\x1b[1;4B": {Type: KeyShiftDown, Alt: true}, - "\x1b[1;4C": {Type: KeyShiftRight, Alt: true}, - "\x1b[1;4D": {Type: KeyShiftLeft, Alt: true}, - - "\x1b[1;5A": {Type: KeyCtrlUp}, - "\x1b[1;5B": {Type: KeyCtrlDown}, - "\x1b[1;5C": {Type: KeyCtrlRight}, - "\x1b[1;5D": {Type: KeyCtrlLeft}, - "\x1b[Oa": {Type: KeyCtrlUp, Alt: true}, // urxvt - "\x1b[Ob": {Type: KeyCtrlDown, Alt: true}, // urxvt - "\x1b[Oc": {Type: KeyCtrlRight, Alt: true}, // urxvt - "\x1b[Od": {Type: KeyCtrlLeft, Alt: true}, // urxvt - "\x1b[1;6A": {Type: KeyCtrlShiftUp}, - "\x1b[1;6B": {Type: KeyCtrlShiftDown}, - "\x1b[1;6C": {Type: KeyCtrlShiftRight}, - "\x1b[1;6D": {Type: KeyCtrlShiftLeft}, - "\x1b[1;7A": {Type: KeyCtrlUp, Alt: true}, - "\x1b[1;7B": {Type: KeyCtrlDown, Alt: true}, - "\x1b[1;7C": {Type: KeyCtrlRight, Alt: true}, - "\x1b[1;7D": {Type: KeyCtrlLeft, Alt: true}, - "\x1b[1;8A": {Type: KeyCtrlShiftUp, Alt: true}, - "\x1b[1;8B": {Type: KeyCtrlShiftDown, Alt: true}, - "\x1b[1;8C": {Type: KeyCtrlShiftRight, Alt: true}, - "\x1b[1;8D": {Type: KeyCtrlShiftLeft, Alt: true}, - - // Miscellaneous keys - "\x1b[Z": {Type: KeyShiftTab}, - - "\x1b[2~": {Type: KeyInsert}, - "\x1b[3;2~": {Type: KeyInsert, Alt: true}, - - "\x1b[3~": {Type: KeyDelete}, - "\x1b[3;3~": {Type: KeyDelete, Alt: true}, - - "\x1b[5~": {Type: KeyPgUp}, - "\x1b[5;3~": {Type: KeyPgUp, Alt: true}, - "\x1b[5;5~": {Type: KeyCtrlPgUp}, - "\x1b[5^": {Type: KeyCtrlPgUp}, // urxvt - "\x1b[5;7~": {Type: KeyCtrlPgUp, Alt: true}, - - "\x1b[6~": {Type: KeyPgDown}, - "\x1b[6;3~": {Type: KeyPgDown, Alt: true}, - "\x1b[6;5~": {Type: KeyCtrlPgDown}, - "\x1b[6^": {Type: KeyCtrlPgDown}, // urxvt - "\x1b[6;7~": {Type: KeyCtrlPgDown, Alt: true}, - - "\x1b[1~": {Type: KeyHome}, - "\x1b[H": {Type: KeyHome}, // xterm, lxterm - "\x1b[1;3H": {Type: KeyHome, Alt: true}, // xterm, lxterm - "\x1b[1;5H": {Type: KeyCtrlHome}, // xterm, lxterm - "\x1b[1;7H": {Type: KeyCtrlHome, Alt: true}, // xterm, lxterm - "\x1b[1;2H": {Type: KeyShiftHome}, // xterm, lxterm - "\x1b[1;4H": {Type: KeyShiftHome, Alt: true}, // xterm, lxterm - "\x1b[1;6H": {Type: KeyCtrlShiftHome}, // xterm, lxterm - "\x1b[1;8H": {Type: KeyCtrlShiftHome, Alt: true}, // xterm, lxterm - - "\x1b[4~": {Type: KeyEnd}, - "\x1b[F": {Type: KeyEnd}, // xterm, lxterm - "\x1b[1;3F": {Type: KeyEnd, Alt: true}, // xterm, lxterm - "\x1b[1;5F": {Type: KeyCtrlEnd}, // xterm, lxterm - "\x1b[1;7F": {Type: KeyCtrlEnd, Alt: true}, // xterm, lxterm - "\x1b[1;2F": {Type: KeyShiftEnd}, // xterm, lxterm - "\x1b[1;4F": {Type: KeyShiftEnd, Alt: true}, // xterm, lxterm - "\x1b[1;6F": {Type: KeyCtrlShiftEnd}, // xterm, lxterm - "\x1b[1;8F": {Type: KeyCtrlShiftEnd, Alt: true}, // xterm, lxterm - - "\x1b[7~": {Type: KeyHome}, // urxvt - "\x1b[7^": {Type: KeyCtrlHome}, // urxvt - "\x1b[7$": {Type: KeyShiftHome}, // urxvt - "\x1b[7@": {Type: KeyCtrlShiftHome}, // urxvt - - "\x1b[8~": {Type: KeyEnd}, // urxvt - "\x1b[8^": {Type: KeyCtrlEnd}, // urxvt - "\x1b[8$": {Type: KeyShiftEnd}, // urxvt - "\x1b[8@": {Type: KeyCtrlShiftEnd}, // urxvt - - // Function keys, Linux console - "\x1b[[A": {Type: KeyF1}, // linux console - "\x1b[[B": {Type: KeyF2}, // linux console - "\x1b[[C": {Type: KeyF3}, // linux console - "\x1b[[D": {Type: KeyF4}, // linux console - "\x1b[[E": {Type: KeyF5}, // linux console - - // Function keys, X11 - "\x1bOP": {Type: KeyF1}, // vt100, xterm - "\x1bOQ": {Type: KeyF2}, // vt100, xterm - "\x1bOR": {Type: KeyF3}, // vt100, xterm - "\x1bOS": {Type: KeyF4}, // vt100, xterm + Shift = input.Shift + Alt = input.Alt + Ctrl = input.Ctrl + Meta = input.Meta - "\x1b[1;3P": {Type: KeyF1, Alt: true}, // vt100, xterm - "\x1b[1;3Q": {Type: KeyF2, Alt: true}, // vt100, xterm - "\x1b[1;3R": {Type: KeyF3, Alt: true}, // vt100, xterm - "\x1b[1;3S": {Type: KeyF4, Alt: true}, // vt100, xterm + // These modifiers are used with the Kitty protocol. - "\x1b[11~": {Type: KeyF1}, // urxvt - "\x1b[12~": {Type: KeyF2}, // urxvt - "\x1b[13~": {Type: KeyF3}, // urxvt - "\x1b[14~": {Type: KeyF4}, // urxvt + Hyper = input.Hyper + Super = input.Super // Windows/Command keys - "\x1b[15~": {Type: KeyF5}, // vt100, xterm, also urxvt + // These are key lock states. - "\x1b[15;3~": {Type: KeyF5, Alt: true}, // vt100, xterm, also urxvt - - "\x1b[17~": {Type: KeyF6}, // vt100, xterm, also urxvt - "\x1b[18~": {Type: KeyF7}, // vt100, xterm, also urxvt - "\x1b[19~": {Type: KeyF8}, // vt100, xterm, also urxvt - "\x1b[20~": {Type: KeyF9}, // vt100, xterm, also urxvt - "\x1b[21~": {Type: KeyF10}, // vt100, xterm, also urxvt - - "\x1b[17;3~": {Type: KeyF6, Alt: true}, // vt100, xterm - "\x1b[18;3~": {Type: KeyF7, Alt: true}, // vt100, xterm - "\x1b[19;3~": {Type: KeyF8, Alt: true}, // vt100, xterm - "\x1b[20;3~": {Type: KeyF9, Alt: true}, // vt100, xterm - "\x1b[21;3~": {Type: KeyF10, Alt: true}, // vt100, xterm - - "\x1b[23~": {Type: KeyF11}, // vt100, xterm, also urxvt - "\x1b[24~": {Type: KeyF12}, // vt100, xterm, also urxvt - - "\x1b[23;3~": {Type: KeyF11, Alt: true}, // vt100, xterm - "\x1b[24;3~": {Type: KeyF12, Alt: true}, // vt100, xterm - - "\x1b[1;2P": {Type: KeyF13}, - "\x1b[1;2Q": {Type: KeyF14}, - - "\x1b[25~": {Type: KeyF13}, // vt100, xterm, also urxvt - "\x1b[26~": {Type: KeyF14}, // vt100, xterm, also urxvt - - "\x1b[25;3~": {Type: KeyF13, Alt: true}, // vt100, xterm - "\x1b[26;3~": {Type: KeyF14, Alt: true}, // vt100, xterm - - "\x1b[1;2R": {Type: KeyF15}, - "\x1b[1;2S": {Type: KeyF16}, - - "\x1b[28~": {Type: KeyF15}, // vt100, xterm, also urxvt - "\x1b[29~": {Type: KeyF16}, // vt100, xterm, also urxvt - - "\x1b[28;3~": {Type: KeyF15, Alt: true}, // vt100, xterm - "\x1b[29;3~": {Type: KeyF16, Alt: true}, // vt100, xterm - - "\x1b[15;2~": {Type: KeyF17}, - "\x1b[17;2~": {Type: KeyF18}, - "\x1b[18;2~": {Type: KeyF19}, - "\x1b[19;2~": {Type: KeyF20}, - - "\x1b[31~": {Type: KeyF17}, - "\x1b[32~": {Type: KeyF18}, - "\x1b[33~": {Type: KeyF19}, - "\x1b[34~": {Type: KeyF20}, - - // Powershell sequences. - "\x1bOA": {Type: KeyUp, Alt: false}, - "\x1bOB": {Type: KeyDown, Alt: false}, - "\x1bOC": {Type: KeyRight, Alt: false}, - "\x1bOD": {Type: KeyLeft, Alt: false}, -} - -// unknownInputByteMsg is reported by the input reader when an invalid -// utf-8 byte is detected on the input. Currently, it is not handled -// further by bubbletea. However, having this event makes it possible -// to troubleshoot invalid inputs. -type unknownInputByteMsg byte - -func (u unknownInputByteMsg) String() string { - return fmt.Sprintf("?%#02x?", int(u)) -} - -// unknownCSISequenceMsg is reported by the input reader when an -// unrecognized CSI sequence is detected on the input. Currently, it -// is not handled further by bubbletea. However, having this event -// makes it possible to troubleshoot invalid inputs. -type unknownCSISequenceMsg []byte - -func (u unknownCSISequenceMsg) String() string { - return fmt.Sprintf("?CSI%+v?", []byte(u)[2:]) -} - -var spaceRunes = []rune{' '} - -// readAnsiInputs reads keypress and mouse inputs from a TTY and produces messages -// containing information about the key or mouse events accordingly. -func readAnsiInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error { - var buf [256]byte - - var leftOverFromPrevIteration []byte -loop: - for { - // Read and block. - numBytes, err := input.Read(buf[:]) - if err != nil { - return fmt.Errorf("error reading input: %w", err) - } - b := buf[:numBytes] - if leftOverFromPrevIteration != nil { - b = append(leftOverFromPrevIteration, b...) - } - - // If we had a short read (numBytes < len(buf)), we're sure that - // the end of this read is an event boundary, so there is no doubt - // if we are encountering the end of the buffer while parsing a message. - // However, if we've succeeded in filling up the buffer, there may - // be more data in the OS buffer ready to be read in, to complete - // the last message in the input. In that case, we will retry with - // the left over data in the next iteration. - canHaveMoreData := numBytes == len(buf) - - var i, w int - for i, w = 0, 0; i < len(b); i += w { - var msg Msg - w, msg = detectOneMsg(b[i:], canHaveMoreData) - if w == 0 { - // Expecting more bytes beyond the current buffer. Try waiting - // for more input. - leftOverFromPrevIteration = make([]byte, 0, len(b[i:])+len(buf)) - leftOverFromPrevIteration = append(leftOverFromPrevIteration, b[i:]...) - continue loop - } - - select { - case msgs <- msg: - case <-ctx.Done(): - err := ctx.Err() - if err != nil { - err = fmt.Errorf("found context error while reading input: %w", err) - } - return err - } - } - leftOverFromPrevIteration = nil - } -} - -var ( - unknownCSIRe = regexp.MustCompile(`^\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]`) - mouseSGRRegex = regexp.MustCompile(`(\d+);(\d+);(\d+)([Mm])`) + CapsLock = input.CapsLock + NumLock = input.NumLock + ScrollLock = input.ScrollLock // Defined in Windows API only ) -func detectOneMsg(b []byte, canHaveMoreData bool) (w int, msg Msg) { - // Detect mouse events. - // X10 mouse events have a length of 6 bytes - const mouseEventX10Len = 6 - if len(b) >= mouseEventX10Len && b[0] == '\x1b' && b[1] == '[' { - switch b[2] { - case 'M': - return mouseEventX10Len, MouseMsg(parseX10MouseEvent(b)) - case '<': - if matchIndices := mouseSGRRegex.FindSubmatchIndex(b[3:]); matchIndices != nil { - // SGR mouse events length is the length of the match plus the length of the escape sequence - mouseEventSGRLen := matchIndices[1] + 3 - return mouseEventSGRLen, MouseMsg(parseSGRMouseEvent(b)) - } - } - } - - // Detect bracketed paste. - var foundbp bool - foundbp, w, msg = detectBracketedPaste(b) - if foundbp { - return - } - - // Detect escape sequence and control characters other than NUL, - // possibly with an escape character in front to mark the Alt - // modifier. - var foundSeq bool - foundSeq, w, msg = detectSequence(b) - if foundSeq { - return - } - - // No non-NUL control character or escape sequence. - // If we are seeing at least an escape character, remember it for later below. - alt := false - i := 0 - if b[0] == '\x1b' { - alt = true - i++ - } - - // Are we seeing a standalone NUL? This is not handled by detectSequence(). - if i < len(b) && b[i] == 0 { - return i + 1, KeyMsg{Type: keyNUL, Alt: alt} - } - - // Find the longest sequence of runes that are not control - // characters from this point. - var runes []rune - for rw := 0; i < len(b); i += rw { - var r rune - r, rw = utf8.DecodeRune(b[i:]) - if r == utf8.RuneError || r <= rune(keyUS) || r == rune(keyDEL) || r == ' ' { - // Rune errors are handled below; control characters and spaces will - // be handled by detectSequence in the next call to detectOneMsg. - break - } - runes = append(runes, r) - if alt { - // We only support a single rune after an escape alt modifier. - i += rw - break - } - } - if i >= len(b) && canHaveMoreData { - // We have encountered the end of the input buffer. Alas, we can't - // be sure whether the data in the remainder of the buffer is - // complete (maybe there was a short read). Instead of sending anything - // dumb to the message channel, do a short read. The outer loop will - // handle this case by extending the buffer as necessary. - return 0, nil - } - - // If we found at least one rune, we report the bunch of them as - // a single KeyRunes or KeySpace event. - if len(runes) > 0 { - k := Key{Type: KeyRunes, Runes: runes, Alt: alt} - if len(runes) == 1 && runes[0] == ' ' { - k.Type = KeySpace - } - return i, KeyMsg(k) - } - - // We didn't find an escape sequence, nor a valid rune. Was this a - // lone escape character at the end of the input? - if alt && len(b) == 1 { - return 1, KeyMsg(Key{Type: KeyEscape}) - } - - // The character at the current position is neither an escape - // sequence, a valid rune start or a sole escape character. Report - // it as an invalid byte. - return 1, unknownInputByteMsg(b[0]) -} +// Symbol constants. +const ( + KeyNone = input.KeyNone + + // Special names in C0 + + KeyBackspace = input.KeyNone + KeyTab = input.KeyNone + KeyEnter = input.KeyNone + KeyEscape = input.KeyNone + + // Special names in G0 + + KeySpace = input.KeyNone + KeyDelete = input.KeyNone + + // Special keys + + KeyUp = input.KeyNone + KeyDown = input.KeyNone + KeyRight = input.KeyNone + KeyLeft = input.KeyNone + KeyBegin = input.KeyNone + KeyFind = input.KeyNone + KeyInsert = input.KeyNone + KeySelect = input.KeyNone + KeyPgUp = input.KeyNone + KeyPgDown = input.KeyNone + KeyHome = input.KeyNone + KeyEnd = input.KeyNone + + // Keypad keys + + KeyKpEnter = input.KeyNone + KeyKpEqual = input.KeyNone + KeyKpMultiply = input.KeyNone + KeyKpPlus = input.KeyNone + KeyKpComma = input.KeyNone + KeyKpMinus = input.KeyNone + KeyKpDecimal = input.KeyNone + KeyKpDivide = input.KeyNone + KeyKp0 = input.KeyNone + KeyKp1 = input.KeyNone + KeyKp2 = input.KeyNone + KeyKp3 = input.KeyNone + KeyKp4 = input.KeyNone + KeyKp5 = input.KeyNone + KeyKp6 = input.KeyNone + KeyKp7 = input.KeyNone + KeyKp8 = input.KeyNone + KeyKp9 = input.KeyNone + + // The following are keys defined in the Kitty keyboard protocol. + KeyKpSep = input.KeyNone + KeyKpUp = input.KeyNone + KeyKpDown = input.KeyNone + KeyKpLeft = input.KeyNone + KeyKpRight = input.KeyNone + KeyKpPgUp = input.KeyNone + KeyKpPgDown = input.KeyNone + KeyKpHome = input.KeyNone + KeyKpEnd = input.KeyNone + KeyKpInsert = input.KeyNone + KeyKpDelete = input.KeyNone + KeyKpBegin = input.KeyNone + + // Function keys + + KeyF1 = input.KeyNone + KeyF2 = input.KeyNone + KeyF3 = input.KeyNone + KeyF4 = input.KeyNone + KeyF5 = input.KeyNone + KeyF6 = input.KeyNone + KeyF7 = input.KeyNone + KeyF8 = input.KeyNone + KeyF9 = input.KeyNone + KeyF10 = input.KeyNone + KeyF11 = input.KeyNone + KeyF12 = input.KeyNone + KeyF13 = input.KeyNone + KeyF14 = input.KeyNone + KeyF15 = input.KeyNone + KeyF16 = input.KeyNone + KeyF17 = input.KeyNone + KeyF18 = input.KeyNone + KeyF19 = input.KeyNone + KeyF20 = input.KeyNone + KeyF21 = input.KeyNone + KeyF22 = input.KeyNone + KeyF23 = input.KeyNone + KeyF24 = input.KeyNone + KeyF25 = input.KeyNone + KeyF26 = input.KeyNone + KeyF27 = input.KeyNone + KeyF28 = input.KeyNone + KeyF29 = input.KeyNone + KeyF30 = input.KeyNone + KeyF31 = input.KeyNone + KeyF32 = input.KeyNone + KeyF33 = input.KeyNone + KeyF34 = input.KeyNone + KeyF35 = input.KeyNone + KeyF36 = input.KeyNone + KeyF37 = input.KeyNone + KeyF38 = input.KeyNone + KeyF39 = input.KeyNone + KeyF40 = input.KeyNone + KeyF41 = input.KeyNone + KeyF42 = input.KeyNone + KeyF43 = input.KeyNone + KeyF44 = input.KeyNone + KeyF45 = input.KeyNone + KeyF46 = input.KeyNone + KeyF47 = input.KeyNone + KeyF48 = input.KeyNone + KeyF49 = input.KeyNone + KeyF50 = input.KeyNone + KeyF51 = input.KeyNone + KeyF52 = input.KeyNone + KeyF53 = input.KeyNone + KeyF54 = input.KeyNone + KeyF55 = input.KeyNone + KeyF56 = input.KeyNone + KeyF57 = input.KeyNone + KeyF58 = input.KeyNone + KeyF59 = input.KeyNone + KeyF60 = input.KeyNone + KeyF61 = input.KeyNone + KeyF62 = input.KeyNone + KeyF63 = input.KeyNone + + // The following are keys defined in the Kitty keyboard protocol. + + KeyCapsLock = input.KeyNone + KeyScrollLock = input.KeyNone + KeyNumLock = input.KeyNone + KeyPrintScreen = input.KeyNone + KeyPause = input.KeyNone + KeyMenu = input.KeyNone + + KeyMediaPlay = input.KeyNone + KeyMediaPause = input.KeyNone + KeyMediaPlayPause = input.KeyNone + KeyMediaReverse = input.KeyNone + KeyMediaStop = input.KeyNone + KeyMediaFastForward = input.KeyNone + KeyMediaRewind = input.KeyNone + KeyMediaNext = input.KeyNone + KeyMediaPrev = input.KeyNone + KeyMediaRecord = input.KeyNone + + KeyLowerVol = input.KeyNone + KeyRaiseVol = input.KeyNone + KeyMute = input.KeyNone + + KeyLeftShift = input.KeyNone + KeyLeftAlt = input.KeyNone + KeyLeftCtrl = input.KeyNone + KeyLeftSuper = input.KeyNone + KeyLeftHyper = input.KeyNone + KeyLeftMeta = input.KeyNone + KeyRightShift = input.KeyNone + KeyRightAlt = input.KeyNone + KeyRightCtrl = input.KeyNone + KeyRightSuper = input.KeyNone + KeyRightHyper = input.KeyNone + KeyRightMeta = input.KeyNone + KeyIsoLevel3Shift = input.KeyNone + KeyIsoLevel5Shift = input.KeyNone +) diff --git a/key_other.go b/key_other.go deleted file mode 100644 index b8c46082f8..0000000000 --- a/key_other.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build !windows -// +build !windows - -package tea - -import ( - "context" - "io" -) - -func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error { - return readAnsiInputs(ctx, msgs, input) -} diff --git a/key_sequences.go b/key_sequences.go deleted file mode 100644 index 4ba0f79e34..0000000000 --- a/key_sequences.go +++ /dev/null @@ -1,119 +0,0 @@ -package tea - -import ( - "bytes" - "sort" - "unicode/utf8" -) - -// extSequences is used by the map-based algorithm below. It contains -// the sequences plus their alternatives with an escape character -// prefixed, plus the control chars, plus the space. -// It does not contain the NUL character, which is handled specially -// by detectOneMsg. -var extSequences = func() map[string]Key { - s := map[string]Key{} - for seq, key := range sequences { - key := key - s[seq] = key - if !key.Alt { - key.Alt = true - s["\x1b"+seq] = key - } - } - for i := keyNUL + 1; i <= keyDEL; i++ { - if i == keyESC { - continue - } - s[string([]byte{byte(i)})] = Key{Type: i} - s[string([]byte{'\x1b', byte(i)})] = Key{Type: i, Alt: true} - if i == keyUS { - i = keyDEL - 1 - } - } - s[" "] = Key{Type: KeySpace, Runes: spaceRunes} - s["\x1b "] = Key{Type: KeySpace, Alt: true, Runes: spaceRunes} - s["\x1b\x1b"] = Key{Type: KeyEscape, Alt: true} - return s -}() - -// seqLengths is the sizes of valid sequences, starting with the -// largest size. -var seqLengths = func() []int { - sizes := map[int]struct{}{} - for seq := range extSequences { - sizes[len(seq)] = struct{}{} - } - lsizes := make([]int, 0, len(sizes)) - for sz := range sizes { - lsizes = append(lsizes, sz) - } - sort.Slice(lsizes, func(i, j int) bool { return lsizes[i] > lsizes[j] }) - return lsizes -}() - -// detectSequence uses a longest prefix match over the input -// sequence and a hash map. -func detectSequence(input []byte) (hasSeq bool, width int, msg Msg) { - seqs := extSequences - for _, sz := range seqLengths { - if sz > len(input) { - continue - } - prefix := input[:sz] - key, ok := seqs[string(prefix)] - if ok { - return true, sz, KeyMsg(key) - } - } - // Is this an unknown CSI sequence? - if loc := unknownCSIRe.FindIndex(input); loc != nil { - return true, loc[1], unknownCSISequenceMsg(input[:loc[1]]) - } - - return false, 0, nil -} - -// detectBracketedPaste detects an input pasted while bracketed -// paste mode was enabled. -// -// Note: this function is a no-op if bracketed paste was not enabled -// on the terminal, since in that case we'd never see this -// particular escape sequence. -func detectBracketedPaste(input []byte) (hasBp bool, width int, msg Msg) { - // Detect the start sequence. - const bpStart = "\x1b[200~" - if len(input) < len(bpStart) || string(input[:len(bpStart)]) != bpStart { - return false, 0, nil - } - - // Skip over the start sequence. - input = input[len(bpStart):] - - // If we saw the start sequence, then we must have an end sequence - // as well. Find it. - const bpEnd = "\x1b[201~" - idx := bytes.Index(input, []byte(bpEnd)) - inputLen := len(bpStart) + idx + len(bpEnd) - if idx == -1 { - // We have encountered the end of the input buffer without seeing - // the marker for the end of the bracketed paste. - // Tell the outer loop we have done a short read and we want more. - return true, 0, nil - } - - // The paste is everything in-between. - paste := input[:idx] - - // All there is in-between is runes, not to be interpreted further. - k := Key{Type: KeyRunes, Paste: true} - for len(paste) > 0 { - r, w := utf8.DecodeRune(paste) - if r != utf8.RuneError { - k.Runes = append(k.Runes, r) - } - paste = paste[w:] - } - - return true, inputLen, KeyMsg(k) -} diff --git a/key_test.go b/key_test.go deleted file mode 100644 index 67b0c50ed5..0000000000 --- a/key_test.go +++ /dev/null @@ -1,694 +0,0 @@ -package tea - -import ( - "bytes" - "context" - "errors" - "flag" - "fmt" - "io" - "math/rand" - "reflect" - "runtime" - "sort" - "strings" - "sync" - "testing" - "time" -) - -func TestKeyString(t *testing.T) { - t.Run("alt+space", func(t *testing.T) { - if got := KeyMsg(Key{ - Type: KeySpace, - Alt: true, - }).String(); got != "alt+ " { - t.Fatalf(`expected a "alt+ ", got %q`, got) - } - }) - - t.Run("runes", func(t *testing.T) { - if got := KeyMsg(Key{ - Type: KeyRunes, - Runes: []rune{'a'}, - }).String(); got != "a" { - t.Fatalf(`expected an "a", got %q`, got) - } - }) - - t.Run("invalid", func(t *testing.T) { - if got := KeyMsg(Key{ - Type: KeyType(99999), - }).String(); got != "" { - t.Fatalf(`expected a "", got %q`, got) - } - }) -} - -func TestKeyTypeString(t *testing.T) { - t.Run("space", func(t *testing.T) { - if got := KeySpace.String(); got != " " { - t.Fatalf(`expected a " ", got %q`, got) - } - }) - - t.Run("invalid", func(t *testing.T) { - if got := KeyType(99999).String(); got != "" { - t.Fatalf(`expected a "", got %q`, got) - } - }) -} - -type seqTest struct { - seq []byte - msg Msg -} - -// buildBaseSeqTests returns sequence tests that are valid for the -// detectSequence() function. -func buildBaseSeqTests() []seqTest { - td := []seqTest{} - for seq, key := range sequences { - key := key - td = append(td, seqTest{[]byte(seq), KeyMsg(key)}) - if !key.Alt { - key.Alt = true - td = append(td, seqTest{[]byte("\x1b" + seq), KeyMsg(key)}) - } - } - // Add all the control characters. - for i := keyNUL + 1; i <= keyDEL; i++ { - if i == keyESC { - // Not handled in detectSequence(), so not part of the base test - // suite. - continue - } - td = append(td, seqTest{[]byte{byte(i)}, KeyMsg{Type: i}}) - td = append(td, seqTest{[]byte{'\x1b', byte(i)}, KeyMsg{Type: i, Alt: true}}) - if i == keyUS { - i = keyDEL - 1 - } - } - - // Additional special cases. - td = append(td, - // Unrecognized CSI sequence. - seqTest{ - []byte{'\x1b', '[', '-', '-', '-', '-', 'X'}, - unknownCSISequenceMsg([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'}), - }, - // A lone space character. - seqTest{ - []byte{' '}, - KeyMsg{Type: KeySpace, Runes: []rune(" ")}, - }, - // An escape character with the alt modifier. - seqTest{ - []byte{'\x1b', ' '}, - KeyMsg{Type: KeySpace, Runes: []rune(" "), Alt: true}, - }, - ) - return td -} - -func TestDetectSequence(t *testing.T) { - td := buildBaseSeqTests() - for _, tc := range td { - t.Run(fmt.Sprintf("%q", string(tc.seq)), func(t *testing.T) { - hasSeq, width, msg := detectSequence(tc.seq) - if !hasSeq { - t.Fatalf("no sequence found") - } - if width != len(tc.seq) { - t.Errorf("parser did not consume the entire input: got %d, expected %d", width, len(tc.seq)) - } - if !reflect.DeepEqual(tc.msg, msg) { - t.Errorf("expected event %#v (%T), got %#v (%T)", tc.msg, tc.msg, msg, msg) - } - }) - } -} - -func TestDetectOneMsg(t *testing.T) { - td := buildBaseSeqTests() - // Add tests for the inputs that detectOneMsg() can parse, but - // detectSequence() cannot. - td = append(td, - // Mouse event. - seqTest{ - []byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)}, - MouseMsg{X: 32, Y: 16, Type: MouseWheelUp, Button: MouseButtonWheelUp, Action: MouseActionPress}, - }, - // SGR Mouse event. - seqTest{ - []byte("\x1b[<0;33;17M"), - MouseMsg{X: 32, Y: 16, Type: MouseLeft, Button: MouseButtonLeft, Action: MouseActionPress}, - }, - // Runes. - seqTest{ - []byte{'a'}, - KeyMsg{Type: KeyRunes, Runes: []rune("a")}, - }, - seqTest{ - []byte{'\x1b', 'a'}, - KeyMsg{Type: KeyRunes, Runes: []rune("a"), Alt: true}, - }, - seqTest{ - []byte{'a', 'a', 'a'}, - KeyMsg{Type: KeyRunes, Runes: []rune("aaa")}, - }, - // Multi-byte rune. - seqTest{ - []byte("☃"), - KeyMsg{Type: KeyRunes, Runes: []rune("☃")}, - }, - seqTest{ - []byte("\x1b☃"), - KeyMsg{Type: KeyRunes, Runes: []rune("☃"), Alt: true}, - }, - // Standalone control chacters. - seqTest{ - []byte{'\x1b'}, - KeyMsg{Type: KeyEscape}, - }, - seqTest{ - []byte{byte(keySOH)}, - KeyMsg{Type: KeyCtrlA}, - }, - seqTest{ - []byte{'\x1b', byte(keySOH)}, - KeyMsg{Type: KeyCtrlA, Alt: true}, - }, - seqTest{ - []byte{byte(keyNUL)}, - KeyMsg{Type: KeyCtrlAt}, - }, - seqTest{ - []byte{'\x1b', byte(keyNUL)}, - KeyMsg{Type: KeyCtrlAt, Alt: true}, - }, - // Invalid characters. - seqTest{ - []byte{'\x80'}, - unknownInputByteMsg(0x80), - }, - ) - - if runtime.GOOS != "windows" { - // Sadly, utf8.DecodeRune([]byte(0xfe)) returns a valid rune on windows. - // This is incorrect, but it makes our test fail if we try it out. - td = append(td, seqTest{ - []byte{'\xfe'}, - unknownInputByteMsg(0xfe), - }) - } - - for _, tc := range td { - t.Run(fmt.Sprintf("%q", string(tc.seq)), func(t *testing.T) { - width, msg := detectOneMsg(tc.seq, false /* canHaveMoreData */) - if width != len(tc.seq) { - t.Errorf("parser did not consume the entire input: got %d, expected %d", width, len(tc.seq)) - } - if !reflect.DeepEqual(tc.msg, msg) { - t.Errorf("expected event %#v (%T), got %#v (%T)", tc.msg, tc.msg, msg, msg) - } - }) - } -} - -func TestReadLongInput(t *testing.T) { - input := strings.Repeat("a", 1000) - msgs := testReadInputs(t, bytes.NewReader([]byte(input))) - if len(msgs) != 1 { - t.Errorf("expected 1 messages, got %d", len(msgs)) - } - km := msgs[0] - k := Key(km.(KeyMsg)) - if k.Type != KeyRunes { - t.Errorf("expected key runes, got %d", k.Type) - } - if len(k.Runes) != 1000 || !reflect.DeepEqual(k.Runes, []rune(input)) { - t.Errorf("unexpected runes: %+v", k) - } - if k.Alt { - t.Errorf("unexpected alt") - } -} - -func TestReadInput(t *testing.T) { - type test struct { - keyname string - in []byte - out []Msg - } - testData := []test{ - {"a", - []byte{'a'}, - []Msg{ - KeyMsg{ - Type: KeyRunes, - Runes: []rune{'a'}, - }, - }, - }, - {" ", - []byte{' '}, - []Msg{ - KeyMsg{ - Type: KeySpace, - Runes: []rune{' '}, - }, - }, - }, - {"a alt+a", - []byte{'a', '\x1b', 'a'}, - []Msg{ - KeyMsg{Type: KeyRunes, Runes: []rune{'a'}}, - KeyMsg{Type: KeyRunes, Runes: []rune{'a'}, Alt: true}, - }, - }, - {"a alt+a a", - []byte{'a', '\x1b', 'a', 'a'}, - []Msg{ - KeyMsg{Type: KeyRunes, Runes: []rune{'a'}}, - KeyMsg{Type: KeyRunes, Runes: []rune{'a'}, Alt: true}, - KeyMsg{Type: KeyRunes, Runes: []rune{'a'}}, - }, - }, - {"ctrl+a", - []byte{byte(keySOH)}, - []Msg{ - KeyMsg{ - Type: KeyCtrlA, - }, - }, - }, - {"ctrl+a ctrl+b", - []byte{byte(keySOH), byte(keySTX)}, - []Msg{ - KeyMsg{Type: KeyCtrlA}, - KeyMsg{Type: KeyCtrlB}, - }, - }, - {"alt+a", - []byte{byte(0x1b), 'a'}, - []Msg{ - KeyMsg{ - Type: KeyRunes, - Alt: true, - Runes: []rune{'a'}, - }, - }, - }, - {"abcd", - []byte{'a', 'b', 'c', 'd'}, - []Msg{ - KeyMsg{ - Type: KeyRunes, - Runes: []rune{'a', 'b', 'c', 'd'}, - }, - }, - }, - {"up", - []byte("\x1b[A"), - []Msg{ - KeyMsg{ - Type: KeyUp, - }, - }, - }, - {"wheel up", - []byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)}, - []Msg{ - MouseMsg{ - X: 32, - Y: 16, - Type: MouseWheelUp, - Button: MouseButtonWheelUp, - Action: MouseActionPress, - }, - }, - }, - {"left motion release", - []byte{ - '\x1b', '[', 'M', byte(32) + 0b0010_0000, byte(32 + 33), byte(16 + 33), - '\x1b', '[', 'M', byte(32) + 0b0000_0011, byte(64 + 33), byte(32 + 33), - }, - []Msg{ - MouseMsg(MouseEvent{ - X: 32, - Y: 16, - Type: MouseLeft, - Button: MouseButtonLeft, - Action: MouseActionMotion, - }), - MouseMsg(MouseEvent{ - X: 64, - Y: 32, - Type: MouseRelease, - Button: MouseButtonNone, - Action: MouseActionRelease, - }), - }, - }, - {"shift+tab", - []byte{'\x1b', '[', 'Z'}, - []Msg{ - KeyMsg{ - Type: KeyShiftTab, - }, - }, - }, - {"enter", - []byte{'\r'}, - []Msg{KeyMsg{Type: KeyEnter}}, - }, - {"alt+enter", - []byte{'\x1b', '\r'}, - []Msg{ - KeyMsg{ - Type: KeyEnter, - Alt: true, - }, - }, - }, - {"insert", - []byte{'\x1b', '[', '2', '~'}, - []Msg{ - KeyMsg{ - Type: KeyInsert, - }, - }, - }, - {"alt+ctrl+a", - []byte{'\x1b', byte(keySOH)}, - []Msg{ - KeyMsg{ - Type: KeyCtrlA, - Alt: true, - }, - }, - }, - {"?CSI[45 45 45 45 88]?", - []byte{'\x1b', '[', '-', '-', '-', '-', 'X'}, - []Msg{unknownCSISequenceMsg([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'})}, - }, - // Powershell sequences. - {"up", - []byte{'\x1b', 'O', 'A'}, - []Msg{KeyMsg{Type: KeyUp}}, - }, - {"down", - []byte{'\x1b', 'O', 'B'}, - []Msg{KeyMsg{Type: KeyDown}}, - }, - {"right", - []byte{'\x1b', 'O', 'C'}, - []Msg{KeyMsg{Type: KeyRight}}, - }, - {"left", - []byte{'\x1b', 'O', 'D'}, - []Msg{KeyMsg{Type: KeyLeft}}, - }, - {"alt+enter", - []byte{'\x1b', '\x0d'}, - []Msg{KeyMsg{Type: KeyEnter, Alt: true}}, - }, - {"alt+backspace", - []byte{'\x1b', '\x7f'}, - []Msg{KeyMsg{Type: KeyBackspace, Alt: true}}, - }, - {"ctrl+@", - []byte{'\x00'}, - []Msg{KeyMsg{Type: KeyCtrlAt}}, - }, - {"alt+ctrl+@", - []byte{'\x1b', '\x00'}, - []Msg{KeyMsg{Type: KeyCtrlAt, Alt: true}}, - }, - {"esc", - []byte{'\x1b'}, - []Msg{KeyMsg{Type: KeyEsc}}, - }, - {"alt+esc", - []byte{'\x1b', '\x1b'}, - []Msg{KeyMsg{Type: KeyEsc, Alt: true}}, - }, - {"[a b] o", - []byte{ - '\x1b', '[', '2', '0', '0', '~', - 'a', ' ', 'b', - '\x1b', '[', '2', '0', '1', '~', - 'o', - }, - []Msg{ - KeyMsg{Type: KeyRunes, Runes: []rune("a b"), Paste: true}, - KeyMsg{Type: KeyRunes, Runes: []rune("o")}, - }, - }, - {"[a\x03\nb]", - []byte{ - '\x1b', '[', '2', '0', '0', '~', - 'a', '\x03', '\n', 'b', - '\x1b', '[', '2', '0', '1', '~'}, - []Msg{ - KeyMsg{Type: KeyRunes, Runes: []rune("a\x03\nb"), Paste: true}, - }, - }, - } - if runtime.GOOS != "windows" { - // Sadly, utf8.DecodeRune([]byte(0xfe)) returns a valid rune on windows. - // This is incorrect, but it makes our test fail if we try it out. - testData = append(testData, - test{"?0xfe?", - []byte{'\xfe'}, - []Msg{unknownInputByteMsg(0xfe)}, - }, - test{"a ?0xfe? b", - []byte{'a', '\xfe', ' ', 'b'}, - []Msg{ - KeyMsg{Type: KeyRunes, Runes: []rune{'a'}}, - unknownInputByteMsg(0xfe), - KeyMsg{Type: KeySpace, Runes: []rune{' '}}, - KeyMsg{Type: KeyRunes, Runes: []rune{'b'}}, - }, - }, - ) - } - - for i, td := range testData { - t.Run(fmt.Sprintf("%d: %s", i, td.keyname), func(t *testing.T) { - msgs := testReadInputs(t, bytes.NewReader(td.in)) - var buf strings.Builder - for i, msg := range msgs { - if i > 0 { - buf.WriteByte(' ') - } - if s, ok := msg.(fmt.Stringer); ok { - buf.WriteString(s.String()) - } else { - fmt.Fprintf(&buf, "%#v:%T", msg, msg) - } - } - - title := buf.String() - if title != td.keyname { - t.Errorf("expected message titles:\n %s\ngot:\n %s", td.keyname, title) - } - - if len(msgs) != len(td.out) { - t.Fatalf("unexpected message list length: got %d, expected %d\n%#v", len(msgs), len(td.out), msgs) - } - - if !reflect.DeepEqual(td.out, msgs) { - t.Fatalf("expected:\n%#v\ngot:\n%#v", td.out, msgs) - } - }) - } -} - -func testReadInputs(t *testing.T, input io.Reader) []Msg { - // We'll check that the input reader finishes at the end - // without error. - var wg sync.WaitGroup - var inputErr error - ctx, cancel := context.WithCancel(context.Background()) - defer func() { - cancel() - wg.Wait() - if inputErr != nil && !errors.Is(inputErr, io.EOF) { - t.Fatalf("unexpected input error: %v", inputErr) - } - }() - - // The messages we're consuming. - msgsC := make(chan Msg) - - // Start the reader in the background. - wg.Add(1) - go func() { - defer wg.Done() - inputErr = readAnsiInputs(ctx, msgsC, input) - msgsC <- nil - }() - - var msgs []Msg -loop: - for { - select { - case msg := <-msgsC: - if msg == nil { - // end of input marker for the test. - break loop - } - msgs = append(msgs, msg) - case <-time.After(2 * time.Second): - t.Errorf("timeout waiting for input event") - break loop - } - } - return msgs -} - -// randTest defines the test input and expected output for a sequence -// of interleaved control sequences and control characters. -type randTest struct { - data []byte - lengths []int - names []string -} - -// seed is the random seed to randomize the input. This helps check -// that all the sequences get ultimately exercised. -var seed = flag.Int64("seed", 0, "random seed (0 to autoselect)") - -// genRandomData generates a randomized test, with a random seed unless -// the seed flag was set. -func genRandomData(logfn func(int64), length int) randTest { - // We'll use a random source. However, we give the user the option - // to override it to a specific value for reproduceability. - s := *seed - if s == 0 { - s = time.Now().UnixNano() - } - // Inform the user so they know what to reuse to get the same data. - logfn(s) - return genRandomDataWithSeed(s, length) -} - -// genRandomDataWithSeed generates a randomized test with a fixed seed. -func genRandomDataWithSeed(s int64, length int) randTest { - src := rand.NewSource(s) - r := rand.New(src) - - // allseqs contains all the sequences, in sorted order. We sort - // to make the test deterministic (when the seed is also fixed). - type seqpair struct { - seq string - name string - } - var allseqs []seqpair - for seq, key := range sequences { - allseqs = append(allseqs, seqpair{seq, key.String()}) - } - sort.Slice(allseqs, func(i, j int) bool { return allseqs[i].seq < allseqs[j].seq }) - - // res contains the computed test. - var res randTest - - for len(res.data) < length { - alt := r.Intn(2) - prefix := "" - esclen := 0 - if alt == 1 { - prefix = "alt+" - esclen = 1 - } - kind := r.Intn(3) - switch kind { - case 0: - // A control character. - if alt == 1 { - res.data = append(res.data, '\x1b') - } - res.data = append(res.data, 1) - res.names = append(res.names, prefix+"ctrl+a") - res.lengths = append(res.lengths, 1+esclen) - - case 1, 2: - // A sequence. - seqi := r.Intn(len(allseqs)) - s := allseqs[seqi] - if strings.HasPrefix(s.name, "alt+") { - esclen = 0 - prefix = "" - alt = 0 - } - if alt == 1 { - res.data = append(res.data, '\x1b') - } - res.data = append(res.data, s.seq...) - res.names = append(res.names, prefix+s.name) - res.lengths = append(res.lengths, len(s.seq)+esclen) - } - } - return res -} - -// TestDetectRandomSequencesLex checks that the lex-generated sequence -// detector works over concatenations of random sequences. -func TestDetectRandomSequencesLex(t *testing.T) { - runTestDetectSequence(t, detectSequence) -} - -func runTestDetectSequence( - t *testing.T, detectSequence func(input []byte) (hasSeq bool, width int, msg Msg), -) { - for i := 0; i < 10; i++ { - t.Run("", func(t *testing.T) { - td := genRandomData(func(s int64) { t.Logf("using random seed: %d", s) }, 1000) - - t.Logf("%#v", td) - - // tn is the event number in td. - // i is the cursor in the input data. - // w is the length of the last sequence detected. - for tn, i, w := 0, 0, 0; i < len(td.data); tn, i = tn+1, i+w { - hasSequence, width, msg := detectSequence(td.data[i:]) - if !hasSequence { - t.Fatalf("at %d (ev %d): failed to find sequence", i, tn) - } - if width != td.lengths[tn] { - t.Errorf("at %d (ev %d): expected width %d, got %d", i, tn, td.lengths[tn], width) - } - w = width - - s, ok := msg.(fmt.Stringer) - if !ok { - t.Errorf("at %d (ev %d): expected stringer event, got %T", i, tn, msg) - } else { - if td.names[tn] != s.String() { - t.Errorf("at %d (ev %d): expected event %q, got %q", i, tn, td.names[tn], s.String()) - } - } - } - }) - } -} - -// TestDetectRandomSequencesMap checks that the map-based sequence -// detector works over concatenations of random sequences. -func TestDetectRandomSequencesMap(t *testing.T) { - runTestDetectSequence(t, detectSequence) -} - -// BenchmarkDetectSequenceMap benchmarks the map-based sequence -// detector. -func BenchmarkDetectSequenceMap(b *testing.B) { - td := genRandomDataWithSeed(123, 10000) - for i := 0; i < b.N; i++ { - for j, w := 0, 0; j < len(td.data); j += w { - _, w, _ = detectSequence(td.data[j:]) - } - } -} diff --git a/key_windows.go b/key_windows.go deleted file mode 100644 index c4b8aa75fb..0000000000 --- a/key_windows.go +++ /dev/null @@ -1,277 +0,0 @@ -//go:build windows -// +build windows - -package tea - -import ( - "context" - "fmt" - "io" - - "github.com/erikgeiser/coninput" - localereader "github.com/mattn/go-localereader" - "golang.org/x/sys/windows" -) - -func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error { - if coninReader, ok := input.(*conInputReader); ok { - return readConInputs(ctx, msgs, coninReader.conin) - } - - return readAnsiInputs(ctx, msgs, localereader.NewReader(input)) -} - -func readConInputs(ctx context.Context, msgsch chan<- Msg, con windows.Handle) error { - var ps coninput.ButtonState // keep track of previous mouse state - for { - events, err := coninput.ReadNConsoleInputs(con, 16) - if err != nil { - return fmt.Errorf("read coninput events: %w", err) - } - - for _, event := range events { - var msgs []Msg - switch e := event.Unwrap().(type) { - case coninput.KeyEventRecord: - if !e.KeyDown || e.VirtualKeyCode == coninput.VK_SHIFT { - continue - } - - for i := 0; i < int(e.RepeatCount); i++ { - msgs = append(msgs, KeyMsg{ - Type: keyType(e), - Runes: []rune{e.Char}, - Alt: e.ControlKeyState.Contains(coninput.LEFT_ALT_PRESSED | coninput.RIGHT_ALT_PRESSED), - }) - } - case coninput.WindowBufferSizeEventRecord: - msgs = append(msgs, WindowSizeMsg{ - Width: int(e.Size.X), - Height: int(e.Size.Y), - }) - case coninput.MouseEventRecord: - event := mouseEvent(ps, e) - if event.Type != MouseUnknown { - msgs = append(msgs, event) - } - ps = e.ButtonState - case coninput.FocusEventRecord, coninput.MenuEventRecord: - // ignore - default: // unknown event - continue - } - - // Send all messages to the channel - for _, msg := range msgs { - select { - case msgsch <- msg: - case <-ctx.Done(): - err := ctx.Err() - if err != nil { - return fmt.Errorf("coninput context error: %w", err) - } - return err - } - } - } - } -} - -func mouseEventButton(p, s coninput.ButtonState) (button MouseButton, action MouseAction) { - btn := p ^ s - action = MouseActionPress - if btn&s == 0 { - action = MouseActionRelease - } - - if btn == 0 { - switch { - case s&coninput.FROM_LEFT_1ST_BUTTON_PRESSED > 0: - button = MouseButtonLeft - case s&coninput.FROM_LEFT_2ND_BUTTON_PRESSED > 0: - button = MouseButtonMiddle - case s&coninput.RIGHTMOST_BUTTON_PRESSED > 0: - button = MouseButtonRight - case s&coninput.FROM_LEFT_3RD_BUTTON_PRESSED > 0: - button = MouseButtonBackward - case s&coninput.FROM_LEFT_4TH_BUTTON_PRESSED > 0: - button = MouseButtonForward - } - return - } - - switch { - case btn == coninput.FROM_LEFT_1ST_BUTTON_PRESSED: // left button - button = MouseButtonLeft - case btn == coninput.RIGHTMOST_BUTTON_PRESSED: // right button - button = MouseButtonRight - case btn == coninput.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button - button = MouseButtonMiddle - case btn == coninput.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward) - button = MouseButtonBackward - case btn == coninput.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward) - button = MouseButtonForward - } - - return button, action -} - -func mouseEvent(p coninput.ButtonState, e coninput.MouseEventRecord) MouseMsg { - ev := MouseMsg{ - X: int(e.MousePositon.X), - Y: int(e.MousePositon.Y), - Alt: e.ControlKeyState.Contains(coninput.LEFT_ALT_PRESSED | coninput.RIGHT_ALT_PRESSED), - Ctrl: e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED | coninput.RIGHT_CTRL_PRESSED), - Shift: e.ControlKeyState.Contains(coninput.SHIFT_PRESSED), - } - switch e.EventFlags { - case coninput.CLICK, coninput.DOUBLE_CLICK: - ev.Button, ev.Action = mouseEventButton(p, e.ButtonState) - if ev.Action == MouseActionRelease { - ev.Type = MouseRelease - } - switch ev.Button { - case MouseButtonLeft: - ev.Type = MouseLeft - case MouseButtonMiddle: - ev.Type = MouseMiddle - case MouseButtonRight: - ev.Type = MouseRight - case MouseButtonBackward: - ev.Type = MouseBackward - case MouseButtonForward: - ev.Type = MouseForward - } - case coninput.MOUSE_WHEELED: - if e.WheelDirection > 0 { - ev.Button = MouseButtonWheelUp - ev.Type = MouseWheelUp - } else { - ev.Button = MouseButtonWheelDown - ev.Type = MouseWheelDown - } - case coninput.MOUSE_HWHEELED: - if e.WheelDirection > 0 { - ev.Button = MouseButtonWheelRight - ev.Type = MouseWheelRight - } else { - ev.Button = MouseButtonWheelLeft - ev.Type = MouseWheelLeft - } - case coninput.MOUSE_MOVED: - ev.Button, _ = mouseEventButton(p, e.ButtonState) - ev.Action = MouseActionMotion - ev.Type = MouseMotion - } - - return ev -} - -func keyType(e coninput.KeyEventRecord) KeyType { - code := e.VirtualKeyCode - - switch code { - case coninput.VK_RETURN: - return KeyEnter - case coninput.VK_BACK: - return KeyBackspace - case coninput.VK_TAB: - return KeyTab - case coninput.VK_SPACE: - return KeyRunes // this could be KeySpace but on unix space also produces KeyRunes - case coninput.VK_ESCAPE: - return KeyEscape - case coninput.VK_UP: - return KeyUp - case coninput.VK_DOWN: - return KeyDown - case coninput.VK_RIGHT: - return KeyRight - case coninput.VK_LEFT: - return KeyLeft - case coninput.VK_HOME: - return KeyHome - case coninput.VK_END: - return KeyEnd - case coninput.VK_PRIOR: - return KeyPgUp - case coninput.VK_NEXT: - return KeyPgDown - case coninput.VK_DELETE: - return KeyDelete - default: - if e.ControlKeyState&(coninput.LEFT_CTRL_PRESSED|coninput.RIGHT_CTRL_PRESSED) == 0 { - return KeyRunes - } - - switch e.Char { - case '@': - return KeyCtrlAt - case '\x01': - return KeyCtrlA - case '\x02': - return KeyCtrlB - case '\x03': - return KeyCtrlC - case '\x04': - return KeyCtrlD - case '\x05': - return KeyCtrlE - case '\x06': - return KeyCtrlF - case '\a': - return KeyCtrlG - case '\b': - return KeyCtrlH - case '\t': - return KeyCtrlI - case '\n': - return KeyCtrlJ - case '\v': - return KeyCtrlK - case '\f': - return KeyCtrlL - case '\r': - return KeyCtrlM - case '\x0e': - return KeyCtrlN - case '\x0f': - return KeyCtrlO - case '\x10': - return KeyCtrlP - case '\x11': - return KeyCtrlQ - case '\x12': - return KeyCtrlR - case '\x13': - return KeyCtrlS - case '\x14': - return KeyCtrlT - case '\x15': - return KeyCtrlU - case '\x16': - return KeyCtrlV - case '\x17': - return KeyCtrlW - case '\x18': - return KeyCtrlX - case '\x19': - return KeyCtrlY - case '\x1a': - return KeyCtrlZ - case '\x1b': - return KeyCtrlCloseBracket - case '\x1c': - return KeyCtrlBackslash - case '\x1f': - return KeyCtrlUnderscore - } - - switch code { - case coninput.VK_OEM_4: - return KeyCtrlOpenBracket - } - - return KeyRunes - } -} diff --git a/mouse.go b/mouse.go index add8d02931..0d60d57e65 100644 --- a/mouse.go +++ b/mouse.go @@ -1,91 +1,21 @@ package tea -import "strconv" +import ( + "github.com/charmbracelet/x/exp/term/input" +) // MouseMsg contains information about a mouse event and are sent to a programs // update function when mouse activity occurs. Note that the mouse must first // be enabled in order for the mouse events to be received. -type MouseMsg MouseEvent - -// String returns a string representation of a mouse event. -func (m MouseMsg) String() string { - return MouseEvent(m).String() -} - -// MouseEvent represents a mouse event, which could be a click, a scroll wheel -// movement, a cursor movement, or a combination. -type MouseEvent struct { - X int - Y int - Shift bool - Alt bool - Ctrl bool - Action MouseAction - Button MouseButton - - // Deprecated: Use MouseAction & MouseButton instead. - Type MouseEventType -} - -// IsWheel returns true if the mouse event is a wheel event. -func (m MouseEvent) IsWheel() bool { - return m.Button == MouseButtonWheelUp || m.Button == MouseButtonWheelDown || - m.Button == MouseButtonWheelLeft || m.Button == MouseButtonWheelRight -} - -// String returns a string representation of a mouse event. -func (m MouseEvent) String() (s string) { - if m.Ctrl { - s += "ctrl+" - } - if m.Alt { - s += "alt+" - } - if m.Shift { - s += "shift+" - } - - if m.Button == MouseButtonNone { - if m.Action == MouseActionMotion || m.Action == MouseActionRelease { - s += mouseActions[m.Action] - } else { - s += "unknown" - } - } else if m.IsWheel() { - s += mouseButtons[m.Button] - } else { - btn := mouseButtons[m.Button] - if btn != "" { - s += btn - } - act := mouseActions[m.Action] - if act != "" { - s += " " + act - } - } - - return s -} - -// MouseAction represents the action that occurred during a mouse event. -type MouseAction int - -// Mouse event actions. -const ( - MouseActionPress MouseAction = iota - MouseActionRelease - MouseActionMotion +type ( + MouseEvent = input.Mouse + MouseMsg = input.MouseDownEvent + MouseDownMsg = input.MouseDownEvent + MouseUpMsg = input.MouseUpEvent + MouseWheelMsg = input.MouseWheelEvent + MouseMotionMsg = input.MouseMotionEvent ) -var mouseActions = map[MouseAction]string{ - MouseActionPress: "press", - MouseActionRelease: "release", - MouseActionMotion: "motion", -} - -// MouseButton represents the button that was pressed during a mouse event. -type MouseButton int - // Mouse event buttons // // This is based on X11 mouse button codes. @@ -104,205 +34,16 @@ type MouseButton int // // Other buttons are not supported. const ( - MouseButtonNone MouseButton = iota - MouseButtonLeft - MouseButtonMiddle - MouseButtonRight - MouseButtonWheelUp - MouseButtonWheelDown - MouseButtonWheelLeft - MouseButtonWheelRight - MouseButtonBackward - MouseButtonForward - MouseButton10 - MouseButton11 + MouseNone = input.MouseNone + MouseLeft = input.MouseLeft + MouseMiddle = input.MouseMiddle + MouseRight = input.MouseRight + MouseWheelUp = input.MouseWheelUp + MouseWheelDown = input.MouseWheelDown + MouseWheelLeft = input.MouseWheelLeft + MouseWheelRight = input.MouseWheelRight + MouseBackward = input.MouseBackward + MouseForward = input.MouseForward + MouseExtra1 = input.MouseExtra1 + MouseExtra2 = input.MouseExtra2 ) - -var mouseButtons = map[MouseButton]string{ - MouseButtonNone: "none", - MouseButtonLeft: "left", - MouseButtonMiddle: "middle", - MouseButtonRight: "right", - MouseButtonWheelUp: "wheel up", - MouseButtonWheelDown: "wheel down", - MouseButtonWheelLeft: "wheel left", - MouseButtonWheelRight: "wheel right", - MouseButtonBackward: "backward", - MouseButtonForward: "forward", - MouseButton10: "button 10", - MouseButton11: "button 11", -} - -// MouseEventType indicates the type of mouse event occurring. -// -// Deprecated: Use MouseAction & MouseButton instead. -type MouseEventType int - -// Mouse event types. -// -// Deprecated: Use MouseAction & MouseButton instead. -const ( - MouseUnknown MouseEventType = iota - MouseLeft - MouseRight - MouseMiddle - MouseRelease // mouse button release (X10 only) - MouseWheelUp - MouseWheelDown - MouseWheelLeft - MouseWheelRight - MouseBackward - MouseForward - MouseMotion -) - -// Parse SGR-encoded mouse events; SGR extended mouse events. SGR mouse events -// look like: -// -// ESC [ < Cb ; Cx ; Cy (M or m) -// -// where: -// -// Cb is the encoded button code -// Cx is the x-coordinate of the mouse -// Cy is the y-coordinate of the mouse -// M is for button press, m is for button release -// -// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates -func parseSGRMouseEvent(buf []byte) MouseEvent { - str := string(buf[3:]) - matches := mouseSGRRegex.FindStringSubmatch(str) - if len(matches) != 5 { - // Unreachable, we already checked the regex in `detectOneMsg`. - panic("invalid mouse event") - } - - b, _ := strconv.Atoi(matches[1]) - px := matches[2] - py := matches[3] - release := matches[4] == "m" - m := parseMouseButton(b, true) - - // Wheel buttons don't have release events - // Motion can be reported as a release event in some terminals (Windows Terminal) - if m.Action != MouseActionMotion && !m.IsWheel() && release { - m.Action = MouseActionRelease - m.Type = MouseRelease - } - - x, _ := strconv.Atoi(px) - y, _ := strconv.Atoi(py) - - // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). - m.X = x - 1 - m.Y = y - 1 - - return m -} - -const x10MouseByteOffset = 32 - -// Parse X10-encoded mouse events; the simplest kind. The last release of X10 -// was December 1986, by the way. The original X10 mouse protocol limits the Cx -// and Cy coordinates to 223 (=255-032). -// -// X10 mouse events look like: -// -// ESC [M Cb Cx Cy -// -// See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking -func parseX10MouseEvent(buf []byte) MouseEvent { - v := buf[3:6] - m := parseMouseButton(int(v[0]), false) - - // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). - m.X = int(v[1]) - x10MouseByteOffset - 1 - m.Y = int(v[2]) - x10MouseByteOffset - 1 - - return m -} - -// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates -func parseMouseButton(b int, isSGR bool) MouseEvent { - var m MouseEvent - e := b - if !isSGR { - e -= x10MouseByteOffset - } - - const ( - bitShift = 0b0000_0100 - bitAlt = 0b0000_1000 - bitCtrl = 0b0001_0000 - bitMotion = 0b0010_0000 - bitWheel = 0b0100_0000 - bitAdd = 0b1000_0000 // additional buttons 8-11 - - bitsMask = 0b0000_0011 - ) - - if e&bitAdd != 0 { - m.Button = MouseButtonBackward + MouseButton(e&bitsMask) - } else if e&bitWheel != 0 { - m.Button = MouseButtonWheelUp + MouseButton(e&bitsMask) - } else { - m.Button = MouseButtonLeft + MouseButton(e&bitsMask) - // X10 reports a button release as 0b0000_0011 (3) - if e&bitsMask == bitsMask { - m.Action = MouseActionRelease - m.Button = MouseButtonNone - } - } - - // Motion bit doesn't get reported for wheel events. - if e&bitMotion != 0 && !m.IsWheel() { - m.Action = MouseActionMotion - } - - // Modifiers - m.Alt = e&bitAlt != 0 - m.Ctrl = e&bitCtrl != 0 - m.Shift = e&bitShift != 0 - - // backward compatibility - switch { - case m.Button == MouseButtonLeft && m.Action == MouseActionPress: - m.Type = MouseLeft - case m.Button == MouseButtonMiddle && m.Action == MouseActionPress: - m.Type = MouseMiddle - case m.Button == MouseButtonRight && m.Action == MouseActionPress: - m.Type = MouseRight - case m.Button == MouseButtonNone && m.Action == MouseActionRelease: - m.Type = MouseRelease - case m.Button == MouseButtonWheelUp && m.Action == MouseActionPress: - m.Type = MouseWheelUp - case m.Button == MouseButtonWheelDown && m.Action == MouseActionPress: - m.Type = MouseWheelDown - case m.Button == MouseButtonWheelLeft && m.Action == MouseActionPress: - m.Type = MouseWheelLeft - case m.Button == MouseButtonWheelRight && m.Action == MouseActionPress: - m.Type = MouseWheelRight - case m.Button == MouseButtonBackward && m.Action == MouseActionPress: - m.Type = MouseBackward - case m.Button == MouseButtonForward && m.Action == MouseActionPress: - m.Type = MouseForward - case m.Action == MouseActionMotion: - m.Type = MouseMotion - switch m.Button { - case MouseButtonLeft: - m.Type = MouseLeft - case MouseButtonMiddle: - m.Type = MouseMiddle - case MouseButtonRight: - m.Type = MouseRight - case MouseButtonBackward: - m.Type = MouseBackward - case MouseButtonForward: - m.Type = MouseForward - } - default: - m.Type = MouseUnknown - } - - return m -} diff --git a/mouse_test.go b/mouse_test.go deleted file mode 100644 index 30f6ee364b..0000000000 --- a/mouse_test.go +++ /dev/null @@ -1,939 +0,0 @@ -package tea - -import ( - "fmt" - "testing" -) - -func TestMouseEvent_String(t *testing.T) { - tt := []struct { - name string - event MouseEvent - expected string - }{ - { - name: "unknown", - event: MouseEvent{ - Action: MouseActionPress, - Button: MouseButtonNone, - Type: MouseUnknown, - }, - expected: "unknown", - }, - { - name: "left", - event: MouseEvent{ - Action: MouseActionPress, - Button: MouseButtonLeft, - Type: MouseLeft, - }, - expected: "left press", - }, - { - name: "right", - event: MouseEvent{ - Action: MouseActionPress, - Button: MouseButtonRight, - Type: MouseRight, - }, - expected: "right press", - }, - { - name: "middle", - event: MouseEvent{ - Action: MouseActionPress, - Button: MouseButtonMiddle, - Type: MouseMiddle, - }, - expected: "middle press", - }, - { - name: "release", - event: MouseEvent{ - Action: MouseActionRelease, - Button: MouseButtonNone, - Type: MouseRelease, - }, - expected: "release", - }, - { - name: "wheel up", - event: MouseEvent{ - Action: MouseActionPress, - Button: MouseButtonWheelUp, - Type: MouseWheelUp, - }, - expected: "wheel up", - }, - { - name: "wheel down", - event: MouseEvent{ - Action: MouseActionPress, - Button: MouseButtonWheelDown, - Type: MouseWheelDown, - }, - expected: "wheel down", - }, - { - name: "wheel left", - event: MouseEvent{ - Action: MouseActionPress, - Button: MouseButtonWheelLeft, - Type: MouseWheelLeft, - }, - expected: "wheel left", - }, - { - name: "wheel right", - event: MouseEvent{ - Action: MouseActionPress, - Button: MouseButtonWheelRight, - Type: MouseWheelRight, - }, - expected: "wheel right", - }, - { - name: "motion", - event: MouseEvent{ - Action: MouseActionMotion, - Button: MouseButtonNone, - Type: MouseMotion, - }, - expected: "motion", - }, - { - name: "shift+left release", - event: MouseEvent{ - Type: MouseLeft, - Action: MouseActionRelease, - Button: MouseButtonLeft, - Shift: true, - }, - expected: "shift+left release", - }, - { - name: "shift+left", - event: MouseEvent{ - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - Shift: true, - }, - expected: "shift+left press", - }, - { - name: "ctrl+shift+left", - event: MouseEvent{ - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - Shift: true, - Ctrl: true, - }, - expected: "ctrl+shift+left press", - }, - { - name: "alt+left", - event: MouseEvent{ - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - Alt: true, - }, - expected: "alt+left press", - }, - { - name: "ctrl+left", - event: MouseEvent{ - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - Ctrl: true, - }, - expected: "ctrl+left press", - }, - { - name: "ctrl+alt+left", - event: MouseEvent{ - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - Alt: true, - Ctrl: true, - }, - expected: "ctrl+alt+left press", - }, - { - name: "ctrl+alt+shift+left", - event: MouseEvent{ - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - Alt: true, - Ctrl: true, - Shift: true, - }, - expected: "ctrl+alt+shift+left press", - }, - { - name: "ignore coordinates", - event: MouseEvent{ - X: 100, - Y: 200, - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - }, - expected: "left press", - }, - { - name: "broken type", - event: MouseEvent{ - Type: MouseEventType(-100), - Action: MouseAction(-110), - Button: MouseButton(-120), - }, - expected: "", - }, - } - - for i := range tt { - tc := tt[i] - - t.Run(tc.name, func(t *testing.T) { - actual := tc.event.String() - - if tc.expected != actual { - t.Fatalf("expected %q but got %q", - tc.expected, - actual, - ) - } - }) - } -} - -func TestParseX10MouseEvent(t *testing.T) { - encode := func(b byte, x, y int) []byte { - return []byte{ - '\x1b', - '[', - 'M', - byte(32) + b, - byte(x + 32 + 1), - byte(y + 32 + 1), - } - } - - tt := []struct { - name string - buf []byte - expected MouseEvent - }{ - // Position. - { - name: "zero position", - buf: encode(0b0000_0000, 0, 0), - expected: MouseEvent{ - X: 0, - Y: 0, - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - }, - }, - { - name: "max position", - buf: encode(0b0000_0000, 222, 222), // Because 255 (max int8) - 32 - 1. - expected: MouseEvent{ - X: 222, - Y: 222, - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - }, - }, - // Simple. - { - name: "left", - buf: encode(0b0000_0000, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - }, - }, - { - name: "left in motion", - buf: encode(0b0010_0000, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseLeft, - Action: MouseActionMotion, - Button: MouseButtonLeft, - }, - }, - { - name: "middle", - buf: encode(0b0000_0001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseMiddle, - Action: MouseActionPress, - Button: MouseButtonMiddle, - }, - }, - { - name: "middle in motion", - buf: encode(0b0010_0001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseMiddle, - Action: MouseActionMotion, - Button: MouseButtonMiddle, - }, - }, - { - name: "right", - buf: encode(0b0000_0010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRight, - Action: MouseActionPress, - Button: MouseButtonRight, - }, - }, - { - name: "right in motion", - buf: encode(0b0010_0010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRight, - Action: MouseActionMotion, - Button: MouseButtonRight, - }, - }, - { - name: "motion", - buf: encode(0b0010_0011, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseMotion, - Action: MouseActionMotion, - Button: MouseButtonNone, - }, - }, - { - name: "wheel up", - buf: encode(0b0100_0000, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelUp, - Action: MouseActionPress, - Button: MouseButtonWheelUp, - }, - }, - { - name: "wheel down", - buf: encode(0b0100_0001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelDown, - Action: MouseActionPress, - Button: MouseButtonWheelDown, - }, - }, - { - name: "wheel left", - buf: encode(0b0100_0010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelLeft, - Action: MouseActionPress, - Button: MouseButtonWheelLeft, - }, - }, - { - name: "wheel right", - buf: encode(0b0100_0011, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelRight, - Action: MouseActionPress, - Button: MouseButtonWheelRight, - }, - }, - { - name: "release", - buf: encode(0b0000_0011, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRelease, - Action: MouseActionRelease, - Button: MouseButtonNone, - }, - }, - { - name: "backward", - buf: encode(0b1000_0000, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseBackward, - Action: MouseActionPress, - Button: MouseButtonBackward, - }, - }, - { - name: "forward", - buf: encode(0b1000_0001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseForward, - Action: MouseActionPress, - Button: MouseButtonForward, - }, - }, - { - name: "button 10", - buf: encode(0b1000_0010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseUnknown, - Action: MouseActionPress, - Button: MouseButton10, - }, - }, - { - name: "button 11", - buf: encode(0b1000_0011, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseUnknown, - Action: MouseActionPress, - Button: MouseButton11, - }, - }, - // Combinations. - { - name: "alt+right", - buf: encode(0b0000_1010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Type: MouseRight, - Action: MouseActionPress, - Button: MouseButtonRight, - }, - }, - { - name: "ctrl+right", - buf: encode(0b0001_0010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Ctrl: true, - Type: MouseRight, - Action: MouseActionPress, - Button: MouseButtonRight, - }, - }, - { - name: "left in motion", - buf: encode(0b0010_0000, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: false, - Type: MouseLeft, - Action: MouseActionMotion, - Button: MouseButtonLeft, - }, - }, - { - name: "alt+right in motion", - buf: encode(0b0010_1010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Type: MouseRight, - Action: MouseActionMotion, - Button: MouseButtonRight, - }, - }, - { - name: "ctrl+right in motion", - buf: encode(0b0011_0010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Ctrl: true, - Type: MouseRight, - Action: MouseActionMotion, - Button: MouseButtonRight, - }, - }, - { - name: "ctrl+alt+right", - buf: encode(0b0001_1010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Ctrl: true, - Type: MouseRight, - Action: MouseActionPress, - Button: MouseButtonRight, - }, - }, - { - name: "ctrl+wheel up", - buf: encode(0b0101_0000, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Ctrl: true, - Type: MouseWheelUp, - Action: MouseActionPress, - Button: MouseButtonWheelUp, - }, - }, - { - name: "alt+wheel down", - buf: encode(0b0100_1001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Type: MouseWheelDown, - Action: MouseActionPress, - Button: MouseButtonWheelDown, - }, - }, - { - name: "ctrl+alt+wheel down", - buf: encode(0b0101_1001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Ctrl: true, - Type: MouseWheelDown, - Action: MouseActionPress, - Button: MouseButtonWheelDown, - }, - }, - // Overflow position. - { - name: "overflow position", - buf: encode(0b0010_0000, 250, 223), // Because 255 (max int8) - 32 - 1. - expected: MouseEvent{ - X: -6, - Y: -33, - Type: MouseLeft, - Action: MouseActionMotion, - Button: MouseButtonLeft, - }, - }, - } - - for i := range tt { - tc := tt[i] - - t.Run(tc.name, func(t *testing.T) { - actual := parseX10MouseEvent(tc.buf) - - if tc.expected != actual { - t.Fatalf("expected %#v but got %#v", - tc.expected, - actual, - ) - } - }) - } -} - -// func TestParseX10MouseEvent_error(t *testing.T) { -// tt := []struct { -// name string -// buf []byte -// }{ -// { -// name: "empty buf", -// buf: nil, -// }, -// { -// name: "wrong high bit", -// buf: []byte("\x1a[M@A1"), -// }, -// { -// name: "short buf", -// buf: []byte("\x1b[M@A"), -// }, -// { -// name: "long buf", -// buf: []byte("\x1b[M@A11"), -// }, -// } -// -// for i := range tt { -// tc := tt[i] -// -// t.Run(tc.name, func(t *testing.T) { -// _, err := parseX10MouseEvent(tc.buf) -// -// if err == nil { -// t.Fatalf("expected error but got nil") -// } -// }) -// } -// } - -func TestParseSGRMouseEvent(t *testing.T) { - encode := func(b, x, y int, r bool) []byte { - re := 'M' - if r { - re = 'm' - } - return []byte(fmt.Sprintf("\x1b[<%d;%d;%d%c", b, x+1, y+1, re)) - } - - tt := []struct { - name string - buf []byte - expected MouseEvent - }{ - // Position. - { - name: "zero position", - buf: encode(0, 0, 0, false), - expected: MouseEvent{ - X: 0, - Y: 0, - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - }, - }, - { - name: "225 position", - buf: encode(0, 225, 225, false), - expected: MouseEvent{ - X: 225, - Y: 225, - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - }, - }, - // Simple. - { - name: "left", - buf: encode(0, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseLeft, - Action: MouseActionPress, - Button: MouseButtonLeft, - }, - }, - { - name: "left in motion", - buf: encode(32, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseLeft, - Action: MouseActionMotion, - Button: MouseButtonLeft, - }, - }, - { - name: "left release", - buf: encode(0, 32, 16, true), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRelease, - Action: MouseActionRelease, - Button: MouseButtonLeft, - }, - }, - { - name: "middle", - buf: encode(1, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseMiddle, - Action: MouseActionPress, - Button: MouseButtonMiddle, - }, - }, - { - name: "middle in motion", - buf: encode(33, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseMiddle, - Action: MouseActionMotion, - Button: MouseButtonMiddle, - }, - }, - { - name: "middle release", - buf: encode(1, 32, 16, true), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRelease, - Action: MouseActionRelease, - Button: MouseButtonMiddle, - }, - }, - { - name: "right", - buf: encode(2, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRight, - Action: MouseActionPress, - Button: MouseButtonRight, - }, - }, - { - name: "right release", - buf: encode(2, 32, 16, true), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRelease, - Action: MouseActionRelease, - Button: MouseButtonRight, - }, - }, - { - name: "motion", - buf: encode(35, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseMotion, - Action: MouseActionMotion, - Button: MouseButtonNone, - }, - }, - { - name: "wheel up", - buf: encode(64, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelUp, - Action: MouseActionPress, - Button: MouseButtonWheelUp, - }, - }, - { - name: "wheel down", - buf: encode(65, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelDown, - Action: MouseActionPress, - Button: MouseButtonWheelDown, - }, - }, - { - name: "wheel left", - buf: encode(66, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelLeft, - Action: MouseActionPress, - Button: MouseButtonWheelLeft, - }, - }, - { - name: "wheel right", - buf: encode(67, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelRight, - Action: MouseActionPress, - Button: MouseButtonWheelRight, - }, - }, - { - name: "backward", - buf: encode(128, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseBackward, - Action: MouseActionPress, - Button: MouseButtonBackward, - }, - }, - { - name: "backward in motion", - buf: encode(160, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseBackward, - Action: MouseActionMotion, - Button: MouseButtonBackward, - }, - }, - { - name: "forward", - buf: encode(129, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseForward, - Action: MouseActionPress, - Button: MouseButtonForward, - }, - }, - { - name: "forward in motion", - buf: encode(161, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseForward, - Action: MouseActionMotion, - Button: MouseButtonForward, - }, - }, - // Combinations. - { - name: "alt+right", - buf: encode(10, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Type: MouseRight, - Action: MouseActionPress, - Button: MouseButtonRight, - }, - }, - { - name: "ctrl+right", - buf: encode(18, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Ctrl: true, - Type: MouseRight, - Action: MouseActionPress, - Button: MouseButtonRight, - }, - }, - { - name: "ctrl+alt+right", - buf: encode(26, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Ctrl: true, - Type: MouseRight, - Action: MouseActionPress, - Button: MouseButtonRight, - }, - }, - { - name: "alt+wheel press", - buf: encode(73, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Type: MouseWheelDown, - Action: MouseActionPress, - Button: MouseButtonWheelDown, - }, - }, - { - name: "ctrl+wheel press", - buf: encode(81, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Ctrl: true, - Type: MouseWheelDown, - Action: MouseActionPress, - Button: MouseButtonWheelDown, - }, - }, - { - name: "ctrl+alt+wheel press", - buf: encode(89, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Alt: true, - Ctrl: true, - Type: MouseWheelDown, - Action: MouseActionPress, - Button: MouseButtonWheelDown, - }, - }, - { - name: "ctrl+alt+shift+wheel press", - buf: encode(93, 32, 16, false), - expected: MouseEvent{ - X: 32, - Y: 16, - Shift: true, - Alt: true, - Ctrl: true, - Type: MouseWheelDown, - Action: MouseActionPress, - Button: MouseButtonWheelDown, - }, - }, - } - - for i := range tt { - tc := tt[i] - - t.Run(tc.name, func(t *testing.T) { - actual := parseSGRMouseEvent(tc.buf) - if tc.expected != actual { - t.Fatalf("expected %#v but got %#v", - tc.expected, - actual, - ) - } - }) - } -} diff --git a/options.go b/options.go index 8aee6da6be..8df71c8478 100644 --- a/options.go +++ b/options.go @@ -49,6 +49,14 @@ func WithInputTTY() ProgramOption { } } +// WithEnvironment sets the environment variables that will be used in the +// program. +func WithEnvironment(env []string) ProgramOption { + return func(p *Program) { + p.environ = env + } +} + // WithoutSignalHandler disables the signal handler that Bubble Tea sets up for // Programs. This is useful if you want to handle signals yourself. func WithoutSignalHandler() ProgramOption { diff --git a/screen.go b/screen.go index c17e5b6c69..d6ca2c134f 100644 --- a/screen.go +++ b/screen.go @@ -1,13 +1,16 @@ package tea +import "github.com/charmbracelet/x/exp/term/input" + // WindowSizeMsg is used to report the terminal size. It's sent to Update once // initially and then on every terminal resize. Note that Windows does not // have support for reporting when resizes occur as it does not support the // SIGWINCH signal. -type WindowSizeMsg struct { - Width int - Height int -} +type WindowSizeMsg = input.WindowSizeEvent + +// PaseMsg is used to report the terminal paste event. It's sent to Update when +// a bracketed paste event occurs. +type PasteMsg = input.PasteEvent // ClearScreen is a special command that tells the program to clear the screen // before the next update. This can be used to move the cursor to the top left diff --git a/tea.go b/tea.go index 1333a4b449..85974eaf7c 100644 --- a/tea.go +++ b/tea.go @@ -21,7 +21,7 @@ import ( "sync/atomic" "syscall" - "github.com/muesli/cancelreader" + "github.com/charmbracelet/x/exp/term/input" "golang.org/x/sync/errgroup" "golang.org/x/term" ) @@ -152,9 +152,16 @@ type Program struct { // ttyInput is null if input is not a TTY. ttyInput *os.File previousTtyInputState *term.State - cancelReader cancelreader.CancelReader readLoopDone chan struct{} + // input look driver + inputReader *input.Driver + + // environ is the environment variables that will be used in the program. + // it's of a []string because that's the format of environ(7), it allows + // for duplicates, and that's what os.Environ() and others return. + environ []string + // was the altscreen active before releasing the terminal? altScreenWasActive bool ignoreSignals uint32 @@ -528,7 +535,7 @@ func (p *Program) Run() (Model, error) { // Subscribe to user input. if p.input != nil { - if err := p.initCancelReader(); err != nil { + if err := p.initInputReader(); err != nil { return model, err } } @@ -553,12 +560,12 @@ func (p *Program) Run() (Model, error) { p.cancel() // Check if the cancel reader has been setup before waiting and closing. - if p.cancelReader != nil { + if p.inputReader != nil { // Wait for input loop to finish. - if p.cancelReader.Cancel() { + if p.inputReader.Cancel() { p.waitForReadLoop() } - _ = p.cancelReader.Close() + _ = p.inputReader.Close() } // Wait for all handlers to finish. @@ -645,7 +652,7 @@ func (p *Program) shutdown(kill bool) { // reader. You can return control to the Program with RestoreTerminal. func (p *Program) ReleaseTerminal() error { atomic.StoreUint32(&p.ignoreSignals, 1) - p.cancelReader.Cancel() + p.inputReader.Cancel() p.waitForReadLoop() if p.renderer != nil { @@ -666,7 +673,7 @@ func (p *Program) RestoreTerminal() error { if err := p.initTerminal(); err != nil { return err } - if err := p.initCancelReader(); err != nil { + if err := p.initInputReader(); err != nil { return err } if p.altScreenWasActive { diff --git a/tty.go b/tty.go index 879f47ac55..311e1badb9 100644 --- a/tty.go +++ b/tty.go @@ -1,11 +1,14 @@ package tea import ( + "context" "errors" "fmt" "io" + "strings" "time" + "github.com/charmbracelet/x/exp/term/input" "github.com/muesli/cancelreader" "golang.org/x/term" ) @@ -53,24 +56,63 @@ func (p *Program) restoreInput() error { return nil } -// initCancelReader (re)commences reading inputs. -func (p *Program) initCancelReader() error { - var err error - p.cancelReader, err = newInputReader(p.input) +// initInputReader (re)commences reading inputs. +func (p *Program) initInputReader() error { + var term string + for i := len(p.environ) - 1; i >= 0; i-- { + // We iterate backwards to find the last TERM variable set in the + // environment. This is because the last one is the one that will be + // used by the terminal. + parts := strings.SplitN(p.environ[i], "=", 2) + if len(parts) == 2 && parts[0] == "TERM" { + term = parts[1] + break + } + } + + // Initialize the input reader. + // This need to be done after the terminal has been initialized and set to + // raw mode. + // On Windows, this will change the console mode to enable mouse and window + // events. + var flags int // TODO: make configurable through environment variables? + drv, err := input.NewDriver(p.input, term, flags) if err != nil { - return fmt.Errorf("error creating cancelreader: %w", err) + return err } + p.inputReader = drv p.readLoopDone = make(chan struct{}) go p.readLoop() return nil } +func readInputs(ctx context.Context, msgs chan<- Msg, reader *input.Driver) error { + for { + events, err := reader.ReadEvents() + if err != nil { + return err + } + + for _, e := range events { + select { + case msgs <- e: + case <-ctx.Done(): + err := ctx.Err() + if err != nil { + err = fmt.Errorf("found context error while reading input: %w", err) + } + return err + } + } + } +} + func (p *Program) readLoop() { defer close(p.readLoopDone) - err := readInputs(p.ctx, p.msgs, p.cancelReader) + err := readInputs(p.ctx, p.msgs, p.inputReader) if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) { select { case <-p.ctx.Done(): diff --git a/tutorials/go.mod b/tutorials/go.mod index 79edeec48d..23e6a50d38 100644 --- a/tutorials/go.mod +++ b/tutorials/go.mod @@ -5,17 +5,16 @@ go 1.18 require github.com/charmbracelet/bubbletea v0.25.0 require ( - github.com/charmbracelet/x/exp/term v0.0.0-20240322170634-ebda89b611f2 // indirect + github.com/charmbracelet/x/exp/term v0.0.0-20240322183009-053b3d26b99d // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/term v0.17.0 // indirect - golang.org/x/text v0.13.0 // indirect ) replace github.com/charmbracelet/bubbletea => ../ diff --git a/tutorials/go.sum b/tutorials/go.sum index c33e842eea..e0954834b3 100644 --- a/tutorials/go.sum +++ b/tutorials/go.sum @@ -1,10 +1,8 @@ -github.com/charmbracelet/x/exp/term v0.0.0-20240322170634-ebda89b611f2 h1:53tB77HCI013H68JTiH1kX6rtSJQPB6Cu4AmXj3cCzs= -github.com/charmbracelet/x/exp/term v0.0.0-20240322170634-ebda89b611f2/go.mod h1:madZtB2OVDOG+ZnLruGITVZceYy047W+BLQ1MNQzbWg= +github.com/charmbracelet/x/exp/term v0.0.0-20240322183009-053b3d26b99d h1:hVjAuiFYTDIfmZ/TPRFWlOnDvOCoQ48PMl2g/N/W5xY= +github.com/charmbracelet/x/exp/term v0.0.0-20240322183009-053b3d26b99d/go.mod h1:madZtB2OVDOG+ZnLruGITVZceYy047W+BLQ1MNQzbWg= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= @@ -16,6 +14,9 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -23,6 +24,4 @@ golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=