# 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
# Field access
# Nested field access
# Array index
# Array slice
# Iterate array elements
# 2
# 3
# Pipe filters together
# 2
# Optional field access (no error if missing)
# Array/object construction
# Builtin functions
echo '[[1, 2], [3, 4]]' | jq 'flatten'
# [1, 2, 3, 4]
# 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)
# JSONL
echo -e '{"a": 1}\n{"a": 2}\n{"a": 3}' | jarq
```
---
## Milestone 6: Pipes
**Goal**: Chain filters with `|`.
**Examples**:
```
```
**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!