toml_viewer 2.0.0
toml_viewer: ^2.0.0 copied to clipboard
A Flutter widget for displaying TOML files as interactive, searchable, lazily-rendered tree views with partial-parse error reporting, a raw source view, light/dark theming, custom styling, and extensi [...]
import 'package:flutter/material.dart';
import 'package:toml_viewer/toml_viewer.dart';
void main() {
runApp(const DemoApp());
}
class DemoApp extends StatefulWidget {
const DemoApp({super.key});
@override
State<DemoApp> createState() => _DemoAppState();
}
class _DemoAppState extends State<DemoApp> {
ThemeMode _themeMode = ThemeMode.system;
void _toggleTheme() {
setState(() {
_themeMode = switch (_themeMode) {
ThemeMode.light => ThemeMode.dark,
ThemeMode.dark => ThemeMode.light,
ThemeMode.system =>
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
Brightness.dark
? ThemeMode.light
: ThemeMode.dark,
};
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'TOML Viewer Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: Colors.teal,
brightness: Brightness.light,
useMaterial3: true,
),
darkTheme: ThemeData(
colorSchemeSeed: Colors.teal,
brightness: Brightness.dark,
useMaterial3: true,
),
themeMode: _themeMode,
home: DemoHome(onToggleTheme: _toggleTheme),
);
}
}
class DemoHome extends StatelessWidget {
const DemoHome({super.key, required this.onToggleTheme});
final VoidCallback onToggleTheme;
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 8,
child: Scaffold(
appBar: AppBar(
title: const Text('TOML Viewer Demo'),
actions: [
IconButton(
icon: Icon(
Theme.of(context).brightness == Brightness.dark
? Icons.light_mode
: Icons.dark_mode,
),
tooltip: 'Toggle theme',
onPressed: onToggleTheme,
),
],
bottom: const TabBar(
isScrollable: true,
tabs: [
Tab(text: 'Search & Source'),
Tab(text: 'Asset File'),
Tab(text: 'Inline String'),
Tab(text: 'From Map'),
Tab(text: 'Error Handling'),
Tab(text: 'Controller'),
Tab(text: 'Custom Style'),
Tab(text: 'Builders'),
],
),
),
body: const TabBarView(
children: [
_SearchDemo(),
_AssetDemo(),
_InlineStringDemo(),
_FromMapDemo(),
_ErrorHandlingDemo(),
_ControllerDemo(),
_CustomStyleDemo(),
_BuildersDemo(),
],
),
),
);
}
}
/// A tab body with a title, an optional subtitle, and a viewer filling the
/// rest of the space.
class _Demo extends StatelessWidget {
const _Demo({required this.title, this.subtitle, required this.child});
final String title;
final String? subtitle;
final Widget child;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.labelLarge),
if (subtitle != null)
Text(subtitle!, style: Theme.of(context).textTheme.bodySmall),
const SizedBox(height: 12),
Expanded(child: child),
],
),
);
}
}
// ── Tab 1: Search and the raw source view ──
class _SearchDemo extends StatefulWidget {
const _SearchDemo();
@override
State<_SearchDemo> createState() => _SearchDemoState();
}
class _SearchDemoState extends State<_SearchDemo> {
final TextEditingController _search = TextEditingController();
TomlViewMode _mode = TomlViewMode.tree;
static const String _toml = '''
[app]
name = "My App"
version = "1.2.3"
[app.window]
width = 1024
height = 768
fullscreen = false
[network]
timeout = 30
base_url = "https://api.example.com"
[[users]]
name = "Alice"
role = "admin"
[[users]]
name = "Bob"
role = "editor"
''';
@override
void dispose() {
_search.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _Demo(
title: 'Search and raw source',
subtitle: 'Matches are highlighted and their ancestors auto-expand',
child: Column(
children: [
Row(
children: [
Expanded(
child: TextField(
controller: _search,
decoration: const InputDecoration(
isDense: true,
prefixIcon: Icon(Icons.search),
hintText: 'Filter keys and values…',
border: OutlineInputBorder(),
),
onChanged: (_) => setState(() {}),
),
),
const SizedBox(width: 8),
SegmentedButton<TomlViewMode>(
segments: const [
ButtonSegment(
value: TomlViewMode.tree,
icon: Icon(Icons.account_tree_outlined),
),
ButtonSegment(
value: TomlViewMode.source,
icon: Icon(Icons.code),
),
],
selected: {_mode},
onSelectionChanged: (s) => setState(() => _mode = s.first),
),
],
),
const SizedBox(height: 12),
Expanded(
child: TomlView(
content: _toml,
mode: _mode,
searchQuery: _search.text,
emptyBuilder: (_) => const Center(child: Text('No matches')),
),
),
],
),
);
}
}
// ── Tab 2: Asset file with theme-aware config ──
class _AssetDemo extends StatelessWidget {
const _AssetDemo();
@override
Widget build(BuildContext context) {
return _Demo(
title: 'Loaded from assets/demo.toml',
subtitle: 'Colours follow the ambient theme when no config is supplied',
child: TomlView.asset(
'assets/demo.toml',
config: TomlViewerConfig.of(context).copyWith(expandMode: false),
),
);
}
}
// ── Tab 3: Inline TOML string ──
class _InlineStringDemo extends StatelessWidget {
const _InlineStringDemo();
static const String _toml = '''
[app]
name = "My App"
version = "1.2.3"
debug = true
[app.window]
width = 1024
height = 768
fullscreen = false
[network]
timeout = 30
retries = 3
base_url = "https://api.example.com"
[[users]]
name = "Alice"
role = "admin"
[[users]]
name = "Bob"
role = "editor"
[[users]]
name = "Charlie"
role = "viewer"
''';
@override
Widget build(BuildContext context) {
return const _Demo(
title: 'Parsed from an inline TOML string',
subtitle: 'Long-press any value to copy it',
child: TomlView(content: _toml),
);
}
}
// ── Tab 4: From a pre-parsed Map ──
class _FromMapDemo extends StatelessWidget {
const _FromMapDemo();
static const Map<String, dynamic> _data = <String, dynamic>{
'title': 'Pre-parsed data',
'count': 42,
'pi': 3.14159,
'enabled': true,
'tags': ['flutter', 'toml', 'viewer'],
'mixed': ['a', 1, true],
'nested': {
'level1': {
'level2': {'deep_value': 'hello from the deep'},
},
},
};
@override
Widget build(BuildContext context) {
return const _Demo(
title: 'Rendered from a Map<String, dynamic>',
subtitle: 'Arrays report their element type: Array of String[3]',
child: TomlView.fromMap(_data),
);
}
}
// ── Tab 5: Error handling ──
class _ErrorHandlingDemo extends StatefulWidget {
const _ErrorHandlingDemo();
@override
State<_ErrorHandlingDemo> createState() => _ErrorHandlingDemoState();
}
class _ErrorHandlingDemoState extends State<_ErrorHandlingDemo> {
final List<String> _errorLog = [];
static const String _brokenToml = '''
# This section is valid
[database]
server = "192.168.1.1"
port = 5432
# This section has an error (unterminated string)
[broken_section]
name = "missing end quote
value = 123
# A multi-line array survives a neighbouring failure
[matrices]
identity = [
[1, 0],
[0, 1],
]
# This section is also valid
[logging]
level = "info"
enabled = true
''';
void _recordErrors(List<TomlParseError> errors) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
setState(() {
_errorLog
..clear()
..addAll(
errors.map((e) => 'Line ${e.line}:${e.column} — ${e.message}'));
});
});
}
@override
Widget build(BuildContext context) {
return _Demo(
title: 'Partial parse with inline error highlighting',
subtitle: 'Valid sections still render; broken ones are annotated',
child: Column(
children: [
Expanded(
flex: 3,
child: TomlView(
content: _brokenToml,
config: TomlViewerConfig.of(context)
.copyWith(onParseErrors: _recordErrors),
),
),
if (_errorLog.isNotEmpty) ...[
const Divider(),
Align(
alignment: Alignment.centerLeft,
child: Text(
'onParseErrors log:',
style: Theme.of(context).textTheme.labelMedium,
),
),
const SizedBox(height: 4),
Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: ListView.builder(
itemCount: _errorLog.length,
itemBuilder: (context, i) => Text(
_errorLog[i],
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontFamily: 'monospace',
fontFamilyFallback: TomlViewerStyle.monospaceFallback,
),
),
),
),
),
],
],
),
);
}
}
// ── Tab 6: Expand controller ──
class _ControllerDemo extends StatefulWidget {
const _ControllerDemo();
@override
State<_ControllerDemo> createState() => _ControllerDemoState();
}
class _ControllerDemoState extends State<_ControllerDemo> {
late final TomlExpandController _controller =
TomlExpandController(defaultExpanded: false);
static const String _toml = '''
[server]
host = "localhost"
port = 8080
[server.tls]
enabled = true
cert = "/etc/ssl/cert.pem"
key = "/etc/ssl/key.pem"
[database]
url = "postgres://localhost/mydb"
pool_size = 10
[cache]
driver = "redis"
ttl = 3600
[[routes]]
path = "/api/users"
method = "GET"
[[routes]]
path = "/api/users"
method = "POST"
''';
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _Demo(
title: 'Programmatic expand/collapse control',
subtitle: 'expandAll and collapseAll reach nodes you never tapped',
child: Column(
children: [
Wrap(
spacing: 8,
runSpacing: 8,
children: [
FilledButton.tonalIcon(
onPressed: _controller.expandAll,
icon: const Icon(Icons.unfold_more, size: 18),
label: const Text('Expand All'),
),
FilledButton.tonalIcon(
onPressed: _controller.collapseAll,
icon: const Icon(Icons.unfold_less, size: 18),
label: const Text('Collapse All'),
),
OutlinedButton(
onPressed: () => _controller.expandToPath('server.tls'),
child: const Text('Reveal server.tls'),
),
OutlinedButton(
onPressed: () => _controller.expandToPath('routes[1]'),
child: const Text('Reveal routes[1]'),
),
OutlinedButton(
onPressed: _controller.reset,
child: const Text('Reset'),
),
],
),
const SizedBox(height: 12),
Expanded(
child: TomlView(content: _toml, expandController: _controller),
),
],
),
);
}
}
// ── Tab 7: Custom style ──
class _CustomStyleDemo extends StatelessWidget {
const _CustomStyleDemo();
static const String _toml = '''
[project]
name = "toml_viewer"
version = "2.0.0"
description = "A Flutter TOML viewer"
[project.dependencies]
flutter = ">=3.27.0"
toml = "^0.18.0"
[[contributors]]
name = "Sudhi S"
role = "maintainer"
[[contributors]]
name = "Open Source"
role = "community"
''';
@override
Widget build(BuildContext context) {
return _Demo(
title: 'Custom style: fonts, spacing, icons, separator',
subtitle: 'TomlViewerStyle controls everything except colours',
child: TomlView(
content: _toml,
config: TomlViewerConfig.of(context).copyWith(
style: const TomlViewerStyle(
rootKeyStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
nonRootKeyStyle: TextStyle(fontSize: 14),
tableKeyStyle: TextStyle(fontSize: 14, fontStyle: FontStyle.italic),
valueStyle: TextStyle(
fontSize: 14,
fontFamily: 'monospace',
fontFamilyFallback: TomlViewerStyle.monospaceFallback,
),
typeStyle: TextStyle(fontSize: 12, fontStyle: FontStyle.italic),
indentation: 24,
rowSpacing: 6,
rowVerticalPadding: 6,
separator: ' : ',
separatorGap: 6,
collapsedIcon: Icons.add_circle_outline,
expandedIcon: Icons.remove_circle_outline,
expandIconSize: 18,
),
),
),
);
}
}
// ── Tab 8: Custom builders & interaction callbacks ──
class _BuildersDemo extends StatelessWidget {
const _BuildersDemo();
static const String _toml = '''
[server]
host = "localhost"
port = 8080
debug = true
max_connections = 1000
timeout = 30.5
[paths]
home = "/home/user"
config = "/etc/app/config.toml"
''';
static void _snack(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), duration: const Duration(seconds: 1)),
);
}
@override
Widget build(BuildContext context) {
return _Demo(
title: 'Custom builders & interaction',
subtitle: 'Tap a value for a SnackBar; long-press copies the path',
child: TomlView(
content: _toml,
config: TomlViewerConfig.of(context).copyWith(
copyMode: TomlCopyMode.pathAndValue,
valueBuilder: (context, value, path) {
if (value is bool) {
return Chip(
avatar: Icon(
value ? Icons.check_circle : Icons.cancel,
size: 16,
color: value ? Colors.green : Colors.red,
),
label: Text('$value', style: const TextStyle(fontSize: 12)),
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
}
if (value is num) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primaryContainer
.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'$value',
style: TextStyle(
fontFamily: 'monospace',
fontFamilyFallback: TomlViewerStyle.monospaceFallback,
color: Theme.of(context).colorScheme.primary,
),
),
);
}
return null; // default for everything else
},
onValueTap: (context, key, value, path) =>
_snack(context, 'Tapped $path = $value'),
onValueCopied: (context, key, copied, path) =>
_snack(context, 'Copied: $copied'),
),
),
);
}
}