toml_viewer 1.0.0
toml_viewer: ^1.0.0 copied to clipboard
A Flutter widget for displaying TOML files as interactive, expandable tree views with syntax highlighting, partial-parse error reporting, light/dark theme support, custom styling, extensible builders, [...]
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 {
final VoidCallback onToggleTheme;
const DemoHome({super.key, required this.onToggleTheme});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 7,
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: '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: [
_AssetDemo(),
_InlineStringDemo(),
_FromMapDemo(),
_ErrorHandlingDemo(),
_ControllerDemo(),
_CustomStyleDemo(),
_BuildersDemo(),
],
),
),
);
}
}
// ── Tab 1: Asset file with theme-aware config ──
class _AssetDemo extends StatelessWidget {
const _AssetDemo();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Loaded from assets/demo.toml',
style: Theme.of(context).textTheme.labelLarge,
),
Text(
'Colours auto-adapt to light/dark theme via TomlViewerConfig.of(context)',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
Expanded(
child: TomlView.asset(
'assets/demo.toml',
config: TomlViewerConfig.of(context, expandMode: false),
),
),
],
),
);
}
}
// ── Tab 2: Inline TOML string ──
class _InlineStringDemo extends StatelessWidget {
const _InlineStringDemo();
static const _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 Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Parsed from an inline TOML string',
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 12),
Expanded(
child: TomlView(
content: _toml,
config: TomlViewerConfig.of(context),
),
),
],
),
);
}
}
// ── Tab 3: From a pre-parsed Map ──
class _FromMapDemo extends StatelessWidget {
const _FromMapDemo();
static const _data = <String, dynamic>{
'title': 'Pre-parsed data',
'count': 42,
'pi': 3.14159,
'enabled': true,
'tags': ['flutter', 'toml', 'viewer'],
'nested': {
'level1': {
'level2': {
'deep_value': 'hello from the deep',
},
},
},
};
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Rendered from a Map<String, dynamic>',
style: Theme.of(context).textTheme.labelLarge,
),
Text(
'Useful when you already have parsed data or want to display any map as a tree',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
Expanded(
child: TomlView.fromMap(
_data,
config: TomlViewerConfig.of(context),
),
),
],
),
);
}
}
// ── Tab 4: Error handling ──
class _ErrorHandlingDemo extends StatefulWidget {
const _ErrorHandlingDemo();
@override
State<_ErrorHandlingDemo> createState() => _ErrorHandlingDemoState();
}
class _ErrorHandlingDemoState extends State<_ErrorHandlingDemo> {
final List<String> _errorLog = [];
static const _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
# This section is also valid
[logging]
level = "info"
enabled = true
''';
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Partial parse with inline error highlighting',
style: Theme.of(context).textTheme.labelLarge,
),
Text(
'Valid sections render normally; broken sections show error annotations',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
Expanded(
flex: 3,
child: TomlView(
content: _brokenToml,
config: TomlViewerConfig.of(
context,
onParseErrors: (errors) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
setState(() {
_errorLog.clear();
for (final e in errors) {
_errorLog.add('Line ${e.line}:${e.column} - ${e.message}');
}
});
});
},
),
),
),
if (_errorLog.isNotEmpty) ...[
const Divider(),
Text(
'Error callback log:',
style: Theme.of(context).textTheme.labelMedium,
),
const SizedBox(height: 4),
Expanded(
flex: 1,
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',
),
),
),
),
),
],
],
),
);
}
}
// ── Tab 5: Expand controller ──
class _ControllerDemo extends StatefulWidget {
const _ControllerDemo();
@override
State<_ControllerDemo> createState() => _ControllerDemoState();
}
class _ControllerDemoState extends State<_ControllerDemo> {
late final TomlExpandController _controller;
@override
void initState() {
super.initState();
_controller = TomlExpandController(defaultExpanded: false);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
static const _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"
handler = "listUsers"
[[routes]]
path = "/api/users"
method = "POST"
handler = "createUser"
''';
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Programmatic expand/collapse control',
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 8),
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.toggle('server'),
child: const Text('Toggle server'),
),
OutlinedButton(
onPressed: () => _controller.toggle('database'),
child: const Text('Toggle database'),
),
OutlinedButton(
onPressed: () => _controller.toggle('server.tls'),
child: const Text('Toggle server.tls'),
),
],
),
const SizedBox(height: 12),
Expanded(
child: TomlView(
content: _toml,
expandController: _controller,
config: TomlViewerConfig.of(context, expandMode: false),
),
),
],
),
);
}
}
// ── Tab 6: Custom style (fonts, spacing, icons, separator) ──
class _CustomStyleDemo extends StatelessWidget {
const _CustomStyleDemo();
static const _toml = '''
[project]
name = "toml_viewer"
version = "0.1.0"
description = "A Flutter TOML viewer"
[project.dependencies]
flutter = ">=3.0.0"
toml = "^0.15.0"
[[contributors]]
name = "Sudhi S"
role = "maintainer"
[[contributors]]
name = "Open Source"
role = "community"
''';
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Custom style: fonts, spacing, icons, separator',
style: Theme.of(context).textTheme.labelLarge,
),
Text(
'TomlViewerStyle controls indentation, row spacing, separator text, expand icons, and per-element TextStyles',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
Expanded(
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',
),
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 7: Custom builders & interaction callbacks ──
class _BuildersDemo extends StatelessWidget {
const _BuildersDemo();
static const _toml = '''
[server]
host = "localhost"
port = 8080
debug = true
max_connections = 1000
timeout = 30.5
[paths]
home = "/home/user"
config = "/etc/app/config.toml"
''';
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Custom builders & tap interactions',
style: Theme.of(context).textTheme.labelLarge,
),
Text(
'valueBuilder renders booleans as icons and numbers with a tinted background; tap any value for a SnackBar',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
Expanded(
child: TomlView(
content: _toml,
config: TomlViewerConfig.of(context).copyWith(
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.toString(),
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.toString(),
style: TextStyle(
fontFamily: 'monospace',
color: Theme.of(context).colorScheme.primary,
),
),
);
}
return null; // default for everything else
},
onValueTap: (context, key, value, path) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Tapped $path = $value'),
duration: const Duration(seconds: 1),
),
);
},
onValueLongPress: (context, key, value, path) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Long-pressed $path'),
duration: const Duration(seconds: 1),
),
);
},
),
),
),
],
),
);
}
}