File: mouse.py

package info (click to toggle)
python-blessed 1.25-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 8,812 kB
  • sloc: python: 14,645; makefile: 13; sh: 7
file content (248 lines) | stat: -rw-r--r-- 9,535 bytes parent folder | download
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')