Skip to content

Commit 7ca4351

Browse files
committed
chore: initial commit with project setup, examples, and docs
1 parent ccc7a28 commit 7ca4351

32 files changed

Lines changed: 11231 additions & 1 deletion

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# https://dart.dev/guides/libraries/private-files
2+
# Created by `dart pub`
3+
.dart_tool/
4+
5+
# Avoid committing pubspec.lock for library packages; see
6+
# https://dart.dev/guides/libraries/private-files#pubspeclock.
7+
pubspec.lock
8+
9+
# Coverage outputs (keep lcov.info tracked)
10+
coverage/*
11+
#!coverage/lcov.info

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## [0.1.0]
2+
3+
🎉 Initial public release.

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2025 Sascha
3+
Copyright (c) 2025 grumpypixel (Sascha Stojanov)
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
# flatconfig
2+
*A minimal Ghostty-style `key = value` configuration parser for Dart and Flutter.*
3+
4+
[![pub package](https://img.shields.io/pub/v/flatconfig.svg)](https://pub.dev/packages/flatconfig)
5+
[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6+
[![Dart Version](https://img.shields.io/badge/dart-%3E%3D3.0.0-blue.svg)](https://dart.dev)
7+
8+
flatconfig is a flat, minimal `key = value` configuration format for Dart and Flutter — easy to read, trivial to hand-edit, and simple to round-trip.
9+
Inspired by 👻 [Ghostty](https://ghostty.org)-style configuration files.
10+
11+
It provides a simple, predictable alternative to verbose formats like YAML or JSON
12+
for small, human-editable configuration files. Ideal for tools, CLIs, and Flutter apps
13+
that need structured settings without heavy dependencies.
14+
15+
---
16+
17+
## Highlights
18+
19+
- 🧩 **Tiny syntax:** `key = value` (values may be quoted)
20+
- 📦 **Pure Dart**, minimal dependencies (only `meta`)
21+
- 📝 **Supports duplicates**, preserves entry order
22+
- 🔐 **Strict or lenient parsing**, optional callbacks for invalid lines
23+
- 📁 **Async/sync file I/O**, handles UTF-8 BOM and any line endings
24+
- 🧠 **Typed accessors** for durations, bytes, colors, URIs, JSON, enums, ratios, percents, lists, sets, maps, and ranges
25+
- 🧱 **Collapse helpers** to deduplicate keys (first occurrence or last write)
26+
- 🧰 **Pretty-print and debug dumps**
27+
- 🔁 **Round-tripping** with configurable quoting and escaping
28+
29+
---
30+
31+
## Usage
32+
33+
Add `flatconfig` as a dependency to your `pubspec.yaml`:
34+
35+
```yaml
36+
dependencies:
37+
flatconfig: ^0.1.0
38+
```
39+
40+
Then import it in your Dart code:
41+
42+
```dart
43+
import 'package:flatconfig/flatconfig.dart';
44+
```
45+
46+
---
47+
48+
## Quick Start 🚀
49+
50+
```dart
51+
import 'package:flatconfig/flatconfig.dart';
52+
53+
void main() {
54+
const raw = '''
55+
# Example config
56+
background = 282c34
57+
keybind = ctrl+z=close_surface
58+
font-family =
59+
''';
60+
61+
final doc = FlatConfig.parse(raw);
62+
63+
print(doc['background']); // 282c34
64+
print(doc.valuesOf('keybind')); // [ctrl+z=close_surface]
65+
print(doc['font-family']); // null → explicit reset
66+
}
67+
```
68+
69+
---
70+
71+
## Data model
72+
73+
```dart
74+
// A single key/value pair (value may be null for explicit resets: "key =")
75+
class FlatEntry {
76+
final String key;
77+
final String? value;
78+
}
79+
80+
// A parsed document that preserves order and duplicates.
81+
class FlatDocument {
82+
final List<FlatEntry> entries;
83+
84+
// Frequently used:
85+
Map<String, String?> toMap(); // last value per key
86+
String? operator [](String key); // shorthand for latest[key]
87+
Iterable<String> get keys; // first occurrence order
88+
List<String?> valuesOf(String key);
89+
bool has(String key);
90+
bool hasNonNull(String key);
91+
}
92+
```
93+
94+
---
95+
96+
## Parsing
97+
98+
### Strings
99+
100+
```dart
101+
final doc = FlatConfig.parse(
102+
raw,
103+
options: const FlatParseOptions(
104+
strict: false, // throw on invalid lines if true
105+
commentPrefix: '#', // set '' to disable comments
106+
decodeEscapesInQuoted: false, // decode \" and \\ inside quotes
107+
),
108+
);
109+
```
110+
111+
- Lines starting with `commentPrefix` are ignored.
112+
- Unquoted values are trimmed; quoted values preserve whitespace and `=`.
113+
- Empty unquoted values → `null` (explicit reset).
114+
- Duplicate keys are preserved; the last one wins in `toMap()`.
115+
116+
### Files
117+
118+
```dart
119+
import 'dart:io';
120+
import 'package:flatconfig/flatconfig.dart';
121+
122+
final fromFile = await parseFlatFile('config.conf');
123+
124+
// Sync variant:
125+
final sync = File('config.conf').parseFlatSync();
126+
```
127+
128+
- Handles UTF-8 BOM
129+
- Supports `\n`, `\r\n`, and `\r` line endings
130+
- Works with async and sync file I/O
131+
132+
---
133+
134+
## Encoding & Round-Tripping
135+
136+
```dart
137+
final out = doc.encodeToString(
138+
options: const FlatEncodeOptions(
139+
quoteIfWhitespace: true, // quote values with outer spaces
140+
alwaysQuote: false, // force quotes on all non-null values
141+
escapeQuoted: false, // escape \" and \\ while encoding
142+
),
143+
);
144+
```
145+
146+
### Writing to Files
147+
148+
```dart
149+
await File('out.conf').writeFlat(doc);
150+
File('out.conf').writeFlatSync(doc);
151+
```
152+
153+
- Lossy by design: comments and blank lines are not preserved
154+
- `null` values are written as key `=`
155+
156+
---
157+
158+
## Duplicate Keys → Collapse
159+
160+
```dart
161+
final collapsedFirst = doc.collapse(); // keep first position, last value wins
162+
final collapsedLast = doc.collapse(order: CollapseOrder.lastWrite);
163+
164+
final keepMulti = doc.collapse(multiValueKeys: {'keybind'});
165+
final dynamicMulti = doc.collapse(isMultiValueKey: (k) => k.startsWith('mv_'));
166+
167+
final dropResets = doc.collapse(dropNulls: true); // omit keys with null
168+
```
169+
170+
---
171+
172+
## Typed Accessors (Examples)
173+
174+
```dart
175+
final b = doc.getBytes('size'); // SI (kB/MB/...) and IEC (KiB/MiB/...)
176+
final cc = doc.getColor('color'); // {a, r, g, b}
177+
final d = doc.getDuration('timeout'); // "150ms", "2s", "5m", "3h", "1d"
178+
final e = doc.getEnum('mode', {'prod': 1, 'dev': 2}); // case-insensitive
179+
final co = doc.getHexColor('color'); // #rgb, #rgba, #rrggbb, #aarrggbb → 0xAARRGGBB
180+
final j = doc.getJson('payload'); // parsed JSON object
181+
final p = doc.getPercent('alpha'); // "80%", "0.8", "80" → 0.8
182+
final r = doc.getRatio('video'); // "16:9" → 1.777...
183+
final u = doc.getUri('endpoint'); // relative or absolute URI
184+
185+
// Collections
186+
final list = doc.getList('features'); // "A, b , a" → ["A","b","a"]
187+
final set = doc.getSet('features'); // → {"a","b"} (case-insensitive)
188+
189+
// Ranges
190+
final dIn = doc.getDoubleInRange('gamma', min: 0.5, max: 2.0);
191+
final iIn = doc.getIntInRange('retries', min: 0, max: 10);
192+
193+
// Require* methods throw FormatException on missing/invalid values
194+
final sz = doc.requireBytes('size');
195+
final ms = doc.requireDuration('timeout');
196+
final col = doc.requireHexColor('color');
197+
final pct = doc.requirePercent('alpha');
198+
```
199+
200+
### Mini-Documents & Pairs
201+
202+
```dart
203+
// Single key=value inside a value
204+
final pair = doc.getKeyValue('keybind');
205+
// e.g. "ctrl+z=close_surface" → ('ctrl+z','close_surface')
206+
207+
// Mini-document in a single value
208+
final sub = doc.getDocument('db'); // "host=foo, port=5432"
209+
print(sub.toMap()); // {host: foo, port: 5432}
210+
211+
// List of mini-documents
212+
final servers = doc.getListOfDocuments('servers');
213+
// "host=foo,port=8080 | host=bar,port=9090" → List<FlatDocument>
214+
215+
// Host[:port]
216+
final hp = doc.getHostPort('listen'); // "[::1]:8080" → ('::1', 8080)
217+
```
218+
219+
### Other Convenience Methods
220+
221+
```dart
222+
doc.getTrimmed('name'); // trimmed value
223+
doc.getStringOr('title', 'Untitled'); // default fallback
224+
doc.isEnabled('feature_x'); // truthy/falsey strings
225+
doc.isOneOf('env', {'dev', 'prod'}); // case-insensitive
226+
doc.requireKeys(['host', 'port']); // throws on first missing key
227+
```
228+
229+
All `require*` methods throw a `FormatException` with context on invalid data.
230+
231+
---
232+
233+
## Debug & Pretty Print
234+
235+
```dart
236+
print(doc.debugDump());
237+
// [0] a = 1
238+
// [1] b = null
239+
// ...
240+
241+
print(doc.toPrettyString(
242+
includeIndexes: true,
243+
sortByKey: true,
244+
alignColumns: true,
245+
));
246+
```
247+
248+
---
249+
250+
## End-to-End Example
251+
252+
```dart
253+
import 'dart:io';
254+
import 'package:flatconfig/flatconfig.dart';
255+
256+
Future<void> main() async {
257+
final result = await parseFlatFile('config.conf');
258+
259+
final doc = result;
260+
final updated = FlatDocument([
261+
...doc.entries,
262+
const FlatEntry('note', ' keep whitespace '),
263+
]);
264+
265+
await File('out.conf').writeFlat(updated);
266+
}
267+
```
268+
269+
---
270+
271+
## Format Rules & Limits
272+
273+
- Only full-line comments (default prefix `#`)
274+
- Inline comments are not supported
275+
- Lines without `=` are ignored in non-strict mode
276+
- Unquoted values are trimmed; quoted values preserve whitespace and `=`
277+
- Empty unquoted values become `null` (explicit reset)
278+
- Encoding is lossy (comments and blank lines are dropped)
279+
280+
---
281+
282+
### See also
283+
284+
- 🧠 [Ghostty Configuration Format](https://ghostty.org/docs/config)
285+
- 🧰 [Dart Configuration File Libraries on pub.dev](https://pub.dev/packages?q=config)
286+
287+
---
288+
289+
## License
290+
291+
[MIT](LICENSE)
292+
293+
---
294+
295+
Made with ❤️ in Dart.
296+
Contributions welcome on [GitHub → grumpypixel/flatconfig](https://github.com/grumpypixel/flatconfig)

analysis_options.yaml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Lightweight, package-friendly lints for a Dart library.
2+
# Uses the official recommended ruleset and adds a few sensible tweaks.
3+
4+
include: package:lints/recommended.yaml
5+
6+
analyzer:
7+
language:
8+
strict-casts: true
9+
strict-inference: true
10+
strict-raw-types: true
11+
# errors:
12+
# prefer_expression_function_bodies: error
13+
exclude:
14+
- "**/*.g.dart"
15+
- "build/**"
16+
- ".dart_tool/**"
17+
- "coverage/**"
18+
# If your example/ is a full app and noisy in CI, exclude it.
19+
# Otherwise, feel free to remove the next line to analyze examples too.
20+
- "example/**"
21+
22+
linter:
23+
rules:
24+
# For libraries with a src/ layout, relative imports keep things simple.
25+
always_use_package_imports: false
26+
prefer_relative_imports: true
27+
28+
# Libraries benefit from public API docs.
29+
public_member_api_docs: true
30+
31+
# Pragmatic defaults (tune to taste):
32+
avoid_print: false # fine for examples/tests; adjust in apps
33+
combinators_ordering: true
34+
directives_ordering: true
35+
omit_local_variable_types: false # allow explicit local types when clearer
36+
prefer_expression_function_bodies: true
37+
prefer_final_locals: true # encourages immutability in functions
38+
sort_constructors_first: true
39+
sort_pub_dependencies: true
40+
sort_unnamed_constructors_first: true
41+
unnecessary_library_name: false

0 commit comments

Comments
 (0)