Initial commit: Accessible SSH Terminal
This commit is contained in:
113
lib/ui/widgets/ansi_text_parser.dart
Normal file
113
lib/ui/widgets/ansi_text_parser.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user