jarq 0.4.0

An interactive jq-like JSON query tool with a TUI
Documentation
# jarq - Milestones

## jq Quick Reference

jq is a command-line JSON processor. Here's the core syntax you'll be implementing:

```bash
# Identity - return input unchanged
echo '{"a": 1}' | jq '.'
# {"a": 1}

# Field access
echo '{"name": "alice", "age": 30}' | jq '.name'
# "alice"

# Nested field access
echo '{"user": {"name": "alice"}}' | jq '.user.name'
# "alice"

# Array index
echo '[10, 20, 30]' | jq '.[1]'
# 20

# Array slice
echo '[0, 1, 2, 3, 4]' | jq '.[2:4]'
# [2, 3]

# Iterate array elements
echo '[1, 2, 3]' | jq '.[]'
# 1
# 2
# 3

# Pipe filters together
echo '{"items": [{"id": 1}, {"id": 2}]}' | jq '.items[] | .id'
# 1
# 2

# Optional field access (no error if missing)
echo '{"a": 1}' | jq '.b?'
# null

# Array/object construction
echo '{"a": 1, "b": 2}' | jq '[.a, .b]'
# [1, 2]

echo '{"x": 1}' | jq '{y: .x}'
# {"y": 1}

# Builtin functions
echo '[3, 1, 2]' | jq 'length'
# 3

echo '[3, 1, 2]' | jq 'sort'
# [1, 2, 3]

echo '[[1, 2], [3, 4]]' | jq 'flatten'
# [1, 2, 3, 4]

echo '[1, 2, 3]' | jq 'map(. * 2)'
# [2, 4, 6]

echo '[1, 2, 3, 4]' | jq 'select(. > 2)'
# 3
# 4
```

---

## Milestone 1: Static JSON Viewer

**Goal**: Display pretty-printed JSON in ratatui with scrolling.

**Features**:
- Load JSON from stdin or file argument
- Pretty-print with syntax highlighting (keys, strings, numbers, booleans)
- Vertical scrolling with j/k or arrow keys
- Quit with q

**Test input**:
```json
{"users": [{"name": "alice", "active": true}, {"name": "bob", "active": false}]}
```

---

## Milestone 2: Identity Filter

**Goal**: Add a filter input field that starts with `.` (identity).

**Features**:
- Text input at top or bottom of screen
- Parse `.` as identity filter (returns entire input)
- Display parse errors inline
- Re-render output when filter changes

**Parser** (nom):
```rust
fn parse_identity(input: &str) -> IResult<&str, Filter> {
    map(char('.'), |_| Filter::Identity)(input)
}
```

---

## Milestone 3: Field Access

**Goal**: Implement `.foo` and `.foo.bar` syntax.

**Examples**:
```
.name         -> "alice"
.user.email   -> "alice@example.com"
.missing      -> null (or error, your choice)
```

**Parser addition**:
```rust
fn parse_field(input: &str) -> IResult<&str, Filter> {
    preceded(
        char('.'),
        map(identifier, |name| Filter::Field(name.to_string()))
    )(input)
}
```

**Evaluator**:
```rust
fn eval(filter: &Filter, value: &Value) -> Result<Value> {
    match filter {
        Filter::Identity => Ok(value.clone()),
        Filter::Field(name) => {
            match value.get(name) {
                Some(v) => Ok(v.clone()),
                None => Ok(Value::Null), // or error
            }
        }
    }
}
```

---

## Milestone 4: Array Indexing

**Goal**: Implement `.[n]`, `.[]`, and `.["field"]` syntax.

**Examples**:
```
.[0]           -> first element
.[2]           -> third element
.[-1]          -> last element
.[]            -> iterate all elements (produces multiple outputs)
.["field"]     -> field access (for special chars like .["foo-bar"])
.["foo"]["bar"] -> chained quoted field access
```

**Key decision**: `.[]` produces multiple values. You'll need:
```rust
fn eval(filter: &Filter, value: &Value) -> Result<Vec<Value>>
```

**Note**: `.["field"]` provides field access for names with special characters (hyphens, spaces, etc.) that can't use the `.field` syntax.

---

## Milestone 5: JSONL/NDJSON Support

**Goal**: Handle newline-delimited JSON (one JSON value per line).

**Examples**:
```bash
# Input (3 separate JSON values, one per line):
{"name": "alice", "score": 95}
{"name": "bob", "score": 87}
{"name": "charlie", "score": 92}

# Filter: .name
"alice"
"bob"
"charlie"

# Filter: .score
95
87
92
```

**Behavior**:
- Detect JSONL automatically (try single JSON first, fall back to line-by-line)
- Store as `Vec<Value>` internally (already supported by eval)
- Filter applies to each input value independently
- Results displayed consecutively (no blank lines between)

**Implementation notes**:
- Update `read_input()` or add `parse_input()` that handles both formats
- Single JSON: `vec![serde_json::from_str(&input)?]`
- JSONL: `input.lines().map(|l| serde_json::from_str(l)).collect()`
- Skip empty lines in JSONL mode
- App already handles `Vec<Value>` from filter evaluation

**Test inputs**:
```bash
# Single JSON (existing behavior)
echo '{"a": 1}' | jarq

# JSONL
echo -e '{"a": 1}\n{"a": 2}\n{"a": 3}' | jarq
```

---

## Milestone 6: Pipes

**Goal**: Chain filters with `|`.

**Examples**:
```
.users | .[0]           -> first user
.users | .[] | .name    -> all user names
```

**Parser**:
```rust
fn parse_pipe(input: &str) -> IResult<&str, Filter> {
    let (input, first) = parse_simple_filter(input)?;
    let (input, rest) = many0(preceded(
        delimited(space0, char('|'), space0),
        parse_simple_filter
    ))(input)?;

    Ok((input, rest.into_iter().fold(first, |acc, f| Filter::Pipe(Box::new(acc), Box::new(f)))))
}
```

**Evaluator**: For pipes with `.[]`, apply the right side to each output of the left side (flatmap).

---

## Milestone 7: Live Filtering UX ✓

**Status**: Complete (implemented during earlier milestones)

- ✓ Debounce filter parsing (required for usable UX - avoids null flashing)
- ✓ Error messages with position indication
- Deferred: Spinner for large inputs, syntax highlighting, filter history

---

## Milestone 8: Output Modes ✓

**Goal**: Add toggleable jq-compatible input/output modes.

**Features**:

### Raw Output (`r` to toggle, like jq `-r`)
- When output is a string, print without quotes
- `"hello"``hello`
- Useful for extracting values to use in shell scripts
- Non-strings render normally

### Slurp Mode (`s` to toggle, like jq `-s`)
- Collect all inputs into a single array before filtering
- JSONL `{"a":1}\n{"a":2}` becomes `[{"a":1},{"a":2}]`
- Filter runs once on the combined array
- Useful with JSONL: `.[] | .name` vs slurp + `.[0].name`

### Compact Output (`c` to toggle, like jq `-c`)
- Render JSON on single lines instead of pretty-printed
- Useful for seeing more data at once
- `{"name": "alice", "age": 30}` instead of multi-line

**UI indicators**:
- Show active modes in status bar: `[raw] [slurp] [compact]`
- Or single indicator: `[rsc]` for all three

**Implementation notes**:
- Add `raw_output: bool`, `slurp_mode: bool`, `compact_output: bool` to App state
- Raw: modify `json_to_lines()` to handle string specially
- Slurp: wrap inputs in array before filtering
- Compact: add `json_to_lines_compact()` or flag parameter

---

## Stretch Goals

If you finish early:

- **Copy filter to clipboard**: Keybinding (e.g., `Ctrl+Y` or `Ctrl+C` when not selecting) to copy the current filter expression to system clipboard for use with jq in terminal
- **Non-interactive mode**: `jarq file.json '.users[].name'` or `echo '{}' | jarq '.foo'` outputs filtered result to stdout and exits (like jq), skipping the TUI
- **Optional access**: `.foo?` returns null instead of error
- **Array slices**: `.[2:5]`, `.[:-1]`
- **Object construction**: `{name: .foo, id: .bar}`
- **Array construction**: `[.foo, .bar]`
- **Builtins**: `length`, `keys`, `values`, `type`
- **select()**: `select(.age > 21)`
- **map()**: `map(.name)`

---

## Suggested File Structure

```
src/
  main.rs          # CLI args, setup, run loop
  app.rs           # App state (input, filter, output, scroll position)
  ui.rs            # ratatui rendering
  parser.rs        # nom parser for jq syntax
  eval.rs          # Filter evaluation against JSON values
  filter.rs        # Filter AST types
```

---

## Filter AST

A reasonable starting point:

```rust
#[derive(Debug, Clone)]
pub enum Filter {
    Identity,                           // .
    Field(String),                      // .foo
    Index(i64),                         // .[0], .[-1]
    Iterate,                            // .[]
    Pipe(Box<Filter>, Box<Filter>),     // a | b
    // Later:
    // Optional(Box<Filter>),           // .foo?
    // Slice(Option<i64>, Option<i64>), // .[1:3]
    // ObjectConstruct(Vec<(String, Filter)>),
    // ArrayConstruct(Vec<Filter>),
}
```

Good luck!