import 'package:famedlysdk/famedlysdk.dart'; import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; class LogViewer extends StatefulWidget { @override _LogViewerState createState() => _LogViewerState(); } class _LogViewerState extends State { Level logLevel = Level.debug; double fontSize = 14; @override Widget build(BuildContext context) { final outputEvents = Logs() .outputEvents .where((e) => e.level.index >= logLevel.index) .toList(); return Scaffold( backgroundColor: Colors.black, appBar: AppBar( title: Text(logLevel.toString()), leading: BackButton(), actions: [ IconButton( icon: Icon(Icons.zoom_in_outlined), onPressed: () => setState(() => fontSize++), ), IconButton( icon: Icon(Icons.zoom_out_outlined), onPressed: () => setState(() => fontSize--), ), PopupMenuButton( itemBuilder: (context) => Level.values .map((level) => PopupMenuItem( child: Text(level.toString()), value: level, )) .toList(), onSelected: (Level level) => setState(() => logLevel = level), ), ], ), body: ListView.builder( itemCount: outputEvents.length, itemBuilder: (context, i) => SingleChildScrollView( scrollDirection: Axis.horizontal, child: RichText( text: TextSpan( style: TextStyle(fontSize: fontSize), children: _AnsiParser(outputEvents[i].lines.join('\n')).spans, ), ), ), ), ); } } class _AnsiParser { static const TEXT = 0, BRACKET = 1, CODE = 2; final String text; Color foreground; Color background; List spans; _AnsiParser(this.text) { final s = this.text; spans = []; var state = TEXT; StringBuffer buffer; var text = StringBuffer(); var code = 0; List codes; for (var i = 0, n = s.length; i < n; i++) { var c = s[i]; switch (state) { case TEXT: if (c == '\u001b') { state = BRACKET; buffer = StringBuffer(c); code = 0; codes = []; } else { text.write(c); } break; case BRACKET: buffer.write(c); if (c == '[') { state = CODE; } else { state = TEXT; text.write(buffer); } break; case CODE: buffer.write(c); var codeUnit = c.codeUnitAt(0); if (codeUnit >= 48 && codeUnit <= 57) { code = code * 10 + codeUnit - 48; continue; } else if (c == ';') { codes.add(code); code = 0; continue; } else { if (text.isNotEmpty) { spans.add(createSpan(text.toString())); text.clear(); } state = TEXT; if (c == 'm') { codes.add(code); handleCodes(codes); } else { text.write(buffer); } } break; } } spans.add(createSpan(text.toString())); } void handleCodes(List codes) { if (codes.isEmpty) { codes.add(0); } switch (codes[0]) { case 0: foreground = getColor(0, true); background = getColor(0, false); break; case 38: foreground = getColor(codes[2], true); break; case 39: foreground = getColor(0, true); break; case 48: background = getColor(codes[2], false); break; case 49: background = getColor(0, false); } } Color getColor(int colorCode, bool foreground) { switch (colorCode) { case 0: return foreground ? Colors.black : Colors.transparent; case 12: return Colors.lightBlue[300]; case 208: return Colors.orange[300]; case 196: return Colors.red[300]; case 199: return Colors.pink[300]; default: return Colors.white; } } TextSpan createSpan(String text) { return TextSpan( text: text, style: TextStyle( color: foreground, backgroundColor: background, ), ); } }