Module: Tuile::Mouse

Defined in:
lib/tuile/mouse.rb,
lib/tuile/mouse/router.rb,
sig/tuile.rbs

Overview

The mouse: the events, their parser, and the tracking levels Screen#run_event_loop asks the terminal for. Who gets one is Router's story.

Four of the five classes are what Mouse.parse reads off the wire; DragEvent is the router's own, a move while something holds the grab. Each is a Data.define including the Event marker — no inheritance, so a case matches either one class or Mouse::Event for all of them. Enter and exit have no class at all: nothing is parsed, they are the difference between two hovered chains.

Coordinates are screen-absolute and 0-based everywhere.

Defined Under Namespace

Modules: Event Classes: DownEvent, DragEvent, MoveEvent, Router, ScrollEvent, UpEvent

Constant Summary collapse

LEVELS =

The capture_mouse: levels, each a strict superset of the one before, and each unlocking one tier of events: :clicks (mode 1000) gives down, up and scroll; :drag (1002) adds drag; :hover (1003) adds move, enter and exit (R_mouse_reporting).

Returns:

  • (Array<Symbol>)
%i[clicks drag hover].freeze
MODES =

Returns the DEC private mode each level sets.

Returns:

  • (Hash{Symbol => Integer}) —

    the DEC private mode each level sets.

{ clicks: 1000, drag: 1002, hover: 1003 }.freeze
SGR_MODE =

The SGR encoding mode, requested alongside whichever MODES rung the level picked — encoding and reporting are orthogonal modes. A terminal that does not understand it ignores the DECSET and keeps sending X10 (R_mouse_reporting).

Returns:

  • (Integer)
1006
MODIFIER_BITS =

Button code layout, shared by both encodings: button | 4 shift | 8 meta | 16 ctrl | 32 motion | 64 wheel (R_mouse_reporting).

Returns:

  • (Integer)
4 | 8 | 16
X10_PREFIX =

Returns the X10 report prefix, followed by three biased bytes.

Returns:

  • (String) —

    the X10 report prefix, followed by three biased bytes.

"\e[M"
SGR_PREFIX =

Returns the SGR report prefix, followed by a SGR_REPORT body.

Returns:

  • (String) —

    the SGR report prefix, followed by a SGR_REPORT body.

"\e[<"
SGR_REPORT =

\e[<Cb;x;y then M for a press and m for a release — decimal and uncapped, where X10 packs each coordinate into a byte and dies past 223.

Returns:

  • (Regexp)
/\A\e\[<(\d+);(\d+);(\d+)([Mm])\z/

Class Method Summary collapse

Class Method Details

.level(capture_mouse) ⇒ Symbol?

Normalizes a capture_mouse: argument to a level.

@param capture_mouse — false, true (== :clicks), or one of LEVELS.

@return — the level; nil when tracking is off.

Parameters:

  • capture_mouse (bool, Symbol)

Returns:

  • (Symbol, nil)


131
132
133
134
135
136
137
138
139
140
# File 'lib/tuile/mouse.rb', line 131

def level(capture_mouse)
  case capture_mouse
  when false, nil then nil
  when true then :clicks
  when *LEVELS then capture_mouse
  else
    raise ArgumentError,
          "capture_mouse: expected true, false or one of #{LEVELS}, got #{capture_mouse.inspect}"
  end
end

.parse(key) ⇒ Mouse::Event?

Parses a mouse report in either wire encoding into one of DownEvent, UpEvent, ScrollEvent or MoveEvent. Modifier bits are ignored.

Mouse.parse("\e[M !\"")        # => X10:  DownEvent[:left, 0, 0]
Mouse.parse("\e[<0;1;1M")      # => SGR:  DownEvent[:left, 0, 0]
Mouse.parse("\e[<0;1;1m")      # => SGR:  UpEvent[0, 0]

Which encoding arrived is invisible in the result, deliberately: SGR names the button on a release and X10 cannot, so the button is dropped (D_mouse_dispatch).

A prefix without a whole well-formed report raises rather than truncates: it is always a bug in the upstream key-reader — a short read lost the tail, a long one over-consumed into the next escape sequence — and swallowing it corrupts the next getkey, surfacing as garbled keystrokes in focused inputs rather than as a parser failure pointing at the cause.

@param key — key read via Keys.getkey

@return — nil if key is not a mouse report, or reports a wheel button beyond the four directions.

Parameters:

  • key (String)

Returns:



182
183
184
185
186
# File 'lib/tuile/mouse.rb', line 182

def parse(key)
  return nil unless report?(key)

  key.start_with?(SGR_PREFIX) ? parse_sgr(key) : parse_x10(key)
end

.report?(key) ⇒ Boolean

Whether key is a mouse report — the X10 \e[M prefix or the SGR \e[< one, regardless of length. parse is the place that validates the full shape and raises on malformed input.

@param key — key read via Keys.getkey

Parameters:

  • key (String)

Returns:

  • (Boolean)


159
# File 'lib/tuile/mouse.rb', line 159

def report?(key) = key.start_with?(X10_PREFIX, SGR_PREFIX)

.start_tracking(level) ⇒ String

The escape asking for that level, SGR encoding included:

Mouse.start_tracking(:clicks)   # => "\e[?1006h\e[?1000h"

@param level — one of LEVELS.

@return — the escape enabling that level.

Parameters:

  • level (Symbol)

Returns:

  • (String)


148
# File 'lib/tuile/mouse.rb', line 148

def start_tracking(level) = "\e[?#{SGR_MODE}h\e[?#{MODES.fetch(level)}h"

.stop_tracking(level) ⇒ String

@param level — one of LEVELS.

@return — the escape disabling that level, reporting first.

Parameters:

  • level (Symbol)

Returns:

  • (String)


152
# File 'lib/tuile/mouse.rb', line 152

def stop_tracking(level) = "\e[?#{MODES.fetch(level)}l\e[?#{SGR_MODE}l"