SyncText is a lightweight collaborative editor that discovers peers via a shared-memory registry and exchanges edits over POSIX message queues. It maintains convergence using a pragmatic CRDT-style merge with Last-Writer-Wins arbitration.
Requirements (Linux):
- g++ (C++17)
- POSIX APIs available (shm, mq, stat)
makeRun the comprehensive test suite:
./run_all_tests.shThis runs all 8 major tests:
- Multi-line paste (13 lines)
- Mass insert (50 lines)
- Delete-all operation
- Non-conflicting edits
- Conflicting edits (LWW)
- Structural line insert
- Structural line delete
Individual tests are in the tests/ directory.
The program uses inotify for near-instant change detection (typ. <1s) and falls back to ~1s polling if inotify isn't available.
Tuning idle behavior:
- Spinner is OFF by default. Enable with
SYNCTEXT_SPINNER=1for a 1s idle redraw heartbeat. SYNCTEXT_IDLE_REFRESH_SEC=10adjusts the idle poll timeout (used when spinner is off and/or as a fallback). Larger values reduce wakeups.SYNCTEXT_PERIODIC_MTIME=1enables a periodic file mtime check on each idle timeout even when inotify is available (off by default). This can help on unusual setups where file writes don’t emit inotify events.
If your terminal looks odd after an abrupt kill (e.g., via timeout), restore it with:
stty sane
# or
resetQuick start (no hassle):
./build/editor user1The editor can be launched with the following arguments:
./build/editor <user_id> [merge_batch_n]<user_id>(required): A unique identifier for the user session.[merge_batch_n](optional): The number of incoming updates to accumulate before applying them as a single batch.- Default:
1(immediate merge). - Range:
1to1000. - Example:
./build/editor user1 5will wait for 5 updates before merging. This can improve performance during high-volume edits by reducing the frequency of file writes.
- Default:
If it's your first time (no binary yet), build then run:
make editor && ./build/editor user1When first run, your local file <user_id>_doc.txt is created with the initial content. The program will try to auto-open it for you with this precedence:
SYNCTEXT_EDITORif set (e.g.,SYNCTEXT_EDITOR=gedit)$VISUAL, then$EDITOR- GUI editors (if
$DISPLAYis set):gnome-text-editor,gedit,xed,pluma,mousepad,leafpad,kate,code - Finally
xdg-open - Terminal fallback: open
nanoin a new terminal (x-terminal-emulator,gnome-terminal, orxterm)
Set SYNCTEXT_NO_AUTO_OPEN=1 to disable auto-open entirely.
Notes:
- A shared registry is created at
/synctext_registry(POSIX shared memory). - Each user publishes a POSIX message queue name (e.g.,
/synctext_user_user1) in the registry. - Up to 5 concurrent users are supported.
- Active users list uses heartbeats and process liveness; users are considered inactive if no heartbeat for ~10s or their process has exited.
Duplicate session handling:
- If you start with an already-active
<user_id>in a terminal, an interactive prompt is shown by default: "Press f to force reclaim, q to quit, any other key to retry". Disable withSYNCTEXT_INTERACTIVE_LOGIN=0. - Non-interactive force reclaim: set
SYNCTEXT_FORCE_RECLAIM=1to reclaim. The existing process for that user will receive SIGTERM and should close; you can tune the grace period withSYNCTEXT_RECLAIM_GRACE_MS(default 1500ms). If it doesn’t exit in time and you also setSYNCTEXT_FORCE_KILL=1, it will be SIGKILLed.
Tip: Environment variables must not have spaces around =. Example:
SYNCTEXT_FORCE_RECLAIM=1 ./build/editor user1 # correct
SYNCTEXT_FORCE_RECLAIM = 1 ./build/editor user1 # incorrect on bashTo remove build artifacts:
make cleanThe shared memory object /synctext_registry is not unlinked automatically unless all users exit and the last process decides to cleanup. You can manually remove it if necessary:
# Optional manual cleanup (requires no active users)
# sudo rm /dev/shm/synctext_registry # Typically where shm objects are backed- Start program with
./build/editor <user_id> [merge_batch_n] - User registration & discovery via shared memory registry (up to 5 concurrent users)
- Per-user local document
<user_id>_doc.txtwith initial content - Continuous monitoring with automatic diff detection
- Real-time terminal display showing current document, active users, and change summaries
- Update objects created for each detected change: type, line, column range, old/new payloads, timestamp, and user id
Inline (same line):
- Insert, Delete, Replace
Structural (line-level):
- LineInsert, LineDelete
Block (multi-line batches):
- BlockInsert, BlockDelete
Application order: structural deletes (descending) → structural inserts (ascending) → inline ops rebased on the structural result. Conflicts resolve with Last-Writer-Wins (nanosecond timestamp; ties broken by user id). Same-user adjacency is allowed (no self-conflict).
Block operations are disabled by default for maximum compatibility and stability. Enable cautiously (see Environment).
- Remote-only arbitration: only remote updates participate in conflict resolution to avoid self-dupes.
- Last-Writer-Wins with user-id tiebreaker.
- Per-line local timestamps drop stale remote updates and prevent “resurrects”.
- Coarse stale-remote suppression: remote inserts older than your last local delete epoch are dropped.
- Deferred-merge write with grace window to avoid racing active editors; post-merge echo suppression window avoids re-diffing our own writes.
- Broadcast chunking and background draining to deliver long sequences without storms.
Stability/latency:
SYNCTEXT_DEBOUNCE_MS(default 300)SYNCTEXT_SETTLE_MS(default 150)SYNCTEXT_SETTLE_OVERALL_MS(default 1500)SYNCTEXT_MASS_DIFF_RECHECK_MS(default 180) — recheck suspicious “mass change” bursts
Broadcasting and queues:
SYNCTEXT_BROADCAST_BATCH_N(default 5)SYNCTEXT_MAX_BROADCAST_PER_CYCLE(default 128)SYNCTEXT_SEND_RETRY_COUNT(default 20)SYNCTEXT_SEND_RETRY_DELAY_MS(default 2)SYNCTEXT_MQ_MAXMSG(default 256)
Block operations:
SYNCTEXT_BLOCK_OPS_MODE(default 0): 0=never, 1=auto(threshold), 2=alwaysSYNCTEXT_BLOCK_OPS_MIN_LINES(default 6): minimum lines to trigger block ops in auto
Debugging and status:
SYNCTEXT_DEBUG_LEVEL(0-3; default 0): 0=off, 1=basic, 2=verbose, 3=traceSYNCTEXT_DEBUG_MSG=1(legacy): maps toSYNCTEXT_DEBUG_LEVEL=2if level not setSYNCTEXT_STATUS=1: print a one-line startup banner of key runtime settings
UI and ergonomics:
SYNCTEXT_SPINNER=1show idle spinnerSYNCTEXT_IDLE_REFRESH_SEC(default 5)SYNCTEXT_PERIODIC_MTIME=1force periodic mtime checksSYNCTEXT_HOTKEYS=0|1enable/disable hotkeys (auto-enabled on TTY)SYNCTEXT_NO_AUTO_OPEN=1disable auto-opening the editorSYNCTEXT_EDITORpreferred editor (fallback to $VISUAL, $EDITOR, then GUI/CLI list)
Login/session safety:
SYNCTEXT_INTERACTIVE_LOGIN=0|1(default 1 if TTY)SYNCTEXT_FORCE_RECLAIM=1andSYNCTEXT_RECLAIM_GRACE_MS(default 1500)SYNCTEXT_FORCE_KILL=1if the old process won’t exit
All peers should run the same binary, protocol version, and configuration:
- Build the latest once and share the
./build/editorbinary across peers, or rebuild on each machine from the same commit. - Keep
SYNCTEXT_BLOCK_OPS_MODEaligned across all users. Default is 0 (disabled) — safest. - If you enable block ops (1 or 2), ensure every peer also enables them to avoid mismatched operation types.
- Use
SYNCTEXT_STATUS=1to print a startup banner that helps verify alignment quickly.
You don’t need to type multiple commands every time. From the project root:
make editor && ./build/editor user1This compiles if needed and starts the editor in one line. To always run with status banner and verbose logs:
SYNCTEXT_STATUS=1 SYNCTEXT_DEBUG_LEVEL=2 ./build/editor user1