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, string interpolation, block references, triple-quoted strings, dotted command heads, hex color value support, inline comments, variable middleware for extensible value interception, and a virtual filesystem for testing.
Table of Contents
- Installation
- Quick Start
- Key Features
- Language Features
- Built-in Handlers
- Custom Handlers
- Assignments and Arrays
- Error Handling
- Examples
- Documentation
- License
Installation
dependencies:
i3config: ^2.0.0
dart pub get
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.
For simple AST access without the state machine:
import 'package:i3config/i3config.dart';
void main() {
final config = Config.parse('''
set \$mod Mod4
bar {
status_command i3status
}
''');
for (final stmt in config.statements) {
print('${stmt.runtimeType}: $stmt');
}
}
Key Features
- State Machine — advanced processing pipeline with configurable states
- Handler System — extensible command and block handlers
- Scoped Commands — commands that only work within specific blocks
- Variable Expansion — dynamic variable resolution with scoping
- String Interpolation — double-quoted strings support
$variablereferences - Block References — reference block properties via dotted paths like
bar.main.position - Dotted Command Heads — commands with dotted names (
client.focused,client.background) parse as a single head - Hex Color Values —
#-prefixed hex colors parsed as bare arguments - Triple-Quoted Strings — multi-line literal strings with
"""..."""and'''...''' - File Imports —
includewith variable expansion, nesting, and circular detection - Pluggable Filesystem —
PhysicalFileSystemfor production,VirtualFileSystemfor tests - Error Reporting — configurable warnings for unresolved references with source spans
- Async Support — handlers can be sync or async; the processor awaits them
- Variable Middleware — intercept and transform variable set/get/expand operations with pluggable middleware
- Array Handling — built-in support for array operations via
+= - Context Management — hierarchical variable and option scoping
- Block Hierarchy — navigate parent/child relationships via
ConfigElement.parentandbuildBlockHierarchy() - Typed Variable Access —
getVariableAs<T>(),getString(),getList(),getBool()on Context - Block Registry Helpers —
getChildBlock(),getAllBlocks(),countBlock()for querying registered blocks - Public expandValue() — manually expand Value AST nodes through the context
Language Features
i3conf parses and processes the full i3/Sway config syntax, with several extensions for dynamic configuration.
String Interpolation
Double-quoted strings resolve $variable references. Single-quoted strings are literal.
set $theme dark
set $status "i3status -c $theme"
set $launcher "rofi -font 'Noto Sans $font_size'"
Block References
Reference properties from other blocks using dotted paths.
bar "main" {
status_command i3status
position top
}
set $bar_pos bar.main.position
set $bar_cmd bar.main.status_command
Omitting the identifier matches the first block of that type:
set $first_cmd bar.status_command
Triple-Quoted Strings
Multi-line literal strings delimited by """ or ''':
bindsym $mod+Return exec --no-startup-id """
kitty --class "terminal" \
-e "fish -l"
"""
Content is taken literally — no escape processing or variable interpolation. The formatter auto-switches delimiters when content contains """ or ''', or falls back to a single-quoted string if both are present.
Dotted Command Heads
Commands with dotted names parse as a single head:
client.focused #tabbed #4c7899
client.unfocused #tabbed #285577
client.urgent #tabbed #900000
Hex Color Values
#-prefixed hex colors are parsed as bare argument values:
set $bg #2e3440
set $fg #d8dee9
client.focused #tabbed #4c7899
Inline Comments
Trailing # comments after commands and assignments are preserved:
bindsym $mod+Return exec alacritty # launch terminal
set $mod Mod4 # set mod key
Assignments and Arrays
= assigns a scalar, += appends to an array:
order = "wireless wlan0"
order += "battery 0"
order += "clock"
File Imports with Variable Expansion
Include external config files during processing:
include "modules/bar.conf"
include "$config_dir/colors.conf"
include "~/.config/i3/workspaces.conf"
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 pass through for default property processing.
Custom Handlers
Custom Command Handler
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');
}
Block-Scoped Handlers
Block handlers register commands that only work inside a specific block:
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));
}
}
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.
Variable Middleware
Middleware intercepts variable set, get, and expand operations. Use it for redaction, transformation, validation, audit logging, or cache invalidation.
Context-Level Middleware
Register middleware on a specific context:
class SensitiveMiddleware implements VariableMiddleware {
final Set<String> _keys;
SensitiveMiddleware(this._keys);
@override
dynamic onSet(String name, dynamic value, Context context) => value;
@override
dynamic onGet(String name, dynamic? value, Context context) => value;
@override
String? onExpand(String text, Context context) {
for (final key in _keys) {
text = text.replaceAll('\$$key', '<SENSITIVE>');
}
return text;
}
}
final context = Context();
context.registerVariableMiddleware(SensitiveMiddleware({'password', 'token'}));
context.setVariable('password', 's3cret123');
print(context.expandVariables('login with $password')); // login with <SENSITIVE>
print(context.getVariable('password')); // s3cret123 (raw value preserved)
Middleware can also transform values on set/get, block access by returning null, or skip expansion by returning null from onExpand. Multiple middleware chain in registration order.
Processor-Level Middleware
Register middleware on the processor instead — it automatically propagates to the root context and all child block contexts created during processing:
final processor = ConfigProcessor();
// All variables will be uppercased across every context
processor.registerVariableMiddleware(UppercaseMiddleware());
await processor.processString('''
set \$name alice
block "scope" {
set \$role admin
}
''');
print(processor.context.getVariable('name')); // ALICE
print(processor.context.getVariable('role')); // ADMIN
Processor-level middleware runs before context-level middleware in the chain. Register middleware on the processor before calling process() or processString() to ensure it applies to all variable operations.
Block Lifecycle and Identifiers
Handlers can access the current block's identifier during processing:
class HostBlockHandler extends BaseBlockHandler {
@override
String get blockType => 'host';
@override
void handle(Block block, Context context) {
// currentBlockIdentifier set by processor before handle() is called
final hostname = context.currentBlockIdentifier;
print('Processing host: $hostname');
}
}
Block Hierarchy Navigation
AST nodes expose parent references and hierarchy helpers:
final config = Config.parse('''
host "web-01" {
set $addr "10.0.0.1"
}
''');
// Navigate from child to parent
final block = config.statements.first as Block;
final child = block.body.first;
print(child.parent == block); // true
// Build a full hierarchy map
final hierarchy = config.buildBlockHierarchy();
// hierarchy[block] contains parent -> children relationships
Assignments and Arrays
= and += produce 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 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(String message, Context context, {SourceSpan? span}) {
print('Error at ${span?.start.line ?? '?'}:${span?.start.column ?? '?'}: $message');
}
}
final processor = ConfigProcessor()..setErrorHandler(Logger());
await processor.processString('include "missing.conf"');
Enable warnings for unresolved references:
processor.context.reportUnresolvedVariables = true;
processor.context.reportUnresolvedBlockReferences = true;
Examples
Full runnable examples are in the example/ directory:
interpolation_and_block_ref_example.dart— string interpolation and block referencesdotted_heads_colors_example.dart— dotted command heads and hex colorsi3conf_example.dart— basic state machine usagefile_imports_example.dart— file imports with virtual filesystemformatter_example.dart— formatting config AST back to textblock_scoped_handlers_example.dart— block-scoped command handlerscommand_value_extraction_example.dart— extracting values from commandstriple_quoted_example.dart— multi-line triple-quoted strings
Documentation
- Language Guide — i3 config syntax and library usage
- Block Handlers — processing block types and scoped commands
- Command Handlers — processing individual commands, file imports
- Context and Scoping — variable management and inheritance
- Configuration Examples — real-world config to handler mapping
- Simple AST Iteration — using the parser without the state machine
License
MIT — see LICENSE.
Additional Resources
Libraries
- i3config
- A library for parsing and manipulating i3 window manager configuration files.
- i3config_v2
- i3config — PetitParser implementation with source position tracking