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 ofCurSelobjects with methods likeadd_cursor_next_matchandcollapse_cursorsto handle cursor lifecycle and deduplication. - Capability detection (
src/renderer/vaxis/renderer.zig) checks forcap_multi_cursorbefore enabling hardware cursor features. - The rendering layer translates cursor coordinates into terminal escape sequences using Vaxis functions
addTerminalSecondaryCursorandresetAllTerminalSecondaryCursors. - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →