Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,16 @@ archives = ["/path/to/archive"]
| `rl doctor` | global config, workspace, stock, roll, vocabulary | no | yes with `--fix` | integrity |
| `rl normalize` | current archive workspace, roll, vocabulary | roll, vocabulary | yes | normalization |
| `rl normalize --photos` | photo folders in current archive workspace | archive folders | yes | photo import |
| `rl batch process` | global config, workspace, roll | roll | no | batch update |
| `rl stock edit` | global config, workspace, roll, vocabulary | roll, vocabulary | no | single-roll editing |
| `rl batch` | global config, workspace, roll | roll | no | batch update |

These scopes are deliberate:

- `rl stock edit` is the manual, one-roll editor;
- `rl batch` is the bulk editor;
- `rl search` shares the same filter language as `rl batch`, but never writes;
- `rl normalize` is for structural cleanup and normalization;
- `rl doctor` is for diagnostics and safe repair.

## Lifecycle

Expand Down Expand Up @@ -112,7 +121,7 @@ rl doctor --fix
| `filesystem.py` | archive tree and file discovery |
| `app/workspace/` | config, stock, roll storage, vocabularies |
| `app/flows/` | interactive flows |
| `app/archive/` | search, stats, batch, normalization |
| `app/archive/` | search, stats, batch, selection, normalization |
| `app/diagnostics/` | doctor |
| `messages/` | localized user-facing text |

Expand All @@ -127,3 +136,4 @@ rl doctor --fix
- English is the default UI language.

Quick start — see [docs/getting-started.md](getting-started.md). Environment setup and CI — see [docs/development.md](development.md). Terms and exact command behavior — see [docs/reference.md](reference.md).
Editing flows — see [docs/editing.md](editing.md).
4 changes: 2 additions & 2 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ python -m unittest discover -s tests
| Read-only | `rl search`, `rl scan`, `rl status`, `rl stats`, `rl vocab` | global config, workspace, roll, vocab | no | no |
| Integrity | `rl doctor`, `rl doctor --fix` | global config, workspace, stock, roll, vocab | no | yes |
| Normalization | `rl normalize --tags` | workspace, roll, vocab | roll, vocab | yes |
| Batch | `rl batch process` | workspace, roll | roll | no |
| Batch | `rl batch` | workspace, roll | roll | no |

`rl load --manual` creates a roll from the film dictionary without changing stock.
For `rl features add` and `rl tags add` you can enter several values separated by commas. Autocomplete works per value, duplicates aren't written, `_` is allowed inside a value.
`rl config lang` applies immediately in the current process because user-facing messages resolve the language at render time.
`rl doctor` checks the global config, workspace config, stock, roll metadata, and vocabularies. Its diagnostics stay in English; only the user-facing UI is localized.
Package version comes from git tags at build time. In a source checkout, `rl --version` falls back to the latest git tag if package metadata is not installed.
`rl update` is a runtime convenience command for local installs; it runs `pip install -U git+https://github.com/katrinio/roll.git@main` in the current interpreter.
`rl update` is a runtime convenience command for local installs; it runs `pip install --no-input --upgrade --force-reinstall git+https://github.com/katrinio/roll.git@main` in the current interpreter.

## CI

Expand Down
160 changes: 160 additions & 0 deletions docs/editing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Editing

`roll` has two editing surfaces:

- `rl stock edit` for one roll at a time;
- `rl batch` for many rolls at once.

They solve different problems and should stay separate.

## Boundaries

| Command | Scope | Typical use |
|---|---|---|
| `rl stock edit` | one roll | manual correction, field-by-field judgment |
| `rl batch` | many rolls | repeated update across a filtered set |
| `rl search` | many rolls | structured lookup, optionally with free text |
| `rl normalize` | archive structure | folder shape, keywords normalization, photo import |
| `rl doctor` | integrity | report problems, safe fixes only |

---

## `rl stock edit`

Use this when you want to inspect and adjust one roll by hand.

What it does:

- selects a single roll;
- edits its metadata fields directly;
- keeps the current values available in the prompts;
- applies changes only after you confirm each value in the interactive flow.

Best for:

- correcting one record;
- changing a camera on one roll;
- refining features or keywords on a single roll;
- adjusting the origin fields on one roll.

Not for:

- mass changes across many rolls;
- filtering by year or film name;
- bulk status updates.

---

## `rl batch`

Use this when you want to apply the same change to many rolls.

What it does:

- selects rolls by filters;
- shows a preview of the target set;
- applies one change to the whole selection;
- asks for confirmation before writing.

Selection is based on filters such as:

- year;
- film name;
- camera;
- status;
- tags or keywords when needed.

Within one filter, comma-separated values mean "match any of these".
Across filters, the selection is cumulative.

Best for:

- changing a camera on many rolls;
- moving a group of rolls to a new status;
- adding the same feature or tag to a batch;
- cleaning up a whole year or a film family.

Not for:

- one-off manual corrections;
- browsing a single record in detail;
- workflows that need per-field judgment on each roll.

---

## Boundary

Use this rule:

- one roll, manual judgment, many fields -> `rl stock edit`;
- many rolls, one repeated operation -> `rl batch`.

## `rl search`

Use this when you want to find rolls with the same filter language as `rl batch`, but without writing anything.

What it does:

- accepts the same structured filters as `rl batch`;
- keeps free-text search for ad hoc lookup;
- shows matching rolls and their basic metadata.

Selection is based on the same filters:

- year;
- film name;
- camera;
- status;
- tags or keywords.

Within one filter, comma-separated values mean "match any of these".
Across filters, the selection is cumulative.

Best for:

- finding rolls by year and status;
- narrowing by film family before a batch update;
- looking up tags or camera names;
- searching by a short free-text fragment when you do not need a structured filter.

Not for:

- changing metadata;
- structural normalization;
- integrity repair.

If you need both at different times, start with `stock edit` for the outlier and use `batch` for the rest.

---

## Examples

```bash
rl search --year 2025 --status loaded
```

```bash
rl search --film "Kodak Gold 200, Ilford HP5 Plus" --query balcony
```

```bash
rl stock edit
```

```bash
rl batch --year 2025 --film "Kodak Gold 200, Ilford HP5 Plus" --set camera="Pentax K1000"
```

```bash
rl batch --year 2025 --set status=processed
```

```bash
rl batch --film "Kodak Gold 200" --add-tag "summer,belgrade"
```

## Related

- [Getting Started](getting-started.md)
- [Architecture](architecture.md)
- [Reference](reference.md)
6 changes: 3 additions & 3 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ rl search kir balcony # half a year later — found it
| Find / inspect | `rl search`, `rl scan`, `rl status`, `rl stats [-v]`, `rl vocab` |
| Integrity | `rl doctor`, `rl doctor --fix`, `rl normalize --tags` |
| Photo import | `rl normalize --photos` |
| Batch | `rl batch process` |
| Editing | `rl stock edit`, `rl batch` |

## Out of Scope

sync between machines · cloud · web UI · migrating old formats · image processing

The CLI defaults to English in the global config and `rl config lang` changes it.
`rl --version` prints the current version. If a newer git tag is available in the current checkout, it also prints a short update hint and points to `rl update`.
`rl update` upgrades the installed package from the GitHub repo with `pip`.
`rl update` reinstalls the app from the GitHub repository.
`rl normalize --photos` works in the current archive workspace and can turn a raw photo folder into an archive month based on the dominant photo date.

## Rule

If it doesn't help find a roll from memory faster — it's out of scope.

Architecture and exact behavior — see [docs/architecture.md](architecture.md). Terms and exact rules — see [docs/reference.md](reference.md). Development setup — see [docs/development.md](development.md).
Architecture and exact behavior — see [docs/architecture.md](architecture.md). Editing flows — see [docs/editing.md](editing.md). Terms and exact rules — see [docs/reference.md](reference.md). Development setup — see [docs/development.md](development.md).
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ Start here:
| Doc | What it answers |
|---|---|
| [Getting Started](getting-started.md) | What the tool does and how to begin |
| [Editing](editing.md) | How to use `stock edit` and `batch` |
| [Architecture](architecture.md) | What files exist, what commands read and write, how the archive is structured |
| [Reference](reference.md) | Exact terms, behavior, and rules |
| [Development](development.md) | Setup, checks, and CI |

If you are new to the tool, read [Getting Started](getting-started.md) first, then [Architecture](architecture.md). Use [Reference](reference.md) when you need exact rules.
If you are new to the tool, read [Getting Started](getting-started.md) first, then [Editing](editing.md) and [Architecture](architecture.md). Use [Reference](reference.md) when you need exact rules.
8 changes: 8 additions & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ Brings folder names to a consistent shape: builds a plan, asks for confirmation,

---

## Editing

`rl stock edit` is a single-roll manual editor. `rl batch` is a bulk editor that applies one change to many rolls selected by filters. `rl search` uses the same filter language for read-only lookup and can also keep free-text search.

See [docs/editing.md](editing.md) for the shared editing model and the boundary between the two commands.

---

## Doctor

Integrity check: global config, workspaces, stock, roll metadata, vocabularies, suspicious and unindexed folders. Diagnostics stay in English. `--fix` applies safe fixes and prints them in a separate block; `-v` shows the full list of fixes.
Expand Down
5 changes: 5 additions & 0 deletions src/roll/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from roll.cli import app


if __name__ == "__main__":
app()
93 changes: 80 additions & 13 deletions src/roll/app/archive/batch.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,102 @@
from dataclasses import replace
from pathlib import Path

import typer

from roll.app.workspace.roll_store import update_roll_status
from roll.app.archive.search import find_rolls
from roll.app.archive.search import RollIndex
from roll.app.archive.selection import select_rolls
from roll.app.workspace.roll_store import (
RollMetadata,
load_roll_metadata,
save_roll_metadata,
)
from roll.helpers.output import echo_list
from roll.messages import Msg


def process_archives(archives: list[Path]) -> int:
loaded_rolls: list[Path] = []
return batch_rolls(archives, statuses=["loaded"], status="processed")

for archive in archives:
for roll in find_rolls(archive):
if roll.status == "loaded":
loaded_rolls.append(roll.folder)

if not loaded_rolls:
typer.echo(str(Msg.BATCH_NO_LOADED))
def batch_rolls(
archives: list[Path],
*,
year: str | None = None,
films: list[str] | None = None,
cameras: list[str] | None = None,
statuses: list[str] | None = None,
status: str | None = None,
set_camera: str | None = None,
add_features: list[str] | None = None,
add_tags: list[str] | None = None,
) -> int:
rolls = select_rolls(
archives,
year=year,
films=films,
cameras=cameras,
statuses=statuses,
)

if not rolls:
typer.echo(str(Msg.NO_RESULTS))
return 0

typer.echo(f"{Msg.BATCH_WILL_PROCESS} {len(loaded_rolls)}")
echo_list((str(path) for path in loaded_rolls))
preview = [str(roll.folder) for roll in rolls]
typer.echo(f"{Msg.BATCH_WILL_PROCESS} {len(preview)}")
echo_list(preview)

if not typer.confirm(Msg.BATCH_CONFIRM, default=False):
return 0

changed = 0
for folder in loaded_rolls:
update_roll_status(folder / "roll.toml", "processed")
for roll in rolls:
updated = _apply_changes(
roll,
status=status,
set_camera=set_camera,
add_features=add_features,
add_tags=add_tags,
)
save_roll_metadata(roll.folder / "roll.toml", updated)
changed += 1

typer.echo(f"{Msg.BATCH_PROCESSED} {changed}")
return changed


def _apply_changes(
roll: RollIndex,
*,
status: str | None,
set_camera: str | None,
add_features: list[str] | None,
add_tags: list[str] | None,
) -> RollMetadata:
metadata = load_roll_metadata(roll.folder / "roll.toml")
updated = metadata
if status is not None:
updated = replace(updated, status=status)
if set_camera is not None:
updated = replace(updated, camera=set_camera)
if add_features:
updated = replace(
updated,
features=_merge_unique(updated.features, add_features),
)
if add_tags:
updated = replace(
updated,
keywords=_merge_unique(
updated.keywords, [item.upper() for item in add_tags]
),
)
return updated


def _merge_unique(existing: list[str], new_values: list[str]) -> list[str]:
merged = list(existing)
for value in new_values:
if value not in merged:
merged.append(value)
return merged
Loading
Loading