chore: adopt Dart 3.9 toolchain — honest floor + tall-style reformat (T-353)
Raise the declared minimums in pubspec.yaml to what our deps already require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist 0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is the binding floor. Pin the exact build toolchain in .fvmrc (Flutter 3.44.1). Moving to the Dart 3.9 language level switches `dart format` to the new "tall" style and enables two new lints. This commit is the resulting mechanical churn, isolated from any behaviour change: - whole-tree `dart format` reformat (tall style) - `dart fix` for unnecessary_underscores + use_null_aware_elements No runtime behaviour change; `make test` green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,14 +14,7 @@ import 'package:clide/src/terminal/src/core/state.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _State implements TerminalState {
|
||||
_State({
|
||||
this.viewWidth = 10,
|
||||
this.viewHeight = 5,
|
||||
this.autoWrapMode = true,
|
||||
this.lineFeedMode = false,
|
||||
this.originMode = false,
|
||||
this.reflowEnabled = false,
|
||||
});
|
||||
_State({this.viewWidth = 10, this.viewHeight = 5, this.autoWrapMode = true, this.lineFeedMode = false, this.originMode = false, this.reflowEnabled = false});
|
||||
|
||||
@override
|
||||
int viewWidth;
|
||||
@@ -83,12 +76,7 @@ Buffer _newBuffer({
|
||||
originMode: originMode,
|
||||
reflowEnabled: reflowEnabled,
|
||||
);
|
||||
return Buffer(
|
||||
state,
|
||||
maxLines: maxLines,
|
||||
isAltBuffer: isAltBuffer,
|
||||
wordSeparators: wordSeparators,
|
||||
);
|
||||
return Buffer(state, maxLines: maxLines, isAltBuffer: isAltBuffer, wordSeparators: wordSeparators);
|
||||
}
|
||||
|
||||
void main() {
|
||||
|
||||
@@ -126,14 +126,8 @@ void main() {
|
||||
});
|
||||
|
||||
test('toString shape — bounded and unbounded', () {
|
||||
expect(
|
||||
const BufferSegment(_dummyRange, 4, 2, 8).toString(),
|
||||
'Segment(4, 2 -> 8)',
|
||||
);
|
||||
expect(
|
||||
const BufferSegment(_dummyRange, 0, null, null).toString(),
|
||||
'Segment(0, start -> end)',
|
||||
);
|
||||
expect(const BufferSegment(_dummyRange, 4, 2, 8).toString(), 'Segment(4, 2 -> 8)');
|
||||
expect(const BufferSegment(_dummyRange, 0, null, null).toString(), 'Segment(0, start -> end)');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -39,10 +39,7 @@ void main() {
|
||||
|
||||
test('toString shape is the documented debug format', () {
|
||||
final c = CellData(foreground: 1, background: 2, flags: 4, content: 8);
|
||||
expect(
|
||||
c.toString(),
|
||||
'CellData{foreground: 1, background: 2, flags: 4, content: 8}',
|
||||
);
|
||||
expect(c.toString(), 'CellData{foreground: 1, background: 2, flags: 4, content: 8}');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -374,10 +371,14 @@ void main() {
|
||||
final src = line(12, 'short');
|
||||
final endAnchor = src.createAnchor(12); // viewWidth, past trimmedLength
|
||||
final out = reflow(ring([src]), 12, 4);
|
||||
expect(out.contains(endAnchor.line), isTrue,
|
||||
reason: 'end anchor must land on a line that survives '
|
||||
'lines.replaceWith — otherwise selection.attached is false '
|
||||
'and the selection silently vanishes after resize.');
|
||||
expect(
|
||||
out.contains(endAnchor.line),
|
||||
isTrue,
|
||||
reason:
|
||||
'end anchor must land on a line that survives '
|
||||
'lines.replaceWith — otherwise selection.attached is false '
|
||||
'and the selection silently vanishes after resize.',
|
||||
);
|
||||
});
|
||||
|
||||
test('inner wide-char clamp during _addPart leaves the wide cell to the next iteration', () {
|
||||
|
||||
@@ -49,11 +49,7 @@ void main() {
|
||||
// CJK ideograph — terminal stores it as a width-2 cell, so paintLine
|
||||
// advances `i` twice and the wide-char branch fires.
|
||||
t.write('漢字');
|
||||
final p = TerminalPainter(
|
||||
theme: TerminalThemes.defaultTheme,
|
||||
textStyle: const TerminalStyle(),
|
||||
textScaler: const TextScaler.linear(1.0),
|
||||
);
|
||||
final p = TerminalPainter(theme: TerminalThemes.defaultTheme, textStyle: const TerminalStyle(), textScaler: const TextScaler.linear(1.0));
|
||||
p.paintLine(Canvas(PictureRecorder()), Offset.zero, t.buffer.lines[0]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,12 +19,12 @@ class _Recorder {
|
||||
}
|
||||
|
||||
Widget _host(Widget child) => Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Center(child: SizedBox(width: 200, height: 100, child: child)),
|
||||
),
|
||||
);
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Center(child: SizedBox(width: 200, height: 100, child: child)),
|
||||
),
|
||||
);
|
||||
|
||||
CustomTextEdit _build({
|
||||
required FocusNode focusNode,
|
||||
@@ -43,7 +43,8 @@ CustomTextEdit _build({
|
||||
onDelete: () => r.deletes++,
|
||||
onComposing: r.composings.add,
|
||||
onAction: r.actions.add,
|
||||
onKeyEvent: onKeyEvent ??
|
||||
onKeyEvent:
|
||||
onKeyEvent ??
|
||||
(node, event) {
|
||||
r.keys.add(event);
|
||||
return KeyEventResult.ignored;
|
||||
@@ -230,10 +231,7 @@ void main() {
|
||||
await tester.pump();
|
||||
final state = tester.state<CustomTextEditState>(find.byType(CustomTextEdit));
|
||||
// Send a value shorter than the init state (2 chars) directly.
|
||||
state.updateEditingValue(const TextEditingValue(
|
||||
text: '',
|
||||
selection: TextSelection.collapsed(offset: 0),
|
||||
));
|
||||
state.updateEditingValue(const TextEditingValue(text: '', selection: TextSelection.collapsed(offset: 0)));
|
||||
expect(r.deletes, 1);
|
||||
});
|
||||
|
||||
@@ -244,11 +242,7 @@ void main() {
|
||||
await tester.pumpWidget(_host(_build(focusNode: focus, r: r, autofocus: true)));
|
||||
await tester.pump();
|
||||
final state = tester.state<CustomTextEditState>(find.byType(CustomTextEdit));
|
||||
state.updateEditingValue(const TextEditingValue(
|
||||
text: 'hi',
|
||||
selection: TextSelection.collapsed(offset: 2),
|
||||
composing: TextRange(start: 0, end: 2),
|
||||
));
|
||||
state.updateEditingValue(const TextEditingValue(text: 'hi', selection: TextSelection.collapsed(offset: 2), composing: TextRange(start: 0, end: 2)));
|
||||
expect(r.composings, contains('hi'));
|
||||
});
|
||||
|
||||
|
||||
@@ -238,15 +238,8 @@ void main() {
|
||||
test('writes printable chars one-by-one to writeChar', () {
|
||||
final f = _newParser();
|
||||
f.parser.write('Hi!');
|
||||
expect(
|
||||
f.h.calls.map((c) => c.name).toList(),
|
||||
['writeChar', 'writeChar', 'writeChar'],
|
||||
);
|
||||
expect(f.h.calls.map((c) => c.args.first).toList(), [
|
||||
'H'.codeUnitAt(0),
|
||||
'i'.codeUnitAt(0),
|
||||
'!'.codeUnitAt(0),
|
||||
]);
|
||||
expect(f.h.calls.map((c) => c.name).toList(), ['writeChar', 'writeChar', 'writeChar']);
|
||||
expect(f.h.calls.map((c) => c.args.first).toList(), ['H'.codeUnitAt(0), 'i'.codeUnitAt(0), '!'.codeUnitAt(0)]);
|
||||
});
|
||||
|
||||
test('chars beyond the SBC table go straight to writeChar', () {
|
||||
@@ -289,21 +282,13 @@ void main() {
|
||||
test('ESC 7 / ESC 8 → save / restore cursor', () {
|
||||
final f = _newParser();
|
||||
f.parser.write('\x1b7\x1b8');
|
||||
expect(f.h.calls.map((c) => c.name).toList(), [
|
||||
'saveCursor',
|
||||
'restoreCursor',
|
||||
]);
|
||||
expect(f.h.calls.map((c) => c.name).toList(), ['saveCursor', 'restoreCursor']);
|
||||
});
|
||||
|
||||
test('ESC D / E / H / M', () {
|
||||
final f = _newParser();
|
||||
f.parser.write('\x1bD\x1bE\x1bH\x1bM');
|
||||
expect(f.h.calls.map((c) => c.name).toList(), [
|
||||
'index',
|
||||
'nextLine',
|
||||
'setTapStop',
|
||||
'reverseIndex',
|
||||
]);
|
||||
expect(f.h.calls.map((c) => c.name).toList(), ['index', 'nextLine', 'setTapStop', 'reverseIndex']);
|
||||
});
|
||||
|
||||
test('ESC ( / ESC ) designate G0 / G1 charset', () {
|
||||
@@ -320,10 +305,7 @@ void main() {
|
||||
final f = _newParser();
|
||||
// ESC = enables app keypad mode, ESC > disables it.
|
||||
f.parser.write('\x1b=\x1b>');
|
||||
expect(
|
||||
f.h.named('setAppKeypadMode').map((c) => c.args.first).toList(),
|
||||
[true, false],
|
||||
);
|
||||
expect(f.h.named('setAppKeypadMode').map((c) => c.args.first).toList(), [true, false]);
|
||||
});
|
||||
|
||||
test('unknown ESC byte → unkownEscape', () {
|
||||
@@ -354,12 +336,7 @@ void main() {
|
||||
test('A / B / C / D — default amount = 1, missing param uses default', () {
|
||||
final f = _newParser();
|
||||
f.parser.write('\x1b[A\x1b[B\x1b[C\x1b[D');
|
||||
expect(f.h.calls.map((c) => '${c.name}/${c.args}').toList(), [
|
||||
'moveCursorY/[-1]',
|
||||
'moveCursorY/[1]',
|
||||
'moveCursorX/[1]',
|
||||
'moveCursorX/[-1]',
|
||||
]);
|
||||
expect(f.h.calls.map((c) => '${c.name}/${c.args}').toList(), ['moveCursorY/[-1]', 'moveCursorY/[1]', 'moveCursorX/[1]', 'moveCursorX/[-1]']);
|
||||
});
|
||||
|
||||
test('A / B / C / D — explicit amount + 0-as-1 fallback', () {
|
||||
@@ -412,12 +389,7 @@ void main() {
|
||||
test('J 0/1/2/3 → erase display below/above/all/scrollback', () {
|
||||
final f = _newParser();
|
||||
f.parser.write('\x1b[J\x1b[1J\x1b[2J\x1b[3J');
|
||||
expect(f.h.calls.map((c) => c.name).toList(), [
|
||||
'eraseDisplayBelow',
|
||||
'eraseDisplayAbove',
|
||||
'eraseDisplay',
|
||||
'eraseScrollbackOnly',
|
||||
]);
|
||||
expect(f.h.calls.map((c) => c.name).toList(), ['eraseDisplayBelow', 'eraseDisplayAbove', 'eraseDisplay', 'eraseScrollbackOnly']);
|
||||
});
|
||||
|
||||
test('K 0/1/2 → erase line right / left / all', () {
|
||||
@@ -483,11 +455,7 @@ void main() {
|
||||
test('c / >c / =c — primary, secondary, tertiary device attributes', () {
|
||||
final f = _newParser();
|
||||
f.parser.write('\x1b[c\x1b[>c\x1b[=c');
|
||||
expect(f.h.calls.map((c) => c.name).toList(), [
|
||||
'sendPrimaryDeviceAttributes',
|
||||
'sendSecondaryDeviceAttributes',
|
||||
'sendTertiaryDeviceAttributes',
|
||||
]);
|
||||
expect(f.h.calls.map((c) => c.name).toList(), ['sendPrimaryDeviceAttributes', 'sendSecondaryDeviceAttributes', 'sendTertiaryDeviceAttributes']);
|
||||
});
|
||||
|
||||
test('n 5 → operating status; n 6 → cursor position', () {
|
||||
@@ -595,12 +563,7 @@ void main() {
|
||||
final f = _newParser();
|
||||
f.parser.write('\x1b[?9h\x1b[?1000h\x1b[?1002h\x1b[?1003h');
|
||||
final modes = f.h.named('setMouseMode').map((c) => c.args.first).toList();
|
||||
expect(modes, [
|
||||
MouseMode.clickOnly,
|
||||
MouseMode.upDownScroll,
|
||||
MouseMode.upDownScrollDrag,
|
||||
MouseMode.upDownScrollMove,
|
||||
]);
|
||||
expect(modes, [MouseMode.clickOnly, MouseMode.upDownScroll, MouseMode.upDownScrollDrag, MouseMode.upDownScrollMove]);
|
||||
});
|
||||
|
||||
test('CSI ?9 / ?1000 disabled → setMouseMode(none)', () {
|
||||
@@ -614,12 +577,7 @@ void main() {
|
||||
final f = _newParser();
|
||||
f.parser.write('\x1b[?1005h\x1b[?1006h\x1b[?1015h\x1b[?1006l');
|
||||
final modes = f.h.named('setMouseReportMode').map((c) => c.args.first).toList();
|
||||
expect(modes, [
|
||||
MouseReportMode.utf,
|
||||
MouseReportMode.sgr,
|
||||
MouseReportMode.urxvt,
|
||||
MouseReportMode.normal,
|
||||
]);
|
||||
expect(modes, [MouseReportMode.utf, MouseReportMode.sgr, MouseReportMode.urxvt, MouseReportMode.normal]);
|
||||
});
|
||||
|
||||
test('CSI ?3 / ?5 / ?6 / ?12 / ?66 / ?1004 / ?1007 — column/reverse/origin/blink/keypad/focus/altScroll', () {
|
||||
|
||||
@@ -19,10 +19,7 @@ import 'package:clide/src/terminal/src/core/state.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _State implements TerminalState {
|
||||
_State({
|
||||
this.lineFeedMode = false,
|
||||
this.appKeypadMode = false,
|
||||
});
|
||||
_State({this.lineFeedMode = false, this.appKeypadMode = false});
|
||||
|
||||
@override
|
||||
bool lineFeedMode;
|
||||
@@ -78,10 +75,7 @@ TerminalKeyboardEvent _evt(
|
||||
shift: shift,
|
||||
ctrl: ctrl,
|
||||
alt: alt,
|
||||
state: _State(
|
||||
lineFeedMode: lineFeedMode,
|
||||
appKeypadMode: appKeypadMode,
|
||||
),
|
||||
state: _State(lineFeedMode: lineFeedMode, appKeypadMode: appKeypadMode),
|
||||
altBuffer: altBuffer,
|
||||
platform: platform,
|
||||
);
|
||||
@@ -161,10 +155,7 @@ void main() {
|
||||
group('tokenize', () {
|
||||
test('parses a keyboard-name line', () {
|
||||
final tokens = tokenize('keyboard "Default"').toList();
|
||||
expect(tokens.map((t) => t.type).toList(), [
|
||||
KeytabTokenType.keyboard,
|
||||
KeytabTokenType.input,
|
||||
]);
|
||||
expect(tokens.map((t) => t.type).toList(), [KeytabTokenType.keyboard, KeytabTokenType.input]);
|
||||
expect(tokens.last.value, 'Default');
|
||||
});
|
||||
|
||||
@@ -200,24 +191,15 @@ key Tab : "\\t" # trailing comment
|
||||
''';
|
||||
final tokens = tokenize(source).toList();
|
||||
// Trailing-comment stripping leaves no broken tokens.
|
||||
expect(
|
||||
tokens.map((t) => t.type).toList(),
|
||||
contains(KeytabTokenType.colon),
|
||||
);
|
||||
expect(tokens.map((t) => t.type).toList(), contains(KeytabTokenType.colon));
|
||||
});
|
||||
|
||||
test('tokenize throws TokenizeError on a malformed key line missing colon', () {
|
||||
expect(
|
||||
() => tokenize('key Tab "\\t"').toList(),
|
||||
throwsA(isA<TokenizeError>()),
|
||||
);
|
||||
expect(() => tokenize('key Tab "\\t"').toList(), throwsA(isA<TokenizeError>()));
|
||||
});
|
||||
|
||||
test('tokenize throws TokenizeError on unterminated keyboard line', () {
|
||||
expect(
|
||||
() => tokenize('keyboard X').toList(),
|
||||
throwsA(isA<TokenizeError>()),
|
||||
);
|
||||
expect(() => tokenize('keyboard X').toList(), throwsA(isA<TokenizeError>()));
|
||||
});
|
||||
|
||||
test('KeytabToken toString reflects type + value', () {
|
||||
@@ -291,14 +273,8 @@ key Up +Bogus : "x"
|
||||
|
||||
group('KeytabRecord / KeytabAction toString', () {
|
||||
test('action toString quotes input and bare-prints shortcut', () {
|
||||
expect(
|
||||
KeytabAction(KeytabActionType.input, r'\E[A').toString(),
|
||||
'"\\E[A"',
|
||||
);
|
||||
expect(
|
||||
KeytabAction(KeytabActionType.shortcut, 'scrollUp').toString(),
|
||||
'scrollUp',
|
||||
);
|
||||
expect(KeytabAction(KeytabActionType.input, r'\E[A').toString(), '"\\E[A"');
|
||||
expect(KeytabAction(KeytabActionType.shortcut, 'scrollUp').toString(), 'scrollUp');
|
||||
});
|
||||
|
||||
test('record toString writes +Mode for true and -Mode for false flags', () {
|
||||
@@ -341,10 +317,7 @@ key Up +Shift : scrollLineUp
|
||||
|
||||
test('action unescapedValue returns the raw value for shortcut actions', () {
|
||||
// Input actions go through keytabUnescape; shortcut actions don't.
|
||||
expect(
|
||||
KeytabAction(KeytabActionType.shortcut, r'\Ehello').unescapedValue(),
|
||||
r'\Ehello',
|
||||
);
|
||||
expect(KeytabAction(KeytabActionType.shortcut, r'\Ehello').unescapedValue(), r'\Ehello');
|
||||
});
|
||||
|
||||
test('record toString covers every supported mode flag when set', () {
|
||||
@@ -384,34 +357,20 @@ key Up +Shift : scrollLineUp
|
||||
group('KeytabParser — defensive error paths through hand-crafted tokens', () {
|
||||
test('addTokens throws ParseError on a stray non-keyboard / non-keyDefine token', () {
|
||||
final parser = KeytabParser();
|
||||
expect(
|
||||
() => parser.addTokens([
|
||||
KeytabToken(KeytabTokenType.colon, ':'),
|
||||
]),
|
||||
throwsA(isA<ParseError>()),
|
||||
);
|
||||
expect(() => parser.addTokens([KeytabToken(KeytabTokenType.colon, ':')]), throwsA(isA<ParseError>()));
|
||||
});
|
||||
|
||||
test('_parseName throws when the second token is not an input token', () {
|
||||
final parser = KeytabParser();
|
||||
expect(
|
||||
() => parser.addTokens([
|
||||
KeytabToken(KeytabTokenType.keyboard, 'keyboard'),
|
||||
KeytabToken(KeytabTokenType.keyName, 'Up'),
|
||||
]),
|
||||
() => parser.addTokens([KeytabToken(KeytabTokenType.keyboard, 'keyboard'), KeytabToken(KeytabTokenType.keyName, 'Up')]),
|
||||
throwsA(isA<ParseError>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('_parseKeyDefine throws when the second token is not a keyName', () {
|
||||
final parser = KeytabParser();
|
||||
expect(
|
||||
() => parser.addTokens([
|
||||
KeytabToken(KeytabTokenType.keyDefine, 'key'),
|
||||
KeytabToken(KeytabTokenType.colon, ':'),
|
||||
]),
|
||||
throwsA(isA<ParseError>()),
|
||||
);
|
||||
expect(() => parser.addTokens([KeytabToken(KeytabTokenType.keyDefine, 'key'), KeytabToken(KeytabTokenType.colon, ':')]), throwsA(isA<ParseError>()));
|
||||
});
|
||||
|
||||
test('_parseKeyDefine throws on an unrecognised modeStatus value', () {
|
||||
@@ -470,10 +429,7 @@ key Up +Shift : scrollLineUp
|
||||
|
||||
test('exact match: -Shift on a record with shift=null and no anyModifier', () {
|
||||
final t = build('key Up -Shift : "\\E[A"');
|
||||
expect(
|
||||
t.find(TerminalKey.arrowUp, shift: false)?.action.value,
|
||||
r'\E[A',
|
||||
);
|
||||
expect(t.find(TerminalKey.arrowUp, shift: false)?.action.value, r'\E[A');
|
||||
expect(t.find(TerminalKey.arrowUp, shift: true), isNull);
|
||||
});
|
||||
|
||||
@@ -495,15 +451,9 @@ key Up +NewLine+AppKeyPad+AppScreen+Mac : "match"
|
||||
key Up : "fallback"
|
||||
''');
|
||||
// Wrong newLine mode falls through to fallback.
|
||||
expect(
|
||||
t.find(TerminalKey.arrowUp, newLineMode: false, appKeyPad: true, appScreen: true, macos: true)?.action.value,
|
||||
'fallback',
|
||||
);
|
||||
expect(t.find(TerminalKey.arrowUp, newLineMode: false, appKeyPad: true, appScreen: true, macos: true)?.action.value, 'fallback');
|
||||
// All matching → primary record.
|
||||
expect(
|
||||
t.find(TerminalKey.arrowUp, newLineMode: true, appKeyPad: true, appScreen: true, macos: true)?.action.value,
|
||||
'match',
|
||||
);
|
||||
expect(t.find(TerminalKey.arrowUp, newLineMode: true, appKeyPad: true, appScreen: true, macos: true)?.action.value, 'match');
|
||||
});
|
||||
|
||||
test('-Ansi records are skipped (VT52 not supported yet)', () {
|
||||
@@ -519,10 +469,7 @@ key Up : "fallback"
|
||||
test('appCursorKeys + keyPad gates also filter', () {
|
||||
final t = build('key Up +AppCuKeys+KeyPad : "\\EOA"\nkey Up : "fallback"');
|
||||
expect(t.find(TerminalKey.arrowUp)?.action.value, 'fallback');
|
||||
expect(
|
||||
t.find(TerminalKey.arrowUp, appCursorKeys: true, keyPad: true)?.action.value,
|
||||
r'\EOA',
|
||||
);
|
||||
expect(t.find(TerminalKey.arrowUp, appCursorKeys: true, keyPad: true)?.action.value, r'\EOA');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -591,23 +538,11 @@ key Up : "fallback"
|
||||
(_evt(TerminalKey.arrowUp, alt: true), '\x1b[1;3A'), // alt → 3
|
||||
(_evt(TerminalKey.arrowUp, ctrl: true), '\x1b[1;5A'), // ctrl → 5
|
||||
// Pair codes.
|
||||
(
|
||||
_evt(TerminalKey.arrowUp, shift: true, alt: true),
|
||||
'\x1b[1;4A',
|
||||
),
|
||||
(
|
||||
_evt(TerminalKey.arrowUp, shift: true, ctrl: true),
|
||||
'\x1b[1;6A',
|
||||
),
|
||||
(
|
||||
_evt(TerminalKey.arrowUp, ctrl: true, alt: true),
|
||||
'\x1b[1;7A',
|
||||
),
|
||||
(_evt(TerminalKey.arrowUp, shift: true, alt: true), '\x1b[1;4A'),
|
||||
(_evt(TerminalKey.arrowUp, shift: true, ctrl: true), '\x1b[1;6A'),
|
||||
(_evt(TerminalKey.arrowUp, ctrl: true, alt: true), '\x1b[1;7A'),
|
||||
// Triple.
|
||||
(
|
||||
_evt(TerminalKey.arrowUp, shift: true, alt: true, ctrl: true),
|
||||
'\x1b[1;8A',
|
||||
),
|
||||
(_evt(TerminalKey.arrowUp, shift: true, alt: true, ctrl: true), '\x1b[1;8A'),
|
||||
];
|
||||
for (final c in cases) {
|
||||
expect(h(c.$1), c.$2, reason: '$c');
|
||||
@@ -662,10 +597,7 @@ key Up : "fallback"
|
||||
});
|
||||
|
||||
test('returns null on macOS (Alt is reserved for char composition)', () {
|
||||
expect(
|
||||
h(_evt(TerminalKey.keyA, alt: true, platform: TerminalTargetPlatform.macos)),
|
||||
isNull,
|
||||
);
|
||||
expect(h(_evt(TerminalKey.keyA, alt: true, platform: TerminalTargetPlatform.macos)), isNull);
|
||||
});
|
||||
|
||||
test('returns null for non-letter keys', () {
|
||||
@@ -680,10 +612,7 @@ key Up : "fallback"
|
||||
|
||||
test('routes Ctrl+C to 0x03 via CtrlInputHandler when keytab misses', () {
|
||||
// Default keytab has no entry for Ctrl+keyC, so CtrlInputHandler runs.
|
||||
expect(
|
||||
defaultInputHandler(_evt(TerminalKey.keyC, ctrl: true)),
|
||||
'\x03',
|
||||
);
|
||||
expect(defaultInputHandler(_evt(TerminalKey.keyC, ctrl: true)), '\x03');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,10 +13,7 @@ import 'package:clide/src/terminal/src/core/state.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _State implements TerminalState {
|
||||
_State({
|
||||
this.mouseMode = MouseMode.none,
|
||||
this.mouseReportMode = MouseReportMode.normal,
|
||||
});
|
||||
_State({this.mouseMode = MouseMode.none, this.mouseReportMode = MouseReportMode.normal});
|
||||
|
||||
@override
|
||||
MouseMode mouseMode;
|
||||
@@ -91,12 +88,7 @@ void main() {
|
||||
expect(TerminalMouseButton.wheelDown.id, 64 + 5);
|
||||
expect(TerminalMouseButton.wheelLeft.id, 64 + 6);
|
||||
expect(TerminalMouseButton.wheelRight.id, 64 + 7);
|
||||
for (final b in [
|
||||
TerminalMouseButton.wheelUp,
|
||||
TerminalMouseButton.wheelDown,
|
||||
TerminalMouseButton.wheelLeft,
|
||||
TerminalMouseButton.wheelRight,
|
||||
]) {
|
||||
for (final b in [TerminalMouseButton.wheelUp, TerminalMouseButton.wheelDown, TerminalMouseButton.wheelLeft, TerminalMouseButton.wheelRight]) {
|
||||
expect(b.isWheel, isTrue, reason: '$b');
|
||||
}
|
||||
});
|
||||
@@ -134,14 +126,8 @@ void main() {
|
||||
|
||||
group('MouseReporter (sgr mode)', () {
|
||||
test('M for press, m for release, with raw 1-based coords', () {
|
||||
expect(
|
||||
MouseReporter.report(TerminalMouseButton.middle, TerminalMouseButtonState.down, const CellOffset(10, 20), MouseReportMode.sgr),
|
||||
'\x1b[<1;11;21M',
|
||||
);
|
||||
expect(
|
||||
MouseReporter.report(TerminalMouseButton.middle, TerminalMouseButtonState.up, const CellOffset(10, 20), MouseReportMode.sgr),
|
||||
'\x1b[<1;11;21m',
|
||||
);
|
||||
expect(MouseReporter.report(TerminalMouseButton.middle, TerminalMouseButtonState.down, const CellOffset(10, 20), MouseReportMode.sgr), '\x1b[<1;11;21M');
|
||||
expect(MouseReporter.report(TerminalMouseButton.middle, TerminalMouseButtonState.up, const CellOffset(10, 20), MouseReportMode.sgr), '\x1b[<1;11;21m');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -171,21 +157,11 @@ void main() {
|
||||
|
||||
group('CascadeMouseHandler', () {
|
||||
test('returns the first non-null result; null when all return null', () {
|
||||
const cascade = CascadeMouseHandler([
|
||||
_NullHandler(),
|
||||
_ConstHandler('first'),
|
||||
_ConstHandler('second'),
|
||||
]);
|
||||
expect(
|
||||
cascade(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down)),
|
||||
'first',
|
||||
);
|
||||
const cascade = CascadeMouseHandler([_NullHandler(), _ConstHandler('first'), _ConstHandler('second')]);
|
||||
expect(cascade(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down)), 'first');
|
||||
|
||||
const allNull = CascadeMouseHandler([_NullHandler(), _NullHandler()]);
|
||||
expect(
|
||||
allNull(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down)),
|
||||
isNull,
|
||||
);
|
||||
expect(allNull(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down)), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -198,31 +174,16 @@ void main() {
|
||||
});
|
||||
|
||||
test('clickOnly mode + up → null (only down events report)', () {
|
||||
expect(
|
||||
h(_evt(TerminalMouseButton.middle, TerminalMouseButtonState.up, mode: MouseMode.clickOnly)),
|
||||
isNull,
|
||||
);
|
||||
expect(h(_evt(TerminalMouseButton.middle, TerminalMouseButtonState.up, mode: MouseMode.clickOnly)), isNull);
|
||||
});
|
||||
|
||||
test('clickOnly mode + button id >= 3 (wheel) → null', () {
|
||||
expect(
|
||||
h(_evt(TerminalMouseButton.wheelUp, TerminalMouseButtonState.down, mode: MouseMode.clickOnly)),
|
||||
isNull,
|
||||
);
|
||||
expect(h(_evt(TerminalMouseButton.wheelUp, TerminalMouseButtonState.down, mode: MouseMode.clickOnly)), isNull);
|
||||
});
|
||||
|
||||
test('non-clickOnly modes always return null', () {
|
||||
for (final m in [
|
||||
MouseMode.none,
|
||||
MouseMode.upDownScroll,
|
||||
MouseMode.upDownScrollDrag,
|
||||
MouseMode.upDownScrollMove,
|
||||
]) {
|
||||
expect(
|
||||
h(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down, mode: m)),
|
||||
isNull,
|
||||
reason: '$m',
|
||||
);
|
||||
for (final m in [MouseMode.none, MouseMode.upDownScroll, MouseMode.upDownScrollDrag, MouseMode.upDownScrollMove]) {
|
||||
expect(h(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down, mode: m)), isNull, reason: '$m');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -232,39 +193,22 @@ void main() {
|
||||
|
||||
test('none / clickOnly modes return null', () {
|
||||
for (final m in [MouseMode.none, MouseMode.clickOnly]) {
|
||||
expect(
|
||||
h(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down, mode: m)),
|
||||
isNull,
|
||||
);
|
||||
expect(h(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down, mode: m)), isNull);
|
||||
}
|
||||
});
|
||||
|
||||
test('upDownScroll modes report regular button events', () {
|
||||
for (final m in [
|
||||
MouseMode.upDownScroll,
|
||||
MouseMode.upDownScrollDrag,
|
||||
MouseMode.upDownScrollMove,
|
||||
]) {
|
||||
expect(
|
||||
h(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down, mode: m)),
|
||||
isNotNull,
|
||||
reason: '$m',
|
||||
);
|
||||
for (final m in [MouseMode.upDownScroll, MouseMode.upDownScrollDrag, MouseMode.upDownScrollMove]) {
|
||||
expect(h(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down, mode: m)), isNotNull, reason: '$m');
|
||||
}
|
||||
});
|
||||
|
||||
test('wheel up events are silently dropped (no report on wheel release)', () {
|
||||
expect(
|
||||
h(_evt(TerminalMouseButton.wheelUp, TerminalMouseButtonState.up, mode: MouseMode.upDownScroll)),
|
||||
isNull,
|
||||
);
|
||||
expect(h(_evt(TerminalMouseButton.wheelUp, TerminalMouseButtonState.up, mode: MouseMode.upDownScroll)), isNull);
|
||||
});
|
||||
|
||||
test('wheel down events do report (one click per scroll tick)', () {
|
||||
expect(
|
||||
h(_evt(TerminalMouseButton.wheelDown, TerminalMouseButtonState.down, mode: MouseMode.upDownScroll)),
|
||||
isNotNull,
|
||||
);
|
||||
expect(h(_evt(TerminalMouseButton.wheelDown, TerminalMouseButtonState.down, mode: MouseMode.upDownScroll)), isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -280,12 +224,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('mode=none produces null (no handler reports)', () {
|
||||
expect(
|
||||
defaultMouseHandler(
|
||||
_evt(TerminalMouseButton.left, TerminalMouseButtonState.down, mode: MouseMode.none),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(defaultMouseHandler(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down, mode: MouseMode.none)), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,22 +31,19 @@ void main() {
|
||||
test('JetBrainsMono Bold advance width matches Regular (cell drift = 0)', () {
|
||||
final regularWidth = _measure(FontWeight.normal);
|
||||
final boldWidth = _measure(FontWeight.bold);
|
||||
expect(boldWidth, regularWidth,
|
||||
reason: 'Bold advance width must equal Regular at $_fontSize px '
|
||||
'or the terminal cell grid drifts when bold flips on.');
|
||||
expect(
|
||||
boldWidth,
|
||||
regularWidth,
|
||||
reason:
|
||||
'Bold advance width must equal Regular at $_fontSize px '
|
||||
'or the terminal cell grid drifts when bold flips on.',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
double _measure(FontWeight weight) {
|
||||
final builder = ui.ParagraphBuilder(ui.ParagraphStyle(
|
||||
fontFamily: _family,
|
||||
fontSize: _fontSize,
|
||||
))
|
||||
..pushStyle(ui.TextStyle(
|
||||
fontFamily: _family,
|
||||
fontWeight: weight,
|
||||
fontSize: _fontSize,
|
||||
))
|
||||
final builder = ui.ParagraphBuilder(ui.ParagraphStyle(fontFamily: _family, fontSize: _fontSize))
|
||||
..pushStyle(ui.TextStyle(fontFamily: _family, fontWeight: weight, fontSize: _fontSize))
|
||||
..addText(_sample);
|
||||
final paragraph = builder.build()..layout(const ui.ParagraphConstraints(width: double.infinity));
|
||||
final width = paragraph.maxIntrinsicWidth;
|
||||
|
||||
@@ -18,16 +18,8 @@ import 'package:clide/src/terminal/src/ui/themes.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
TerminalPainter _make({
|
||||
TerminalTheme? theme,
|
||||
TerminalStyle? style,
|
||||
TextScaler? scaler,
|
||||
}) =>
|
||||
TerminalPainter(
|
||||
theme: theme ?? TerminalThemes.defaultTheme,
|
||||
textStyle: style ?? const TerminalStyle(),
|
||||
textScaler: scaler ?? const TextScaler.linear(1.0),
|
||||
);
|
||||
TerminalPainter _make({TerminalTheme? theme, TerminalStyle? style, TextScaler? scaler}) =>
|
||||
TerminalPainter(theme: theme ?? TerminalThemes.defaultTheme, textStyle: style ?? const TerminalStyle(), textScaler: scaler ?? const TextScaler.linear(1.0));
|
||||
|
||||
Canvas _canvas() => Canvas(PictureRecorder());
|
||||
|
||||
@@ -101,11 +93,7 @@ void main() {
|
||||
});
|
||||
|
||||
group('TerminalPainter — paintCursor', () {
|
||||
final cases = <TerminalCursorType>{
|
||||
TerminalCursorType.block,
|
||||
TerminalCursorType.underline,
|
||||
TerminalCursorType.verticalBar,
|
||||
};
|
||||
final cases = <TerminalCursorType>{TerminalCursorType.block, TerminalCursorType.underline, TerminalCursorType.verticalBar};
|
||||
for (final c in cases) {
|
||||
test('focused $c paints without throwing', () {
|
||||
_make().paintCursor(_canvas(), const Offset(0, 0), cursorType: c);
|
||||
@@ -114,12 +102,7 @@ void main() {
|
||||
|
||||
test('unfocused (hasFocus=false) draws a stroked rect regardless of cursor type', () {
|
||||
// Should hit the early-return-with-stroke branch, not the switch.
|
||||
_make().paintCursor(
|
||||
_canvas(),
|
||||
const Offset(0, 0),
|
||||
cursorType: TerminalCursorType.verticalBar,
|
||||
hasFocus: false,
|
||||
);
|
||||
_make().paintCursor(_canvas(), const Offset(0, 0), cursorType: TerminalCursorType.verticalBar, hasFocus: false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -149,12 +132,7 @@ void main() {
|
||||
|
||||
test('paintCellForeground caches the layout — second call hits the cache', () {
|
||||
final p = _make();
|
||||
final cell = CellData(
|
||||
foreground: CellColor.normal,
|
||||
background: CellColor.normal,
|
||||
flags: 0,
|
||||
content: 'A'.codeUnitAt(0),
|
||||
);
|
||||
final cell = CellData(foreground: CellColor.normal, background: CellColor.normal, flags: 0, content: 'A'.codeUnitAt(0));
|
||||
// Two paints with the same cell → second one resolves through the cache.
|
||||
p.paintCellForeground(_canvas(), Offset.zero, cell);
|
||||
p.paintCellForeground(_canvas(), Offset.zero, cell);
|
||||
@@ -164,88 +142,48 @@ void main() {
|
||||
final p = _make();
|
||||
// faint
|
||||
p.paintCellForeground(
|
||||
_canvas(),
|
||||
Offset.zero,
|
||||
CellData(
|
||||
foreground: CellColor.normal,
|
||||
background: CellColor.normal,
|
||||
flags: CellFlags.faint,
|
||||
content: 'B'.codeUnitAt(0),
|
||||
));
|
||||
_canvas(),
|
||||
Offset.zero,
|
||||
CellData(foreground: CellColor.normal, background: CellColor.normal, flags: CellFlags.faint, content: 'B'.codeUnitAt(0)),
|
||||
);
|
||||
// inverse → uses background as foreground
|
||||
p.paintCellForeground(
|
||||
_canvas(),
|
||||
Offset.zero,
|
||||
CellData(
|
||||
foreground: CellColor.normal,
|
||||
background: CellColor.named | 2,
|
||||
flags: CellFlags.inverse,
|
||||
content: 'C'.codeUnitAt(0),
|
||||
));
|
||||
_canvas(),
|
||||
Offset.zero,
|
||||
CellData(foreground: CellColor.normal, background: CellColor.named | 2, flags: CellFlags.inverse, content: 'C'.codeUnitAt(0)),
|
||||
);
|
||||
// underline + space → swaps to U+00A0 internally
|
||||
p.paintCellForeground(
|
||||
_canvas(),
|
||||
Offset.zero,
|
||||
CellData(
|
||||
foreground: CellColor.normal,
|
||||
background: CellColor.normal,
|
||||
flags: CellFlags.underline,
|
||||
content: 0x20,
|
||||
));
|
||||
_canvas(),
|
||||
Offset.zero,
|
||||
CellData(foreground: CellColor.normal, background: CellColor.normal, flags: CellFlags.underline, content: 0x20),
|
||||
);
|
||||
// bold + italic — exercises the flag-driven TextStyle path.
|
||||
p.paintCellForeground(
|
||||
_canvas(),
|
||||
Offset.zero,
|
||||
CellData(
|
||||
foreground: CellColor.normal,
|
||||
background: CellColor.normal,
|
||||
flags: CellFlags.bold | CellFlags.italic,
|
||||
content: 'D'.codeUnitAt(0),
|
||||
));
|
||||
_canvas(),
|
||||
Offset.zero,
|
||||
CellData(foreground: CellColor.normal, background: CellColor.normal, flags: CellFlags.bold | CellFlags.italic, content: 'D'.codeUnitAt(0)),
|
||||
);
|
||||
});
|
||||
|
||||
test('paintCellBackground covers normal early-return, inverse, and named/palette paths', () {
|
||||
final p = _make();
|
||||
// normal + no inverse → early return, no draw.
|
||||
p.paintCellBackground(
|
||||
_canvas(),
|
||||
Offset.zero,
|
||||
CellData(
|
||||
foreground: CellColor.normal,
|
||||
background: CellColor.normal,
|
||||
flags: 0,
|
||||
content: 0,
|
||||
));
|
||||
p.paintCellBackground(_canvas(), Offset.zero, CellData(foreground: CellColor.normal, background: CellColor.normal, flags: 0, content: 0));
|
||||
// inverse → resolves foreground colour and draws.
|
||||
p.paintCellBackground(
|
||||
_canvas(),
|
||||
Offset.zero,
|
||||
CellData(
|
||||
foreground: CellColor.named | 1,
|
||||
background: CellColor.normal,
|
||||
flags: CellFlags.inverse,
|
||||
content: 0,
|
||||
));
|
||||
_canvas(),
|
||||
Offset.zero,
|
||||
CellData(foreground: CellColor.named | 1, background: CellColor.normal, flags: CellFlags.inverse, content: 0),
|
||||
);
|
||||
// explicit background colour → draws.
|
||||
p.paintCellBackground(
|
||||
_canvas(),
|
||||
Offset.zero,
|
||||
CellData(
|
||||
foreground: CellColor.normal,
|
||||
background: CellColor.palette | 5,
|
||||
flags: 0,
|
||||
content: 0,
|
||||
));
|
||||
p.paintCellBackground(_canvas(), Offset.zero, CellData(foreground: CellColor.normal, background: CellColor.palette | 5, flags: 0, content: 0));
|
||||
// double-width cell → exercises the widthScale=2 branch.
|
||||
p.paintCellBackground(
|
||||
_canvas(),
|
||||
Offset.zero,
|
||||
CellData(
|
||||
foreground: CellColor.normal,
|
||||
background: CellColor.named | 4,
|
||||
flags: 0,
|
||||
content: 'A'.codeUnitAt(0) | (2 << CellContent.widthShift),
|
||||
));
|
||||
_canvas(),
|
||||
Offset.zero,
|
||||
CellData(foreground: CellColor.normal, background: CellColor.named | 4, flags: 0, content: 'A'.codeUnitAt(0) | (2 << CellContent.widthShift)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -40,12 +40,12 @@ class _TestViewportOffset extends ViewportOffset {
|
||||
}
|
||||
|
||||
Widget _host(Widget child) => Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Center(child: SizedBox(width: 400, height: 200, child: child)),
|
||||
),
|
||||
);
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Center(child: SizedBox(width: 400, height: 200, child: child)),
|
||||
),
|
||||
);
|
||||
|
||||
void main() {
|
||||
group('EscapeParser — explicit-param CSI cursor moves', () {
|
||||
|
||||
@@ -15,12 +15,12 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
Widget _host(Widget child) => Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Center(child: SizedBox(width: 400, height: 200, child: child)),
|
||||
),
|
||||
);
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Center(child: SizedBox(width: 400, height: 200, child: child)),
|
||||
),
|
||||
);
|
||||
|
||||
void main() {
|
||||
group('RenderTerminal — TerminalView-driven setter updates', () {
|
||||
@@ -31,16 +31,20 @@ void main() {
|
||||
// Pump again with a different value for every TerminalView-exposed
|
||||
// prop that maps to a RenderTerminal setter — exercises the
|
||||
// "value != current" branch on each.
|
||||
await tester.pumpWidget(_host(TerminalView(
|
||||
t,
|
||||
theme: TerminalThemes.whiteOnBlack,
|
||||
textStyle: const TerminalStyle(fontSize: 18),
|
||||
textScaler: const TextScaler.linear(1.2),
|
||||
padding: const EdgeInsets.all(4),
|
||||
autoResize: false,
|
||||
cursorType: TerminalCursorType.underline,
|
||||
alwaysShowCursor: true,
|
||||
)));
|
||||
await tester.pumpWidget(
|
||||
_host(
|
||||
TerminalView(
|
||||
t,
|
||||
theme: TerminalThemes.whiteOnBlack,
|
||||
textStyle: const TerminalStyle(fontSize: 18),
|
||||
textScaler: const TextScaler.linear(1.2),
|
||||
padding: const EdgeInsets.all(4),
|
||||
autoResize: false,
|
||||
cursorType: TerminalCursorType.underline,
|
||||
alwaysShowCursor: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
final state = tester.state<TerminalViewState>(find.byType(TerminalView));
|
||||
expect(state.renderTerminal, isNotNull);
|
||||
@@ -160,7 +164,7 @@ void main() {
|
||||
await tester.pump();
|
||||
final state = tester.state<TerminalViewState>(find.byType(TerminalView));
|
||||
var callbackFired = 0;
|
||||
state.renderTerminal.onEditableRect = (_, __) => callbackFired++;
|
||||
state.renderTerminal.onEditableRect = (_, _) => callbackFired++;
|
||||
// Triggering a layout-affecting change should drive
|
||||
// _notifyEditableRect on the next layout pump.
|
||||
t.write('layout-trigger');
|
||||
|
||||
@@ -49,12 +49,7 @@ void main() {
|
||||
tearDown(() => debugDefaultTargetPlatformOverride = null);
|
||||
|
||||
test('Linux/Windows/Android/Fuchsia use Ctrl-modified bindings', () {
|
||||
for (final p in [
|
||||
TargetPlatform.linux,
|
||||
TargetPlatform.windows,
|
||||
TargetPlatform.android,
|
||||
TargetPlatform.fuchsia,
|
||||
]) {
|
||||
for (final p in [TargetPlatform.linux, TargetPlatform.windows, TargetPlatform.android, TargetPlatform.fuchsia]) {
|
||||
debugDefaultTargetPlatformOverride = p;
|
||||
final map = defaultTerminalShortcuts;
|
||||
// Three entries: copy / paste / select-all.
|
||||
@@ -83,10 +78,7 @@ void main() {
|
||||
});
|
||||
|
||||
group('TerminalActions', () {
|
||||
Widget host(Widget child) => Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: child,
|
||||
);
|
||||
Widget host(Widget child) => Directionality(textDirection: TextDirection.ltr, child: child);
|
||||
|
||||
testWidgets('CopySelectionTextIntent writes selection text to the clipboard', (tester) async {
|
||||
final outputs = <String>[];
|
||||
@@ -95,32 +87,32 @@ void main() {
|
||||
final controller = TerminalController();
|
||||
addTearDown(controller.dispose);
|
||||
// Set a selection covering 'hello'.
|
||||
controller.setSelection(
|
||||
terminal.buffer.createAnchor(0, 0),
|
||||
terminal.buffer.createAnchor(5, 0),
|
||||
);
|
||||
controller.setSelection(terminal.buffer.createAnchor(0, 0), terminal.buffer.createAnchor(5, 0));
|
||||
|
||||
String? clipboardText;
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
(call) async {
|
||||
if (call.method == 'Clipboard.setData') {
|
||||
final args = call.arguments as Map;
|
||||
clipboardText = args['text'] as String?;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (call) async {
|
||||
if (call.method == 'Clipboard.setData') {
|
||||
final args = call.arguments as Map;
|
||||
clipboardText = args['text'] as String?;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
late BuildContext capturedContext;
|
||||
await tester.pumpWidget(host(TerminalActions(
|
||||
terminal: terminal,
|
||||
controller: controller,
|
||||
child: Builder(builder: (context) {
|
||||
capturedContext = context;
|
||||
return const SizedBox();
|
||||
}),
|
||||
)));
|
||||
await tester.pumpWidget(
|
||||
host(
|
||||
TerminalActions(
|
||||
terminal: terminal,
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
capturedContext = context;
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Actions.invoke(capturedContext, CopySelectionTextIntent.copy);
|
||||
await tester.pump();
|
||||
@@ -132,23 +124,26 @@ void main() {
|
||||
final controller = TerminalController();
|
||||
addTearDown(controller.dispose);
|
||||
var setDataCalls = 0;
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
(call) async {
|
||||
if (call.method == 'Clipboard.setData') setDataCalls++;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (call) async {
|
||||
if (call.method == 'Clipboard.setData') setDataCalls++;
|
||||
return null;
|
||||
});
|
||||
|
||||
late BuildContext ctx;
|
||||
await tester.pumpWidget(host(TerminalActions(
|
||||
terminal: terminal,
|
||||
controller: controller,
|
||||
child: Builder(builder: (c) {
|
||||
ctx = c;
|
||||
return const SizedBox();
|
||||
}),
|
||||
)));
|
||||
await tester.pumpWidget(
|
||||
host(
|
||||
TerminalActions(
|
||||
terminal: terminal,
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (c) {
|
||||
ctx = c;
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Actions.invoke(ctx, CopySelectionTextIntent.copy);
|
||||
await tester.pump();
|
||||
@@ -160,31 +155,31 @@ void main() {
|
||||
final terminal = Terminal(maxLines: 100, onOutput: outputs.add);
|
||||
final controller = TerminalController();
|
||||
addTearDown(controller.dispose);
|
||||
controller.setSelection(
|
||||
terminal.buffer.createAnchor(0, 0),
|
||||
terminal.buffer.createAnchor(2, 0),
|
||||
);
|
||||
controller.setSelection(terminal.buffer.createAnchor(0, 0), terminal.buffer.createAnchor(2, 0));
|
||||
expect(controller.selection, isNotNull);
|
||||
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
(call) async {
|
||||
if (call.method == 'Clipboard.getData') {
|
||||
return <String, dynamic>{'text': 'pasted-payload'};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (call) async {
|
||||
if (call.method == 'Clipboard.getData') {
|
||||
return <String, dynamic>{'text': 'pasted-payload'};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
late BuildContext ctx;
|
||||
await tester.pumpWidget(host(TerminalActions(
|
||||
terminal: terminal,
|
||||
controller: controller,
|
||||
child: Builder(builder: (c) {
|
||||
ctx = c;
|
||||
return const SizedBox();
|
||||
}),
|
||||
)));
|
||||
await tester.pumpWidget(
|
||||
host(
|
||||
TerminalActions(
|
||||
terminal: terminal,
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (c) {
|
||||
ctx = c;
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Actions.invoke(ctx, const PasteTextIntent(SelectionChangedCause.keyboard));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -201,14 +196,20 @@ void main() {
|
||||
expect(controller.selection, isNull);
|
||||
|
||||
late BuildContext ctx;
|
||||
await tester.pumpWidget(host(TerminalActions(
|
||||
terminal: terminal,
|
||||
controller: controller,
|
||||
child: Builder(builder: (c) {
|
||||
ctx = c;
|
||||
return const SizedBox();
|
||||
}),
|
||||
)));
|
||||
await tester.pumpWidget(
|
||||
host(
|
||||
TerminalActions(
|
||||
terminal: terminal,
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (c) {
|
||||
ctx = c;
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Actions.invoke(ctx, const SelectAllTextIntent(SelectionChangedCause.keyboard));
|
||||
await tester.pump();
|
||||
|
||||
@@ -24,11 +24,7 @@ class _Recorder {
|
||||
String? lastOscPs;
|
||||
List<String>? lastOscArgs;
|
||||
|
||||
Terminal build({
|
||||
int maxLines = 100,
|
||||
TerminalTargetPlatform platform = TerminalTargetPlatform.linux,
|
||||
bool reflow = true,
|
||||
}) {
|
||||
Terminal build({int maxLines = 100, TerminalTargetPlatform platform = TerminalTargetPlatform.linux, bool reflow = true}) {
|
||||
return Terminal(
|
||||
maxLines: maxLines,
|
||||
platform: platform,
|
||||
|
||||
@@ -53,11 +53,7 @@ void main() {
|
||||
focus.dispose();
|
||||
});
|
||||
|
||||
await tester.pumpWidget(_host(TerminalView(
|
||||
t,
|
||||
controller: controller,
|
||||
focusNode: focus,
|
||||
)));
|
||||
await tester.pumpWidget(_host(TerminalView(t, controller: controller, focusNode: focus)));
|
||||
// Tear down via removing the widget; supplied controllers should NOT
|
||||
// throw on later use (the state didn't dispose them).
|
||||
await tester.pumpWidget(_host(const SizedBox()));
|
||||
@@ -89,10 +85,7 @@ void main() {
|
||||
await testGesture.addPointer(location: viewCenter);
|
||||
// A scroll event with a positive dy → page-down keys; a negative
|
||||
// dy → page-up. Use a magnitude that comfortably rounds to 1+ lines.
|
||||
await tester.sendEventToBinding(PointerScrollEvent(
|
||||
position: viewCenter,
|
||||
scrollDelta: const Offset(0, 100),
|
||||
));
|
||||
await tester.sendEventToBinding(PointerScrollEvent(position: viewCenter, scrollDelta: const Offset(0, 100)));
|
||||
await tester.pump();
|
||||
// Output should contain at least one PgDown escape.
|
||||
expect(r.outputs, isNotEmpty);
|
||||
@@ -108,10 +101,7 @@ void main() {
|
||||
final viewCenter = tester.getCenter(find.byType(TerminalView));
|
||||
final testGesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await testGesture.addPointer(location: viewCenter);
|
||||
await tester.sendEventToBinding(PointerScrollEvent(
|
||||
position: viewCenter,
|
||||
scrollDelta: const Offset(0, 100),
|
||||
));
|
||||
await tester.sendEventToBinding(PointerScrollEvent(position: viewCenter, scrollDelta: const Offset(0, 100)));
|
||||
await tester.pump();
|
||||
expect(r.outputs, isNotEmpty);
|
||||
// Wheel-down id is 64+5=69; normal-mode reporter encodes button
|
||||
@@ -131,10 +121,7 @@ void main() {
|
||||
final viewCenter = tester.getCenter(find.byType(TerminalView));
|
||||
// PointerScaleEvent is a sibling of PointerScrollEvent; the handler
|
||||
// ignores anything other than PointerScrollEvent.
|
||||
await tester.sendEventToBinding(PointerScaleEvent(
|
||||
position: viewCenter,
|
||||
scale: 1.2,
|
||||
));
|
||||
await tester.sendEventToBinding(PointerScaleEvent(position: viewCenter, scale: 1.2));
|
||||
await tester.pump();
|
||||
expect(r.outputs, isEmpty);
|
||||
});
|
||||
@@ -188,11 +175,15 @@ void main() {
|
||||
testWidgets('hardware key event with a non-modifier key reaches Terminal.keyInput', (tester) async {
|
||||
final r = _OutputRecorder();
|
||||
final t = r.build();
|
||||
await tester.pumpWidget(_host(TerminalView(
|
||||
t,
|
||||
autofocus: true,
|
||||
// hardwareKeyboardOnly: true,
|
||||
)));
|
||||
await tester.pumpWidget(
|
||||
_host(
|
||||
TerminalView(
|
||||
t,
|
||||
autofocus: true,
|
||||
// hardwareKeyboardOnly: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
// Wait one frame for autofocus to settle.
|
||||
await tester.pump();
|
||||
// Send a Down arrow keypress through the binding.
|
||||
@@ -241,14 +232,18 @@ void main() {
|
||||
final r = _OutputRecorder();
|
||||
final t = r.build();
|
||||
var overrideCalls = 0;
|
||||
await tester.pumpWidget(_host(TerminalView(
|
||||
t,
|
||||
autofocus: true,
|
||||
onKeyEvent: (focusNode, event) {
|
||||
overrideCalls++;
|
||||
return KeyEventResult.handled;
|
||||
},
|
||||
)));
|
||||
await tester.pumpWidget(
|
||||
_host(
|
||||
TerminalView(
|
||||
t,
|
||||
autofocus: true,
|
||||
onKeyEvent: (focusNode, event) {
|
||||
overrideCalls++;
|
||||
return KeyEventResult.handled;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.pump();
|
||||
@@ -259,11 +254,7 @@ void main() {
|
||||
testWidgets('onKeyEvent returning ignored falls through to the default handler', (tester) async {
|
||||
final r = _OutputRecorder();
|
||||
final t = r.build();
|
||||
await tester.pumpWidget(_host(TerminalView(
|
||||
t,
|
||||
autofocus: true,
|
||||
onKeyEvent: (_, __) => KeyEventResult.ignored,
|
||||
)));
|
||||
await tester.pumpWidget(_host(TerminalView(t, autofocus: true, onKeyEvent: (_, _) => KeyEventResult.ignored)));
|
||||
await tester.pump();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.pump();
|
||||
@@ -308,10 +299,7 @@ void main() {
|
||||
final r = _OutputRecorder();
|
||||
final t = r.build();
|
||||
var tappedAt = 0;
|
||||
await tester.pumpWidget(_host(TerminalView(
|
||||
t,
|
||||
onTapUp: (_, __) => tappedAt++,
|
||||
)));
|
||||
await tester.pumpWidget(_host(TerminalView(t, onTapUp: (_, _) => tappedAt++)));
|
||||
await tester.tapAt(tester.getCenter(find.byType(TerminalView)));
|
||||
// The gesture detector arms a 300 ms double-tap timer after the tap;
|
||||
// explicit duration flushes it (pumpAndSettle waits on animations,
|
||||
@@ -324,10 +312,7 @@ void main() {
|
||||
final t = _OutputRecorder().build();
|
||||
final controller = TerminalController();
|
||||
addTearDown(controller.dispose);
|
||||
controller.setSelection(
|
||||
t.buffer.createAnchor(0, 0),
|
||||
t.buffer.createAnchor(2, 0),
|
||||
);
|
||||
controller.setSelection(t.buffer.createAnchor(0, 0), t.buffer.createAnchor(2, 0));
|
||||
expect(controller.selection, isNotNull);
|
||||
await tester.pumpWidget(_host(TerminalView(t, controller: controller)));
|
||||
await tester.tapAt(tester.getCenter(find.byType(TerminalView)));
|
||||
@@ -340,11 +325,7 @@ void main() {
|
||||
final t = r.build();
|
||||
var downCalls = 0;
|
||||
var upCalls = 0;
|
||||
await tester.pumpWidget(_host(TerminalView(
|
||||
t,
|
||||
onSecondaryTapDown: (_, __) => downCalls++,
|
||||
onSecondaryTapUp: (_, __) => upCalls++,
|
||||
)));
|
||||
await tester.pumpWidget(_host(TerminalView(t, onSecondaryTapDown: (_, _) => downCalls++, onSecondaryTapUp: (_, _) => upCalls++)));
|
||||
final pos = tester.getCenter(find.byType(TerminalView));
|
||||
final gesture = await tester.startGesture(pos, buttons: kSecondaryButton);
|
||||
await gesture.up();
|
||||
@@ -448,10 +429,7 @@ void main() {
|
||||
// deleteDetection seeds the editing value with two spaces; shrinking
|
||||
// it below that length triggers CustomTextEdit.onDelete, which fires
|
||||
// _scrollToBottom + Terminal.keyInput(backspace) in TerminalView.
|
||||
tester.testTextInput.updateEditingValue(const TextEditingValue(
|
||||
text: '',
|
||||
selection: TextSelection.collapsed(offset: 0),
|
||||
));
|
||||
tester.testTextInput.updateEditingValue(const TextEditingValue(text: '', selection: TextSelection.collapsed(offset: 0)));
|
||||
await tester.pump();
|
||||
// Backspace produces a non-empty escape sequence (^?).
|
||||
expect(r.outputs, isNotEmpty);
|
||||
@@ -461,11 +439,7 @@ void main() {
|
||||
final t = _OutputRecorder().build();
|
||||
final focus = FocusNode();
|
||||
addTearDown(focus.dispose);
|
||||
await tester.pumpWidget(_host(TerminalView(
|
||||
t,
|
||||
hardwareKeyboardOnly: true,
|
||||
focusNode: focus,
|
||||
)));
|
||||
await tester.pumpWidget(_host(TerminalView(t, hardwareKeyboardOnly: true, focusNode: focus)));
|
||||
// Tap with no selection-clear short-circuit — the else-branch of
|
||||
// _onTapUp routes through _focusNode.requestFocus when
|
||||
// hardwareKeyboardOnly is set (no CustomTextEdit to delegate to).
|
||||
|
||||
@@ -142,9 +142,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('fromTextStyle picks fontSize / height / fontFamilyFallback through to TerminalStyle', () {
|
||||
final s = TerminalStyle.fromTextStyle(
|
||||
const TextStyle(fontSize: 18, height: 1.5, fontFamilyFallback: ['Mono']),
|
||||
);
|
||||
final s = TerminalStyle.fromTextStyle(const TextStyle(fontSize: 18, height: 1.5, fontFamilyFallback: ['Mono']));
|
||||
expect(s.fontSize, 18);
|
||||
expect(s.height, 1.5);
|
||||
expect(s.fontFamilyFallback, ['Mono']);
|
||||
@@ -152,12 +150,7 @@ void main() {
|
||||
|
||||
test('toTextStyle threads attribute flags into the resulting TextStyle', () {
|
||||
const s = TerminalStyle(fontSize: 16);
|
||||
final t = s.toTextStyle(
|
||||
color: const Color(0xFF112233),
|
||||
bold: true,
|
||||
italic: true,
|
||||
underline: true,
|
||||
);
|
||||
final t = s.toTextStyle(color: const Color(0xFF112233), bold: true, italic: true, underline: true);
|
||||
expect(t.fontSize, 16);
|
||||
expect(t.fontWeight, FontWeight.bold);
|
||||
expect(t.fontStyle, FontStyle.italic);
|
||||
@@ -244,11 +237,7 @@ void main() {
|
||||
group('SelectionMode + TerminalCursorType enums', () {
|
||||
test('all enum values exist and are distinct', () {
|
||||
expect(SelectionMode.values, [SelectionMode.line, SelectionMode.block]);
|
||||
expect(TerminalCursorType.values, [
|
||||
TerminalCursorType.block,
|
||||
TerminalCursorType.underline,
|
||||
TerminalCursorType.verticalBar,
|
||||
]);
|
||||
expect(TerminalCursorType.values, [TerminalCursorType.block, TerminalCursorType.underline, TerminalCursorType.verticalBar]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -28,16 +28,18 @@ void main() {
|
||||
final focus = FocusNode();
|
||||
addTearDown(focus.dispose);
|
||||
|
||||
await tester.pumpWidget(_host(
|
||||
CustomKeyboardListener(
|
||||
focusNode: focus,
|
||||
autofocus: true,
|
||||
onInsert: inserts.add,
|
||||
onComposing: composings.add,
|
||||
onKeyEvent: (_, __) => KeyEventResult.ignored,
|
||||
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
|
||||
await tester.pumpWidget(
|
||||
_host(
|
||||
CustomKeyboardListener(
|
||||
focusNode: focus,
|
||||
autofocus: true,
|
||||
onInsert: inserts.add,
|
||||
onComposing: composings.add,
|
||||
onKeyEvent: (_, _) => KeyEventResult.ignored,
|
||||
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pump();
|
||||
// A character key with non-empty `character` triggers the insert path.
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyA, character: 'a');
|
||||
@@ -50,16 +52,18 @@ void main() {
|
||||
final focus = FocusNode();
|
||||
addTearDown(focus.dispose);
|
||||
|
||||
await tester.pumpWidget(_host(
|
||||
CustomKeyboardListener(
|
||||
focusNode: focus,
|
||||
autofocus: true,
|
||||
onInsert: inserts.add,
|
||||
onComposing: (_) {},
|
||||
onKeyEvent: (_, __) => KeyEventResult.handled,
|
||||
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
|
||||
await tester.pumpWidget(
|
||||
_host(
|
||||
CustomKeyboardListener(
|
||||
focusNode: focus,
|
||||
autofocus: true,
|
||||
onInsert: inserts.add,
|
||||
onComposing: (_) {},
|
||||
onKeyEvent: (_, _) => KeyEventResult.handled,
|
||||
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyA, character: 'a');
|
||||
await tester.pump();
|
||||
@@ -71,16 +75,18 @@ void main() {
|
||||
final focus = FocusNode();
|
||||
addTearDown(focus.dispose);
|
||||
|
||||
await tester.pumpWidget(_host(
|
||||
CustomKeyboardListener(
|
||||
focusNode: focus,
|
||||
autofocus: true,
|
||||
onInsert: inserts.add,
|
||||
onComposing: (_) {},
|
||||
onKeyEvent: (_, __) => KeyEventResult.ignored,
|
||||
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
|
||||
await tester.pumpWidget(
|
||||
_host(
|
||||
CustomKeyboardListener(
|
||||
focusNode: focus,
|
||||
autofocus: true,
|
||||
onInsert: inserts.add,
|
||||
onComposing: (_) {},
|
||||
onKeyEvent: (_, _) => KeyEventResult.ignored,
|
||||
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pump();
|
||||
// ArrowUp has no `character` — ignored result + no character means
|
||||
// _onKeyEvent returns ignored without firing onInsert.
|
||||
|
||||
Reference in New Issue
Block a user