i3config 2.6.0 copy "i3config: ^2.6.0" to clipboard
i3config: ^2.6.0 copied to clipboard

Read i3 config files

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, 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 #

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
  • Triple-Quoted Strings — multi-line literal strings with """...""" and '''...'''
  • 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
  • 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.parent and buildBlockHierarchy()
  • Typed Variable AccessgetVariableAs<T>(), getString(), getList(), getBool() on Context
  • Block Registry HelpersgetChildBlock(), 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 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
  • triple_quoted_example.dart — multi-line triple-quoted strings

Documentation #

License #

MIT — see LICENSE.

Additional Resources #

1
likes
140
points
1.43k
downloads

Documentation

API reference

Publisher

verified publisherglenfordwilliams.com

Weekly Downloads

Read i3 config files

Repository (GitHub)
View/report issues

Topics

#i3config #config

Funding

Consider supporting this project:

www.buymeacoffee.com

License

MIT (license)

Dependencies

artisanal, petitparser, source_span

More

Packages that depend on i3config