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
MODIFIER_BITS =

X10 button code layout: button | 4 shift | 8 meta | 16 ctrl | 32 motion | 64 wheel, button 3 meaning "released" (R_mouse_reporting).

Returns:

  • (Integer)
4 | 8 | 16

Class Method Summary collapse

Class Method Details

.level(capture_mouse) ⇒ Symbol?

Normalizes a capture_mouse: argument to a level.

@param capture_mousefalse, true (== :clicks), or one of LEVELS.

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

Parameters:

  • capture_mouse (bool, Symbol)

Returns:

  • (Symbol, nil)


103
104
105
106
107
108
109
110
111
112
# File 'lib/tuile/mouse.rb', line 103

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) ⇒ Event?

Parses an X10 mouse report (\e[M + 3 bytes: button, x, y) into one of DownEvent, UpEvent, ScrollEvent or MoveEvent. Modifier bits are ignored.

Raises Error when key starts with the mouse prefix but is not exactly 6 bytes long. Both shorter and longer inputs are bugs in the upstream key-reader: a shorter prefix means the tail was lost on the way in, and a longer one means we over-consumed into the next escape sequence. We refuse to silently truncate either case because the trailing \e of an over-read corrupts the next getkey, and the corruption then surfaces as garbled keystrokes in focused inputs rather than as a parser failure pointing at the actual cause.

@param key — key read via Keys.getkey

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

Parameters:

  • key (String)

Returns:



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/tuile/mouse.rb', line 145

def parse(key)
  return nil unless report?(key)
  unless key.bytesize == 6
    raise Tuile::Error,
          "malformed mouse event: expected 6 bytes after \\e[M prefix, got #{key.bytesize}: #{key.inspect}"
  end

  code = (key[3].ord - 32) & ~MODIFIER_BITS
  # XTerm reports coordinates 1-based (column N is encoded as N + 32);
  # subtract 33 so that `x` and `y` are 0-based.
  x = key[4].ord - 33
  y = key[5].ord - 33
  low = code & 3
  if code.anybits?(64)
    direction = %i[up down left right][low]
    direction && ScrollEvent.new(direction, x, y)
  elsif code.anybits?(32)
    MoveEvent.new(button(low), x, y)
  elsif low == 3
    UpEvent.new(x, y)
  else
    DownEvent.new(button(low), x, y)
  end
end

.report?(key) ⇒ Boolean

Whether key is a mouse report. True on the X10 \e[M prefix regardless of length — parse is the place that validates the full 6-byte shape and raises on malformed input.

@param key — key read via Keys.getkey

Parameters:

  • key (String)

Returns:

  • (Boolean)


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

def report?(key) = key.start_with?("\e[M")

.start_tracking(level) ⇒ String

@param level — one of LEVELS.

@return — the escape enabling that level.

Parameters:

  • level (Symbol)

Returns:

  • (String)


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

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

.stop_tracking(level) ⇒ String

@param level — one of LEVELS.

@return — the escape disabling that level.

Parameters:

  • level (Symbol)

Returns:

  • (String)


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

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