i3config 2.3.1
i3config: ^2.3.1 copied to clipboard
Read i3 config files
i3config #
A Dart library for parsing and processing i3/Sway configuration files. Includes a state machine processor with pluggable handlers, scoped contexts, variable expansion, file imports, and a virtual filesystem for testing.
Features #
Core #
- State machine processor –
ConfigProcessorroutes AST nodes through states and handlers - Pluggable handlers – register custom
CommandHandlerandBlockHandlerimplementations - Block-scoped commands – commands that only apply inside a specific block type
- File imports –
includewith variable expansion, nesting, and circular detection - Pluggable filesystem –
PhysicalFileSystemfor real I/O,VirtualFileSystemfor tests - Variable scoping – block-level context with parent inheritance
- Async handlers – handlers can be sync or async; the processor awaits them
AST #
- Type-safe sealed nodes:
Assignment,Block,Command,Comment - Source position tracking with contextual parse errors
- Built-in JSON serialization
Quick Start #
import 'package:i3config/i3config.dart';
Future<void> main() async {
final processor = ConfigProcessor();
await processor.processString('''
set \$mod Mod4
bindsym \$mod+Return exec i3-sensible-terminal
''');
print(processor.context.getVariable('mod')); // Mod4
}
Config.parse builds the AST. ConfigProcessor.process / processString run
the state machine and execute registered handlers.
How It Works #
Config text → Parser → AST → State Machine → Handlers → Context
- Parse –
Config.parse(text)produces an AST of statements - Process –
processor.process(config)routes each element through the state machine - Handle – registered handlers execute per command/block type
- Context – variables, options, and errors accumulate in the scoped context
Built-in Handlers #
ConfigProcessor auto-registers these handlers:
| Command | Handler | Effect |
|---|---|---|
set $var value |
SetCommandHandler |
Stores a variable in the current context |
include "path" |
IncludeHandler |
Reads, parses, and processes another config file |
Unhandled commands are passed through for default property processing.
Custom Command Handlers #
class BindsymHandler extends BaseCommandHandler<void> {
@override
String get commandName => 'bindsym';
@override
void handle(Command command, Context context) {
final key = command.getArgAsString(0, context);
final action = command.getArgAsString(1, context);
context.setVariable('binding_$key', action);
}
}
Future<void> main() async {
final processor = ConfigProcessor()
..registerCommandHandler(BindsymHandler());
await processor.processString('bindsym \$mod+Return exec alacritty');
}
Handler Resolution #
- Block-scoped command handlers (when inside a matching block)
- Global command handlers
- Default command processing
Block-Scoped Handlers #
Block handlers register commands that only work inside a specific block. They also create child contexts – variables set inside the block are local but parent variables remain readable.
class BarBlockHandler extends BaseBlockHandler {
@override
String get blockType => 'bar';
@override
void handle(Block block, Context context) {
print('Bar: ${getBlockIdentifier(block, context)}');
}
@override
void registerScopedCommands(BlockHandlerRegistry registry) {
registry.registerCommand('status_command', StatusHandler());
registry.registerCommand('position', PositionHandler());
}
}
class StatusHandler extends BaseCommandHandler<void> {
@override
String get commandName => 'status_command';
@override
void handle(Command command, Context context) {
context.setVariable('bar_status', command.getArgAsString(0, context));
}
}
class PositionHandler extends BaseCommandHandler<void> {
@override
String get commandName => 'position';
@override
void handle(Command command, Context context) {
context.setVariable('bar_position', command.getArgAsString(0, context));
}
}
Future<void> main() async {
final processor = ConfigProcessor()
..registerBlockHandler(BarBlockHandler());
await processor.processString('''
bar "top" {
status_command i3status
position top
}
''');
}
Inside a bar block, status_command and position resolve through
bar-scoped handlers. Outside, those handlers are inactive.
File Imports #
The built-in IncludeHandler reads and processes external config files
during state machine execution.
await processor.processString('''
set \$config_dir ~/.config/i3
include "\$config_dir/modules/bar.conf"
include "\$config_dir/modules/colors.conf"
''');
Supports:
- Relative and absolute paths
- Variable expansion (
$var/${var}) ~home-directory expansion- Nested includes
- Circular include detection
Pluggable Filesystem #
The IncludeHandler reads files through a FileSystem interface rather than
dart:io directly, so you can swap implementations:
| Implementation | When to Use |
|---|---|
PhysicalFileSystem |
Production (default) |
VirtualFileSystem |
Tests (in-memory) |
import 'package:i3config/i3config.dart';
import 'package:i3config/src/v2/test_vfs.dart';
void main() async {
final vfs = VirtualFileSystem();
vfs.createFile('colors.conf', 'set \$bg "#2e3440"');
final processor = ConfigProcessor(fileSystem: vfs);
await processor.processString('include "colors.conf"');
print(processor.context.getVariable('bg')); // #2e3440
}
The VirtualFileSystem lives in src/v2/test_vfs.dart and is available
in published releases for your own tests.
Assignments and Arrays #
V2 represents = and += as Assignment nodes. Direct assignment produces a
scalar; append assignment builds an array.
await processor.processString('''
order = "wireless wlan0"
order += "battery 0"
order += "clock"
''');
print(processor.context.getVariable('order'));
// [wireless wlan0, battery 0, clock]
Use Config.parse directly to inspect the AST without processing:
final config = Config.parse('order += "wireless"');
for (final a in config.statements.whereType<Assignment>()) {
print('${a.variable} ${a.operator} ${a.values}');
}
Error Handling #
Parse errors throw from Config.parse. Processing errors flow through the
error handler.
class Logger implements ErrorHandler {
@override
void handleError(dynamic error, Context context) {
print('Error: $error');
}
}
final processor = ConfigProcessor()..setErrorHandler(Logger());
await processor.processString('include "missing.conf"');
Installation #
dependencies:
i3config: ^2.0.0
dart pub get
Documentation #
- V2 Guide – state machine architecture, handlers, scoping, filesystem
- Migration Guide – upgrading from V1 to V2
- Examples – runnable Dart example files
V1 (Legacy) #
V1 is available at package:i3config/i3config_v1.dart for legacy
compatibility. All new projects should use V2.
License #
MIT – see LICENSE.