fresh-editor 0.1.56

A lightweight, fast terminal-based text editor with LSP support and TypeScript plugins
Documentation
# Built-in Terminal for Fresh

## Architecture Overview

Fresh's terminal is implemented as a special buffer type backed by `alacritty_terminal` for VT100/ANSI emulation and `portable-pty` for cross-platform PTY management. Terminals can be displayed in any split and support two modes:

- **Terminal mode**: Live interactive shell, input goes to PTY
- **Scrollback mode**: Read-only buffer view with editor navigation/selection

---

## Incremental Scrollback Streaming

The terminal uses an **incremental streaming architecture** that avoids O(n) work on mode switches and session restore. The key insight is that scrollback history is append-only.

### File Structure

Each terminal maintains a single **backing file** containing rendered text:

```
~/.local/share/fresh/terminals/{encoded_workdir}/fresh-terminal-{id}.txt
```

The backing file structure:

```
┌─────────────────────────────────────────┐
│ Scrollback history (append-only)        │  ← grows incrementally as lines
│ Line 1                                  │    scroll off the top of screen
│ Line 2                                  │
│ ...                                     │
│ Line N                                  │
├─────────────────────────────────────────┤
│ Visible screen (rewritable tail)        │  ← present only in scrollback mode
│ Screen line 0                           │    (~50 lines, rewritten each switch)
│ ...                                     │
│ Screen line 49                          │
└─────────────────────────────────────────┘
```

### Data Flow

**During terminal operation (PTY read loop):**

```
PTY output bytes
state.process_output()  ──►  TerminalState (in-memory grid)
check: history_size increased?
   YES ──►  append new scrollback lines to backing file
            (one line at a time, as they scroll off screen)
```

**Exit terminal mode (enter scrollback mode):**

```
1. Append visible screen (~50 lines) to backing file
2. Load backing file as read-only buffer (lazy load, instant)
```

**Re-enter terminal mode:**

```
1. Truncate backing file to scrollback-only (remove visible screen tail)
2. Resume live terminal rendering from TerminalState
```

**Quit while in terminal mode:**

```
1. Append visible screen to backing file (ensure complete state)
2. Save session as normal
```

**Session restore:**

```
1. Load backing file directly (lazy load, instant)
2. User starts in scrollback mode viewing last session state
3. Raw log replay only if user re-enters terminal mode (deferred)
```

### Performance Characteristics

| Operation | Before | After |
|-----------|--------|-------|
| Mode switch | ~500ms (replay + full_content_string) | ~5ms (append 50 lines) |
| Session restore | ~1000ms (replay 2x) | ~10ms (lazy load) |
| PTY read overhead | ~0 | ~0.1ms per scroll (append one line) |

### State Tracking

```rust
pub struct TerminalState {
    term: Term<NullListener>,
    parser: Processor,
    cols: u16,
    rows: u16,
    dirty: bool,
    terminal_title: String,

    // Incremental streaming state
    synced_history_lines: usize,      // lines already written to backing file
    backing_file_history_end: u64,    // byte offset where scrollback ends
}
```

### Key Methods

```rust
impl TerminalState {
    /// Append any new scrollback lines to the backing file.
    /// Called after process_output() in the PTY read loop.
    pub fn flush_new_scrollback<W: Write>(&mut self, writer: &mut W) -> io::Result<usize>;

    /// Append visible screen content to the backing file.
    /// Called when exiting terminal mode.
    pub fn append_visible_screen<W: Write>(&self, writer: &mut W) -> io::Result<()>;

    /// Get byte offset where scrollback ends (for truncation on mode re-entry).
    pub fn backing_file_history_end(&self) -> u64;
}
```

### Terminal Resize Handling

When terminal is resized:
- Old scrollback lines remain as-is (rendered at old width)
- New scrollback lines are rendered at new width
- The editor's line wrapping handles display of mixed-width lines
- No O(n) rewrite of history required

This is a feature: original output is preserved at original width rather than being re-wrapped or truncated.

---

## Raw Log File (Optional)

For re-entering terminal mode after session restore, a raw log of PTY bytes is maintained:

```
~/.local/share/fresh/terminals/{encoded_workdir}/fresh-terminal-{id}.log
```

This file:
- Contains raw VTE escape sequences exactly as received from PTY
- Enables rebuilding full `TerminalState` via replay
- Only needed if user wants to resume live terminal after restore
- Can be disabled if terminals always start fresh on session restore

---

## Mode Switching

### Terminal Mode → Scrollback Mode (Ctrl+Space)

1. Append visible screen to backing file
2. Update `backing_file_history_end` to current file position minus screen size
3. Load backing file as read-only buffer (lazy load)
4. Set `editing_disabled = true`
5. User can navigate, select, copy, search

### Scrollback Mode → Terminal Mode (Ctrl+Space)

1. Truncate backing file to `backing_file_history_end`
2. Set `editing_disabled = false`
3. Resume live rendering from `TerminalState`
4. Scroll view to cursor position (bottom of terminal)

---

## Session Persistence

### Session Save

```rust
struct TerminalSession {
    pub id: u64,
    pub shell: String,
    pub cwd: PathBuf,
    pub cols: u16,
    pub rows: u16,
    pub backing_path: PathBuf,
    pub log_path: PathBuf,  // for optional live terminal resume
}
```

Before saving session:
- If in terminal mode, append visible screen to backing file
- This ensures backing file always contains complete state

### Session Restore

1. Load backing file directly as read-only buffer
2. User sees last session state immediately (lazy load)
3. If user presses Ctrl+Space to enter terminal mode:
   - Spawn new PTY with same shell/cwd
   - Optionally replay raw log to restore `TerminalState`
   - Or start fresh (simpler, recommended default)

---

## Integration with Existing Buffer System

The backing file integrates with Fresh's existing file-backed buffer architecture:

- Files > 1MB use lazy loading (`BufferData::Unloaded`)
- Chunks loaded on-demand as user scrolls
- Full 200K line scrollback (~15MB) loads instantly
- Search, selection, copy all work via normal buffer mechanisms

This is why the incremental streaming approach works: we're not building a new system, we're leveraging the existing efficient buffer infrastructure.

---

## Implementation Checklist

### Core Changes

- [ ] Add `synced_history_lines` and `backing_file_history_end` to `TerminalState`
- [ ] Implement `flush_new_scrollback()` method
- [ ] Implement `append_visible_screen()` method
- [ ] Update PTY read loop to call `flush_new_scrollback()` after `process_output()`
- [ ] Pass backing file writer to PTY read thread

### Mode Switch Changes

- [ ] `sync_terminal_to_buffer()`: append screen + lazy load (no replay)
- [ ] `enter_terminal_mode()`: truncate backing file
- [ ] On quit: ensure visible screen is appended before session save

### Session Restore Changes

- [ ] Load backing file directly (skip log replay)
- [ ] Defer log replay to `enter_terminal_mode()` if needed
- [ ] Consider removing log replay entirely (fresh terminal on restore)

### Cleanup

- [ ] Remove `full_content_string()` method (no longer needed)
- [ ] Remove `replay_terminal_log_into_state()` from restore path
- [ ] Update tests for new architecture

---

## Known Issues

### Critical

1. **Read-only mode accepts input**: Text is inserted into buffer in scrollback mode. Fix: ensure `editing_disabled` is respected.

2. **Keybindings don't work in scrollback mode**: All keys typed as text. Fix: ensure `KeyContext::Normal` is set on mode exit.

### High Priority

3. **View doesn't scroll to cursor on resume**: After scrolling in scrollback mode, resuming terminal mode leaves view at wrong position. Fix: scroll to bottom on mode entry.

### Medium Priority

4. **Inconsistent display between modes**: Line numbers and layout differ. Consider unifying visual presentation.

5. **Status message truncated on narrow terminals**: "Terminal mode disabled..." too long for 80 columns.

---

## Technical Details

### Dependencies

```toml
alacritty_terminal = "0.25"  # VT100/ANSI terminal emulation
portable-pty = "0.9"         # Cross-platform PTY management
```

### alacritty_terminal Capabilities Used

- `Term::grid()` - access to scrollback via negative line indices
- `grid.history_size()` - track scrollback growth
- `grid[Line(-n)]` - read scrollback lines
- `Term::selection` - native selection support
- `Term::selection_to_string()` - copy selected text
- `Term::scroll_display()` - scroll through history

### Scrollback Access

```rust
let grid = term.grid();
let history_size = grid.history_size();

// Scrollback lines: Line(-history_size) to Line(-1)
// Visible screen: Line(0) to Line(rows-1)

for i in (1..=history_size).rev() {
    let line = Line(-(i as i32));
    let row_data = &grid[line];
    // ... write line to backing file
}
```

---

## References

- [alacritty_terminal docs]https://docs.rs/alacritty_terminal/latest/alacritty_terminal/
- [portable-pty docs]https://docs.rs/portable-pty/latest/portable_pty/
- [Zed Terminal Architecture]https://deepwiki.com/zed-industries/zed/3.3-terminal