How to Configure File Type Detection and Associated Settings in Flow Control

Developers configure file type detection in Flow Control by placing <type>.conf files in ~/.config/flow/file_type/ to override built-in defaults, while the editor automatically detects types via file_type_config.guess_file_type using file extensions and shebang heuristics.

Flow Control (neurocyte/flow) implements a data-driven file-type system that determines how files are interpreted for syntax highlighting, language-server integration, and formatting. Understanding how to configure file type detection allows developers to customize editor behavior for any language or file format.

Architecture Overview

The file-type system consists of several coordinated components:

Component Purpose Source File
file_type_config.zig Core API for loading, caching, guessing, and exposing file-type objects src/file_type_config.zig
file_type_lsp.zig Built-in language-server and formatter bindings src/file_type_lsp.zig
editor.zig Detects and assigns file types when opening buffers src/editor.zig
File Type Palette UI overlay for browsing and switching types src/tui/mode/overlay/file_type_palette.zig

How Automatic File Type Detection Works

Flow Control uses a two-stage detection strategy implemented in src/file_type_config.zig.

First-Line Heuristics

The system first examines the file's content for shebangs or language markers via guess_first_line:

fn guess(file_path: ?[]const u8, content: []const u8) ?@This() {
    if (guess_first_line(content)) |ft| return ft;
    // ... extension matching
}

This detects executable scripts regardless of file extension.

Extension-Based Matching

If first-line detection fails, Flow iterates through all registered file types and matches against extensions:

for (get_all_names()) |file_type_name| {
    const file_type = get(file_type_name) catch unreachable orelse unreachable;
    if (file_path) |fp|
        if (syntax.FileType.match_file_type(file_type.extensions orelse continue, fp))
            return file_type;
}

The match_file_type function in the syntax module compares the file path against the type's extension list.

Configuring File Types via User Overrides

Developers can override or extend any file type by creating configuration files in the user config directory.

Configuration File Location

Place files at:

~/.config/flow/file_type/<type>.conf

For example, rust.conf or python.conf.

Configuration Format

The .conf files use a simple key = value syntax with bracket notation for arrays:


# ~/.config/flow/file_type/rust.conf

name = "rust"
description = "Rust"
icon = "🦀"
color = 0xDEA584
language_server = ["rust-analyzer"]
formatter = ["rustfmt"]

Available fields include:

  • name: Identifier for the file type
  • description: Human-readable label
  • icon: Unicode character or emoji for UI display
  • color: Hex color code for theming
  • language_server: Array of command arguments for LSP startup
  • formatter: Array of command arguments for formatting

Loading Mechanism

When file_type_config.get is called, it checks for user configurations before falling back to built-ins:

const file_name = try get_config_file_path(cache_allocator, file_type_name);
defer cache_allocator.free(file_name);

const file: ?std.fs.File = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch null;
if (file) |f| {
    // Parse user-provided *.conf file
    // ...
} else {
    // Fall back to built-in definition
    break :file_type if (syntax.FileType.get_by_name_static(file_type_name))
        |ft| from_file_type(ft) else null;
}

Default LSP and Formatter Bindings

Built-in language server and formatter configurations reside in src/file_type_lsp.zig.

Static Defaults

The file defines defaults for common languages:

pub const python = .{
    .language_server = .{ "uvx", "ty", "server" },
    .formatter = .{ "ruff", "format", "-" },
};

These are collected at compile time:

const static_file_type_lsp_defaults_list = load_file_type_lsp_defaults(@import("file_type_lsp.zig"));
const static_file_type_lsp_defaults = std.StaticStringMap(LspDefaults)
    .initComptime(static_file_type_lsp_defaults_list);

Merging with User Configuration

When creating a file type configuration, from_file_type merges built-in LSP defaults with syntax definitions:

// In from_file_type (lines 25-40 of file_type_config.zig)
const lsp_defaults = file_type_lsp.static_file_type_lsp_defaults.get(file_type.name) orelse .{};

return .{
    .name = file_type.name,
    .language_server = lsp_defaults.language_server,
    .formatter = lsp_defaults.formatter,
    // ... other fields
};

User overrides in .conf files take precedence over these defaults.

Programmatic Access to File Types

Developers can interact with the file-type system programmatically in Zig.

Resolving a File Type by Name

const ftc = @import("file_type_config");

// Resolve a user-provided name
if (ftc.get("python")) |maybe_ft| {
    const ft = maybe_ft orelse return error.UnknownFileType;
    std.debug.print("LSP: {s}\n", .{ft.language_server.?});
    std.debug.print("Formatter: {s}\n", .{ft.formatter.?});
}

Generating Default Configuration

To obtain a template for editing:

pub fn get_default(allocator: std.mem.Allocator, file_type_name: []const u8) ![]const u8 {
    const file_type = syntax.FileType.get_by_name_static(file_type_name) orelse return error.UnknownFileType;
    const config = from_file_type(file_type);
    var content: std.Io.Writer.Allocating = .init(allocator);
    defer content.deinit();
    root.write_config_to_writer(@This(), config, &content.writer) catch {};
    return content.toOwnedSlice();
}

This generates the text representation used when creating new .conf files.

Opening Buffers with Explicit Types

When opening a file, the editor accepts an optional file type override:

// In src/editor.zig (lines 735-762)
editor.open_buffer("/tmp/example", buffer, .{ .file_type = "markdown" });

If no type is specified, Flow calls file_type_config.guess_file_type to detect based on path and content.

Summary

  • Automatic detection relies on file_type_config.guess_file_type, which checks shebangs first, then file extensions against built-in definitions.
  • User customization happens via ~/.config/flow/file_type/<type>.conf files using simple key = value syntax, overriding built-in LSP and formatter settings.
  • Default bindings for language servers and formatters are defined in src/file_type_lsp.zig and merged with user configs at runtime.
  • Programmatic access is available through file_type_config.get for resolution and file_type_config.guess_file_type for detection.
  • UI integration includes the :change_file_type command and palette for interactive selection, with icons and colors rendered via src/tui/status/tabs.zig.

Frequently Asked Questions

How does Flow Control detect file types for files without extensions?

Flow Control uses first-line heuristics via guess_first_line in src/file_type_config.zig. This function examines the file's content for shebangs (e.g., #!/usr/bin/env python) or language-specific markers, allowing it to identify executable scripts and configuration files even when they lack standard extensions.

Where should I place custom file type configuration files?

Place custom configurations in ~/.config/flow/file_type/<type>.conf, where <type> is the identifier of the file type you want to override or create. Flow automatically loads these files at runtime, merging your custom language_server, formatter, icon, and color settings with the built-in defaults defined in src/file_type_lsp.zig.

Can I override the language server for a specific file type?

Yes. Create or edit a .conf file for the target type and specify the language_server array. For example, to use a different Python language server, create ~/.config/flow/file_type/python.conf with language_server = ["pylsp"]. This overrides the default ["uvx", "ty", "server"] defined in src/file_type_lsp.zig.

How do I force a specific file type when opening a file?

Use the :change_file_type command (bound to Ctrl-t by default) to open the file-type palette and select the desired type interactively. Programmatically, you can pass an explicit file_type option to editor.open_buffer in src/editor.zig, which bypasses automatic detection and assigns the specified type directly to the buffer.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →