Mouse Input

Blessed supports mouse input in the terminal! Your applications can respond to clicks, drags, scroll wheel, or track live mouse cursor movement, even at the pixel-level, for creating interactive games and apps.

Overview:

  • Check for support using does_mouse()

  • Enable mouse input with mouse_enabled()

  • Receive events through inkey()

Mouse events work seamlessly with keyboard events - both come through the same inkey() method.

Getting Started

Here is a basic example:

 1#!/usr/bin/env python3
 2from blessed import Terminal
 3
 4term = Terminal()
 5
 6if not term.does_mouse():
 7    print("This example won't work on your terminal!")
 8else:
 9    print("Click anywhere! ^C to quit")
10    with term.cbreak(), term.mouse_enabled():
11        while True:
12            inp = term.inkey()
13            if inp.name and inp.name.startswith('MOUSE_'):
14                print(f"button {inp.name} at (y={inp.y}, x={inp.x})")

The mouse_enabled() context manager enables mouse tracking and automatically disables it when done. Mouse events arrive through inkey() just like keyboard events.

Mouse events can be detected by checking if name starts with 'MOUSE_'. The name includes the button and any modifiers, such as 'MOUSE_LEFT', 'MOUSE_CTRL_LEFT', or 'MOUSE_SCROLL_UP'. You can also use magic method predicates like inp.is_mouse_left().

Note

Mouse coordinates are 0-indexed, matching blessed’s terminal movement functions like move_yx(). The top-left corner is (y=0, x=0), not (1, 1). This allows direct use of mouse coordinates with movement functions.

Understanding Buttons

Mouse events come through inkey() just like keyboard events.

You can detect mouse events using either the name property or magic method predicates:

Using the name property:

The name returns button names with the MOUSE_ prefix, following the pattern MOUSE_[MODIFIERS_]BUTTON[_RELEASED]:

inp = term.inkey()
if inp.name == 'MOUSE_LEFT':
    print("Left button pressed")

Using magic method predicates:

inp = term.inkey()
if inp.is_mouse_left():
    print("Left button pressed")
elif inp.is_mouse_ctrl_left():
    print("Ctrl+Left button pressed")

Button names include:

  • Basic events: MOUSE_LEFT, MOUSE_MIDDLE, MOUSE_RIGHT, MOUSE_SCROLL_UP, MOUSE_SCROLL_DOWN

  • Release events: MOUSE_LEFT_RELEASED, MOUSE_MIDDLE_RELEASED, MOUSE_RIGHT_RELEASED

  • With modifiers: MOUSE_CTRL_LEFT, MOUSE_SHIFT_SCROLL_UP, MOUSE_META_RIGHT, MOUSE_CTRL_SHIFT_META_MIDDLE

Modifiers are included in order CTRL, SHIFT, and META

In this example, all possible combinations may be entered and recorded, see if you have enough fingers for CTRL_SHIFT_META_MIDDLE, imagine the possibilities!

 1#!/usr/bin/env python
 2from blessed import Terminal
 3
 4term = Terminal()
 5counts = dict()
 6
 7with term.fullscreen(), term.cbreak(), term.mouse_enabled():
 8    while True:
 9        inp = term.inkey(timeout=None)
10
11        # Check if this is a mouse event
12        if inp.name and inp.name.startswith('MOUSE_'):
13            # Use the keystroke name for button identification
14            counts[inp.name] = counts.get(inp.name, 0) + 1
15            with term.synchronized_output():
16                print(term.home + term.clear)
17                print(term.bold("Mouse Modifier Example, press Ctrl+C to quit"))
18                print()
19
20                # Display the most recent event
21                print(f"button={inp.name} at (y={inp.y}, x={inp.x})")
22                print()
23
24                # Display totals
25                print("Totals: ")
26                for button_name, count in sorted(counts.items()):
27                    print(f"{button_name}: {count}")

Understanding Mouse Coordinates

Mouse coordinates are accessed by mouse_yx attribute in as tuple (int, int) of the corresponding row and column. This matches the signature of move_yx(). If mouse_yx is used on a keystroke that is not a mouse event, values (-1, -1) are returned.

There is also a mouse_xy attribute that mirrors the signature of move_xy().

 1#!/usr/bin/env python3
 2from blessed import Terminal
 3
 4term = Terminal()
 5
 6if not term.does_mouse():
 7    print("This example won't work on your terminal!")
 8else:
 9    with term.cbreak(), term.fullscreen(), term.mouse_enabled(report_drag=True):
10        print("Click to move cursor! ^C to quit")
11        while True:
12            inp = term.inkey()
13            if inp.name and inp.name.startswith('MOUSE_'):
14                print(term.move_yx(*inp.mouse_yx), end='', flush=True)

Checking Support

Not all terminals support mouse tracking or all kinds of mouse tracking.

Use does_mouse() to check before enabling:

 1#!/usr/bin/env python3
 2from blessed import Terminal
 3
 4term = Terminal()
 5
 6# check basic mouse support
 7if not term.does_mouse():
 8    print(f"mouse_enabled() {term.bright_red('not supported')} on this Terminal")
 9else:
10    # check for, enable, and report all supported advanced features
11    feature_kwargs = {mouse_feature: True
12                      for mouse_feature in ('report_pixels', 'report_drag', 'report_motion')
13                      if term.does_mouse(**{mouse_feature: True})}
14    with term.mouse_enabled(**feature_kwargs):
15        print(f"mouse_enabled({', '.join(feature_kwargs)}) enabled")

The does_mouse() method accepts the same parameters as mouse_enabled() and returns True if all of given modes are supported.

Note

On Windows, does_mouse() always returns True because the native console API provides mouse events on all versions. Native MOUSE_EVENT records are automatically converted to the same event format used on Unix, so the same code works on all platforms.

Using mouse_enabled()

The mouse_enabled() context manager enables the appropriate DEC Private Modes depending on the simplified arguments given.

mouse_enabled() accepts these keyword-only parameters:

  • clicks=True - Enable basic click reporting (default).

  • report_drag=False - Report motion while a button is held.

  • report_motion=False - Report all mouse movement.

  • report_pixels=False - Report position in pixels instead of cells.

  • timeout=1.0 - Timeout for mode queries, in seconds.

Parameter Precedence

The tracking modes have precedence: report_motion > report_drag > clicks. When you enable a higher-precedence mode, it automatically includes the functionality of lower modes. For example, report_motion=True will also track drags and clicks.

report_drag

Reports motion only while a button is held down:

 1#!/usr/bin/env python3
 2from blessed import Terminal
 3
 4term = Terminal()
 5
 6if not term.does_mouse(report_drag=True):
 7    print("This example won't work on your terminal!")
 8else:
 9    with term.cbreak(), term.mouse_enabled(report_drag=True):
10        while True:
11            inp = term.inkey()
12            if inp.name and inp.name.endswith('_MOTION'):
13                print(f"Drag event at ({inp.mouse_yx})")

When using report_drag=True or report_motion=True, you’ll receive motion events in the name attribute with a _MOTION suffix:

  • MOUSE_MOTION - Motion without any button pressed (report_motion)

  • MOUSE_LEFT_MOTION, MOUSE_MIDDLE_MOTION, MOUSE_RIGHT_MOTION - Dragging with a button held, usually follows click, eg. MOUSE_LEFT.

Motion events include modifiers just like click events, for example MOUSE_CTRL_LEFT_MOTION.

report_motion

Reports all mouse clicks movement, even without buttons pressed. The name attribute of MOUSE_MOTION is given when no button or scroll wheel events have occurred, only an updated mouse_yx position.

In this example, the terminal cursor tracks with the mouse pointer because the move_yx() sequence is displayed following any mouse event, especially MOUSE_MOTION, tracking the Keystroke.mouse_yx coordinate.

 1#!/usr/bin/env python
 2from blessed import Terminal
 3
 4term = Terminal()
 5
 6if not term.does_mouse(report_motion=True):
 7    print("This terminal does not support mouse motion tracking!")
 8else:
 9    # Track current color for painting
10    color_idx = 7
11    num_colors = min(256, term.number_of_colors)
12    header = "Mouse wheel sets color=[{0}], LEFT button paints, RIGHT erases, ^C:quit"
13
14    def make_header():
15        return term.home + term.center(header.format(term.color(color_idx)('█')))
16    text = make_header()
17
18    with term.cbreak(), term.fullscreen(), term.mouse_enabled(report_motion=True):
19        while True:
20            print(text, end='', flush=True)
21            inp = term.inkey()
22
23            if inp.name and inp.name.startswith('MOUSE_'):
24                # process scroll wheel changes color
25                _offset = (1 if inp.name == 'MOUSE_SCROLL_UP' else
26                           -1 if inp.name == 'MOUSE_SCROLL_DOWN' else 0)
27                color_idx = (color_idx + _offset) % num_colors
28
29                # and left mouse paints, right erases
30                char = (term.color(color_idx)('█')
31                        if inp.name.startswith('MOUSE_LEFT')
32                        else ' ' if inp.name.startswith('MOUSE_RIGHT')
33                        else '')
34
35                # update draw text using mouse_yx
36                text = make_header() + term.move_yx(*inp.mouse_yx) + char
https://dxtz6bzwq9sxx.cloudfront.net/mouse_paint.gif

Painting is done while the left mouse button is held down, tracking both MOUSE_LEFT and MOUSE_LEFT_MOTION, erased with MOUSE_RIGHT, and color selection changed by MOUSE_SCROLL_UP and MOUSE_SCROLL_DOWN.

Note

When using report_motion=True, process events quickly! Mouse movement generates many events that can fill the input buffer if not consumed promptly.

report_pixels

By default, mouse positions are reported in character cell coordinates - each position corresponds to a single character in the terminal grid.

For higher precision, use report_pixels=True to get pixel coordinates instead. This is especially useful when combined with graphics protocols like Sixel:

 1#!/usr/bin/env python3
 2from blessed import Terminal
 3
 4term = Terminal()
 5
 6if not term.does_mouse(report_pixels=True):
 7    print("This terminal does not support pixel coordinate mouse tracking!")
 8else:
 9    print("Click to display Pixel coordinates, ^C to quit:")
10    with term.cbreak(), term.mouse_enabled(report_pixels=True):
11        while True:
12            event = term.inkey()
13
14            if event.name and event.name.startswith('MOUSE_'):
15                print(f"Pixel position: (y={event.y}, x={event.x})")

When using pixel mode, mouse events still use the same name pattern (e.g., 'MOUSE_LEFT') and magic method predicates (e.g., inp.is_mouse_left()). The x and y properties represent pixels instead of character cells.