garedit
Native Text & Code Editor
A native X11 text editor for the gar desktop suite. Built with Cairo/Pango rendering via gartk, garedit provides responsive editing with full undo/redo history, mouse-driven text selection, word-level navigation, and line number display. Designed for lightweight, fast editing that fits naturally into the gardesk environment. Control running instances via gareditctl for scripting and automation.
Features
- Cairo/Pango text rendering via gartk
- Full undo/redo with 512-snapshot history
- Mouse click, drag selection, and scroll
- Word-level cursor movement and deletion
- Configurable line number gutter
- UTF-8 text with CRLF/LF detection
- Current line highlight
- TOML configuration with CLI overrides
- File open/save prompts with ~ expansion
- Unsaved changes confirmation dialogs
- IPC protocol for external control (gareditctl)
- Consistent gartk theming with gardesk
Quick Start Guide
Get garedit running as a standalone editor or integrate it with the gar desktop environment.
1. Installation
Install garedit using the unified installer or build from source:
2. Opening Files
Open a file directly or start with a scratch buffer:
3. Essential Keybindings
The most commonly used shortcuts:
| Key | Action |
|---|---|
| Ctrl+O | Open file |
| Ctrl+S | Save (or save-as if no path) |
| Ctrl+Shift+S | Save as |
| Ctrl+Z | Undo |
| Ctrl+Shift+Z / Ctrl+Y | Redo |
| Ctrl+A | Select all |
| Ctrl+Q | Quit (with unsaved confirmation) |
| Escape | Clear selection or quit |
4. Integration with gar
Bind garedit to a key in your gar config:
-- Bind garedit to Mod+E
gar.key({ gar.mod, "e", gar.spawn("garedit") })
-- Or open a specific file
gar.key({ gar.mod, "shift", "e", gar.spawn("garedit ~/.config/gar/init.lua") }) 5. Control with gareditctl
Control a running garedit instance via IPC:
6. Configuration Priority
garedit checks configuration in this order:
- Command-line arguments (highest priority)
~/.config/garedit/config.toml— TOML config file- Built-in defaults
Configuration Reference
garedit uses TOML for configuration. The config file is created automatically on first run with default values.
Configuration File
# Font family for editor text (any system font name)
font_family = "monospace"
# Font size in points
font_size = 14.0
# Width of tab character (number of spaces inserted)
tab_width = 4
# Show line numbers in the gutter
show_line_numbers = true Options Reference
| Option | Type | Default | Description |
|---|---|---|---|
| font_family | string | monospace | Font family for editor text |
| font_size | f64 | 14.0 | Font size in points |
| tab_width | usize | 4 | Number of spaces per tab |
| show_line_numbers | bool | true | Show line numbers in gutter |
Command-Line Overrides
All config options can be overridden via CLI flags:
| Flag | Description |
|---|---|
| --font-family <name> | Override font family |
| --font-size <size> | Override font size (float) |
| --tab-width <width> | Override tab width |
| --line-numbers | Force line numbers on |
| --no-line-numbers | Force line numbers off |
| --width <pixels> | Initial window width (default: 980) |
| --height <pixels> | Initial window height (default: 680) |
Example Configurations
Large font for high-DPI display:
font_family = "JetBrains Mono"
font_size = 18.0
tab_width = 4
show_line_numbers = true Minimal, no gutter:
font_family = "Fira Code"
font_size = 12.0
tab_width = 2
show_line_numbers = false Overview
garedit is a native X11 text editor built with Cairo/Pango rendering via gartk. It provides a clean, focused editing experience with core features implemented correctly: full undo/redo, mouse selection, word navigation, and line numbering.
Architecture
- Rendering: Cairo/Pango via gartk for text layout and rendering
- X11 Backend: x11rb with gartk-x11 event loop and window management
- Core: garedit-core library separates document logic from GUI
- IPC: JSON over Unix domain sockets (garedit-ipc protocol)
- Config: TOML with CLI overrides
File Locations
| ~/.config/garedit/config.toml | Configuration file |
| $XDG_RUNTIME_DIR/garedit.sock | IPC socket (primary) |
| /tmp/garedit.sock | IPC socket (fallback) |
Core Components
gareditgareditctlgaredit-coregaredit-ipcArchitecture
garedit follows a clean separation between core editing logic and GUI rendering, making the core library testable independently of X11.
Module Structure
| Crate | Role | Key Types |
|---|---|---|
| garedit-core | Document model, editing operations, undo/redo | Document, Position, Selection, EditCommand |
| garedit-ipc | IPC protocol definitions | Command, Response, ResponseData |
| garedit | GUI application, X11 window, rendering | App, Config |
| gareditctl | CLI control client | clap-based CLI |
Dependencies
- gartk-core: Theme, colors, input event types
- gartk-x11: X11 connection, window, event loop
- gartk-render: Cairo/Pango rendering primitives
- x11rb 0.13: X11 protocol bindings
- cairo-rs / pango / pangocairo: 2D graphics and text layout
- serde + serde_json: IPC serialization
- toml: Configuration parsing
- clap 4.5: CLI argument parsing (gareditctl)
Workspace Structure
The garedit workspace follows the standard gardesk three-crate pattern:
garedit/
├── Cargo.toml # Workspace definition
├── garedit/ # Main editor binary
│ └── src/
│ ├── main.rs # Entry point, CLI args, config loading
│ └── app.rs # App struct, event loop, rendering
├── garedit-core/ # Core editing library (no X11 dependency)
│ └── src/
│ ├── lib.rs # Re-exports
│ ├── buffer.rs # Document model (lines, cursor, history)
│ ├── command.rs # EditCommand enum
│ ├── cursor.rs # Position type
│ └── selection.rs # Selection type (anchor + active)
├── garedit-ipc/ # IPC protocol definitions
│ └── src/
│ └── lib.rs # Command, Response, socket_path()
└── gareditctl/ # CLI control client
└── src/
└── main.rs # clap-based CLI subcommands Build Output
| target/release/garedit | Main editor GUI binary |
| target/release/gareditctl | IPC control client |
Rendering Pipeline
garedit uses Cairo for 2D graphics and Pango for text layout, accessed through gartk's rendering abstractions.
Render Cycle
Rendering is on-demand: edits and events call request_redraw() and the render happens on the next event loop iteration.
- Clear canvas with background color
- Draw gutter background and border (if line numbers enabled)
- For each visible line: draw line highlight, selection background, line number, text
- Draw cursor (2px vertical caret)
- Draw status bar (28px at bottom)
- Flush Cairo context
- Copy surface to X11 window via
put_image
Text Measurement
Line height is calculated as the height of the string "Mg" plus 4px padding. Column positions are measured character-by-character using Pango text measurement for accurate cursor placement with proportional fonts, though monospace fonts are recommended.
Theme Colors
| Theme Property | Used For |
|---|---|
| foreground | Editor text |
| background | Canvas background |
| input_background | Gutter background |
| input_cursor | Cursor caret color |
| selection_background | Text selection (90% alpha) |
| item_hover_background | Current line highlight (60% alpha) |
| border | Gutter border line |
| item_description | Line numbers, status bar dim text |
Event Loop
garedit uses the gartk-x11 event loop which wraps X11 event polling. The loop processes these event types:
| Event | Handler |
|---|---|
| KeyPress | Keyboard shortcuts, text input, prompt handling |
| MousePress | Click-to-position cursor, start drag selection |
| MouseMove | Extend selection during drag |
| MouseRelease | End drag selection |
| Scroll | Scroll viewport (1/6 of visible lines per step) |
| Resize | Regenerate renderer, clamp viewport |
| Expose | Redraw visible area |
| CloseRequested | Quit (with unsaved changes check) |
Window Properties
- Class name: "garedit"
- Default size: 980 x 680 pixels (clamped to monitor bounds)
- Positioning: Centered on the monitor where the pointer is located
- Resizable: Yes, renderer regenerated on resize
Editing
garedit's editing engine lives in the garedit-core crate, fully separated from the GUI. The document model uses a Vec<String> line buffer with character-indexed positions. All editing operations go through the EditCommand enum and are applied via document.apply(command), which creates an undo snapshot before each change.
Available Edit Commands
| Command | Description |
|---|---|
| InsertChar / InsertText | Insert character or string at cursor |
| Newline | Split line at cursor |
| Backspace / Delete | Delete char left / right of cursor |
| DeleteWordBackward / DeleteWordForward | Delete by word boundary |
| DeleteLine | Delete entire current line |
| Move* commands | Cursor movement (see Cursor Movement section) |
| Undo / Redo | History traversal |
Cursor Movement
Cursor position is character-indexed (not byte-indexed), stored as a Position { line, column } with 0-based indices. Movement wraps at line boundaries.
Movement Commands
| Command | Behavior |
|---|---|
| MoveLeft | One character left; wraps to end of previous line |
| MoveRight | One character right; wraps to start of next line |
| MoveUp / MoveDown | One line up/down, column clamped to line length |
| MoveLineStart / MoveLineEnd | Jump to column 0 or end of current line |
| MoveWordLeft / MoveWordRight | Jump by word boundaries, crosses line boundaries |
| MovePageUp / MovePageDown | Move by visible line count minus 1 |
Viewport Scrolling
The cursor is automatically kept visible within the viewport. When the cursor moves outside visible bounds, the viewport scrolls to keep it in view. Scrolling is clamped so you cannot scroll past the end of the document.
Selection
Selection uses an anchor-based model: the anchor stays fixed while the active position (cursor) moves. This allows extending selection in any direction.
Selection Behavior
- Shift+movement: Sets anchor at current position, then moves cursor to extend selection
- Arrow without Shift: Collapses selection and deselects
- Typing/deleting: Replaces the selected text with the new input
- Mouse drag: Click sets anchor, drag extends active position
- Escape: Clears the current selection
Visual Rendering
Selected text is highlighted with the theme's selection_background color at 90% alpha. Multi-line selections correctly span across lines, with each line's selection bounds calculated from character widths via Pango text measurement.
Undo / Redo
garedit uses a snapshot-based undo system. Each edit operation creates a full document state snapshot before the change is applied.
How It Works
- History limit: 512 snapshots by default (configurable via API)
- Snapshot contents: Full line buffer, cursor position, selection state, dirty flag
- Redo clearing: Making a new edit after an undo clears the redo stack
- Dirty tracking: The dirty flag is preserved and restored through undo/redo
Commands
| Ctrl+Z | Undo — restores previous snapshot |
| Ctrl+Shift+Z | Redo — restores next snapshot |
| Ctrl+Y | Redo (alternate binding) |
Word Boundaries
Word-level operations (Ctrl+Left/Right, Ctrl+W, Ctrl+Delete) use a boundary algorithm that distinguishes between word characters, symbols, and whitespace.
Character Classes
- Word characters:
[a-zA-Z0-9_] - Whitespace: spaces, tabs
- Symbols: everything else (punctuation, operators)
Algorithm
Word movement first skips whitespace, then skips all characters of the same class (word chars or symbols). The cursor stops at the transition between character classes or at whitespace. Word movement crosses line boundaries seamlessly.
UI Components
garedit's interface consists of three main visual components: the status bar at the bottom, the line number gutter on the left, and inline prompt dialogs for file operations.
Layout
┌──────┬──────────────────────────────────────────────────┐
│ 1 │ fn main() { │
│ 2 │ println!("hello"); ← content area │
│ 3 │ } │
│ │ ← gutter (64px) │
├──────┴──────────────────────────────────────────────────┤
│ main.rs [+] | saved Ln 2, Col 18 • 3 lines │
└─────────────────────────────────────────────────────────┘
↑ status bar (28px) Status Bar
A 28-pixel status bar at the bottom of the window displays file information and cursor position.
Layout
┌─────────────────────────────────────────────────────────────────┐
│ ~/project/main.rs [+] | saved Ln 42, Col 15 • 380 lines │
└─────────────────────────────────────────────────────────────────┘
↑ file path ↑ dirty ↑ status msg ↑ cursor position ↑ line count - Left side: File path (or "scratch buffer"), dirty indicator
[+], status message - Right side: Line and column (1-based), total line count
- Background: Darkened version of the gutter color (18% darker)
- Border: Thin line at top edge
Prompt Mode
When a file dialog or confirmation is active, the status bar displays the prompt input instead of normal status:
| open path: ~/Documents/file.txt_ | File open prompt (Ctrl+O) |
| save as: ~/project/output.rs_ | Save-as prompt (Ctrl+Shift+S) |
| unsaved changes: discard and quit? [y/N] | Confirmation dialog |
Line Numbers
The line number gutter is a 64-pixel wide panel on the left edge of the editor.
- Width: 64px when enabled, 0px when disabled
- Toggle: Via config (
show_line_numbers) or CLI (--line-numbers/--no-line-numbers) - Font: Same family, 1pt smaller than editor text (minimum 10pt)
- Alignment: Right-aligned within the gutter
- Color: Uses the
item_descriptiontheme color (dim) - Current line: Gets a subtle background highlight matching the current line highlight
- Numbers: 1-based (displayed as 1, 2, 3, ...)
Prompts & Dialogs
garedit uses inline prompts rendered in the status bar area for file operations and confirmations.
Prompt Types
| Trigger | Prompt | Input |
|---|---|---|
| Ctrl+O | Open file | File path (~ expanded) |
| Ctrl+S | Save as (when no path) | File path (~ expanded) |
| Ctrl+Shift+S | Save as (always) | File path (~ expanded) |
| Ctrl+O/Q with unsaved | Confirm discard | y/n (Enter = yes, Escape = no) |
Prompt Keybindings
| Enter | Submit prompt / confirm action |
| Escape | Cancel prompt / deny confirmation |
| Backspace | Delete last character from input |
| Ctrl+U | Clear entire input |
File Handling
Encoding
- Read: UTF-8 (via
std::fs::read_to_string()) - Write: UTF-8 output
- Invalid UTF-8: Returns an error on file open
Line Endings
- Detection: Automatically detects CRLF or LF on open
- Internal: All newlines normalized to LF internally
- Save: Written back with the original line ending style
- API: Newline style can be changed via
set_newline_style()
Internal Representation
Documents are stored as Vec<String> where each element is one line without trailing newline. An empty document always contains at least one empty string. Character indexing is used throughout (not byte indexing), ensuring correct handling of multi-byte UTF-8 characters.
Dirty Tracking
A dirty flag tracks unsaved changes. It's set on any edit and cleared on save. The flag is displayed as [+] in the status bar and triggers confirmation dialogs when opening another file or quitting.
Mouse Support
Click to Position
Left-clicking in the content area positions the cursor at the clicked location. The click coordinates are translated to a document position by calculating the line from Y coordinate (via line height) and the column from X coordinate (via per-character width measurement). Clicks in the gutter or below the status bar are ignored.
Drag Selection
Click-and-drag creates a selection. The click position becomes the anchor, and dragging extends the active end. Releasing the mouse button finalizes the selection.
Scroll Wheel
Scrolling moves the viewport by 1/6 of the visible line count per scroll step. The viewport is clamped so you cannot scroll past the end of the document.
gardesk Integration
gartk Theming
garedit uses the shared gartk Theme for colors, so it matches the look of other gardesk components (garlaunch, garfield, gartop, etc.) out of the box.
Window Manager Integration
- Window class is
"garedit"for gar window rules - Opens on the monitor where the pointer is (follows focus)
- Responds to gar keybindings (Mod+key to launch via
gar.spawn)
gareditctl for Automation
The IPC protocol enables integration from garterm scripts, garfield "open with" actions, and other gardesk tools. The gareditctl open command can specify line and column for jumping to specific locations.
Keyboard Shortcuts
garedit supports both standard editor shortcuts and Emacs-style navigation bindings.
Movement
| Key | Alt Key | Action |
|---|---|---|
| Left | Ctrl+B | Move left |
| Right | Ctrl+F | Move right |
| Up | Ctrl+P | Move up |
| Down | Ctrl+N | Move down |
| Home | Ctrl+Shift+A | Move to line start |
| End | Ctrl+E | Move to line end |
| Page Up | Move page up | |
| Page Down | Move page down | |
| Ctrl+Left | Alt+B | Move word left |
| Ctrl+Right | Alt+F | Move word right |
All movement keys support Shift modifier to extend selection.
Editing
| Key | Action |
|---|---|
| Enter | Insert newline |
| Backspace / Ctrl+H | Delete character left |
| Delete / Ctrl+D | Delete character right |
| Tab | Insert spaces (tab_width count) |
| Ctrl+W | Delete word backward |
| Ctrl+Delete | Delete word forward |
| Ctrl+U | Delete to line start |
| Ctrl+K | Delete to line end |
File Operations
| Key | Action |
|---|---|
| Ctrl+O | Open file (prompts for path) |
| Ctrl+S | Save (save-as if no file path) |
| Ctrl+Shift+S | Save as (always prompts) |
| Ctrl+Q | Quit (confirms if unsaved changes) |
| Escape | Clear selection; quit if no selection |
Undo / Redo
| Key | Action |
|---|---|
| Ctrl+Z | Undo last change |
| Ctrl+Shift+Z | Redo |
| Ctrl+Y | Redo (alternate) |
IPC / API Reference
garedit uses JSON-based IPC over Unix domain sockets for external control. The protocol is defined in the garedit-ipc crate.
Socket Path
The IPC socket is at $XDG_RUNTIME_DIR/garedit.sock (typically /run/user/<uid>/garedit.sock), with fallback to /tmp/garedit.sock.
Commands
| Command | Arguments | Description |
|---|---|---|
| open | path, line?, column? | Open file at optional cursor position (0-based line/column) |
| show | - | Make editor window visible |
| hide | - | Hide editor window |
| toggle | - | Toggle window visibility |
| status | - | Query editor state |
| quit | - | Close the editor |
JSON Protocol
Commands use serde tagged enums with #[serde(tag = "cmd")]:
// Open file at specific position
{"cmd": "open", "path": "/home/user/file.rs", "line": 42, "column": 10}
// Open file (cursor at beginning)
{"cmd": "open", "path": "/home/user/file.rs"}
// Query status
{"cmd": "status"}
// Quit
{"cmd": "quit"} Response Format
// Success
{"success": true}
// Status response
{
"success": true,
"data": {
"type": "status",
"visible": true,
"open_documents": 1,
"focused_document": "/home/user/file.rs"
}
}
// Error
{"success": false, "error": "file not found"} gareditctl Usage
Troubleshooting
Editor won't start
garedit requires a running X11 server. Ensure $DISPLAY is set.
Font rendering looks wrong
garedit uses Pango for font rendering. Make sure the font family in your config is installed on the system.
File won't open (encoding error)
garedit only supports UTF-8 files. Binary files or files with non-UTF-8 encoding will fail to open. Convert files to UTF-8 first with iconv.
Config not loading
Check that your config is valid TOML. Invalid config files are silently replaced with defaults.
gareditctl can't connect
The IPC socket transport is still in development. Currently, gareditctl generates the correct JSON commands but may not be able to communicate with a running instance. Check that $XDG_RUNTIME_DIR/garedit.sock exists and that garedit is running with IPC enabled.
Window too small on HiDPI
Use CLI flags to increase the window size and font: