Cross-platform mouse event handling for Phoenix TUI Framework.
Part of the Phoenix TUI Framework.
Module: github.com/phoenix-tui/phoenix/mouse
- All Mouse Buttons: Left, Right, Middle, Scroll Wheel
- Click Detection: Single, Double, Triple clicks (automatic)
- Drag & Drop: State tracking with threshold detection
- Scroll Wheel: Up/Down with configurable delta
- Modifier Keys: Shift, Ctrl, Alt detection
- Multi-Protocol: SGR (modern), X10 (legacy), URxvt (alternative)
- DDD Architecture: Rich domain models with behavior
- Zero Dependencies: Built from scratch (stdlib only)
go get github.com/phoenix-tui/phoenix/mousepackage main
import (
"github.com/phoenix-tui/phoenix/mouse"
"github.com/phoenix-tui/phoenix/tea"
)
type Model struct {
lastClick string
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.MouseMsg:
switch msg.Button {
case mouse.ButtonLeft:
m.lastClick = "Left click at " + msg.Position.String()
case mouse.ButtonRight:
m.lastClick = "Right click at " + msg.Position.String()
case mouse.ButtonMiddle:
m.lastClick = "Middle click at " + msg.Position.String()
}
}
return m, nil
}mouse.ButtonNone // No button (motion only)
mouse.ButtonLeft // Left mouse button
mouse.ButtonRight // Right mouse button
mouse.ButtonMiddle // Middle button (scroll wheel press)
mouse.ButtonWheelUp // Scroll wheel up
mouse.ButtonWheelDown // Scroll wheel downcase tea.MouseMsg:
button := msg.Button
if button.IsButton() {
// Left, Right, or Middle button
switch button {
case mouse.ButtonLeft:
fmt.Println("Left click")
case mouse.ButtonRight:
fmt.Println("Right click - Context menu?")
case mouse.ButtonMiddle:
fmt.Println("Middle click - Paste?")
}
}
if button.IsWheel() {
// Scroll wheel event
if button == mouse.ButtonWheelUp {
viewport.ScrollUp(3)
} else {
viewport.ScrollDown(3)
}
}mouse.EventPress // Button pressed (start of click/drag)
mouse.EventRelease // Button released (end of click/drag)
mouse.EventClick // Single click (automatic detection)
mouse.EventDoubleClick // Double click (< 500ms between clicks)
mouse.EventTripleClick // Triple click (< 500ms between clicks)
mouse.EventDrag // Mouse drag (motion with button pressed)
mouse.EventMotion // Mouse motion (no button pressed)
mouse.EventScroll // Scroll wheel event// In phoenix/components/input
func (i *Input) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.MouseMsg:
if msg.Type == mouse.EventClick && msg.Button == mouse.ButtonLeft {
// Calculate cursor position from mouse X coordinate
cursorPos := i.calculateCursorFromX(msg.Position.X())
return i.WithCursor(cursorPos), nil
}
}
return i, nil
}type Model struct {
contextMenu *ContextMenu
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.MouseMsg:
// Right click = show context menu
if msg.Type == mouse.EventClick && msg.Button == mouse.ButtonRight {
m.contextMenu = NewContextMenu(msg.Position)
return m, nil
}
// Left click outside = close context menu
if msg.Type == mouse.EventClick && msg.Button == mouse.ButtonLeft {
if m.contextMenu != nil && !m.contextMenu.Contains(msg.Position) {
m.contextMenu = nil
}
}
}
return m, nil
}// In phoenix/components/viewport
func (v *Viewport) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.MouseMsg:
if msg.Type == mouse.EventScroll {
switch msg.Button {
case mouse.ButtonWheelUp:
return v.ScrollUp(3), nil // 3 lines per scroll
case mouse.ButtonWheelDown:
return v.ScrollDown(3), nil
}
}
}
return v, nil
}type Model struct {
dragState mouse.DragState
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.MouseMsg:
switch msg.Type {
case mouse.EventPress:
if msg.Button == mouse.ButtonLeft {
m.dragState.Start(msg.Position)
}
case mouse.EventDrag:
m.dragState.Update(msg.Position)
case mouse.EventRelease:
if m.dragState.IsDrag() {
// Extract file path from drop event
filePath := extractDroppedFile(msg)
if filePath != "" {
return m.InsertFile(filePath), nil
}
}
m.dragState.Reset()
}
}
return m, nil
}func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.MouseMsg:
// Middle click = paste from clipboard (X11 primary selection)
if msg.Type == mouse.EventClick && msg.Button == mouse.ButtonMiddle {
return m, tea.Batch(
clipboard.ReadPrimary(), // X11 primary selection
tea.Printf("Pasted at %s", msg.Position),
)
}
}
return m, nil
}Phoenix automatically detects single, double, and triple clicks:
case tea.MouseMsg:
switch msg.Type {
case mouse.EventClick:
fmt.Println("Single click")
case mouse.EventDoubleClick:
fmt.Println("Double click - Select word?")
case mouse.EventTripleClick:
fmt.Println("Triple click - Select line?")
}Detection Rules:
- Double click: Two clicks within 500ms at same position (+-1 cell tolerance)
- Triple click: Three clicks within 500ms at same position
- Automatic timeout after 500ms
case tea.MouseMsg:
mods := msg.Modifiers
if mods.HasShift() {
fmt.Println("Shift + Click")
}
if mods.HasCtrl() {
fmt.Println("Ctrl + Click - Multi-select?")
}
if mods.HasAlt() {
fmt.Println("Alt + Click")
}
// Common combinations
if mods.HasCtrl() && msg.Button == mouse.ButtonLeft {
// Ctrl + Left Click = Open in new tab?
}
if mods.HasShift() && msg.Button == mouse.ButtonLeft {
// Shift + Left Click = Extend selection
}Phoenix supports multiple mouse protocols with automatic fallback:
\x1b[<0;10;5M # Left press at (10, 5)
\x1b[<0;10;5m # Left release at (10, 5)
\x1b[<2;10;5M # Right press at (10, 5)
\x1b[<64;10;5M # Scroll up at (10, 5)
\x1b[<65;10;5M # Scroll down at (10, 5)
Advantages:
- Supports press/release separately
- Works with large terminals (> 223 columns)
- Modern terminal support (iTerm2, Windows Terminal, Alacritty, Kitty)
\x1b[M !! # Click at (1, 1)
\x1b[M`"# # Click at (96, 35)
Limitations:
- No press/release distinction
- Limited to 223 columns
- Old terminal compatibility only
\x1b[0;10;5M # Click at (10, 5)
Enable different tracking modes:
// Button events only (press/release)
mouse.Enable(mouse.ModeButton)
// Button + motion while pressed (drag detection)
mouse.Enable(mouse.ModeMotion)
// All motion events (even without button)
mouse.Enable(mouse.ModeAllMotion)
// Prefer SGR protocol
mouse.Enable(mouse.ModeButton | mouse.ModeSGR)
// Disable mouse tracking
mouse.Disable()Phoenix TextInput supports mouse click positioning:
input := textinput.New().
WithPlaceholder("Click to position cursor...").
WithMouseSupport(true) // Enable mouse clicks
// User clicks at X coordinate -> cursor moves to that positionPhoenix Viewport supports scroll wheel:
viewport := viewport.New(80, 20).
WithContent(longText).
WithMouseScroll(true) // Enable scroll wheel
// User scrolls wheel -> viewport scrolls up/downPhoenix List supports click selection:
list := list.New(items).
WithMouseSelect(true) // Enable click selection
// User clicks item -> item selected
// User right-clicks item -> context menu?Track drag & drop operations:
type DragState struct {
Active bool
Start Position
Current Position
Delta Position // Current - Start
Distance int // Euclidean distance
}
// Usage
dragState := mouse.NewDragState()
case mouse.EventPress:
dragState.Start(msg.Position)
case mouse.EventDrag:
dragState.Update(msg.Position)
if dragState.Distance() > 10 {
// Drag threshold exceeded
fmt.Printf("Dragging from %s to %s\n",
dragState.Start(), dragState.Current())
}
case mouse.EventRelease:
if dragState.IsDrag() {
// Handle drop
handleDrop(dragState.Start(), dragState.Current())
}
dragState.Reset()Phoenix mouse follows Domain-Driven Design:
mouse/
├── domain/ # Pure business logic
│ ├── value/ # Button, Position, EventType, Modifiers
│ ├── model/ # MouseEvent (aggregate), DragState (entity)
│ └── service/ # ClickDetector, DragTracker, ScrollCalculator
├── infrastructure/ # Technical implementation
│ ├── parser/ # SGR, X10, URxvt parsers
│ ├── ansi/ # ANSI escape sequences
│ └── platform/ # Terminal mode management
├── application/ # Use cases
│ ├── event_processor.go # Enriches events (click detection)
│ └── mouse_handler.go # Coordinates parsing/processing
└── api/ # Public interface
└── mouse.go # Clean, fluent API
| Platform | Protocol | Support |
|---|---|---|
| iTerm2 | SGR (1006) | Full |
| Windows Terminal | SGR (1006) | Full |
| Alacritty | SGR (1006) | Full |
| Kitty | SGR (1006) | Full |
| xterm | SGR (1006) | Full |
| tmux | SGR (1006) | Full |
| Terminal.app (macOS) | SGR (1006) | Full |
| GNOME Terminal | SGR (1006) | Full |
| Old xterm | X10 (1000) | Limited |
| PuTTY | X10 (1000) | Limited |
# Run tests
cd mouse && go test ./...
# With coverage
go test -cover ./...
# Specific test
go test -run TestClickDetector_DoubleClick ./domain/serviceSee examples/ directory:
click_positioning.go- TextInput cursor positioning with left/right clickscroll.go- Viewport scrolling with wheeldrag_drop.go- File drag & drop insertionfull_demo.go- Combined demo (all features)
- Zero allocations in hot paths (event parsing)
- Sub-microsecond click detection
- Efficient protocol parsing (no regex, direct byte parsing)
| Feature | Phoenix | Bubbletea |
|---|---|---|
| Click Detection | Built-in | Manual |
| Drag Tracking | Built-in | Manual |
| Right Click | Full support | Supported |
| Middle Click | Full support | Supported |
| Protocol Abstraction | DDD | Raw events |
| Domain Model | Rich | Anemic |
Q: Does right click work? A: Yes! Full support for left, right, and middle buttons.
Q: How do I show a context menu on right click?
A: Check msg.Button == mouse.ButtonRight and render your context menu component.
Q: Can I detect double-click? A: Yes! Phoenix automatically detects double and triple clicks.
Q: Does scroll wheel work in SSH? A: Yes, if your terminal supports SGR protocol (most modern terminals do).
Q: How do I enable mouse support?
A: Call mouse.Enable() at program start, or use tea.WithMouseCellMotion() in tea.Program options.
MIT
See CONTRIBUTING.md for guidelines.
Part of the Phoenix TUI Framework - Next-generation terminal UI for Go.