Skip to content

Commit

Permalink
Merge pull request #130 from tauu/mouse-support
Browse files Browse the repository at this point in the history
feat: initial mouse support
  • Loading branch information
xtyxtyx committed Sep 20, 2022
2 parents c1f0b94 + 295bbd3 commit 07c1f17
Show file tree
Hide file tree
Showing 23 changed files with 605 additions and 38 deletions.
5 changes: 4 additions & 1 deletion lib/core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
2 changes: 1 addition & 1 deletion lib/src/core/escape/handler.dart
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
2 changes: 1 addition & 1 deletion lib/src/core/escape/parser.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
23 changes: 0 additions & 23 deletions lib/src/core/mouse.dart

This file was deleted.

29 changes: 29 additions & 0 deletions lib/src/core/mouse/button.dart
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions lib/src/core/mouse/button_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
enum TerminalMouseButtonState {
up,

down,
}
114 changes: 114 additions & 0 deletions lib/src/core/mouse/handler.dart
Original file line number Diff line number Diff line change
@@ -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 changes: 31 additions & 0 deletions lib/src/core/mouse/mode.dart
Original file line number Diff line number Diff line change
@@ -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 changes: 48 additions & 0 deletions lib/src/core/mouse/reporter.dart
Original file line number Diff line number Diff line change
@@ -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";
}
}
}
4 changes: 3 additions & 1 deletion lib/src/core/state.dart
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -26,6 +26,8 @@ abstract class TerminalState {

MouseMode get mouseMode;

MouseReportMode get mouseReportMode;

bool get cursorBlinkMode;

bool get cursorVisibleMode;
Expand Down
30 changes: 29 additions & 1 deletion lib/src/terminal.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
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';
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';
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -128,6 +135,7 @@ class Terminal with Observable implements TerminalState, EscapeHandler {
@override
MouseMode get mouseMode => _mouseMode;

@override
MouseReportMode get mouseReportMode => _mouseReportMode;

@override
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions lib/src/terminal_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,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,
);

Expand Down
Loading

0 comments on commit 07c1f17

Please sign in to comment.