scry
grep for images. Find images by what they show, the text in them, or their metadata.

The first command ranks images by what they show, best first. The second finds text in images. The third keeps the photos from summer 2024 that have a GPS location. The fourth shows the matches in scatter mode, where you pick one by eye.
scry prints paths, so it works in pipelines. It runs on your CPU, and after a one-time model download it works offline.
Install
Linux only for now (x86_64 and aarch64).
# Static binary, installed to ~/.local/bin
|
# With cargo-binstall
# From source (Rust 1.94 or newer)
# With Nix
The crate is scry-cli, because scry is taken on crates.io. The binary is scry.
The first -q search downloads the SigLIP 2 model (about 500 MB), and -l needs only its image part (about 190 MB). The first -t or -o search downloads the OCR models (about 12 MB). scry checks each model against a pinned SHA-256 hash and keeps it in ~/.cache/scry, or in $SCRY_CACHE.
Examples
# A query in any language
# Bears, but not teddy bears
# Images that look like this one
# Summer 2024, a Fujifilm camera, 4000 px wide or more
# PNG images from the last 7 days
# The newest screenshot that shows an error
# The text that scry reads in an image, and each invoice number with its file
# The receipts, newest first: choose by meaning, then sort by date
|
# Pick images by eye and open them
|
# Read NUL-separated paths from another tool
| |
Options
scry [OPTIONS] [PATH]...
Each PATH is a file or a directory. With no PATH, scry reads image paths from stdin, one per line, if stdin is a pipe or a file. Otherwise it searches the current directory. An image must pass all the options that you give.
| Option | |
|---|---|
| Meaning | |
-q, --query <TEXT> |
Rank images by how well TEXT describes them, in any language. |
-l, --like <IMAGE> |
Rank images by how similar they look to IMAGE. IMAGE itself is not in the output. |
-x, --not <TEXT> |
Drop images that are closer to TEXT than to the query. Repeatable. Needs -q. |
--min <SCORE> |
Drop images that score below SCORE. Needs -q or -l. See scores. |
| Text in images | |
-t, --text <REGEX> |
Keep images whose text matches REGEX. Repeatable: all must match. |
-i, --ignore-case |
Ignore case in REGEX. Without -i, a REGEX ignores case only if it has no capital letter. |
-o, --show-text |
Print the text after each path and a tab. With -t, print only the parts that match, separated by tabs. |
| Metadata | |
--width, --height <RANGE> |
Image size in pixels. |
--date <RANGE> |
The date the photo was taken (EXIF). For an image without one, such as a screenshot, the time the file changed. |
--camera <REGEX> |
Camera make and model, for example 'fujifilm x-t4'. Ignores case. |
--gps, --no-gps |
Only images with, or without, a GPS location. |
| Files | |
-g, --glob <GLOB> |
Include paths that match GLOB, or exclude them with a leading !. A GLOB with a / matches the path below each PATH. Ignores case. Repeatable. Also filters paths from stdin. |
--hidden |
Search hidden files and directories. |
--no-ignore |
Do not read .gitignore and .ignore files. |
| Output | |
-s, --scatter |
Pick images by eye. See scatter mode. |
--order <ORDER> |
Sort by --date: newest or oldest first. Not with -q or -l. |
-n, --limit <N> |
Print at most N paths, or all. Default: 20 with -q or -l and no --min or -s, else all. |
-S, --score |
Print the score and a tab before each path. Needs -q or -l. |
-p, --preview |
Show a thumbnail above each path when stdout is a terminal. |
-0, --null |
Separate paths with NUL, on stdin and on stdout. |
A RANGE is A..B, A.., ..B or A. Both ends are inclusive.
- Pixels are integers.
- A date is
YYYY,YYYY-MMorYYYY-MM-DDand covers the full period. So--date 2024-06..2024-08ends with August 31. - An age is
Nh,Nd,Nw,NmoorNyback from now, and needs... So--date 7d..is the last 7 days, and..1yis older than one year. - All times are local. To keep only camera photos, add
--camera ..
scry uses one order for each search. With -q or -l, the best match comes first. With --order, the newest or oldest image comes first. Else, scry prints each match when it finds it, and paths from stdin keep their order. A sorted search must see all images before it prints the first path. Then -n cuts the slow steps short: scry reads the text in the images in that order and stops when it has enough matches.
Exit status: 0 scry printed a path, 1 nothing matched, 2 an error (also when nothing matched and some files could not be read), 130 you quit scatter mode.
Scatter mode
scry -s shows the images one at a time, each at a different place on the screen. A new image lands on top and covers the part of the screen that has been visible longest. No image disappears while you can see a part of it, so your eye goes only to the newest one.
With -q or -l, scry scores all images before it shows the first one. Then the best matches come first and are bigger. With --order, the images come in date order. Else, the images come in random order with no model download, so scatter mode starts at once and works well to browse a folder.
| Key | Action |
|---|---|
Space |
Pause or play. |
←, → |
Go back or forward one image, and pause. Hold ← to rewind. → at the newest image shows the next one. |
a to z |
While paused, select the image with that hint letter. |
A to Z |
Show that image full screen. There, ← and → go through the images, Enter prints it with the marked images, Tab marks it, and any other key goes back. |
Tab |
Mark or unmark the selected image. |
Enter |
Print the marked images in mark order, or else the selected image, and quit. With no selection, select the best image in view. |
+, - |
Show images faster or slower. |
], [ |
Make images larger or smaller. |
Esc, Ctrl-C |
Quit and print nothing. |
When you go back, the newest image on screen is selected and gets the hint letter a. So if you see a good image too late, press ← until it is selected, then press Enter, or A to see it full screen. Space plays on from there.
Like fzf, scatter mode draws on the terminal and writes only the result to stdout. So scry -s ... | xargs ... and $(scry -s ...) work.
kitty, Ghostty, WezTerm, foot and other terminals with sixel support show full-resolution images. Other terminals get Unicode half blocks, at low resolution. kitty and Ghostty read the images from shared memory when scry runs on the same computer, which keeps scatter mode fast, also in tmux. In tmux, scry turns on allow-passthrough for its own pane.
How it works
scry runs the fast filters first, and the slow ones only on the images that are left: path filters, then the image header and EXIF, then SigLIP 2 embeddings, then OCR. Inference runs on rten and OCR on ocrs, both in Rust. scry needs no Python, no ONNX Runtime and no GPU.
scry has no vector database. It compares the query with each candidate directly, which is exact and works with all filters.
The cache, ~/.cache/scry/cache.redb, keeps the embeddings, OCR text and metadata of each file until the file changes. The first search over a folder is slow: on an 8-core laptop CPU (Ryzen 9 8945HS), scry embeds or reads the text of about 8 to 10 images per second. Later searches process only new or changed images, and take well under a second.
Scores
-q: the probability (0 to 1) that the text describes the image. It does not depend on the other images, so a--minvalue means the same in every folder.-l: the cosine similarity of the two images, from -1 to 1.-Sprints 3 significant digits, with an exponent below 0.0001:2.5e-6is 0.0000025.
The probabilities are small. A correct image for an English description usually scores above 0.01, and an unrelated image below 0.0001. Queries in other languages score lower. To choose --min, run a search with -S once and look at the scores of your images.
FAQ
Which formats does scry read? JPEG, PNG, GIF, WebP, BMP and TIFF. Not HEIC, AVIF, JPEG XL or camera RAW yet. scry counts the files that it skips and tells you at the end. It also skips images too large to decode safely (more than 256 MiB of pixels).
Why does -t not find a text that I can see?
Use -o to see the text that scry reads. OCR can read words in an order that you do not expect, or read a letter wrong. A meme with "ROAD TRIP" on two lines can read as O ROAD IH TRIP, so -t 'road.*trip' finds it and -t 'road trip' does not. OCR reads only Latin script for now.
Why are some images missing?
By default, scry skips hidden files and the paths in .gitignore. Use --hidden and --no-ignore to include them. scry follows symbolic links to images, but not to directories.
How do I clear the cache, or use scry without internet?
Delete ~/.cache/scry/cache.redb to clear the cache. To use scry on a computer without internet, copy ~/.cache/scry/models from a computer where scry downloaded the models.
Support
If scry saves you time, you can buy me a coffee.
Credits
SigLIP 2 is by Google, under Apache 2.0, and the ONNX export is from onnx-community. The OCR models and the inference runtime are from Robert Knight's ocrs and rten. ratatui-image draws the images in the terminal. The photos in the demo are from Unsplash, through Lorem Picsum.
License
Licensed under either of Apache License 2.0 or MIT license, at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.