A hex editor for macOS. Written in Modula-2, compiled and built with the m2c compiler toolchain.
There's a popular claim that Modula-2 is a terrible choice for AI-assisted development -- that coding agents can't work with it, that the tooling isn't there, that you'd be fighting the model every step of the way. This project exists in part to push back on that idea. Every module in hexed was written with an AI coding agent (Claude Code), from the byte store and undo system up through the SDL2 rendering, search engine, data inspector, and ObjC bridge for the macOS About dialog. The agent handled Modula-2's strict type system, its separation of definition and implementation modules, C FFI bindings, and the kind of low-level byte manipulation that a hex editor demands. It didn't struggle with the language. It worked with it.
The reality is that a well-structured language with clear semantics and strong compile-time checks is exactly what you want when an agent is writing code. The compiler catches mistakes immediately. The module system enforces boundaries. There's no ambiguity about what a procedure signature means. Modula-2 turns out to be a remarkably good fit for this workflow -- not in spite of being a strict, old-school systems language, but because of it.
Hexed displays files as hex, ASCII, and binary simultaneously, with a data inspector, byte histogram, search and replace, undo/redo, and mouse-driven selection. It renders through SDL2 with a dark theme and monospace layout. Files up to 64MB are loaded entirely into memory. Larger files use a paged backend that lazily loads 4KB pages through a 256-page LRU cache, so you can open multi-gigabyte files without waiting for the whole thing to load.
You need m2c installed, plus SDL2 and SDL2_ttf:
brew install sdl2 sdl2_ttf
Then build and run:
m2c build
.m2c/bin/hexed somefile.bin
To build a macOS .app bundle:
bash build_app.sh
This creates .m2c/Hexed.app. You can drag files onto it in Finder, use "Open With", or double-click it to get a file picker dialog. You can also launch it from the terminal:
open .m2c/Hexed.app --args somefile.bin
To package a distributable DMG:
bash build_app.sh --dmg
This creates .m2c/Hexed-1.0.0.dmg with the standard drag-to-Applications layout.
The window is divided into four columns: offset addresses, the hex grid (16 bytes per row, split into two groups of 8), the ASCII column (printable characters or dots), and a side panel showing either a help reference with data inspector or a byte histogram.
The status bar along the bottom shows the filename, file size, cursor position, byte value, edit mode, endianness, and modification state. When bytes are selected, it shows the selection size and offset instead.
The data inspector panel displays the byte at the cursor in hex, decimal, octal, and binary, along with signed/unsigned Int8, Int16, and Int32 interpretations (endian-aware) and the ASCII character if printable. Click individual bits in the binary row to enter binary editing mode.
| Key | Description |
|---|---|
| Arrows | Move by one byte or row |
| Page Up / Down | Move by one page |
| Home / End | Jump to row start / end |
| Cmd+Up / Down | Jump to file start / end |
| Scroll wheel | Scroll view without moving cursor |
Click any byte in the hex or ASCII columns to jump there. Click and drag or shift+arrow to select ranges.
| Key | Description |
|---|---|
| 0-9, A-F | Hex nibble input (hex mode) |
| Typing | ASCII character input (ASCII mode) |
| 0 / 1 | Bit input (binary mode) |
| Tab | Toggle hex / ASCII mode |
| Escape | Clear selection, exit binary mode |
Hex mode takes two keystrokes per byte (high nibble, low nibble). Binary mode is entered by clicking a bit in the inspector; left/right arrows move between bits, wrapping across byte boundaries.
| Key | Description |
|---|---|
| Cmd+S | Save |
| Cmd+Z | Undo |
| Cmd+Y | Redo |
| Cmd+F | Search -- hex or ASCII pattern, Tab to switch; Return to find, Escape to cancel |
| Cmd+G | Find next match |
| Cmd+R | Replace -- three phases: search, replacement, confirm (Y/N/A/Escape) |
| Cmd+L | Go to hex offset |
| Cmd+C | Copy selection as hex pairs (4F A3 FF) |
| Cmd+V | Paste -- auto-detects hex pairs vs raw ASCII; overwrites in place |
| Cmd+E | Fill selection with a hex byte value |
| Cmd+D | Export selection to a file |
| Cmd+B | Toggle byte frequency histogram |
| Cmd+T | Toggle little-endian / big-endian interpretation |
| Cmd+Plus | Zoom in |
| Cmd+Minus | Zoom out |
All multi-byte operations (paste, fill, replace-all) are grouped into a single undo step. Replace overwrites in place and never changes the file size. The histogram shows all 256 byte values color-coded from blue (rare) to red (frequent), with the cursor byte highlighted and its count/percentage in a footer.
Hexed is structured as three layers, enforced by Modula-2's module system. Each layer only depends downward — no circular imports.
The data layer. No UI or rendering knowledge.
- ByteStore — Abstract byte storage with two backends selected at open time based on file size. Files up to 64MB use a memory backend that loads the entire file into a
ByteBuf. Larger files use a paged backend with lazy 4KB page loading, a 256-page LRU cache (1MB resident), andfseek-based random access. AlastSlotcache makes sequential access patterns (search, histogram) O(1) per byte for page lookup instead of scanning all 256 cache entries.ReadBlockprovides bulk 4KB reads to eliminate per-byte function call overhead for scan-heavy operations. - Doc — Document model. Wraps ByteStore and owns cursor position, selection state, edit mode, nibble phase, and visible row tracking. Provides the byte access API that the rest of the app uses. Rejects files over 4GB (CARDINAL range) at open time using a 64-bit size pre-check.
- Cmd — Undo/redo engine. Records
SetByteoperations into a ring buffer. Supports operation grouping so multi-byte edits (paste, fill, replace-all) collapse into a single undo step. - Search — Byte-pattern search engine. Supports hex byte patterns and ASCII string search with wrap-around. Uses bulk
ReadBlockto scan 4KB blocks at a time with overlap to catch patterns at page boundaries. - Sys — C FFI shim (
DEFINITION MODULE FOR "C"). Exposes handle-based file I/O (fopen/fclose/fread/fwrite/fseek), 64-bitfile_size,basename, andfile_existsfrom the m2sys C library.
Presentation and input handling. Depends on Core and the m2gfx graphics library.
- View — All rendering. Draws the hex grid, ASCII column, offset addresses, data inspector, histogram, status bar, search/replace overlays, and scrollbar. Converts byte values to hex display using a
HexDigitconstant string indexed byDIV 16/MOD 16. - Keymap — Event dispatch. Maps keyboard and mouse events to document operations. Handles modal input states (search, goto, fill, replace, export). Computes the byte frequency histogram using bulk block reads.
- App — Main loop. Initializes SDL2, loads fonts, runs the event/render loop at 60fps with idle-wait, handles window resize.
- Theme — Color palette and layout constants. Dark theme with configurable zoom.
- State modules (SearchState, GotoState, FillState, ReplaceState, ExportState, HistogramState, EndianState) — Shared mutable state for modal UI. These exist to break what would otherwise be circular dependencies between Keymap (writes state) and View (reads state).
- MacBridge — ObjC FFI for the native macOS About dialog and file picker.
- Main — Entry point. Parses command-line arguments, calls
App.Run.
- m2gfx — SDL2 graphics abstraction (Gfx, Font, Canvas, Events). Backed by
gfx_bridge.cwhich wraps SDL2 and SDL2_ttf. - m2bytes —
ByteBufgrowable byte buffer with geometric growth, used by the memory backend. - m2sys — C shim library for file I/O, path utilities, and system calls.
Two-tier storage. Small files get the simplicity of a flat in-memory buffer. Large files get lazy paging without loading the whole file. The threshold (64MB) is high enough that most files people hex-edit fit in memory, but the paged path means multi-gigabyte files open instantly.
Page cache with sequential fast-path. The 256-page LRU cache keeps 1MB resident for the paged backend. A lastSlot hint avoids linear cache scans for sequential access — the common case for search, histogram, and scroll rendering. Bulk ReadBlock returns up to one page of data per call, letting callers process 4KB at a time instead of issuing per-byte requests through the full call chain.
State modules for modal UI. Modula-2 forbids circular imports. Search, replace, goto, fill, and histogram all need state written by the event handler and read by the renderer. Dedicated state modules with getter/setter pairs act as a shared mailbox, keeping Keymap and View independent.
No heap allocation in the hot path. Rendering, search, and histogram use stack-allocated buffers. The only heap allocation is ByteBuf for the memory backend (one allocation at file open). The paged backend's 256-page array lives in the Store record itself.
C FFI via DEFINITION MODULE FOR "C". Sys.def declares C functions with Modula-2 signatures. The compiler emits direct C calls with no wrapper overhead. This is used for file I/O (m2sys), graphics (gfx_bridge), and the macOS native bridge (mac_bridge).
MIT

