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 [...]

TOML Viewer

Turn a TOML configuration file into a tree your users can actually read.
Expandable, searchable, copyable — and fast enough for files with thousands of keys.

pub package pub points Flutter >=3.27 License: MIT platforms

Expanding a TOML tree, switching to dark mode, searching for a key, and viewing the raw source

Every frame above is rendered by the real widget — see tool/make_gif.sh.


Contents #


New here? Start with this #

TOML is a plain-text format for configuration files — the settings an app reads when it starts. It's designed to be easy for a person to write, and you'll find it in pubspec-adjacent tooling, Rust's Cargo.toml, Python's pyproject.toml, and countless server configs.

Reading one inside an app is another matter. A settings screen, an admin panel, or a debug tool that dumps raw text makes the user scroll through punctuation hunting for one value.

This package renders that same file as a tree. Sections become branches you open and close, values are colour-coded by type, and there's a search box.

Left: a raw config.toml file as plain text. Right: the same file rendered as an interactive tree with coloured keys and values.

It's one widget. You give it TOML; it gives you the view on the right.

Why you might want it #

🌲 Browsable tree Tables and arrays expand and collapse; values are coloured by type.
Fast on big files Only the rows on screen are built. 5,000 keys costs about the same as 50.
🔍 Search Filter by key or value; ancestors of a match open automatically and hits are highlighted.
📄 Raw source view Flip to the original text with line numbers and errors highlighted.
🩹 Survives broken files A malformed section is annotated in place — the valid ones still render.
🌗 Light and dark Follows the ambient Theme, or set a palette explicitly.
🎨 Yours to restyle Every colour, text style, icon, and spacing value is a property; three builders let you replace the widgets outright.
📋 Copy and select Long-press to copy a value or its path; the whole tree is selectable text.
Accessible Each row is one labelled semantics node that announces its expanded state.
📦 No extra baggage One dependency (toml). Works on all six Flutter platforms.

Install #

dependencies:
  toml_viewer: ^2.0.0
flutter pub add toml_viewer

Your first viewer (30 seconds) #

import 'package:flutter/material.dart';
import 'package:toml_viewer/toml_viewer.dart';

class ConfigScreen extends StatelessWidget {
  const ConfigScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Configuration')),
      body: const TomlView(
        content: '''
[server]
host = "localhost"
port = 8080

[server.tls]
enabled = true
''',
      ),
    );
  }
}

That's the whole integration. Colours already follow your app's light or dark theme, and rows are already expandable.

Give it a bounded height. TomlView scrolls, so put it in a Scaffold body, an Expanded, or a SizedBox — not directly inside an unbounded Column. To embed it in a scroll view you already have, pass shrinkWrap: true.

Where the data comes from #

Five constructors, one for each place TOML tends to live.

// 1. A string you already have.
TomlView(content: tomlText)

// 2. A Flutter asset declared in pubspec.yaml.
TomlView.asset('assets/config.toml')

// 3. A Map you parsed yourself — or any Map at all, TOML or not.
TomlView.fromMap({'title': 'My App', 'debug': true})

// 4. Anything async: a file, an HTTP response, a database row.
TomlView.loader(() => File('/etc/app/config.toml').readAsString())
TomlView.loader(() async => (await http.get(uri)).body)

// 5. A stream — re-parses and re-renders on every emission.
TomlView.stream(configFileWatcher)
Why is there no TomlView.file() or .network()?

Because both would cost you something. A File parameter means importing dart:io, which does not compile for web — a platform this package supports. A .network() constructor means depending on an HTTP client, which every consumer would then inherit whether they use it or not.

TomlView.loader covers both in one line without either cost, and it works for sources nobody anticipated.

Features #

TomlView(
  content: toml,
  searchQuery: _searchController.text,
  emptyBuilder: (_) => const Center(child: Text('No matches')),
)

Matching keys and values are highlighted. Every ancestor of a match is kept and force-expanded, so a hit five levels down is reachable without hunting for it. Pass caseSensitiveSearch: true for exact matching.

Wire it to a TextField and rebuild on change — that's all the demo does.

Raw source view #

TomlView(content: toml, mode: TomlViewMode.source)

The original text, with a line-number gutter and every error line tinted. Handy when a user reports "my config isn't working" and you want to show them exactly which line the parser rejected.

TomlSourceView is exported too, if you want it outside a TomlView.

Expand and collapse #

Tap any row with a chevron. For programmatic control, pass a controller:

final controller = TomlExpandController(defaultExpanded: false);

TomlView(content: toml, expandController: controller);

controller.expandAll();
controller.collapseAll();
controller.expandToPath('server.tls');        // opens server, then server.tls
controller.expandToPath('routes[1].method');  // understands array indices

// Persist what the user opened, and put it back next time:
final saved = controller.overrides;
controller.restore(saved);

Theming #

// Follows the ambient Theme's brightness:
TomlView(content: toml)

// Or choose explicitly:
TomlView(content: toml, config: const TomlViewerConfig.dark())

Set it once for a whole subtree with an InheritedWidget:

TomlViewerTheme(
  config: const TomlViewerConfig.dark(
    style: TomlViewerStyle(indentation: 20),
  ),
  child: TomlView(content: toml),
)

A TomlView resolves its config in this order: its own config argument → the nearest TomlViewerTheme → a palette matching the ambient Theme.

Custom colours live in one object:

TomlViewerConfig(
  colors: TomlViewerColors.dark.copyWith(
    value: Colors.amber,
    tableKey: Colors.cyan,
  ),
)

Styling #

TomlView(
  content: toml,
  config: TomlViewerConfig.of(context).copyWith(
    style: const TomlViewerStyle(
      rootKeyStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
      indentation: 24,
      separator: ' : ',
      collapsedIcon: Icons.add_circle_outline,
      expandedIcon: Icons.remove_circle_outline,
    ),
  ),
)

There's a monospaced preset, and a font stack that actually resolves to a monospaced face on every platform (plain 'monospace' doesn't, on iOS, macOS, or the web):

config.copyWith(style: TomlViewerStyle.monospace)

const TextStyle(
  fontFamily: 'monospace',
  fontFamilyFallback: TomlViewerStyle.monospaceFallback,
)

copyWith only ever sets values — passing null means "leave unchanged". To remove one, use clear:

// Hide the expand icons, and let long values wrap instead of ellipsising:
style.clear(collapsedIcon: true, expandedIcon: true, maxValueLines: true)

Custom rendering #

Three builders, each replacing a different amount. Return null from any of them to fall back to the default.

TomlViewerConfig(
  // Just the value:
  valueBuilder: (context, value, path) =>
      value is bool ? Icon(value ? Icons.check : Icons.close) : null,

  // Just the key:
  keyBuilder: (context, key, path, isRoot) => Text(key.toUpperCase()),

  // The entire row, expand icon and all:
  rowBuilder: (context, key, value, path, isExpanded, onToggle) => null,
)

Interaction and clipboard #

TomlViewerConfig(
  copyMode: TomlCopyMode.pathAndValue, // long-press copies "server.port = 8080"
  onValueTap: (context, key, value, path) => print('$path = $value'),
  onValueCopied: (context, key, copied, path) =>
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Copied $copied')),
      ),
)

TomlCopyMode is value (the default), path, pathAndValue, or none. The tree is also wrapped in a SelectionArea, so ordinary text selection works — turn that off with enableTextSelection: false.

Error handling #

A broken TOML file doesn't produce an empty screen. The parser retries section by section, so the valid parts still render and the broken ones get an annotation showing the offending line.

TomlView(
  content: brokenToml,
  config: TomlViewerConfig(
    showInlineErrors: true,   // the banner and inline annotations
    maxRenderedErrors: 50,    // cap the widgets a pathological file can create
    onParseErrors: (errors) {
      for (final e in errors) {
        debugPrint('Line ${e.line}:${e.column} — ${e.message}');
      }
    },
  ),
)

Duplicate keys — which TOML forbids — are reported rather than silently resolved.

How it works #

Worth understanding if you're rendering large files, or wondering why there's a node model in the public API.

Pipeline: TOML text, TomlParser, TomlNode tree, flattenVisible, SliverList.builder. Everything from TomlNode onwards is lazy.

The important part is that the widget never builds a nested widget tree. It flattens the document into the list of rows that are currently visible and hands that to a SliverList.builder. Three consequences:

Collapsed branches are free Their children are never even constructed.
Off-screen rows are free A 5,000-key document builds fewer than 100 row widgets.
Toggling is cheap One expand rebuilds the list once, not every visible row.

Parsing over 50 KB is moved to a background isolate automatically, so a large file doesn't jank the frame it loads on.

These aren't claims in a README — they're assertions in test/toml_lazy_rendering_test.dart.

Anatomy of a row #

Where to look when you want to change how something appears.

A single row labelled with the configuration property controlling each part: indentation, expand icon, key colour and style, separator, separator gap, type label, and rowBuilder for the whole row.

Configuration reference #

TomlViewerConfig #

Property Default Description
expandMode true Whether nodes start expanded
colors TomlViewerColors.light The colour palette
style TomlViewerStyle.fallback Text and layout styling
showInlineErrors true Show the error banner and annotations
errorMessageStyle null Override for error text
maxRenderedErrors 100 Cap on rendered error widgets
onParseErrors null Called with every parse error
keyBuilder null Custom key widget; return null for the default
valueBuilder null Custom value widget; return null for the default
rowBuilder null Custom whole-row widget; return null for the default
onValueTap null Tap on a primitive value
onValueLongPress null Long-press on a primitive value
copyMode TomlCopyMode.value What a long-press copies
onValueCopied null Called with the text written to the clipboard
enableTextSelection true Wrap the tree in a SelectionArea

TomlViewerColors #

value · type · symbol · nonRootKey · rootKey · tableKey · arrayIndex · arrayExpandable · errorHighlight · errorText · searchHighlight · lineNumber

Presets: TomlViewerColors.light, .dark, .of(context).

TomlViewerStyle #

Property Default Description
rootKeyStylearrayExpandableStyle null Per-element text styles
searchMatchStyle null Style for the matched substring
indentation 14.0 Pixels per nesting level
rowSpacing 4.0 Space between rows
rowVerticalPadding 4.0 Padding inside a row
separator ' = ' Text between key and value
separatorGap 3.0 Gap after the separator
maxValueLines 1 Lines before ellipsis; null to wrap freely
maxValueLength 512 Characters before truncation; null to disable
collapsedIcon Icons.chevron_right null hides it
expandedIcon Icons.expand_more null hides it
expandIconSize 16.0 Icon size
expandIconColor null Falls back to colors.symbol

Presets: TomlViewerStyle.fallback, .monospace, and the .monospaceFallback font stack.

TomlView #

Beyond the source and config: mode, searchQuery, caseSensitiveSearch, expandController, padding, shrinkWrap, physics, scrollController, loadingBuilder, errorBuilder, emptyBuilder.

Working with the parsed tree #

The node model is public, so the parsed document is yours to walk — useful for exports, validation, or building a completely different UI on the same data.

final result = await TomlParser.parse(tomlText);
final root = TomlNode.root(result.data!);

void visit(TomlNode node) {
  print('${'  ' * node.depth}${node.path}: ${node.typeName}');
  for (final child in node.children) {
    visit(child);
  }
}
visit(root);

TomlNode is a sealed class — TomlLeafNode, TomlTableNode, and TomlArrayNode — so a switch over it is exhaustive:

final label = switch (node) {
  TomlLeafNode(:final value) => 'leaf: $value',
  TomlTableNode(:final table) => 'table of ${table.length}',
  TomlArrayNode(:final list) => 'array of ${list.length}',
};

The same flattening the widget uses is exported too:

final rows = flattenVisible(root, isExpanded: (_) => true, query: 'host');

Public API #

Export Description
TomlView The main widget: content, .asset(), .fromMap(), .loader(), .stream()
TomlViewMode tree or source
TomlSourceView Raw TOML with line numbers and error highlighting
TomlViewerConfig Behaviour, builders, callbacks, clipboard
TomlViewerColors The colour palette
TomlViewerStyle Text style, layout, and icons
TomlViewerTheme InheritedWidget providing a config to a subtree
TomlCopyMode What a long-press copies
TomlExpandController Expand/collapse ChangeNotifier
TomlNode, TomlLeafNode, TomlTableNode, TomlArrayNode The parsed tree
flattenVisible, TomlVisibleRow Tree-to-rows flattening, including search
TomlRow The row widget, if you build your own list
TomlFormat Type names, collection labels, value formatting
TomlParseResult, TomlParseError Parse results with partial-success support
TomlErrorLine The inline error annotation widget

FAQ and troubleshooting #

I get "Vertical viewport was given unbounded height".

TomlView scrolls, so it needs a bounded height. Wrap it in Expanded inside a Column, give it a SizedBox(height: …), or pass shrinkWrap: true to let it size itself when it's already inside another scroll view.

Long string values are cut off with an ellipsis.

That's maxValueLines, which defaults to 1 so one enormous value can't dominate the view. To wrap freely:

config.copyWith(style: config.style.clear(maxValueLines: true))

maxValueLength (default 512) separately truncates the text and appends a character count. Clear it the same way.

Can I show JSON or any other map?

Yes — TomlView.fromMap takes any Map<String, dynamic>, so a decoded JSON document renders identically. Only the labels ("Table") are TOML-flavoured.

collapseAll() doesn't collapse anything.

Fixed in 2.0.0. In 1.x it only affected nodes that had already been toggled, so a freshly rendered tree ignored it. Upgrade.

My arrays say "Array of Table[3]" for a list of numbers.

Also a 1.x bug, fixed in 2.0.0 — the element type was read from the list rather than its elements. You should now see Array of int[3].

Does it handle very large files?

Yes. Rendering is viewport-bound, and parsing over 50 KB moves to a background isolate. The practical limit is the parse itself, not the widget.

How do I persist which nodes the user had open?
final saved = controller.overrides;   // Map<String, bool>, ready for JSON
controller.restore(saved);
Can I use it without Material?

It needs the Material library imported (it uses InkWell and Icons), but it supplies its own transparent Material, so it works without a Scaffold or a Material ancestor — inside a plain Container, a dialog, or a custom shell.

Upgrading from 1.x #

The CHANGELOG has the full list. The three changes most likely to touch your code:

// 1. Colours moved into TomlViewerColors.
TomlViewerConfig(valueColor: Colors.red)                              // before
TomlViewerConfig(colors: TomlViewerColors(value: Colors.red))         // after

// 2. TomlViewerConfig.of takes only a context; chain copyWith for the rest.
TomlViewerConfig.of(context, expandMode: false)                       // before
TomlViewerConfig.of(context).copyWith(expandMode: false)              // after

// 3. TomlViewerTheme no longer takes a separate style.
TomlViewerTheme(config: c, style: s, child: v)                        // before
TomlViewerTheme(config: c.copyWith(style: s), child: v)               // after

The old colour getters (valueColor, keyColor, …) still work for reads; they are deprecated and will be removed in 3.0.0.

Try the example app #

git clone https://github.com/sudhi001/toml_viewer
cd toml_viewer/example
flutter run

Eight tabs, one per feature: search and source, asset loading, inline strings, maps, error handling, the expand controller, custom styling, and builders.

Requirements #

  • Dart SDK >=3.6.0 <4.0.0
  • Flutter >=3.27.0Color.withValues and Color.r/g/b are used throughout and arrived in that release
  • Platforms Android · iOS · Linux · macOS · Web · Windows
  • Dependencies just toml

Contributing #

Contributions are welcome — see CONTRIBUTING.md.

dart format .
flutter analyze --fatal-infos     # zero issues expected
flutter test                      # 141 tests
flutter test --tags golden        # 7 golden tests, generated on macOS

CI runs all of the above, plus the example app and a pub publish --dry-run. The demo GIF and the diagrams above are generated, not hand-drawn:

./tool/make_gif.sh                # renders frames from the widget, encodes the GIF
python3 tool/make_diagrams.py     # emits the light and dark SVG pairs

Bugs or requests #

Open an issue. Minimal reproductions are gratefully received.

License #

MIT — see LICENSE.

Author #

Sudhi SGitHub · support@sudhi.in

5
likes
150
points
91
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