Initial commit: Accessible SSH Terminal

This commit is contained in:
2025-12-22 01:02:44 -06:00
commit 7a967ef759
133 changed files with 6629 additions and 0 deletions

View 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;
}
}
}