A simple, fast macOS CSV editor for people who work with data every day.
CSV files are everywhere — exports from databases, analytics platforms, CRMs, financial systems — yet the tools for editing them range from sluggish (Excel) to hostile (Numbers). Rowly fills the gap: a purpose-built desktop app that opens instantly, handles large files without freezing, and feels like it belongs on your Mac.
Anyone who works with CSVs regularly knows the pain:
- Excel is overkill. Opening a CSV in Excel means slow startup, formula misinterpretation, and the constant risk of silent data corruption (dates, leading zeros, long numbers).
- Text editors are too raw. Vim or VS Code show you the text, but navigating and editing a 50-column file as raw comma-delimited text is error-prone and tedious.
- Web tools require upload. Cloud-based editors introduce privacy concerns and network dependency for files that should stay local.
The market lacks a lightweight, privacy-respecting, keyboard-driven CSV editor that treats the format as a first-class citizen.
Rowly is a native macOS desktop application designed for a single job: making CSV editing fast, reliable, and pleasant. No account required. No cloud uploads. Your data never leaves your machine.
-
Speed over features. Large files parse in a background thread so parsing does not block the UI. Virtual scrolling renders only visible rows and columns, keeping DOM work bounded as files grow.
-
Keyboard-first, mouse-friendly. Every action is reachable via keyboard shortcuts. Arrow keys navigate cells, Enter edits in place, Cmd+K opens a command palette. But nothing requires memorization — every action is also available through menus and toolbars.
-
Data integrity by default. Rowly reads and writes plain text. No proprietary formats, no formula evaluation, no silent type coercion. What you see is exactly what gets saved.
-
Native Mac experience. Hidden titlebar with inset traffic lights, system theme integration, standard macOS menu bar, and familiar shortcuts (Cmd+S, Cmd+Z, Cmd+W). It feels like it shipped with the OS.
Open multiple CSV files simultaneously with full state isolation per tab. Each tab tracks its own undo history, filter state, and unsaved changes independently. Switch between files with Cmd+Option+Arrow or click the tab bar.
A 50-level undo stack captures every edit with descriptive labels ("Edit cell", "Delete row", "Paste"). Rapid keystrokes within a 2-second window are coalesced into a single entry, so undoing a burst of typing reverts the whole burst — not character by character.
Cmd+F opens an inline find bar that searches across headers and cell values. Navigate matches with arrow buttons or Enter, replace individually or all at once. The current match is highlighted in the grid for spatial context.
Click any column header to apply a substring filter. Multiple filters combine with AND logic, letting you drill into exactly the rows you need. Filtered views can be exported as new CSV files (Shift+Cmd+E), preserving only the rows you care about.
Cmd+K opens a searchable command palette — type to fuzzy-filter any available action. Grouped by section, with keyboard shortcut hints inline. Designed for power users who want to stay in flow without reaching for the mouse.
Drop a .csv file onto the window to open it immediately. No dialog, no friction.
Cmd+B toggles a resizable sidebar showing your 15 most recently opened files. One click to reopen, with a remove option to keep the list tidy.
Drag column borders to resize. Double-click a border to auto-fit the column to its content. A dynamic row-number gutter keeps data aligned.
Light, Dark, or System — persisted across sessions. Follows macOS appearance changes in real time when set to System.
The architecture is driven by two non-negotiable requirements: UI responsiveness and data safety.
┌──────────────────────────────────────────────────────┐
│ Electron Shell │
│ │
│ ┌─────────────┐ IPC (contextBridge) ┌───────────┐│
│ │ Main Process │◄────────────────────►│ Renderer ││
│ │ │ │ (React) ││
│ │ File I/O │ │ ││
│ │ Native Menu │ │ Zustand ││
│ │ Settings │ │ Virtual ││
│ │ Recent Files│ │ Grid ││
│ └──────┬───────┘ └────────────┘│
│ │ │
│ ┌──────▼───────┐ │
│ │ Worker Thread │ │
│ │ (PapaParse) │ │
│ └───────────────┘ │
└──────────────────────────────────────────────────────┘
-
Worker thread for CSV parsing. Parsing is CPU-bound. Running PapaParse in a dedicated Node.js worker thread keeps the main process responsive while a file is decoded and parsed.
-
Zustand for state. Lightweight, minimal boilerplate, and pairs naturally with React's rendering model. Shallow selectors prevent unnecessary re-renders — critical when the grid contains thousands of cells. The store encapsulates undo/redo stacks, tab snapshots, and filter state in a single coherent model.
-
TanStack Virtual for the grid. Only rows visible in the viewport are rendered to the DOM. This makes scroll performance independent of file size — a 100-row file and a 100,000-row file feel identical.
-
Electron with context isolation. Node integration is disabled in the renderer. All file system access goes through a typed IPC bridge (
contextBridge), enforcing a clean security boundary. The sandbox is enabled.
| Decision | Rationale |
|---|---|
| Electron over Tauri | Mature ecosystem, predictable Node.js worker model, reliable macOS integration |
| PapaParse over manual parsing | Battle-tested RFC 4180 compliance, handles edge cases (quoted fields, newlines in values) |
| Zustand over Redux | Less ceremony for a single-app store; built-in shallow equality for performance |
| Virtual scrolling over pagination | Preserves spatial context; users can scroll freely without cognitive load of page numbers |
| Local storage over SQLite | Settings and recents are simple key-value data; electron-store is zero-config JSON persistence |
| Layer | Technology |
|---|---|
| Desktop framework | Electron 30 |
| Build system | electron-vite + Vite 5 |
| UI framework | React 18 |
| State management | Zustand 5 |
| Table utilities | TanStack Table + TanStack Virtual |
| CSV engine | PapaParse (in Node.js worker thread) |
| Validation | Zod |
| Persistence | electron-store |
| Language | TypeScript 5.6 (strict) |
# Install dependencies
npm install
# Start in development mode (hot-reload)
npm run dev
# Build for production
npm run build
# Preview the production build
npm run previewnpm run lint # ESLint across all source files
npm run check-types # Full TypeScript type check (no emit)
npm run verify:mas # Verify a packaged MAS signature, entitlements, and document typesSecurity-scoped bookmarks are only returned by Electron's MAS build, so persistent file access must be checked with a development-signed MAS package rather than npm run dev.
- Run
npm run make, thennpm run verify:mas. - Open a CSV with Rowly's Open dialog, quit Rowly completely, relaunch, and reopen it from both the sidebar and File → Open Recent without another prompt.
- Repeat after opening a CSV from Finder and after dragging one into Rowly. Files first delivered by Finder or drag-and-drop may require one explicit Allow Access action before Rowly can persist permission across launches.
- Save a new file, quit, relaunch, edit it, and save again.
- Move or rename a recent file and confirm Locate… updates the stored entry.
- Repeat with files in Downloads, iCloud Drive, an external volume, and with identically named files in different folders.
- Confirm Rowly's Dock menu and File → Open Recent stay in sync when entries are opened, removed, or cleared.
src/
├── main/ # Electron main process
│ ├── index.ts # Window creation, IPC handlers
│ ├── menu.ts # Native macOS menu bar
│ ├── settingsStore.ts # Theme preferences (electron-store)
│ ├── recentFiles.ts # Recent file tracking
│ └── services/
│ ├── fileManager.ts # File I/O orchestration
│ ├── csvWorker.ts # Worker thread lifecycle
│ └── csvWorkerScript.ts # PapaParse worker
├── preload/ # Secure IPC bridge (contextBridge)
├── renderer/ # React application
│ ├── App.tsx
│ ├── state/gridStore.ts # Zustand store (undo, tabs, filters)
│ ├── hooks/ # File operation hooks
│ └── components/
│ ├── DataGrid.tsx # Virtualized spreadsheet grid
│ ├── Toolbar.tsx # File actions, undo/redo
│ ├── TabBar.tsx # Multi-tab navigation
│ ├── FindBar.tsx # Search and replace
│ ├── CommandPalette.tsx # Cmd+K launcher
│ ├── SettingsDialog.tsx # Theme selection
│ ├── RecentFilesPanel.tsx # Sidebar
│ └── StatusBar.tsx # Row/column count, progress
└── shared/types.ts # Shared TypeScript types
The foundation is solid — what follows is guided by real usage patterns and user feedback:
- Diff preview before save. Show a visual summary of what changed since the last save, giving users confidence before committing edits to disk.
- Typed columns and validation. Let users declare column types (date, number, email) and surface validation errors inline — catching bad data before it propagates downstream.
- Broader test coverage. CSV worker integration tests cover fidelity and malformed input; state-level tests for undo/redo, tab lifecycle, and filter logic are still planned.
- Export formats. Save as TSV, JSON, or fixed-width — broadening the utility without losing the CSV-first identity.
- Delimiter controls. Auto-detection supports commas, semicolons, pipes, and tabs; a manual override remains planned for ambiguous files.
Rowly follows Electron security best practices:
- Context isolation enabled — renderer code cannot access Node.js APIs directly.
- Node integration disabled in the renderer process.
- Sandbox enabled — OS-level process isolation.
- No remote content — the app loads only local files; no network requests, no telemetry, no analytics.
- All file operations flow through a typed IPC bridge, making the attack surface explicit and auditable.
Private — not currently published.