- DIY
- A
Windows Recall Alternative on Linux
I created a simple Windows Recall alternative on Linux, showing what I achieved and how it works.
Yes, yes, you read that correctly. The equivalent of that same functionality without clouds, without subscriptions, without neural networks, and without a bunch of dependencies. And it turns out, implementing it wasn't so difficult after all.
For those who aren't aware, Windows Recall is a feature that Microsoft announced in 2024 for Copilot+ PC. The concept is simple: the system periodically takes screenshots of the screen, uses AI to recognize text and content on them, and then you can find anything you’ve ever seen on the screen simply by typing a query. Something like “find that price website I looked at last week,” and it finds it. It sounds convenient, but it caused a lot of debates about privacy. Later, they postponed the feature, reworked it, and so on.
I'll be honest, I've never actually used Recall. I switched to Linux a long time ago and don't use Windows at all. I read about it in articles and news, watched a couple of videos, and got the gist of it.
And this is where things got interesting. Over several years, I accumulated a number of scripts for my Wayland setup, such as screenshots, clipboard utilities, and stuff like that. At some point, I added a bit more logic to them, and suddenly, it turned into something very similar to that Recall feature. Not intentionally, it just happened that way.
Let me warn you right away, this is not a universal solution and won't work for everyone. It's tailored for a specific stack: Wayland, tofi, grim, cliphist. If you’re using GNOME or KDE, you might need to adapt it or just skip it entirely. But if you’re already using a similar set of utilities, integrating this will be very easy, and it's not “code for the sake of code.” I actually use this for real.
What this actually consists of
The whole system relies on a few utilities that are commonly used in a Wayland environment:
grim- takes a screenshot of a screen areaslurp- selects an area with the mousetesseract- OCR, recognizes text in an imagetofi- a dmenu-like launcher, used for searchingwl-clipboard- clipboard copyingcliphist- clipboard historynotify-send- notifications
These are all separate small Unix utilities that do one thing and do it well. Compose their powers, as they say.
How it works
A system of two scripts. The first one screen.sh takes a screenshot and immediately launches tesseract in the background, which recognizes the text and saves it next to the image as a .txt file. So we get a pair: screenshot-2025-03-27-143055.png and screenshot-2025-03-27-143055.txt.
The second script ssearch.sh takes all these .txt files, builds a list from them for tofi, and you can search for the needed screenshot by text. Found it — the image opens, the text is copied to the clipboard. And that’s the whole recall.
screen.sh screenshot with OCR
#!/bin/bash
set -euo pipefail
SCREENSHOT_DIR="${SCREENSHOT_DIR:-$HOME/Pictures/screens}"
OCR_LANG="${OCR_LANG:-rus+eng}"
SLURP_ARGS=(-d -b 1B1F2866 -c 89b4faff -w 1)
GRIM_ARGS=(-t png -l 3)
die() { notify-send "screen.sh" "$1" -i dialog-error -t 4000; exit 1; }
need() { for cmd in "$@"; do command -v "$cmd" &>/dev/null || die "missing: $cmd"; done; }
Environment variables so they can be overridden without editing the script. OCR_LANG=rus+eng means tesseract will recognize both Russian and English simultaneously, which is important because I have mixed content on my screen.
run_ocr() {
tesseract "$1" - -l "$OCR_LANG" --psm 3 2>/dev/null \
| grep -v '^*$' \
| sed 's/\+/ /g; s/^ //; s/ $//'
}
--psm 3 is an automatic page segmentation mode, suitable for arbitrary content. stderr is sent to /dev/null because tesseract likes to print garbage there even when everything is fine. grep and sed remove empty lines and extra spaces.
save_sidecar() {
run_ocr "$1" > "${1%.png}.txt" 2>/dev/null || true
}
The sidecar is always created even if tesseract finds nothing; the file is created empty. This is needed so that the indexer in ssearch.sh understands that this PNG has already been processed and does not touch it again.
command -v tesseract &>/dev/null && { save_sidecar "$FILEPATH" & disown; }
& disown runs it in the background and detaches it from the current process. The screenshot notification appears immediately, tesseract runs in parallel and does not slow things down.
By the way, the notification is interactive:
ACTION=$(notify-send "screenshot saved" "$FILENAME" \
-i "$FILEPATH" \
-t 7000 \
-A "default=open folder" \
-A "edit=edit" \
-A "ocr=copy text")
case "${ACTION:-}" in
default) open_folder "$FILEPATH" ;;
edit) open_editor "$FILEPATH" ;;
ocr) ocr_to_clipboard "$FILEPATH" ;;
esac
Three buttons: open folder, open in editor (swappy by default), or OCR directly to clipboard. This works via libnotify action, requires swaync or a dunst version that supports actions.
There is also a --ocr mode that does not save the file at all; you just select an area and the text goes directly to the clipboard. Convenient when you need to quickly copy text from an image.
ssearch.sh search by history
if ; then
while IFS= read -r png; do
txt="${png%.png}.txt"
&& continue
run_ocr "$png" > "$txt" 2>/dev/null || true
done < <(find "$SCREENSHOT_DIR" -name "screenshot-*.png" -type f | sort)
fi
--index is needed if you already have accumulated screenshots without .txt files next to them. Run it once; it processes everything. Checks for .txt before running tesseract, avoiding unnecessary work.
if ; then
while IFS= read -r txt; do
png="${txt%.txt}.png"
|| continue
base=$(basename "$txt" .txt)
dt="${base#screenshot-}"
stamp="${dt:0:10} ${dt:11:2}:${dt:13:2}:${dt:15:2}"
snippet=$(head -3 "$txt" | tr '\n' ' ' | cut -c1-120)
printf '%s\t%s\t%s\n' "$png" "$stamp" "$snippet"
done < <(find "$SCREENSHOT_DIR" -name "screenshot-*.txt" -type f | sort -r)
fi
--list outputs a TSV with the PNG path, date, and the start of the text. Sorted descending with the newest on top. head -3 takes the first three lines from the OCR, usually enough for a preview in tofi.
The main mode is a pipe through tofi:
selected=$(
"$0" --list | while IFS=$'\t' read -r png stamp snippet; do
clean=$(printf '%s' "$snippet" | iconv -f utf-8 -t utf-8 -c 2>/dev/null | tr '\t\n' ' ')
printf '%s | %s\t%s\n' "$stamp" "$clean" "$png"
done \
| tofi --prompt-text "> " \
--fuzzy-match true \
--auto-accept-single false \
--width 1100 \
--height 600
) || exit 0
iconv -f utf-8 -t utf-8 -c removes corrupted bytes that tesseract sometimes outputs as garbage, especially on poor-quality screenshots. --fuzzy-match true in tofi allows searching approximately rather than exact matches, which helps when OCR makes mistakes.
After selection:
png=$(printf '%s' "$selected" | awk -F'\t' '{print $NF}')
if ; then
txt="${png%.png}.txt"
&& cat "$txt" | iconv -f utf-8 -t utf-8 -c | wl-copy
xdg-open "$png"
fi
Text copied to clipboard, image opens. You can either immediately paste the recognized text or view the original screenshot.
Installation
Packages for Arch:
sudo pacman -S grim slurp tesseract tesseract-data-rus wl-clipboard libnotify swappy
tesseract-data-eng will likely be installed as a dependency, but it’s better to check.
Put the scripts wherever you like, then make them executable:
chmod +x screen.sh ssearch.sh
In Hyprland, bind:
bind = , Print, exec, ~/wayland/scripts/screen.sh
bind = SHIFT, Print, exec, ~/wayland/scripts/screen.sh --ocr
bind = SUPER, F, exec, ~/wayland/scripts/ssearch.sh
For Sway, do the same using bindsym.
If you already have a bunch of old screenshots:
./ssearch.sh --index
Wait. This depends on the number of files and processor speed.
What’s wrong and what can be improved
OCR doesn’t work perfectly. On dark themes with small fonts, tesseract sometimes produces nonsense. You can tweak --psm and --oem, I find psm 3 works best for mixed content. The search is done by OCR text, not meaning. That is, you need to remember approximately what was written there. But honestly, there’s no AI, no vector search, just fuzzy string matching. The folder with screenshots grows over time. You can set up rotation with a systemd timer or cron to delete files older than N days, or just clean it manually from time to time.
My scripts are on GitHub https://github.com/Jahamars/wayland
Write comment