markless 0.9.24

A terminal markdown viewer with image support
Documentation
# Markless - Claude Code Instructions
**Follow Strict RED/GREEN TDD in all development.**

1. Write a failing test FIRST that describes the behavior you want
2. Run the test - watch it FAIL (RED)
3. Write the minimum code to make it pass (GREEN)
4. Refactor while keeping tests green
5. NEVER write feature code without a failing test first

## Project Overview

Markless is a Rust TUI markdown viewer with:
- Image support (Kitty, Sixel, iTerm2, half-block fallback)
- Table of contents sidebar
- File watching / live reload
- Syntax-highlighted code blocks
- Search and mouse selection + copy

## Architecture

### The Elm Architecture (TEA)

This project uses TEA. All state changes flow through:

```
Event -> Message -> update(Model, Message) -> Model -> view(Model) -> Frame
```

**Rules:**
- `update()` must be a pure function - no side effects
- Side effects (file I/O, external open, clipboard) happen in `app/effects.rs`
- State lives in `Model`

### Module Organization

```
src/
├── app/           # TEA model, update, input, effects, event loop
├── config.rs      # CLI flags + config persistence
├── document/      # Markdown parsing and rendered document types
├── highlight/     # Syntax highlighting (syntect)
├── image/         # Image loading and protocol detection
├── input/         # Low-level input helpers
├── perf.rs        # Performance logging
├── search/        # Search helpers
├── ui/            # Rendering, overlays, styles
└── watcher/       # File watching wrapper
```

## Development Practices

### Red-Green-Refactor TDD

Always write tests first.

### Test Commands

```bash
cargo test                    # Run all tests
cargo test <pattern>          # Run matching tests
cargo test -- --nocapture     # Show println! output
```

### Linting & Formatting

```bash
cargo fmt                     # Format code
cargo fmt --check             # Check formatting (CI)
cargo clippy                  # Lint
cargo clippy -- -D warnings   # Lint, fail on warnings (CI)
./scripts/check.sh            # Run all local checks (format + tests)
```

A pre-commit hook auto-formats staged `.rs` files. To enable:

```bash
git config core.hooksPath .githooks
```

### Documentation

```bash
cargo doc --open              # Build and view docs
```

All public items must have doc comments with examples where appropriate.

## Code Style

### Error Handling & Panic Prevention

Production code must never panic. A TUI panic leaves the terminal in raw mode, breaking the user's shell.

- Use `anyhow` for application errors
- **No `unwrap()` or `expect()` in non-test code.** Use fallible alternatives instead:
  - `slice.get(i)` / `slice.first()` / `slice.get(i).copied()` instead of `slice[i]`
  - `map.get(&key)` with `let Some(v) = … else { continue/return }` instead of `map[&key]` or `.expect()`
  - `str.get(..n)` instead of `&str[..n]` (byte slicing can panic on non-UTF-8 boundaries)
  - `Option::unwrap_or()` / `unwrap_or_default()` / `unwrap_or_else()` for defaults
- **Poisoned mutexes:** use `lock().unwrap_or_else(PoisonError::into_inner)` — don't propagate the panic
- **Fallible init:** prefer `try_init()` over `init()` (e.g. `ratatui::try_init()`) and add `.context()` for a user-friendly message
- **Bounds checks:** always guard index/slice access on vectors, `HashMap` lookups, and `Layout::split` results — especially when the data comes from parsed input or external state
- When a missing value means "skip this item" (e.g. a missing node in a graph edge), log with `tracing::warn!` and `continue` rather than panicking

### Naming Conventions

- Types: `PascalCase`
- Functions/methods: `snake_case`
- Constants: `SCREAMING_SNAKE_CASE`
- Test functions: `test_<what>_<condition>_<expected>`

### Imports

Group imports in this order:
1. `std`
2. External crates
3. `crate::...`

## Key Types

```rust
struct Model {
    document: Document,
    viewport: Viewport,
    toc_visible: bool,
    watch_enabled: bool,
    // ...
}

enum Message {
    ScrollUp(usize),
    ScrollDown(usize),
    ToggleToc,
    FileChanged,
    // ...
}

enum LineType {
    Paragraph,
    Heading(u8),
    CodeBlock,
    // ...
}
```

## Dependencies

| Crate | Purpose |
|-------|---------|
| `ratatui` | TUI framework |
| `crossterm` | Terminal backend |
| `comrak` | Markdown parsing |
| `ratatui-image` | Terminal image rendering |
| `syntect` | Syntax highlighting |
| `notify` | File watching |
| `clap` | CLI argument parsing |

## Common Tasks

### Adding a New Message Type

1. Add variant to `Message` in `src/app/update.rs`
2. Handle it in `update()`
3. Add input mapping in `src/app/input.rs`
4. Write tests for the state transition

### Adding a New UI Element

1. Implement rendering in `src/ui/render.rs` or `src/ui/overlays.rs`
2. Add state to `Model` if needed
3. Add tests (unit or UI buffer tests)

### Adding Markdown Support

1. Enable in comrak `Options` (see `src/document/parser.rs`)
2. Handle AST nodes in the parser
3. Add tests in `src/document/parser.rs`

## Notes

- Image placeholders are `[Image: Alt text]` when images are not rendered.
- Image captions (alt text) render above images when heights are known.
- Selection is line-based and copies text as a fixed-width block; links copy as URLs.