Files
vterm/vterm/mouse.py
2026-09-08 12:48:52 +02:00

100 lines
4.1 KiB
Python

""" SGR mouse reporting while the app tracks the mouse; otherwise sublime keeps drag_select """
import sublime_plugin
from .terminal import Terminal
# SGR button numbers for the wheel; 64 is the "scroll" bit plus direction
WHEEL_UP = 64
WHEEL_DOWN = 65
# lines per wheel tick, as iTerm2 delivers per notch; pi scrolls exactly one
# line per report and has no setting of its own
WHEEL_LINES = 3
def _event_to_cell(view, terminal, event, clamp=False):
"""Map a mouse event to 1-based (col, row) on the live screen, or None.
`clamp` pulls a point above or below the screen onto the nearest row: the
wheel still belongs to the app, while a click there does not."""
pt = view.window_to_text((event["x"], event["y"]))
row, col = view.rowcol(pt)
row -= terminal.offset
screen = terminal.screen
if row < 0 or row >= screen.lines:
if not clamp:
return None
row = min(max(row, 0), screen.lines - 1)
return (min(col, screen.columns - 1) + 1, row + 1)
class VtermMouseEventListener(sublime_plugin.EventListener):
"""Hands button-1 clicks to the app, but only while it tracks the mouse."""
def on_text_command(self, view, name, args):
"""Replace drag_select with a mouse report; other click variants stay native."""
terminal = Terminal.from_id(view.id())
if not terminal:
return
if not terminal.screen.mouse_reporting_enabled():
# leave selection, caret and scrolling to sublime
return
if name == "drag_select" and args and args.get("event", {}).get("button") == 1 and len(args) == 1:
return ("vterm_mouse_click", {"event": args["event"]})
class VtermMouseClickCommand(sublime_plugin.TextCommand):
"""Reports a click to the app, or focuses the tab if it was not active."""
def run(self, edit, event):
"""Focus the tab on the first click, report press and release after that."""
terminal = Terminal.from_id(self.view.id())
if not terminal:
return
window = self.view.window()
if window and window.active_view() != self.view:
# we replaced drag_select, which is what focuses the group; this
# click only focuses, as clicking an unfocused terminal window does
window.focus_view(self.view)
return
cell = _event_to_cell(self.view, terminal, event)
if cell is None:
return
terminal.send_mouse(0, cell[0], cell[1], pressed=True)
terminal.send_mouse(0, cell[0], cell[1], pressed=False)
def want_event(self):
"""Ask sublime for the click coordinates."""
return True
class VtermScrollCommand(sublime_plugin.TextCommand):
"""Bound to scroll wheel in the mousemap while the app tracks the mouse or is on the alt screen."""
def run(self, edit, event=None, up=False):
"""Report the wheel to the app, or fake arrow keys on the alt screen.
Only bound while the app owns the wheel; otherwise sublime scrolls the
view over the real scrollback text and this never runs.
"""
terminal = Terminal.from_id(self.view.id())
if not terminal:
return
if terminal.screen.mouse_reporting_enabled():
cell = _event_to_cell(self.view, terminal, event, clamp=True) if event else None
if cell is None:
cell = (1, terminal.screen.lines // 2 + 1)
# one report per line, never one batched write: pi matches the SGR
# form against a whole stdin chunk and drops anything longer
for _ in range(WHEEL_LINES):
terminal.send_mouse(WHEEL_UP if up else WHEEL_DOWN, cell[0], cell[1], pressed=True)
elif terminal.screen.alternate_buffer_mode:
# alternate scroll: an alt-screen app that does not track the mouse
# gets arrow keys per wheel tick, as kitty/iTerm2/xterm do
for _ in range(WHEEL_LINES):
terminal.send_key(key="up" if up else "down")
def want_event(self):
"""Ask sublime for the wheel coordinates."""
return True