Parcourir la source

Merge branch 'master' into pr/tauu/131

xuty il y a 3 ans
Parent
commit
d39711035a

+ 4 - 1
lib/core.dart

@@ -13,7 +13,10 @@ export 'src/core/escape/handler.dart';
 export 'src/core/escape/parser.dart';
 export 'src/core/input/handler.dart';
 export 'src/core/input/keys.dart';
-export 'src/core/mouse.dart';
+export 'src/core/mouse/button.dart';
+export 'src/core/mouse/button_state.dart';
+export 'src/core/mouse/handler.dart';
+export 'src/core/mouse/mode.dart';
 export 'src/core/state.dart';
 export 'src/terminal.dart';
 export 'src/utils/platform.dart';

+ 1 - 1
lib/src/core/escape/handler.dart

@@ -1,4 +1,4 @@
-import 'package:xterm/src/core/mouse.dart';
+import 'package:xterm/src/core/mouse/mode.dart';
 
 abstract class EscapeHandler {
   void writeChar(int char);

+ 1 - 1
lib/src/core/escape/parser.dart

@@ -1,5 +1,5 @@
 import 'package:xterm/src/core/color.dart';
-import 'package:xterm/src/core/mouse.dart';
+import 'package:xterm/src/core/mouse/mode.dart';
 import 'package:xterm/src/core/escape/handler.dart';
 import 'package:xterm/src/utils/ascii.dart';
 import 'package:xterm/src/utils/byte_consumer.dart';

+ 0 - 23
lib/src/core/mouse.dart

@@ -1,23 +0,0 @@
-/// https://terminalguide.namepad.de/mouse/
-enum MouseMode {
-  none,
-
-  clickOnly,
-
-  upDownScroll,
-
-  upDownScrollDrag,
-
-  upDownScrollMove,
-}
-
-/// https://terminalguide.namepad.de/mouse/
-enum MouseReportMode {
-  normal,
-
-  utf,
-
-  sgr,
-
-  urxvt,
-}

+ 29 - 0
lib/src/core/mouse/button.dart

@@ -0,0 +1,29 @@
+enum TerminalMouseButton {
+  left(id: 0),
+
+  middle(id: 1),
+
+  right(id: 2),
+
+  wheelUp(id: 64 + 4, isWheel: true),
+
+  wheelDown(id: 64 + 5, isWheel: true),
+
+  wheelLeft(id: 64 + 6, isWheel: true),
+
+  wheelRight(id: 64 + 7, isWheel: true),
+  ;
+
+  /// The id that is used to report a button press or release to the terminal.
+  ///
+  /// Mouse wheel up / down use button IDs 4 = 0100 (binary) and 5 = 0101 (binary).
+  /// The bits three and four of the button are transposed by 64 and 128
+  /// respectively, when reporting the id of the button and have have to be
+  /// adjusted correspondingly.
+  final int id;
+
+  /// Whether this button is a mouse wheel button.
+  final bool isWheel;
+
+  const TerminalMouseButton({required this.id, this.isWheel = false});
+}

+ 5 - 0
lib/src/core/mouse/button_state.dart

@@ -0,0 +1,5 @@
+enum TerminalMouseButtonState {
+  up,
+
+  down,
+}

+ 114 - 0
lib/src/core/mouse/handler.dart

@@ -0,0 +1,114 @@
+import 'package:xterm/src/core/buffer/cell_offset.dart';
+import 'package:xterm/src/core/mouse/button_state.dart';
+import 'package:xterm/src/core/mouse/mode.dart';
+import 'package:xterm/src/core/mouse/button.dart';
+import 'package:xterm/src/core/mouse/reporter.dart';
+import 'package:xterm/src/utils/platform.dart';
+import 'package:xterm/src/core/state.dart';
+
+class TerminalMouseEvent {
+  /// The button that is pressed or released.
+  final TerminalMouseButton button;
+
+  /// The current state of the button.
+  final TerminalMouseButtonState buttonState;
+
+  /// The position of button state change.
+  final CellOffset position;
+
+  /// The state of the terminal.
+  final TerminalState state;
+
+  /// The platform of the terminal.
+  final TerminalTargetPlatform platform;
+
+  TerminalMouseEvent({
+    required this.button,
+    required this.buttonState,
+    required this.position,
+    required this.state,
+    required this.platform,
+  });
+}
+
+const defaultMouseHandler = CascadeMouseHandler([
+  ClickMouseHandler(),
+  UpDownMouseHandler(),
+]);
+
+abstract class TerminalMouseHandler {
+  const TerminalMouseHandler();
+
+  String? call(TerminalMouseEvent event);
+}
+
+class CascadeMouseHandler implements TerminalMouseHandler {
+  final List<TerminalMouseHandler> _handlers;
+
+  const CascadeMouseHandler(this._handlers);
+
+  @override
+  String? call(TerminalMouseEvent event) {
+    for (var handler in _handlers) {
+      final result = handler(event);
+      if (result != null) {
+        return result;
+      }
+    }
+    return null;
+  }
+}
+
+class ClickMouseHandler implements TerminalMouseHandler {
+  const ClickMouseHandler();
+
+  @override
+  String? call(TerminalMouseEvent event) {
+    switch (event.state.mouseMode) {
+      case MouseMode.clickOnly:
+        // Only clicks and only the first 3 buttons are reported.
+        if (event.buttonState == TerminalMouseButtonState.down &&
+            (event.button.id < 3)) {
+          return MouseReporter.report(
+            event.button,
+            event.buttonState,
+            event.position,
+            event.state.mouseReportMode,
+          );
+        }
+        return null;
+      case MouseMode.none:
+      case MouseMode.upDownScroll:
+      case MouseMode.upDownScrollDrag:
+      case MouseMode.upDownScrollMove:
+        return null;
+    }
+  }
+}
+
+class UpDownMouseHandler implements TerminalMouseHandler {
+  const UpDownMouseHandler();
+
+  @override
+  String? call(TerminalMouseEvent event) {
+    switch (event.state.mouseMode) {
+      case MouseMode.none:
+      case MouseMode.clickOnly:
+        return null;
+      case MouseMode.upDownScroll:
+      case MouseMode.upDownScrollDrag:
+      case MouseMode.upDownScrollMove:
+        // Up events are never reported for mouse wheel buttons.
+        if (event.button.isWheel &&
+            event.buttonState == TerminalMouseButtonState.up) {
+          return null;
+        }
+        return MouseReporter.report(
+          event.button,
+          event.buttonState,
+          event.position,
+          event.state.mouseReportMode,
+        );
+    }
+  }
+}

+ 31 - 0
lib/src/core/mouse/mode.dart

@@ -0,0 +1,31 @@
+/// https://terminalguide.namepad.de/mouse/
+enum MouseMode {
+  none,
+
+  clickOnly,
+
+  upDownScroll,
+
+  upDownScrollDrag,
+
+  upDownScrollMove,
+}
+
+/// https://terminalguide.namepad.de/mouse/
+enum MouseReportMode {
+  /// The default mouse reporting mode where digits are encoded as bytes with
+  /// `32 + code`. This mode has a range from 1 to 223.
+  normal,
+
+  /// When code < 96 this is the same as [normal], otherwise the `code + 32` is
+  /// encoded as 2 bytes in UTF-8. This mode has a range from 1 to 2015.
+  utf,
+
+  /// In this mode the code are encoded as 10-based numbers. Tha range is
+  /// unlimited.
+  sgr,
+
+  /// Similar to [sgr], the difference is that the button id is encoded as
+  /// `32 + code`.
+  urxvt,
+}

+ 48 - 0
lib/src/core/mouse/reporter.dart

@@ -0,0 +1,48 @@
+import 'package:xterm/src/core/buffer/cell_offset.dart';
+import 'package:xterm/src/core/mouse/mode.dart';
+import 'package:xterm/src/core/mouse/button.dart';
+import 'package:xterm/src/core/mouse/button_state.dart';
+
+abstract class MouseReporter {
+  static String report(
+    TerminalMouseButton button,
+    TerminalMouseButtonState state,
+    CellOffset position,
+    MouseReportMode reportMode,
+  ) {
+    // x and y offsets have to be incremented by 1 as the offset if 0-based,
+    // The position has to be reported using 1-based coordinates.
+    final x = position.x + 1;
+    final y = position.y + 1;
+    switch (reportMode) {
+      case MouseReportMode.normal:
+      case MouseReportMode.utf:
+        // Button ID 3 is used to signal a button release.
+        final buttonID = state == TerminalMouseButtonState.up ? 3 : button.id;
+        // The button ID is reported as shifted by 32 to produce a printable
+        // character.
+        final btn = String.fromCharCode(32 + buttonID);
+        // Normal mode only supports a maximum position of 223, while utf
+        // supports positions up to 2015. Both modes send a null byte if the
+        // position exceeds that limit.
+        final col = (reportMode == MouseReportMode.normal && x > 223) ||
+                (reportMode == MouseReportMode.utf && x > 2015)
+            ? '\x00'
+            : String.fromCharCode(32 + x);
+        final row = (reportMode == MouseReportMode.normal && y > 223) ||
+                (reportMode == MouseReportMode.utf && y > 2015)
+            ? '\x00'
+            : String.fromCharCode(32 + y + 1);
+        return "\x1b[M$btn$col$row";
+      case MouseReportMode.sgr:
+        final buttonID = button.id;
+        final upDown = state == TerminalMouseButtonState.down ? 'M' : 'm';
+        return "\x1b[<$buttonID;$x;$y$upDown";
+      case MouseReportMode.urxvt:
+        // The button ID uses the same id as to report it as in normal mode.
+        final buttonID =
+            32 + (state == TerminalMouseButtonState.up ? 3 : button.id);
+        return "\x1b[$buttonID;$x;${y}M";
+    }
+  }
+}

+ 3 - 1
lib/src/core/state.dart

@@ -1,5 +1,5 @@
 import 'package:xterm/src/core/cursor.dart';
-import 'package:xterm/src/core/mouse.dart';
+import 'package:xterm/src/core/mouse/mode.dart';
 
 abstract class TerminalState {
   int get viewWidth;
@@ -26,6 +26,8 @@ abstract class TerminalState {
 
   MouseMode get mouseMode;
 
+  MouseReportMode get mouseReportMode;
+
   bool get cursorBlinkMode;
 
   bool get cursorVisibleMode;

+ 29 - 1
lib/src/terminal.dart

@@ -1,6 +1,7 @@
 import 'dart:math' show max;
 
 import 'package:xterm/src/core/buffer/buffer.dart';
+import 'package:xterm/src/core/buffer/cell_offset.dart';
 import 'package:xterm/src/core/buffer/line.dart';
 import 'package:xterm/src/core/cursor.dart';
 import 'package:xterm/src/core/escape/emitter.dart';
@@ -8,7 +9,10 @@ import 'package:xterm/src/core/escape/handler.dart';
 import 'package:xterm/src/core/escape/parser.dart';
 import 'package:xterm/src/core/input/handler.dart';
 import 'package:xterm/src/core/input/keys.dart';
-import 'package:xterm/src/core/mouse.dart';
+import 'package:xterm/src/core/mouse/mode.dart';
+import 'package:xterm/src/core/mouse/button.dart';
+import 'package:xterm/src/core/mouse/button_state.dart';
+import 'package:xterm/src/core/mouse/handler.dart';
 import 'package:xterm/src/core/state.dart';
 import 'package:xterm/src/core/tabs.dart';
 import 'package:xterm/src/utils/ascii.dart';
@@ -44,8 +48,11 @@ class Terminal with Observable implements TerminalState, EscapeHandler {
     this.onResize,
     this.platform = TerminalTargetPlatform.unknown,
     this.inputHandler = defaultInputHandler,
+    this.mouseHandler = defaultMouseHandler,
   });
 
+  TerminalMouseHandler? mouseHandler;
+
   late final _parser = EscapeParser(this);
 
   final _emitter = const EscapeEmitter();
@@ -128,6 +135,7 @@ class Terminal with Observable implements TerminalState, EscapeHandler {
   @override
   MouseMode get mouseMode => _mouseMode;
 
+  @override
   MouseReportMode get mouseReportMode => _mouseReportMode;
 
   @override
@@ -234,6 +242,26 @@ class Terminal with Observable implements TerminalState, EscapeHandler {
     }
   }
 
+  // Handle a mouse event and return true if it was handled.
+  bool mouseInput(
+    TerminalMouseButton button,
+    TerminalMouseButtonState buttonState,
+    CellOffset position,
+  ) {
+    final output = mouseHandler?.call(TerminalMouseEvent(
+      button: button,
+      buttonState: buttonState,
+      position: position,
+      state: this,
+      platform: platform,
+    ));
+    if (output != null) {
+      onOutput?.call(output);
+      return true;
+    }
+    return false;
+  }
+
   /// Resize the terminal screen. [newWidth] and [newHeight] should be greater
   /// than 0. Text reflow is currently not implemented and will be avaliable in
   /// the future.

+ 15 - 1
lib/src/terminal_view.dart

@@ -130,7 +130,7 @@ class TerminalView extends StatefulWidget {
 }
 
 class TerminalViewState extends State<TerminalView> {
-  late final FocusNode _focusNode;
+  late FocusNode _focusNode;
 
   late final ShortcutManager _shortcutManager;
 
@@ -169,9 +169,15 @@ class TerminalViewState extends State<TerminalView> {
       _focusNode = widget.focusNode ?? FocusNode();
     }
     if (oldWidget.controller != widget.controller) {
+      if (oldWidget.controller == null) {
+        _controller.dispose();
+      }
       _controller = widget.controller ?? TerminalController();
     }
     if (oldWidget.scrollController != widget.scrollController) {
+      if (oldWidget.scrollController == null) {
+        _scrollController.dispose();
+      }
       _scrollController = widget.scrollController ?? ScrollController();
     }
     super.didUpdateWidget(oldWidget);
@@ -182,6 +188,12 @@ class TerminalViewState extends State<TerminalView> {
     if (widget.focusNode == null) {
       _focusNode.dispose();
     }
+    if (widget.controller == null) {
+      _controller.dispose();
+    }
+    if (widget.scrollController == null) {
+      _scrollController.dispose();
+    }
     _shortcutManager.dispose();
     super.dispose();
   }
@@ -259,12 +271,14 @@ class TerminalViewState extends State<TerminalView> {
 
     child = TerminalGestureHandler(
       terminalView: this,
+      terminalController: _controller,
       onTapUp: _onTapUp,
       onTapDown: _onTapDown,
       onSecondaryTapDown:
           widget.onSecondaryTapDown != null ? _onSecondaryTapDown : null,
       onSecondaryTapUp:
           widget.onSecondaryTapUp != null ? _onSecondaryTapUp : null,
+      readOnly: widget.readOnly,
       child: child,
     );
 

+ 41 - 3
lib/src/ui/controller.dart

@@ -1,11 +1,21 @@
 import 'package:flutter/material.dart';
+import 'package:meta/meta.dart';
 import 'package:xterm/src/core/buffer/cell_offset.dart';
 import 'package:xterm/src/core/buffer/range.dart';
 import 'package:xterm/src/core/buffer/range_block.dart';
 import 'package:xterm/src/core/buffer/range_line.dart';
+import 'package:xterm/src/ui/pointer_input.dart';
 import 'package:xterm/src/ui/selection_mode.dart';
 
 class TerminalController with ChangeNotifier {
+  TerminalController({
+    SelectionMode selectionMode = SelectionMode.line,
+    PointerInputs pointerInputs = const PointerInputs.none(),
+    bool suspendPointerInput = false,
+  })  : _selectionMode = selectionMode,
+        _pointerInputs = pointerInputs,
+        _suspendPointerInputs = suspendPointerInput;
+
   BufferRange? _selection;
 
   BufferRange? get selection => _selection;
@@ -14,12 +24,19 @@ class TerminalController with ChangeNotifier {
 
   SelectionMode get selectionMode => _selectionMode;
 
-  TerminalController({SelectionMode selectionMode = SelectionMode.line})
-      : _selectionMode = selectionMode;
-
   /// Set selection on the terminal to [range]. For now [range] could be either
   /// a [BufferRangeLine] or a [BufferRangeBlock]. This is not effected by
   /// [selectionMode].
+  PointerInputs _pointerInputs;
+
+  /// The set of pointer events which will be used as mouse input for the terminal.
+  PointerInputs get pointerInput => _pointerInputs;
+
+  bool _suspendPointerInputs;
+
+  /// True if sending pointer events to the terminal is suspended.
+  bool get suspendedPointerInputs => _suspendPointerInputs;
+
   void setSelection(BufferRange? range) {
     range = range?.normalized;
 
@@ -72,6 +89,27 @@ class TerminalController with ChangeNotifier {
     notifyListeners();
   }
 
+  // Select which type of pointer events are send to the terminal.
+  void setPointerInputs(PointerInputs pointerInput) {
+    _pointerInputs = pointerInput;
+    notifyListeners();
+  }
+
+  // Toggle sending pointer events to the terminal.
+  void setSuspendPointerInput(bool suspend) {
+    _suspendPointerInputs = suspend;
+    notifyListeners();
+  }
+
+  // Returns true if this type of PointerInput should be send to the Terminal.
+  @internal
+  bool shouldSendPointerInput(PointerInput pointerInput) {
+    // Always return false if pointer input is suspended.
+    return _suspendPointerInputs
+        ? false
+        : _pointerInputs.inputs.contains(pointerInput);
+  }
+
   void addHighlight(BufferRange? range) {
     // TODO: implement addHighlight
   }

+ 9 - 1
lib/src/ui/gesture/gesture_detector.dart

@@ -12,6 +12,8 @@ class TerminalGestureDetector extends StatefulWidget {
     this.onTapDown,
     this.onSecondaryTapDown,
     this.onSecondaryTapUp,
+    this.onTertiaryTapDown,
+    this.onTertiaryTapUp,
     this.onLongPressStart,
     this.onLongPressMoveUpdate,
     this.onLongPressUp,
@@ -34,6 +36,10 @@ class TerminalGestureDetector extends StatefulWidget {
 
   final GestureTapDownCallback? onDoubleTapDown;
 
+  final GestureTapDownCallback? onTertiaryTapDown;
+
+  final GestureTapUpCallback? onTertiaryTapUp;
+
   final GestureLongPressStartCallback? onLongPressStart;
 
   final GestureLongPressMoveUpdateCallback? onLongPressMoveUpdate;
@@ -110,7 +116,9 @@ class _TerminalGestureDetectorState extends State<TerminalGestureDetector> {
           ..onTapDown = _handleTapDown
           ..onTapUp = _handleTapUp
           ..onSecondaryTapDown = widget.onSecondaryTapDown
-          ..onSecondaryTapUp = widget.onSecondaryTapUp;
+          ..onSecondaryTapUp = widget.onSecondaryTapUp
+          ..onTertiaryTapDown = widget.onTertiaryTapDown
+          ..onTertiaryTapUp = widget.onTertiaryTapUp;
       },
     );
 

+ 99 - 4
lib/src/ui/gesture/gesture_handler.dart

@@ -1,23 +1,33 @@
 import 'package:flutter/gestures.dart';
 import 'package:flutter/widgets.dart';
+import 'package:xterm/src/core/mouse/button.dart';
+import 'package:xterm/src/core/mouse/button_state.dart';
 import 'package:xterm/src/terminal_view.dart';
+import 'package:xterm/src/ui/controller.dart';
 import 'package:xterm/src/ui/gesture/gesture_detector.dart';
+import 'package:xterm/src/ui/pointer_input.dart';
 import 'package:xterm/src/ui/render.dart';
 
 class TerminalGestureHandler extends StatefulWidget {
   const TerminalGestureHandler({
     super.key,
     required this.terminalView,
+    required this.terminalController,
     this.child,
     this.onTapUp,
     this.onSingleTapUp,
     this.onTapDown,
     this.onSecondaryTapDown,
     this.onSecondaryTapUp,
+    this.onTertiaryTapDown,
+    this.onTertiaryTapUp,
+    this.readOnly = false,
   });
 
   final TerminalViewState terminalView;
 
+  final TerminalController terminalController;
+
   final Widget? child;
 
   final GestureTapUpCallback? onTapUp;
@@ -30,6 +40,12 @@ class TerminalGestureHandler extends StatefulWidget {
 
   final GestureTapUpCallback? onSecondaryTapUp;
 
+  final GestureTapDownCallback? onTertiaryTapDown;
+
+  final GestureTapUpCallback? onTertiaryTapUp;
+
+  final bool readOnly;
+
   @override
   State<TerminalGestureHandler> createState() => _TerminalGestureHandlerState();
 }
@@ -48,10 +64,12 @@ class _TerminalGestureHandlerState extends State<TerminalGestureHandler> {
     return TerminalGestureDetector(
       child: widget.child,
       onTapUp: widget.onTapUp,
-      onSingleTapUp: widget.onSingleTapUp,
-      onTapDown: widget.onTapDown,
-      onSecondaryTapDown: widget.onSecondaryTapDown,
-      onSecondaryTapUp: widget.onSecondaryTapUp,
+      onSingleTapUp: onSingleTapUp,
+      onTapDown: onTapDown,
+      onSecondaryTapDown: onSecondaryTapDown,
+      onSecondaryTapUp: onSecondaryTapUp,
+      onTertiaryTapDown: onSecondaryTapDown,
+      onTertiaryTapUp: onSecondaryTapUp,
       onLongPressStart: onLongPressStart,
       onLongPressMoveUpdate: onLongPressMoveUpdate,
       // onLongPressUp: onLongPressUp,
@@ -61,6 +79,83 @@ class _TerminalGestureHandlerState extends State<TerminalGestureHandler> {
     );
   }
 
+  bool get _shouldSendTapEvent =>
+      !widget.readOnly &&
+      widget.terminalController.shouldSendPointerInput(PointerInput.tap);
+
+  void _tapDown(
+    GestureTapDownCallback? callback,
+    TapDownDetails details,
+    TerminalMouseButton button, {
+    bool forceCallback = false,
+  }) {
+    // Check if the terminal should and can handle the tap down event.
+    var handled = false;
+    if (_shouldSendTapEvent) {
+      handled = renderTerminal.mouseEvent(
+        button,
+        TerminalMouseButtonState.down,
+        details.localPosition,
+      );
+    }
+    // If the event was not handled by the terminal, use the supplied callback.
+    if (!handled || forceCallback) {
+      callback?.call(details);
+    }
+  }
+
+  void _tapUp(
+    GestureTapUpCallback? callback,
+    TapUpDetails details,
+    TerminalMouseButton button, {
+    bool forceCallback = false,
+  }) {
+    // Check if the terminal should and can handle the tap up event.
+    var handled = false;
+    if (_shouldSendTapEvent) {
+      handled = renderTerminal.mouseEvent(
+        button,
+        TerminalMouseButtonState.up,
+        details.localPosition,
+      );
+    }
+    // If the event was not handled by the terminal, use the supplied callback.
+    if (!handled || forceCallback) {
+      callback?.call(details);
+    }
+  }
+
+  void onTapDown(TapDownDetails details) {
+    // onTapDown is special, as it will always call the supplied callback.
+    // The TerminalView depends on it to bring the terminal into focus.
+    _tapDown(
+      widget.onTapDown,
+      details,
+      TerminalMouseButton.left,
+      forceCallback: true,
+    );
+  }
+
+  void onSingleTapUp(TapUpDetails details) {
+    _tapUp(widget.onSingleTapUp, details, TerminalMouseButton.left);
+  }
+
+  void onSecondaryTapDown(TapDownDetails details) {
+    _tapDown(widget.onSecondaryTapDown, details, TerminalMouseButton.right);
+  }
+
+  void onSecondaryTapUp(TapUpDetails details) {
+    _tapUp(widget.onSecondaryTapUp, details, TerminalMouseButton.right);
+  }
+
+  void onTertiaryTapDown(TapDownDetails details) {
+    _tapDown(widget.onTertiaryTapDown, details, TerminalMouseButton.middle);
+  }
+
+  void onTertiaryTapUp(TapUpDetails details) {
+    _tapUp(widget.onTertiaryTapUp, details, TerminalMouseButton.right);
+  }
+
   void onDoubleTapDown(TapDownDetails details) {
     renderTerminal.selectWord(details.localPosition);
   }

+ 29 - 0
lib/src/ui/pointer_input.dart

@@ -0,0 +1,29 @@
+enum PointerInput {
+  /// Taps / buttons presses & releases.
+  tap,
+
+  /// Scroll / mouse wheels events.
+  scroll,
+
+  /// Drag events, a pointer is in a down state and dragged across the terminal.
+  drag,
+
+  /// Move events, a pointer is in an up state and moved across the terminal.
+  move,
+}
+
+class PointerInputs {
+  final Set<PointerInput> inputs;
+
+  const PointerInputs(this.inputs);
+
+  const PointerInputs.none() : inputs = const <PointerInput>{};
+
+  const PointerInputs.all()
+      : inputs = const <PointerInput>{
+          PointerInput.tap,
+          PointerInput.scroll,
+          PointerInput.drag,
+          PointerInput.move,
+        };
+}

+ 12 - 0
lib/src/ui/render.dart

@@ -10,6 +10,8 @@ import 'package:xterm/src/core/buffer/cell_offset.dart';
 import 'package:xterm/src/core/buffer/range.dart';
 import 'package:xterm/src/core/cell.dart';
 import 'package:xterm/src/core/buffer/line.dart';
+import 'package:xterm/src/core/mouse/button.dart';
+import 'package:xterm/src/core/mouse/button_state.dart';
 import 'package:xterm/src/terminal.dart';
 import 'package:xterm/src/ui/char_metrics.dart';
 import 'package:xterm/src/ui/controller.dart';
@@ -295,6 +297,16 @@ class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
     }
   }
 
+  /// Send a mouse event at [offset] with [button] being currently in [buttonState].
+  bool mouseEvent(
+    TerminalMouseButton button,
+    TerminalMouseButtonState buttonState,
+    Offset offset,
+  ) {
+    final position = getCellOffset(offset);
+    return _terminal.mouseInput(button, buttonState, position);
+  }
+
   void _notifyEditableRect() {
     final cursor = localToGlobal(_cursorOffset);
 

+ 1 - 1
lib/src/utils/debugger.dart

@@ -1,6 +1,6 @@
 import 'package:xterm/src/core/escape/handler.dart';
 import 'package:xterm/src/core/escape/parser.dart';
-import 'package:xterm/src/core/mouse.dart';
+import 'package:xterm/src/core/mouse/mode.dart';
 import 'package:xterm/src/utils/observable.dart';
 
 class TerminalCommand {

+ 1 - 0
lib/ui.dart

@@ -2,6 +2,7 @@ export 'src/terminal_view.dart';
 export 'src/ui/controller.dart';
 export 'src/ui/cursor_type.dart';
 export 'src/ui/keyboard_visibility.dart';
+export 'src/ui/pointer_input.dart';
 export 'src/ui/selection_mode.dart';
 export 'src/ui/shortcut/shortcuts.dart';
 export 'src/ui/terminal_text_style.dart';

+ 1 - 1
test/src/core/escape/parser_test.mocks.dart

@@ -5,7 +5,7 @@
 // ignore_for_file: no_leading_underscores_for_library_prefixes
 import 'package:mockito/mockito.dart' as _i1;
 import 'package:xterm/src/core/escape/handler.dart' as _i2;
-import 'package:xterm/src/core/mouse.dart' as _i3;
+import 'package:xterm/src/core/mouse/mode.dart' as _i3;
 
 // ignore_for_file: type=lint
 // ignore_for_file: avoid_redundant_argument_values

+ 51 - 0
test/src/core/mouse/reporter_test.dart

@@ -0,0 +1,51 @@
+import 'package:test/test.dart';
+import 'package:xterm/src/core/mouse/reporter.dart';
+import 'package:xterm/xterm.dart';
+
+void main() {
+  group('MouseReporter', () {
+    test('report() supports normal mode', () {
+      final output = MouseReporter.report(
+        TerminalMouseButton.left,
+        TerminalMouseButtonState.down,
+        CellOffset(0, 0),
+        MouseReportMode.normal,
+      );
+
+      expect(output, equals('\x1B[M !"'));
+    });
+
+    test('report() supports utf mode', () {
+      final output = MouseReporter.report(
+        TerminalMouseButton.left,
+        TerminalMouseButtonState.down,
+        CellOffset(0, 0),
+        MouseReportMode.utf,
+      );
+
+      expect(output, equals('\x1B[M !"'));
+    });
+
+    test('report() supports sgr mode', () {
+      final output = MouseReporter.report(
+        TerminalMouseButton.left,
+        TerminalMouseButtonState.down,
+        CellOffset(0, 0),
+        MouseReportMode.sgr,
+      );
+
+      expect(output, equals('\x1B[<0;1;1M'));
+    });
+
+    test('report() supports urxvt mode', () {
+      final output = MouseReporter.report(
+        TerminalMouseButton.left,
+        TerminalMouseButtonState.down,
+        CellOffset(0, 0),
+        MouseReportMode.urxvt,
+      );
+
+      expect(output, equals('\x1B[32;1;1M'));
+    });
+  });
+}

+ 27 - 0
test/src/terminal_test.dart

@@ -22,6 +22,33 @@ void main() {
       expect(handler2.events, isNotEmpty);
     });
   });
+
+  group('Terminal.mouseInput', () {
+    test('can handle mouse events', () {
+      final output = <String>[];
+
+      final terminal = Terminal(onOutput: output.add);
+
+      terminal.mouseInput(
+        TerminalMouseButton.left,
+        TerminalMouseButtonState.down,
+        CellOffset(10, 10),
+      );
+
+      expect(output, isEmpty);
+
+      // enable mouse reporting
+      terminal.write('\x1b[?1000h');
+
+      terminal.mouseInput(
+        TerminalMouseButton.left,
+        TerminalMouseButtonState.down,
+        CellOffset(10, 10),
+      );
+
+      expect(output, ['\x1B[M +,']);
+    });
+  });
 }
 
 class _TestInputHandler implements TerminalInputHandler {

+ 67 - 0
test/src/terminal_view_test.dart

@@ -1,5 +1,6 @@
 import 'dart:io';
 
+import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
@@ -174,6 +175,72 @@ void main() {
     );
   });
 
+  group('TerminalController.pointerInputs', () {
+    testWidgets('works', (WidgetTester tester) async {
+      final output = <String>[];
+
+      final terminal = Terminal(onOutput: output.add);
+
+      // enable mouse reporting
+      terminal.write('\x1b[?1000h');
+
+      final terminalView = TerminalController(
+        pointerInputs: PointerInputs.all(),
+      );
+
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Scaffold(
+            body: TerminalView(
+              terminal,
+              controller: terminalView,
+            ),
+          ),
+        ),
+      );
+
+      final pointer = TestPointer(1, PointerDeviceKind.mouse);
+
+      await tester.sendEventToBinding(pointer.down(Offset(1, 1)));
+
+      await tester.pumpAndSettle();
+
+      expect(output, isNotEmpty);
+    });
+
+    testWidgets('does not respond when disabled', (WidgetTester tester) async {
+      final output = <String>[];
+
+      final terminal = Terminal(onOutput: output.add);
+
+      // enable mouse reporting
+      terminal.write('\x1b[?1000h');
+
+      final terminalView = TerminalController(
+        pointerInputs: PointerInputs.none(),
+      );
+
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Scaffold(
+            body: TerminalView(
+              terminal,
+              controller: terminalView,
+            ),
+          ),
+        ),
+      );
+
+      final pointer = TestPointer(1, PointerDeviceKind.mouse);
+
+      await tester.sendEventToBinding(pointer.down(Offset(1, 1)));
+
+      await tester.pumpAndSettle();
+
+      expect(output, isEmpty);
+    });
+  });
+
   group('TerminalView.autofocus', () {
     testWidgets('works', (WidgetTester tester) async {
       final terminal = Terminal();