This project is inspired by the King himself, Terry A. Davis (RIP). I noticed that he had most of his career and life on tape. I thought to myself, maybe it would be nice to have a utility to record your journal entries talking. Not a bad idea huh? This tool was made with AI (Sorry Terry, you probably wouldn't like me writing a tool like this using AI). This tool was originally written with bash, until I realized it was getting too big and the speed was suffering.
- ⚡ Blazing Fast Rust Core: Instant sub-millisecond CLI startup, zero Python dependency, zero interpreter overhead.
- 🖼️ In-Terminal Storyboard & Interactive MPV Peek: Fast in-terminal 2x2 storyboard previews in
fzfviachafa, plus instant floating video peek (Ctrl-P / Space insidefzf). - 📼 Optional Retro VHS / Camcorder OSD Overlay: Opt-in authentic on-screen timestamp and title stacked in the bottom-left corner with embedded retro fonts (
vt323,silkscreen,press_start_2p,share_tech_mono). - 📼 Ultra-Compact SVT-AV1 + Opus: Compress hours of speech video into negligible disk space (~15 MB per hour in
terrymode). - 🔒 Zero-Disk-Leak RAM Streaming: Encrypted videos pipe directly through RAM into
mpv(gpg -d | mpv -) without writing plaintext to disk. - 🚀 Zero-Friction Recording: Run
vj recordand close preview to save. Background encoding runs silently via low OS priority (ionice/nice). - 📅 Native Jalali (Solar Hijri) & Gregorian: Seamless calendar timestamps calculated natively in Rust with zero lag.
- 📱 Built-In Mobile Web Upload Server: Native async HTTP server with embedded drag-and-drop web UI and in-terminal ANSI QR code (
vj inbox-server). - 🔍 Vim-First
fzfBrowser: Interactive browsing with live metadata, note, and storyboard contact sheet preview panes. - 🗑️ Batch Deletion & Interactive
fzfVault Management: Multi-select deletion with live preview panes (vj delete) or batch deletion by IDs (vj delete id1 id2 ...). - ⚙️ Modern TOML Configuration: Clean XDG-standard configuration at
~/.config/vj/config.toml. - 🐚 Auto Shell Completions: Native completions for Fish, Bash, Zsh, PowerShell, and Elvish via
clap_complete. - 🦄 Emacs & Org-Mode Integration: Native
vj.elpackage for clickablevj:hyperlinks, in-buffer storyboard previews, and recording directly inside Org-mode.
vj is designed and tested for GNU/Linux and relies on standard CLI tools for video capture, rendering, and security:
ffmpeg(compiled withlibsvtav1,libopus,afftdn,drawtext) — video capture, audio filters, and AV1 encodingmpv— smooth, zero-disk RAM-streamed video playbackfzf— interactive TUI navigation and multi-select deletiongnupg(optional) — AES-256 encrypted vault managementchafa(ortimg/viu) — terminal cell storyboard rendering
Install dependencies via your package manager:
# Arch Linux
sudo pacman -S ffmpeg mpv fzf gnupg chafa
# Ubuntu / Debian
sudo apt update && sudo apt install ffmpeg mpv fzf gnupg chafa
# Fedora
sudo dnf install ffmpeg mpv fzf gnupg chafaNote
vj is currently tested and built for GNU/Linux environments (x86_64 and aarch64).
Download the latest release tarball directly from GitHub Releases:
# For x86_64 Linux:
curl -sSL https://github.com/MiliAxe/vj-rs/releases/latest/download/vj-linux-x86_64.tar.gz | tar -xz -C ~/.local/bin
# For aarch64 (ARM64) Linux:
curl -sSL https://github.com/MiliAxe/vj-rs/releases/latest/download/vj-linux-aarch64.tar.gz | tar -xz -C ~/.local/bin
# Install shell completions
vj completions install# Clone the repository
git clone https://github.com/MiliAxe/vj-rs.git
cd vj-rs
# Compile and install to ~/.local/bin with completions
make install
# Or system-wide install (optional)
sudo make install PREFIX=/usr/localTo uninstall:
make uninstallvj stores each entry in a self-contained, human-readable timestamp folder under your journal directory (~/Videos/Journal/entries by default):
~/Videos/Journal/
├── entries/
│ ├── 1405-06-04_12-33-03/
│ │ ├── video.mkv # Compressed SVT-AV1 + Opus video (or video.mkv.gpg)
│ │ ├── thumb.jpg # 2x2 storyboard contact sheet (or thumb.jpg.gpg)
│ │ ├── meta.json # Profile, resolution, fps, tags, timestamp (or meta.json.gpg)
│ │ └── note.md # Markdown notes written during/after recording (or note.md.gpg)
│ └── 1405-06-05_18-20-00/
│ └── ...
└── inbox/ # Drop zone for mobile uploads & external files to import
- Zero Plaintext on Disk: When encrypted, video streams are decrypted in-memory and piped directly into
mpv(gpg -d | mpv -) without writing plaintext video to disk. - Asynchronous Compression: New recordings are captured instantly to a temporary buffer and compressed in the background via low I/O and CPU scheduling (
nice&ionice), leaving your terminal immediately available.
vj features an instant preview workflow inside fzf (vj play / vj delete):
- In-Terminal 2x2 Storyboard:
- In
vj play,vj delete, orvj preview <id>, entries display a 4-frame contact sheet rendered directly inside the terminal cells usingchafa(ortimg/viu).
- In
- Interactive Floating Video Peek:
- While browsing in
vj playorvj delete, press Ctrl-P or Space to pop up a floating, borderless muted video loop in the corner of your screen. Press q or Esc to dismiss.
- While browsing in
All overlay features are completely disabled by default. When you want the retro camcorder aesthetic, pass -O / --overlay or enable retro_overlay = true in config.toml.
| Font Identifier | Style / Era | Description |
|---|---|---|
vt323 (default) |
DEC VT323 CRT / VHS | Iconic tall retro VHS & CRT phosphor terminal font |
silkscreen |
90s Handheld Camcorder | Ultra-crisp pixel matrix font, ideal for compact/potato |
press_start_2p |
8-Bit Arcade / Micro | Classic 1980s retro gaming & computer pixel typography |
share_tech_mono |
Cyberpunk HUD / Sci-Fi | Modern vintage high-tech monospace HUD display font |
View all recommended fonts and styles:
vj fonts- Bottom-Left Corner (Stacked):
- Top Line: Custom entry title (e.g.
Trip to Japan) - Bottom Line: Date and time timestamp (e.g.
1405-05-30 18:12:05)
- Top Line: Custom entry title (e.g.
# Clean recording (no overlay by default)
vj record
# Opt-in to retro OSD overlay
vj record -O
# Record with custom title (stacked right above timestamp in bottom-left)
vj record -O -t "Trip to Japan"
# Record with custom font size (e.g. 28px)
vj record -O --font-size 28
# Record with 90s camcorder pixel font & white styling
vj record -O --overlay-font silkscreen --overlay-style camcorder_white --font-size 18vj supports user-defined lifecycle hooks configured in config.toml. Every hook command runs via sh -c and receives the event payload both as VJ_* environment variables and as JSON on stdin.
Available events:
| Event | Fires when | Aborts on blocking failure |
|---|---|---|
pre_record |
Before ffmpeg capture starts | Yes — recording is cancelled |
post_record |
After entry folder + meta.json are written | No |
post_encode |
After AV1 encode (+ thumbnail + encryption) completes, including background encoders | No |
post_import |
After each inbox file is imported | No |
pre_play |
Before mpv launches | Yes — playback is cancelled |
post_play |
After playback finishes | No |
pre_delete |
Before an entry directory is removed | Yes — deletion is cancelled |
post_delete |
After the entry directory is removed | No |
Payload fields: event, entry_id, entry_dir, profile, title, tags, encrypted, file → available as $VJ_EVENT, $VJ_ENTRY_ID, $VJ_ENTRY_DIR, $VJ_PROFILE, $VJ_TITLE, $VJ_TAGS, $VJ_ENCRYPTED, $VJ_FILE.
In addition to the $VJ_* environment variables, every hook receives the complete event payload as a single line of JSON piped to its stdin. This makes hooks scriptable with standard tools (jq, Python, Node, ...) without fragile string parsing of shell variables:
{"event":"post_encode","entry_id":"1405-06-04_12-33-03","entry_dir":"/home/user/Videos/Journal/entries/1405-06-04_12-33-03","profile":"terry","title":"Trip to Japan","tags":["dev","log"],"encrypted":true,"file":"/home/user/Videos/Journal/entries/1405-06-04_12-33-03/video.mkv.gpg"}Example — using jq to read structured fields from stdin:
[[hooks.post_encode]]
# Extract tags as a JSON array for downstream tooling
run = "jq -r '.tags[]' >> \"$VJ_ENTRY_DIR/tag_index.txt\""Notes on the payload format:
- Fields that don't apply to an event are
null(e.g.fileisnullforpre_record, since the video doesn't exist yet). The corresponding$VJ_*variable is set to an empty string fornull. - Booleans arrive as JSON
true/falseon stdin and as the stringstrue/falsein$VJ_*variables. tagsis a JSON array on stdin and a comma-separated string in$VJ_TAGS.
The [[hooks.<event>]] syntax is a TOML array of tables: simply repeat the header once per hook. Every occurrence appends an independent hook, and they run sequentially, top-to-bottom in file order. Each hook gets its own blocking flag, so mixing fire-and-forget and gate-keeping hooks on the same event is fine:
# ~/.config/vj/config.toml
[[hooks.post_encode]]
run = "notify-send vj \"Entry $VJ_ENTRY_ID encoded\""
[[hooks.post_encode]]
run = "rclone copy \"$VJ_ENTRY_DIR\" remote:journal-backup >> /tmp/vj_sync.log 2>&1"
[[hooks.post_encode]]
# Transcribe new entries with whisper
run = "whisper.cpp -m base.en \"$VJ_FILE\" > \"$VJ_ENTRY_DIR/transcript.txt\" &"Ordering & failure semantics across multiple hooks:
- Hooks execute one after another, in the order written above.
- If any hook fails with a non-zero exit and
blocking = true, dispatch stops immediately: the operation is aborted (forpre_*events) and any remaining hooks for that event do not run. - If a non-
blockinghook fails, only a warning is printed — the remaining hooks still run.
Inspect and debug hooks:
vj hooks # List all configured hooks
vj hooks --test post_encode # Fire a test payload at every post_encode hook| Command | Description | Example |
|---|---|---|
vj record |
Start live webcam capture & preview | vj record -p terry |
vj record -c |
Select camera interactively or by device path/index | vj record -c / vj record -c 1 |
vj record -D |
Record with microphone noise suppression (afftdn) |
vj record -D |
vj record -t "..." |
Record with title, tags, or notes | vj record -t "Life Update" --tags "dev,log" -n |
vj record -O |
Record with retro OSD date/time overlay | vj record -O --overlay-font silkscreen --font-size 20 |
vj import |
Multi-select import from inbox with video preview pane | vj import |
vj import -D [files...] |
Import videos with microphone noise suppression | vj import ~/Downloads/vid.mp4 -D |
vj inbox-server |
Start local upload server with phone QR code | vj inbox-server 8080 |
vj play |
Interactive fzf browser with live storyboard & metadata |
vj play |
vj play <id> |
Play specific entry directly in mpv |
vj play 1405-05-30_12-33-03 |
vj preview <id> |
Print metadata, note, and terminal storyboard | vj preview 1405-05-30_12-33-03 |
vj preview-inbox <file> |
Inspect format, resolution, codec, and duration | vj preview-inbox ~/video.mp4 |
vj hooks |
List configured lifecycle hooks | vj hooks |
vj hooks --test <event> |
Fire a test payload at an event's hooks | vj hooks --test post_encode |
vj list |
List entries in formatted table (-q for raw IDs) |
vj list -q |
vj random |
Jump into a random historical recording | vj random |
vj delete |
Interactive fzf multi-select browser to delete entries |
vj delete |
vj delete [ids...] |
Batch delete one or multiple entries by ID | vj delete 1405-05-30_12-33-03 1405-05-30_14-00-00 |
vj delete -f [ids...] |
Delete entries without confirmation prompt | vj delete -f 1405-05-30_12-33-03 |
vj encrypt <id|all> |
Encrypt entry or whole vault with AES-256 | vj encrypt all |
vj decrypt <id|all> |
Decrypt entry or whole vault to plaintext | vj decrypt all |
vj stats |
Display storage, streaks, and Retro CRT contribution heatmap | vj stats |
vj stats -m 6 |
Display past 6 months in contribution heatmap | vj stats -m 6 |
vj stats -y |
Display full 12 months (1 year) in contribution heatmap | vj stats -y |
vj profiles |
List available built-in & custom compression profiles | vj profiles |
vj fonts |
List recommended retro fonts and styles | vj fonts |
vj config |
Open configuration in $EDITOR (nvim/vim) |
vj config |
vj completions |
Output or auto-install shell completions | vj completions install |
Track your daily journaling consistency directly inside your terminal (vj stats):
================== JOURNAL STATS ==================
Location: /home/mili/Videos/Journal/entries
Inbox: /home/mili/Videos/Journal/inbox
Calendar: jalali
Total Entries: 24
Plaintext: 24
Encrypted: 0
Storage: 192.4 MB
Recorded Days: 18
Streaks:
:: Current Streak: 4 days (1405-06-02 -> Today)
:: Longest Streak: 11 days
:: Active Days: 18 / 90 days (20%)
Activity (Past 3 Months):
Kho Tir Mor Sha
Sat ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
Sun ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ▒
Mon ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ▒
Tue ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
Wed ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ▓
Thu ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
Fri ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ▒
Legend: ░ 0 ▒ 1 ▓ 2 █ 3+
# View 3 months (default)
vj stats
# View 6 months
vj stats -m 6
# View full 12 months (1 year)
vj stats -y| Profile | Resolution & FPS | Video Codec & Settings | Audio (Opus) | Est. (10 min) | Est. (1 hour) |
|---|---|---|---|---|---|
potato |
320×240 @ 10fps | SVT-AV1 (CRF 48, hqdn3d, unsharp) |
10 kbps Mono VoIP | ~2.0 MB | ~12 MB |
compact |
480×360 @ 12fps | SVT-AV1 (CRF 44, hqdn3d, unsharp) |
12 kbps Mono VoIP | ~4.5 MB | ~27 MB |
terry (default) |
640×480 @ 15fps | SVT-AV1 (CRF 38, hqdn3d, unsharp) |
14 kbps Mono VoIP | ~8.0 MB | ~48 MB |
balanced |
1280×720 @ 24fps | SVT-AV1 (CRF 30, hqdn3d) |
32 kbps Stereo | ~22 MB | ~130 MB |
hq |
1920×1080 @ 30fps | SVT-AV1 (CRF 24) | 64 kbps Stereo | ~60 MB | ~360 MB |
# Storage directory for journal entries
journal_dir = "~/Videos/Journal/entries"
# Inbox directory for incoming mobile uploads
inbox_dir = "~/Videos/Journal/inbox"
# Calendar system: "jalali" (1405-05-30) or "gregorian" (2026-08-22)
date_calendar = "jalali"
# Default compression profile ("terry", "potato", "compact", "balanced", "hq")
default_profile = "terry"
# Audio Noise Suppression (afftdn)
denoise = false # Set to true to automatically denoise all audio
# Activity Heatmap Duration
stats_months = 3 # Default months to show in stats heatmap (3, 6, or 12)
# Retro OSD Overlay Settings (disabled by default)
retro_overlay = false # Overlays are completely OFF by default
overlay_font = "vt323" # "vt323", "silkscreen", "press_start_2p", "share_tech_mono", or font path
# overlay_font_size = 24 # Custom font size in pixels (default: auto proportional to resolution)
overlay_style = "vhs_yellow" # "vhs_yellow", "camcorder_white", "green", "amber", "cyan"
overlay_show_title = true # When overlay is enabled, show custom title stacked above date
# Hardware capture devices
camera_dev = "/dev/video0"
audio_src = "default"
editor = "nvim"
inbox_port = 8080
# Keyless encryption (optional):
# key_file = "~/.config/vj/key"
# passphrase = ""
# Custom Profiles
[profiles.retro]
resolution = "320x240"
fps = 10
vcodec = "libsvtav1"
vpreset = 4
vcrf = 48
acodec = "libopus"
achannels = 1
abitrate = "10k"
vfilter = "scale=320:240,fps=10,hqdn3d=5:4:7:5,unsharp=3:3:0.5"
afilter = "highpass=f=80,loudnorm=I=-16:TP=-1.5:LRA=11"
extra_flags = "-svtav1-params tune=0:film-grain=0"