i3config

Pub Version Pub Points Pub Popularity Dart SDK License GitHub

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, dotted command heads, hex color value support, inline comments, and a virtual filesystem for testing.

Table of Contents

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 $variable references
  • 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
  • File Importsinclude with variable expansion, nesting, and circular detection
  • Pluggable FilesystemPhysicalFileSystem for production, VirtualFileSystem for tests
  • Error Reporting — configurable warnings for unresolved references with source spans
  • Async Support — handlers can be sync or async; the processor awaits them
  • Array Handling — built-in support for array operations via +=
  • Context Management — hierarchical variable and option scoping

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

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.

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 references
  • dotted_heads_colors_example.dart — dotted command heads and hex colors
  • i3conf_example.dart — basic state machine usage
  • file_imports_example.dart — file imports with virtual filesystem
  • formatter_example.dart — formatting config AST back to text
  • block_scoped_handlers_example.dart — block-scoped command handlers
  • command_value_extraction_example.dart — extracting values from commands

Documentation

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