toml_viewer 2.0.0 copy "toml_viewer: ^2.0.0" to clipboard
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 [...]

example/lib/main.dart

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'),
        ),
      ),
    );
  }
}
5
likes
150
points
93
downloads
screenshot

Documentation

API reference

Publisher

verified publishersudhi.in

Weekly Downloads

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 extensible builders.

Repository (GitHub)
View/report issues
Contributing

Topics

#toml #viewer #config #tree-view #json-viewer

Funding

Consider supporting this project:

github.com

License

MIT (license)

Dependencies

flutter, toml

More

Packages that depend on toml_viewer