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 [...]
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.
Every frame above is rendered by the real widget — see tool/make_gif.sh.
Contents #
- New here? Start with this
- Why you might want it
- Install
- Your first viewer
- Where the data comes from
- Features
- How it works
- Anatomy of a row
- Configuration reference
- Working with the parsed tree
- Public API
- FAQ and troubleshooting
- Upgrading from 1.x
- Requirements
- Contributing
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.
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.
TomlViewscrolls, so put it in aScaffoldbody, anExpanded, or aSizedBox— not directly inside an unboundedColumn. To embed it in a scroll view you already have, passshrinkWrap: 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 #
Search #
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.
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.
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 |
|---|---|---|
rootKeyStyle … arrayExpandableStyle |
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.0—Color.withValuesandColor.r/g/bare 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 S — GitHub · support@sudhi.in
