114 lines
3.8 KiB
Dart
114 lines
3.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
class AnsiTextParser {
|
|
// Matches CSI (Control Sequence Introducer) sequences: ESC [ ... FinalByte
|
|
// And OSC (Operating System Command) sequences: ESC ] ... BEL(\x07) or ST(\x1B\)
|
|
static final RegExp _ansiRegex = RegExp(r'\x1B\[[0-9;?]*[ -/]*[@-~]|\x1B\].*?(\x07|\x1B\\)');
|
|
|
|
static String strip(String text) {
|
|
// Remove ANSI codes
|
|
var clean = text.replaceAll(_ansiRegex, '');
|
|
// Remove control chars (same regex as in parse)
|
|
clean = clean.replaceAll(RegExp(r'[\x00-\x08\x0B\x0C\x0E-\x1F]'), '');
|
|
return clean;
|
|
}
|
|
|
|
TextSpan parse(String text) {
|
|
final List<TextSpan> spans = [];
|
|
final matches = _ansiRegex.allMatches(text);
|
|
|
|
int currentIndex = 0;
|
|
TextStyle currentStyle = const TextStyle(color: Colors.white, fontFamily: 'monospace');
|
|
|
|
for (final match in matches) {
|
|
if (match.start > currentIndex) {
|
|
final plainText = text.substring(currentIndex, match.start);
|
|
// Clean up common non-printable control chars from the plain text segment
|
|
// \x07 (Bell), \x08 (Backspace - simple strip, handling logic is complex for stream)
|
|
final cleanText = plainText.replaceAll(RegExp(r'[\x00-\x08\x0B\x0C\x0E-\x1F]'), '');
|
|
|
|
if (cleanText.isNotEmpty) {
|
|
spans.add(TextSpan(
|
|
text: cleanText,
|
|
style: currentStyle,
|
|
));
|
|
}
|
|
}
|
|
|
|
final String sequence = match.group(0)!;
|
|
// We only process SGR (Select Graphic Rendition) which ends in 'm'
|
|
// All other sequences (cursor movement, clear screen, etc.) are stripped.
|
|
if (sequence.endsWith('m')) {
|
|
currentStyle = _parseSgrSequence(sequence, currentStyle);
|
|
}
|
|
|
|
currentIndex = match.end;
|
|
}
|
|
|
|
if (currentIndex < text.length) {
|
|
final plainText = text.substring(currentIndex);
|
|
final cleanText = plainText.replaceAll(RegExp(r'[\x00-\x08\x0B\x0C\x0E-\x1F]'), '');
|
|
if (cleanText.isNotEmpty) {
|
|
spans.add(TextSpan(
|
|
text: cleanText,
|
|
style: currentStyle,
|
|
));
|
|
}
|
|
}
|
|
|
|
return TextSpan(children: spans);
|
|
}
|
|
|
|
TextStyle _parseSgrSequence(String sequence, TextStyle currentStyle) {
|
|
// Remove \x1B[ and m
|
|
final content = sequence.substring(2, sequence.length - 1);
|
|
final parts = content.split(';');
|
|
|
|
TextStyle newStyle = currentStyle;
|
|
|
|
if (parts.isEmpty || (parts.length == 1 && parts.first.isEmpty)) {
|
|
// Empty usually means reset 0
|
|
return const TextStyle(color: Colors.white, fontFamily: 'monospace');
|
|
}
|
|
|
|
for (final part in parts) {
|
|
final int? value = int.tryParse(part);
|
|
if (value == null) continue;
|
|
|
|
if (value == 0) {
|
|
newStyle = const TextStyle(color: Colors.white, fontFamily: 'monospace');
|
|
} else if (value == 1) {
|
|
newStyle = newStyle.copyWith(fontWeight: FontWeight.bold);
|
|
} else if (value >= 30 && value <= 37) {
|
|
newStyle = newStyle.copyWith(color: _getAnsiColor(value));
|
|
} else if (value >= 90 && value <= 97) {
|
|
newStyle = newStyle.copyWith(color: _getAnsiColor(value));
|
|
}
|
|
// Add background colors or other styles if needed
|
|
}
|
|
return newStyle;
|
|
}
|
|
|
|
Color _getAnsiColor(int code) {
|
|
switch (code) {
|
|
case 30: return Colors.black;
|
|
case 31: return Colors.red;
|
|
case 32: return Colors.green;
|
|
case 33: return Colors.yellow;
|
|
case 34: return Colors.blue;
|
|
case 35: return Colors.purple;
|
|
case 36: return Colors.cyan;
|
|
case 37: return Colors.white;
|
|
case 90: return Colors.grey;
|
|
case 91: return Colors.redAccent;
|
|
case 92: return Colors.greenAccent;
|
|
case 93: return Colors.yellowAccent;
|
|
case 94: return Colors.blueAccent;
|
|
case 95: return Colors.purpleAccent;
|
|
case 96: return Colors.cyanAccent;
|
|
case 97: return Colors.white;
|
|
default: return Colors.white;
|
|
}
|
|
}
|
|
}
|