How Flow Control Implements Multi-Cursor Editing: A Deep Dive into the Zig Source Code

Flow Control implements multi-cursor editing through a three-layer architecture: the editor core manages arbitrary cursor objects (CurSel) in src/tui/editor.zig, the renderer detects terminal capabilities in src/renderer/vaxis/renderer.zig, and the terminal layer emits Vaxis-driven escape sequences to display hardware cursors.

Multi-cursor editing in Flow Control, the Zig-based terminal editor from neurocyte/flow, enables simultaneous text manipulation across multiple locations. This feature relies on sophisticated coordination between the editor's data model and the terminal's rendering capabilities to deliver hardware-accelerated cursor display where supported.

The Three-Layer Architecture of Multi-Cursor Editing in Flow Control

The implementation spans three tightly coupled layers that bridge user input with terminal hardware:

Layer Responsibility Key Implementation
Editor core Stores cursor objects and logic for adding, merging, and removing them src/tui/editor.zig – CurSel struct, cursels list, add_cursor_* helpers
Renderer capability detection Detects terminal support for true multi-cursor control src/renderer/vaxis/renderer.zig – cap_multi_cursor parsing
Terminal rendering Emits escape sequences to create, move, and clear extra cursors src/renderer/vaxis/renderer.zig – clear_all_multi_cursors, show_multi_cursor_yx

Layer 1: Editor Core and CurSel Management

The CurSel Data Structure

The foundation of multi-cursor editing in Flow Control rests on the CurSel struct defined in src/tui/editor.zig. The editor maintains a list of nullable CurSel objects (cursels: CurSel.List) where the first element represents the primary cursor and subsequent items represent secondary cursors.

// src/tui/editor.zig
pub const CurSel = struct {
    // cursor and selection state
    cursor: Cursor,
    selection: ?Selection,
    // ...
};

Adding Secondary Cursors

Flow Control exposes several public commands for creating secondary cursors, each implemented as a method on the Editor struct:

// src/tui/editor.zig
pub fn add_cursor_up(self: *Self, ctx: Context) Result { ... }          // line 3984
pub fn add_cursor_down(self: *Self, ctx: Context) Result { ... }        // line 4011
pub fn add_cursor_next_match(self: *Self, ctx: Context) Result { ... } // line 4024
pub fn add_cursor_all_matches(self: *Self, _: Context) Result { ... } // line 4048
pub fn add_cursors_to_line_ends(self: *Self, _: Context) Result { ... } // line 4084

These helpers ultimately invoke internal logic such as add_cursors_to_cursel_line_ends (line 4067), which duplicates a cursor for every line-end of the current selection.

Merging and Clearing Cursors

When secondary cursors overlap, collapse_cursors (line 1978) deduplicates them by merging selections or discarding exact duplicates. When the editor loses focus or the user disables the feature, clear_all_cursors (line 2170) empties the list, triggering the UI layer to request terminal cleanup via self.rdr_.clear_all_multi_cursors() (line 662 of src/tui/tui.zig).

Layer 2: Terminal Capability Detection

Before rendering secondary cursors, Flow Control verifies that the terminal supports hardware multi-cursor control. During renderer initialization in src/renderer/vaxis/renderer.zig, the code parses the terminal's capability list:

// src/renderer/vaxis/renderer.zig (lines 414-417)
.cap_multi_cursor => {
    self.logger.print("multi cursor capability detected", .{});
    self.vx.caps.multi_cursor = true;
}

Only when self.vx.caps.multi_cursor is true does the editor enable secondary-cursor rendering and invoke the display functions.

Layer 3: Rendering Secondary Cursors via Vaxis

Clearing Multi-Cursors

The renderer sends "clear all secondary cursors" escape sequences through the underlying Vaxis library:

// src/renderer/vaxis/renderer.zig (lines 586-588)
pub fn clear_all_multi_cursors(self: *Self) !void {
    try self.vx.resetAllTerminalSecondaryCursors(self.allocator);
}

Positioning Hardware Cursors

For each secondary cursor, the editor calls show_multi_cursor_yx(y, x), which forwards coordinates to Vaxis for translation into terminal-specific escape codes:

// src/renderer/vaxis/renderer.zig (lines 590-592)
pub fn show_multi_cursor_yx(self: *Self, y: c_int, x: c_int) !void {
    try self.vx.addTerminalSecondaryCursor(self.allocator, @intCast(y), @intCast(x));
}

In the UI code (src/tui/editor.zig), secondary cursors are drawn in soft-render mode when the terminal cannot draw hardware cursors; otherwise, render_term_cursor_secondary forwards the request:

// src/tui/editor.zig (lines 59-61)
inline fn render_term_cursor_secondary(self: *Self, pos: Cursor) void {
    const y, const x = self.plane.rel_yx_to_abs(@intCast(pos.row), @intCast(pos.col));
    tui.rdr().show_multi_cursor_yx(y, x) catch return;
}

Windows Support Status

The Win32 renderer currently contains only a placeholder stub at lines 500-504 of src/renderer/win32/renderer.zig, indicating that native Windows console multi-cursor support remains a work-in-progress.

Key Bindings and User Commands

Multi-cursor editing in Flow Control is exposed through specific commands mapped in src/keybind/parse_flow.zig. Users can bind these to keys in their configuration:

// src/keybind/parse_flow.zig (line 55 and surrounding)
"Ctrl-d" => command.add_cursor_next_match,   // Add cursor at next match
"Ctrl-Shift-d" => command.add_cursor_all_matches, // Add cursors to all matches
"Ctrl-Shift-l" => command.add_cursors_to_line_ends, // Add cursors to line ends

When enable_terminal_cursor is set to true in the configuration, Flow Control utilizes hardware cursors where possible:


# flow/config.toml

[editor]
enable_terminal_cursor = true      # required for hardware multi-cursors

Summary

  • Flow Control implements multi-cursor editing through a three-tier architecture separating data management from terminal rendering.
  • The editor core (src/tui/editor.zig) maintains a list of CurSel objects with methods like add_cursor_next_match and collapse_cursors to handle cursor lifecycle and deduplication.
  • Capability detection (src/renderer/vaxis/renderer.zig) checks for cap_multi_cursor before enabling hardware cursor features.
  • The rendering layer translates cursor coordinates into terminal escape sequences using Vaxis functions addTerminalSecondaryCursor and resetAllTerminalSecondaryCursors.
  • Windows support remains pending with a TODO stub in src/renderer/win32/renderer.zig.

Frequently Asked Questions

What is the CurSel struct in Flow Control?

The CurSel struct defined in src/tui/editor.zig is the fundamental data structure representing both a cursor position and an optional selection range. Flow Control stores these in a list called cursels where the first element serves as the primary cursor and subsequent elements function as secondary cursors for multi-cursor editing operations.

How does Flow Control detect if my terminal supports multi-cursor editing?

During initialization, the Vaxis renderer in src/renderer/vaxis/renderer.zig parses the terminal's capability string. When it encounters the cap_multi_cursor token, it sets self.vx.caps.multi_cursor = true. The editor only attempts to render hardware secondary cursors when this flag is enabled, falling back to software rendering otherwise.

Can I use multi-cursor editing on Windows?

Currently, multi-cursor editing on Windows is not fully implemented. The Win32 renderer at src/renderer/win32/renderer.zig contains only a TODO placeholder (lines 500-504) where the multi-cursor rendering logic will eventually reside. Windows users currently rely on the standard single-cursor mode until this stub is implemented.

What happens when two cursors overlap in Flow Control?

When secondary cursors land on the same position or selection, the editor invokes collapse_cursors (line 1978 in src/tui/editor.zig). This function deduplicates overlapping cursors by merging their selections using a.merge(b_sel) or discarding exact duplicates, ensuring that only one cursor remains at any given position to prevent conflicting edits.

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 →