Skip to content

Commit

Permalink
(v2) fix: windows: enable mouse mode on demand (#1341)
Browse files Browse the repository at this point in the history
* fix: windows: enable mouse mode on demand

This fixes an issue where mouse mode is always enabled on Windows. With
this patch, we enable mouse events only it is requested.

Needs: charmbracelet/x#386
Related: #1313

* refactor: mouse: properly handle mouse mode

Since mouse mode has a special case on Windows, we now have
separate msg types for mouse events.

* chore: go mod tidy
  • Loading branch information
aymanbagabas authored Mar 11, 2025
1 parent 0153aab commit cc204f6
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 30 deletions.
2 changes: 1 addition & 1 deletion examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ require (
github.com/charmbracelet/lipgloss v1.0.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.12-0.20250212155406-f75055277088 // indirect
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a // indirect
github.com/charmbracelet/x/input v0.3.3 // indirect
github.com/charmbracelet/x/input v0.3.4 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/charmbracelet/x/windows v0.2.0 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHE
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20241016014612-3b4d04043233 h1:2bTR/MtnJuq9RrCZSPwCOO34YSDByKL6nzXQMnsKK6U=
github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20241016014612-3b4d04043233/go.mod h1:cw9df32BXdkcd0LzAHsFMmvXOsrrlDKazIW8PCq0cPM=
github.com/charmbracelet/x/input v0.3.3 h1:JfLopDmw7pFy6Sezr42RkRfy1UPTh3xDR4sM3PXrrGw=
github.com/charmbracelet/x/input v0.3.3/go.mod h1:JI8RcvdZWQIhn09VzeK3hdp4lTz7+yhiEdpEQtZN+2c=
github.com/charmbracelet/x/input v0.3.4 h1:Mujmnv/4DaitU0p+kIsrlfZl/UlmeLKw1wAP3e1fMN0=
github.com/charmbracelet/x/input v0.3.4/go.mod h1:JI8RcvdZWQIhn09VzeK3hdp4lTz7+yhiEdpEQtZN+2c=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw=
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/charmbracelet/x/ansi v0.8.0
github.com/charmbracelet/x/cellbuf v0.0.12-0.20250212155406-f75055277088
github.com/charmbracelet/x/exp/golden v0.0.0-20241212170349-ad4b7ae0f25f
github.com/charmbracelet/x/input v0.3.3
github.com/charmbracelet/x/input v0.3.4
github.com/charmbracelet/x/term v0.2.1
github.com/muesli/cancelreader v0.2.2
golang.org/x/sync v0.11.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ github.com/charmbracelet/x/cellbuf v0.0.12-0.20250212155406-f75055277088 h1:m6tA
github.com/charmbracelet/x/cellbuf v0.0.12-0.20250212155406-f75055277088/go.mod h1:dKfNBxLovpvzzxAP6/GZfs5eb7vNxHlUDnwGhRmvIdY=
github.com/charmbracelet/x/exp/golden v0.0.0-20241212170349-ad4b7ae0f25f h1:UytXHv0UxnsDFmL/7Z9Q5SBYPwSuRLXHbwx+6LycZ2w=
github.com/charmbracelet/x/exp/golden v0.0.0-20241212170349-ad4b7ae0f25f/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/input v0.3.3 h1:JfLopDmw7pFy6Sezr42RkRfy1UPTh3xDR4sM3PXrrGw=
github.com/charmbracelet/x/input v0.3.3/go.mod h1:JI8RcvdZWQIhn09VzeK3hdp4lTz7+yhiEdpEQtZN+2c=
github.com/charmbracelet/x/input v0.3.4 h1:Mujmnv/4DaitU0p+kIsrlfZl/UlmeLKw1wAP3e1fMN0=
github.com/charmbracelet/x/input v0.3.4/go.mod h1:JI8RcvdZWQIhn09VzeK3hdp4lTz7+yhiEdpEQtZN+2c=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw=
Expand Down
28 changes: 15 additions & 13 deletions screen.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,24 @@ func ExitAltScreen() Msg {
return disableModeMsg{ansi.AltScreenSaveCursorMode}
}

// enableMouseCellMotionMsg is an internal message that signals to enable mouse cell
// motion events.
type enableMouseCellMotionMsg struct{}

// EnableMouseCellMotion is a special command that enables mouse click,
// release, and wheel events. Mouse movement events are also captured if
// a mouse button is pressed (i.e., drag events).
//
// Because commands run asynchronously, this command should not be used in your
// model's Init function. Use the WithMouseCellMotion ProgramOption instead.
func EnableMouseCellMotion() Msg {
return sequenceMsg{
func() Msg { return enableModeMsg{ansi.ButtonEventMouseMode} },
func() Msg { return enableModeMsg{ansi.SgrExtMouseMode} },
}
return enableMouseCellMotionMsg{}
}

// enableMouseAllMotionMsg is an internal message that signals to enable mouse
// all motion events.
type enableMouseAllMotionMsg struct{}

// EnableMouseAllMotion is a special command that enables mouse click, release,
// wheel, and motion events, which are delivered regardless of whether a mouse
// button is pressed, effectively enabling support for hover interactions.
Expand All @@ -68,19 +73,16 @@ func EnableMouseCellMotion() Msg {
// Because commands run asynchronously, this command should not be used in your
// model's Init function. Use the WithMouseAllMotion ProgramOption instead.
func EnableMouseAllMotion() Msg {
return sequenceMsg{
func() Msg { return enableModeMsg{ansi.AnyEventMouseMode} },
func() Msg { return enableModeMsg{ansi.SgrExtMouseMode} },
}
return enableMouseAllMotionMsg{}
}

// disableMouse motionMsg is an internal message that signals to disable mouse
// motion events.
type disableMouseMotionMsg struct{}

// DisableMouse is a special command that stops listening for mouse events.
func DisableMouse() Msg {
return sequenceMsg{
func() Msg { return disableModeMsg{ansi.ButtonEventMouseMode} },
func() Msg { return disableModeMsg{ansi.AnyEventMouseMode} },
func() Msg { return disableModeMsg{ansi.SgrExtMouseMode} },
}
return disableMouseMotionMsg{}
}

// HideCursor is a special command for manually instructing Bubble Tea to hide
Expand Down
88 changes: 80 additions & 8 deletions tea.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ type Program struct {
inputReader *input.Reader
traceInput bool // true if input should be traced
readLoopDone chan struct{}
mouseMode bool // indicates whether we should enable mouse on Windows

// modes keeps track of terminal modes that have been enabled or disabled.
modes ansi.Modes
Expand Down Expand Up @@ -569,6 +570,15 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
p.execute(ansi.ResetMode(msg.Mode))
}

case enableMouseCellMotionMsg:
p.enableMouse(false)

case enableMouseAllMotionMsg:
p.enableMouse(true)

case disableMouseMotionMsg:
p.disableMouse()

case readClipboardMsg:
p.execute(ansi.RequestSystemClipboard)

Expand Down Expand Up @@ -895,7 +905,7 @@ func (p *Program) Run() (Model, error) {
// Init the input reader and initial model.
model := p.initialModel
if p.input != nil {
if err := p.initInputReader(); err != nil {
if err := p.initInputReader(false); err != nil {
return model, err
}
}
Expand Down Expand Up @@ -925,12 +935,12 @@ func (p *Program) Run() (Model, error) {
// We store the state of grapheme clustering after we query it and get
// a response in the eventLoop.
}
if p.startupOptions&withMouseCellMotion != 0 {
p.execute(ansi.SetButtonEventMouseMode + ansi.SetSgrExtMouseMode)
p.modes.Set(ansi.ButtonEventMouseMode, ansi.SgrExtMouseMode)
} else if p.startupOptions&withMouseAllMotion != 0 {
p.execute(ansi.SetAnyEventMouseMode + ansi.SetSgrExtMouseMode)
p.modes.Set(ansi.AnyEventMouseMode, ansi.SgrExtMouseMode)

// Enable mouse mode.
cellMotion := p.startupOptions&withMouseCellMotion != 0
allMotion := p.startupOptions&withMouseAllMotion != 0
if cellMotion || allMotion {
p.enableMouse(allMotion)
}

if p.startupOptions&withReportFocus != 0 {
Expand Down Expand Up @@ -1111,7 +1121,7 @@ func (p *Program) RestoreTerminal() error {
if err := p.initTerminal(); err != nil {
return err
}
if err := p.initInputReader(); err != nil {
if err := p.initInputReader(false); err != nil {
return err
}
if p.modes.IsReset(ansi.AltScreenSaveCursorMode) {
Expand Down Expand Up @@ -1268,3 +1278,65 @@ func (p *Program) requestKeyboardEnhancements() {
p.execute(ansi.RequestKittyKeyboard)
}
}

// enableMouse enables mouse events on the terminal. When all is true, it will
// enable [ansi.AnyEventMouseMode], otherwise, it will use
// [ansi.ButtonEventMouseMode].
// Note this has no effect on Windows since we use the Windows Console API.
func (p *Program) enableMouse(all bool) {
if runtime.GOOS == "windows" {
// XXX: This is used to enable mouse mode on Windows. We need
// to reinitialize the cancel reader to get the mouse events to
// work.
if !p.mouseMode {
p.mouseMode = true
if p.inputReader != nil {
// Only reinitialize if the input reader has been initialized.
p.initInputReader(true) //nolint:errcheck
}
}
}

if all {
p.execute(ansi.SetAnyEventMouseMode + ansi.SetSgrExtMouseMode)
p.modes.Set(ansi.AnyEventMouseMode, ansi.SgrExtMouseMode)
} else {
p.execute(ansi.SetButtonEventMouseMode + ansi.SetSgrExtMouseMode)
p.modes.Set(ansi.ButtonEventMouseMode, ansi.SgrExtMouseMode)
}
}

// disableMouse disables mouse events on the terminal.
// Note this has no effect on Windows since we use the Windows Console API.
func (p *Program) disableMouse() {
if runtime.GOOS == "windows" {
// XXX: On Windows, mouse mode is enabled on the input reader
// level. We need to instruct the input reader to stop reading
// mouse events.
if p.mouseMode {
p.mouseMode = false
if p.inputReader != nil {
// Only reinitialize if the input reader has been initialized.
p.initInputReader(true) //nolint:errcheck
}
}
}

var modes []ansi.Mode
if p.modes.IsSet(ansi.AnyEventMouseMode) {
modes = append(modes, ansi.AnyEventMouseMode)
}
if p.modes.IsSet(ansi.ButtonEventMouseMode) {
modes = append(modes, ansi.ButtonEventMouseMode)
}
if len(modes) > 0 {
modes = append(modes, ansi.SgrExtMouseMode)
for _, m := range modes {
// We could combine all of these modes into one single sequence,
// but we're being cautious here for terminals that might not support
// that format i.e. `CSI ? 10003 ; 1006 l`.
p.execute(ansi.ResetMode(m))
p.modes.Reset(m)
}
}
}
2 changes: 1 addition & 1 deletion testdata/TestClearMsg/mouse_disable.golden
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[?2004h[?1003h[?1006h[?1002l[?1003l[?1006l[?25lsuccess[?25h[?2004l
[?2004h[?1003h[?1006h[?1003l[?1006l[?25lsuccess[?25h[?2004l
Expand Down
13 changes: 11 additions & 2 deletions tty.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,24 @@ func (p *Program) restoreInput() error {
}

// initInputReader (re)commences reading inputs.
func (p *Program) initInputReader() error {
func (p *Program) initInputReader(cancel bool) error {
if cancel && p.inputReader != nil {
p.inputReader.Cancel()
p.waitForReadLoop()
}

term := p.getenv("TERM")

// 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 // XXX: make configurable through environment variables?
var flags int
if p.mouseMode {
flags |= input.FlagMouseMode
}

drv, err := input.NewReader(p.input, term, flags)
if err != nil {
return fmt.Errorf("bubbletea: error initializing input reader: %w", err)
Expand Down

0 comments on commit cc204f6

Please sign in to comment.