1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
|
"""Sub-module providing mouse event handling."""
# std imports
import re
from typing import Match
class MouseEvent: # pylint: disable=too-many-instance-attributes
"""
Mouse event with button, coordinates, and modifier information.
A unified mouse event structure that supports both legacy and SGR mouse protocols. Provides a
dynamic button property that returns human-readable button names like "LEFT", "SCROLL_UP",
"CTRL_LEFT", etc.
:ivar int button_value: Raw button number (0=left, 1=middle, 2=right, 64=scroll up, 65=scroll
down, or higher for extended buttons).
:ivar int x: Horizontal position (0-indexed, in cells or pixels depending on mode).
:ivar int y: Vertical position (0-indexed, in cells or pixels depending on mode).
:ivar bool released: True if this is a button release event.
:ivar bool shift: True if Shift modifier is pressed.
:ivar bool meta: True if Meta/Alt modifier is pressed.
:ivar bool ctrl: True if Ctrl modifier is pressed.
:ivar bool is_motion: True if motion is being reported (drag or all- motion mode).
:ivar bool is_wheel: True if this is a scroll wheel event.
"""
def __init__(self, button_value: int, x: int, y: int, released: bool,
shift: bool, meta: bool, ctrl: bool, is_motion: bool, is_wheel: bool):
# pylint: disable=too-many-positional-arguments
"""
Initialize a MouseEvent.
:param int button_value: Raw button number.
:param int x: Horizontal position.
:param int y: Vertical position.
:param bool released: Whether this is a button release event.
:param bool shift: Whether Shift modifier is pressed.
:param bool meta: Whether Meta/Alt modifier is pressed.
:param bool ctrl: Whether Ctrl modifier is pressed.
:param bool is_motion: Whether motion is being reported.
:param bool is_wheel: Whether this is a scroll wheel event.
"""
self.button_value = button_value
self.x = x
self.y = y
self.released = released
self.shift = shift
self.meta = meta
self.ctrl = ctrl
self.is_motion = is_motion
self.is_wheel = is_wheel
def _get_base_button_name(self) -> str:
"""
Get base button name without modifiers or state.
:rtype: str
:returns: Base button name like "LEFT", "MIDDLE", "RIGHT", or "BUTTON_6".
"""
if self.button_value < 66:
return {
0: "LEFT",
1: "MIDDLE",
2: "RIGHT",
}.get(self.button_value, '')
# Extended buttons (button_value >= 66)
return f"BUTTON_{self.button_value - 60}"
@property
def button(self) -> str:
"""
Return human-readable button name.
Generates button names that include modifiers, button type, motion/release state:
- "LEFT", "MIDDLE", "RIGHT" for standard mouse buttons
- "LEFT_RELEASED", "MIDDLE_RELEASED", "RIGHT_RELEASED" for button releases
- "SCROLL_UP", "SCROLL_DOWN" for wheel events
- "MOTION" for mouse movement with no button pressed
- "LEFT_MOTION", "MIDDLE_MOTION", "RIGHT_MOTION" for drag events
- "CTRL_LEFT", "SHIFT_SCROLL_UP", "CTRL_SHIFT_META_MOTION" with modifiers
- "BUTTON_6", "BUTTON_7", etc. for extended mouse buttons
:rtype: str
:returns: Button name with modifiers, button type, and motion/release state.
"""
button_name = ''
# Add modifiers in order: ctrl, shift, meta
for modifier in ('ctrl', 'shift', 'meta'):
if getattr(self, modifier):
button_name += f'{modifier.upper()}_'
# Handle wheel events first (legacy uses button_value 0/1, SGR uses 64/65)
if self.is_wheel:
# Legacy wheel: button_value 0=up, 1=down
# SGR wheel: button_value 64=up, 65=down
if self.button_value in {0, 64}:
button_name += "SCROLL_UP"
elif self.button_value in {1, 65}:
button_name += "SCROLL_DOWN"
# Wheel events don't have motion or release variants in typical usage
return button_name
# Handle motion events specially
if self.is_motion:
# Motion with no button pressed (button_value=3 means no button in SGR motion)
if self.button_value == 3:
button_name += "MOTION"
else:
# Dragging with a specific button
button_name += f"{self._get_base_button_name()}_MOTION"
else:
# Regular click or release events
button_name += self._get_base_button_name()
# Add release state (only for non-motion events)
if self.released:
button_name += "_RELEASED"
return button_name
def __repr__(self) -> str:
"""Return succinct representation showing only active attributes."""
# Always show button_value, x, y
parts = [f'button_value={self.button_value}', f'x={self.x}', f'y={self.y}']
# Only show boolean flags when True
for bool_name in ('released', 'shift', 'meta', 'ctrl', 'is_motion', 'is_wheel'):
if getattr(self, bool_name):
parts.append(f'{bool_name}=True')
return f"MouseEvent({', '.join(parts)})"
@classmethod
def from_sgr_match(cls, match: Match[str]) -> 'MouseEvent':
"""
Parse SGR mouse event from regex match.
Handles both SGR (mode 1006) and SGR-Pixels (mode 1016) since they
use identical wire formats: CSI < b;x;y m/M. The difference is semantic:
- Mode 1006: coordinates represent character cell positions
- Mode 1016: coordinates represent pixel positions
Applications must interpret x,y coordinates based on which mode was enabled.
The protocol sends 1-indexed coordinates (top-left is 1,1), but we convert
to 0-indexed (top-left is 0,0) to match blessed's terminal movement functions.
:param Match match: Regex match object with groups 'b', 'x', 'y', 'type'.
:rtype: MouseEvent
:returns: Parsed MouseEvent instance.
"""
b = int(match.group('b'))
x = int(match.group('x')) - 1 # Convert from 1-indexed to 0-indexed
y = int(match.group('y')) - 1 # Convert from 1-indexed to 0-indexed
event_type = match.group('type')
released = event_type == 'm'
# Extract modifiers from button code
shift = bool(b & 4)
meta = bool(b & 8)
ctrl = bool(b & 16)
# Extract motion/drag flags
is_motion = bool(b & 32)
# Check for wheel events (button 64-65) by masking modifiers
# Wheel events: button & ~(shift|meta|ctrl|motion) gives base button
base_button = b & ~(4 | 8 | 16 | 32)
is_wheel = base_button in {64, 65} # wheel up/down
# Get base button (0-2 for left/middle/right, or 64-65 for wheel)
button = b & 3 if not is_wheel else base_button
return cls(
button_value=button,
x=x,
y=y,
released=released,
shift=shift,
meta=meta,
ctrl=ctrl,
is_motion=is_motion,
is_wheel=is_wheel
)
@classmethod
def from_legacy_match(cls, match: Match[str]) -> 'MouseEvent':
"""
Parse legacy mouse event (X10/1000/1002/1003) from regex match.
The protocol sends 1-indexed coordinates (top-left is 1,1), but we convert to 0-indexed
(top-left is 0,0) to match blessed's terminal movement functions.
:param Match match: Regex match object with groups 'cb', 'cx', 'cy'.
:rtype: MouseEvent
:returns: Parsed MouseEvent instance.
"""
cb = ord(match.group('cb')) - 32
cx = ord(match.group('cx')) - 32 - 1 # Convert from 1-indexed to 0-indexed
cy = ord(match.group('cy')) - 32 - 1 # Convert from 1-indexed to 0-indexed
# Extract button and modifiers from cb
button = cb & 3
released = button == 3
if released:
button = 0 # Release doesn't specify which button
# Extract modifier flags
shift = bool(cb & 4)
meta = bool(cb & 8)
ctrl = bool(cb & 16)
# Extract motion/drag flags
is_motion = bool(cb & 32)
# Wheel events
is_wheel = cb >= 64
if is_wheel:
button = cb - 64 # 0=wheel up, 1=wheel down
return cls(
button_value=button,
x=cx,
y=cy,
released=released,
shift=shift,
meta=meta,
ctrl=ctrl,
is_motion=is_motion,
is_wheel=is_wheel
)
# Backwards compatibility aliases
MouseSGREvent = MouseEvent
MouseLegacyEvent = MouseEvent
# Mouse event patterns (shared across multiple DEC modes)
# SGR mouse format (modes 1006 and 1016): ESC [ < b ; x ; y M/m
# The optional '<' allows backward compatibility with non-standard implementations
RE_PATTERN_MOUSE_SGR = re.compile(r'\x1b\[<?(?P<b>\d+);(?P<x>\d+);(?P<y>\d+)(?P<type>[mM])')
# Legacy mouse format (modes 1000, 1002, 1003): ESC [ M cb cx cy
RE_PATTERN_MOUSE_LEGACY = re.compile(r'\x1b\[M(?P<cb>.)(?P<cx>.)(?P<cy>.)')
__all__ = ('MouseEvent', 'MouseSGREvent', 'MouseLegacyEvent',
'RE_PATTERN_MOUSE_SGR', 'RE_PATTERN_MOUSE_LEGACY')
|