A fast, interactive command-line tool for organizing files with tags using fuzzy finding and persistent storage.
- 🏷️ Tag-based file organization - Organize files using flexible tags instead of rigid folder structures
- 🔍 Interactive fuzzy finding - Browse and select files using an intuitive fuzzy finder interface
- 👁️ Preview pane - See file content with syntax highlighting before selecting (uses bat/syntect)
- ⚡ Fast queries - O(1) tag lookups using reverse indexing with sled database
- 🎯 Multi-select - Select multiple tags and files at once
- 🔮 Virtual tags - Query files by metadata (size, modification time, extension, etc.) without database storage
- 💾 Saved filters - Save complex search criteria as named filters for quick recall
- 🧹 Database cleanup - Maintain database integrity by removing missing files and untagged entries
- 💾 Persistent storage - Reliable embedded database with automatic flushing
- 📊 Multiple databases - Manage separate databases for different projects
git clone https://github.com/xerinox/tagr.git
cd tagr
cargo build --releaseWhen you run tagr for the first time, it will guide you through an interactive setup:
./target/release/tagrYou'll be prompted for:
- Database name (default: "default")
- Database location (default:
~/.local/share/tagr/<database_name>)
The configuration is saved to ~/.config/tagr/config.toml.
# Tag some files
tagr tag README.md documentation markdown
tagr tag src/main.rs rust code source
tagr tag src/lib.rs rust code library
# Browse files interactively (default command)
tagr
# Or explicitly
tagr browse
# Search for files by tag (non-interactive)
tagr search rust
# List all tags
tagr list tags
# Remove tags from a file
tagr untag README.md markdown
# Clean up missing files
tagr cleanupThe browse command opens an interactive fuzzy finder that can be used in two ways:
# Launch browse mode
tagr
# or
tagr browseStage 1: Tag Selection
- Displays all available tags in the database
- Multi-select enabled via TAB key
- Fuzzy matching for quick filtering
- Press Enter to proceed to file selection
Stage 2: File Selection
- Shows all files matching ANY of the selected tags
- Files displayed with their tags inline:
file.txt [tag1, tag2, tag3] - Multi-select enabled via TAB key
- Fuzzy matching for filtering
- Press Enter to confirm final selection
You can now pre-populate the browse mode with search criteria, skipping the tag selection stage:
# Browse with a general query (searches both filenames and tags)
tagr browse documents
# Browse files with specific tags
tagr browse -t rust -t programming
# Browse with file patterns (glob syntax)
tagr browse -f "*.txt" -f "*.md"
# Exclude specific tags
tagr browse -t documents -e archived
# Combine multiple criteria
tagr browse -t rust -f "src/*.rs" -e testThis behaves exactly like tagr search, but instead of printing results directly, it opens the fuzzy finder pre-filtered with matching files. You can then:
- Further filter with fuzzy matching
- Multi-select files
- Execute commands on selections
# Open selected files in your editor
tagr browse documents -x "nvim {}"
# Copy selected files
tagr browse -t images -x "cp {} /backup/"
# Preview file content
tagr browse -t config -x "cat {}"| Key | Action |
|---|---|
| ↑↓ or Ctrl+J/K | Navigate |
| TAB | Select/deselect (multi-select) |
| Enter | Confirm and proceed |
| ESC / Ctrl+C | Cancel |
| Type | Filter via fuzzy matching |
# Traditional browse
tagr
# Browse documents matching pattern
tagr browse -f "*.txt"
# Browse Rust files with specific tag, then edit
tagr browse -t tutorial -f "*.rs" -x "nvim {}"
# Browse any doc format, excluding archived
tagr browse -t documentation -e archivedThe preview pane displays file content when browsing files in interactive mode, helping you make informed selections without leaving the fuzzy finder.
- Syntax highlighting - Automatically highlights code files using
bat(if installed) or built-insyntect - Smart fallbacks - Plain text preview if syntax highlighting unavailable or disabled
- Binary file metadata - Shows file size, modification time, permissions for non-text files
- ANSI color support - Preserves syntax highlighting colors in the preview
- Configurable - Control preview size, position, and features
Preview is enabled by default when browsing:
# Browse with preview (default)
tagr browse
# Disable preview
tagr browse --no-preview
# Customize preview lines
tagr browse --preview-lines 100
# Change preview position (right/bottom/top)
tagr browse --preview-position bottom
# Adjust preview width (percentage)
tagr browse --preview-width 60Add preview settings to ~/.config/tagr/config.toml:
[preview]
enabled = true
max_file_size = 5242880 # 5MB
max_lines = 50
syntax_highlighting = true
show_line_numbers = true
preview_position = "right" # right, bottom, or top
preview_width_percent = 50 # 0-100Preview uses a hybrid approach for best results:
- First choice: Uses
batcommand (if installed) - respects your bat theme and config - Fallback: Uses built-in
syntectlibrary with default theme - Final fallback: Plain text if syntax highlighting disabled
To install bat for better syntax highlighting:
# macOS
brew install bat
# Ubuntu/Debian
apt install bat
# Arch Linux
pacman -S bat
# Cargo
cargo install batSyntax highlighting can be disabled via:
- Configuration:
syntax_highlighting = falsein config.toml - CLI flag:
--no-previewwhen browsing - Compile-time:
cargo build --no-default-features(removes syntect dependency)
# Tag a file
tagr tag <file> <tags...>
# Add tags to existing file (no duplicates)
tagr add-tags <file> <tags...>
# Remove specific tags
tagr untag <file> <tags...>
# Show tags for a file
tagr show <file># Interactive browse (default)
tagr
tagr browse
tagr b
# Pre-populated browse with query
tagr browse documents
tagr browse -t rust -t programming
tagr browse -f "*.txt" -f "*.md"
tagr browse -t config -e archived
# Browse with command execution
tagr browse -t images -x "cp {} /backup/"
# List all tags
tagr list tags
tagr l tags
# List all files
tagr list files
tagr l filesThe tagr search command supports flexible multi-criteria querying with independent AND/OR logic for both tags and file patterns.
# Single tag search
tagr search -t rust
# Multiple tags - AND logic (default: files must have ALL tags)
tagr search -t rust -t tutorial
# Multiple tags - OR logic (files must have ANY tag)
tagr search -t rust -t python -t javascript --any-tag# Single file pattern
tagr search -t tutorial -f "*.rs"
# Multiple file patterns - AND logic (default: match ALL patterns)
tagr search -t rust -f "*.rs" -f "src/*"
# Multiple file patterns - OR logic (match ANY pattern)
tagr search -t config -f "*.toml" -f "*.yaml" --any-fileThe key feature is independent control of AND/OR logic for tags vs. file patterns:
# Tags AND, Files OR
# Files must have BOTH "rust" AND "library" tags
# AND match EITHER "*.rs" OR "*.md"
tagr search -t rust -t library --all-tags -f "*.rs" -f "*.md" --any-file
# Tags OR, Files AND
# Files must have EITHER "rust" OR "python" tag
# AND match BOTH "src/*" AND "*test*" patterns
tagr search -t rust -t python --any-tag -f "src/*" -f "*test*" --all-files# Exclude specific tags
tagr search -t rust -e deprecated
# Multiple exclusions
tagr search -t documentation -e old -e archived
# Complex: OR search with exclusions
tagr search -t rust -t python --any-tag -e beginner -e deprecated# Regex for tags
tagr search -t "config.*" --regex-tag
# Matches: config-dev, config-prod, config-test, etc.
# Regex for file patterns
tagr search -t source -f "src/.*\\.rs$" --regex-file
# Regex for both
tagr search -t "lang-.*" --regex-tag -f ".*\\.(rs|toml)$" --regex-file# Find all Rust test files
tagr search -t rust -t test -f "*test*.rs" -f "tests/*.rs" --any-file
# Find source files across multiple languages (not tests)
tagr search -t rust -t python --any-tag -f "src/*.rs" -f "src/*.py" --any-file -e test
# Find all documentation in any format
tagr search -t documentation -f "*.md" -f "*.txt" --any-file
# Production Rust library code (complex query)
tagr search \
-t rust -t library -t production --all-tags \
-f "src/*.rs" -f "lib/*.rs" --any-file \
-e test -e deprecated -e experimentaltagr search --help
# Key options:
# -t, --tag <TAG> Tags to search for (multiple allowed)
# --any-tag Match ANY tag (OR logic)
# --all-tags Match ALL tags (AND logic, default)
# -f, --file <PATTERN> File patterns (glob or regex)
# --any-file Match ANY file pattern (OR logic)
# --all-files Match ALL file patterns (AND logic, default)
# -e, --exclude <TAG> Exclude files with these tags
# --regex-tag Use regex for tag matching
# --regex-file Use regex for file patterns
# -q, --quiet Output only file paths (for piping)# Pipe to xargs
tagr search -q -t rust -t tutorial -f "*.rs" | xargs nvim
# Count results
tagr search -q -t python -t test | wc -l
# Execute commands on results
for file in $(tagr search -q -t config); do
echo "Processing $file"
cat "$file"
doneAll search operations are highly efficient:
- Tag lookups: O(1) via reverse index
- Complex queries: < 20ms for 10,000 files
- Pattern filtering: Only on result set, not entire database
# List databases
tagr db list
# Add new database
tagr db add <name> <path>
# Set default database
tagr db set-default <name>
# Remove database
tagr db remove <name>Save complex search criteria as named filters for quick recall, eliminating the need to repeatedly type complex queries.
Filters are perfect for searches you run frequently:
- Finding all Rust tutorial files:
tagr search -t rust -t tutorial -f "*.rs" - Reviewing production code:
tagr search -t rust -t production -e deprecated -e test - Checking documentation:
tagr search -t documentation -f "*.md" -f "*.txt" --any-file
Instead of retyping these, save them once and recall instantly!
# Create a filter with tags
tagr filter create rust-tutorials \
-d "Find Rust tutorial files" \
-t rust -t tutorial \
-f "*.rs"
# Create a filter with all criteria
tagr filter create prod-rust \
-d "Production Rust code (no tests/deprecated)" \
-t rust -t production --all-tags \
-f "src/*.rs" -f "lib/*.rs" --any-file \
-e test -e deprecated
# Create with regex
tagr filter create config-files \
-d "All configuration files" \
-t config \
-f ".*\\.(toml|yaml|json)$" --regex-file# List all saved filters
tagr filter list
tagr filter ls
# Show detailed filter information
tagr filter show rust-tutorials
# Rename a filter
tagr filter rename rust-tutorials rust-beginner-tutorials
tagr filter mv rust-tutorials rust-beginner-tutorials
# Delete a filter
tagr filter delete rust-tutorials
tagr filter rm rust-tutorials
# Delete without confirmation
tagr filter delete rust-tutorials --force
tagr filter rm rust-tutorials -fFilters work seamlessly with tagr search and tagr browse commands:
# Use a saved filter
tagr search --filter rust-tutorials
tagr search -F rust-tutorials
# Load in browse mode
tagr browse --filter prod-rust
tagr browse -F prod-rust
# Combine filter with additional criteria
tagr search -F rust-tutorials -e beginner
tagr browse -F config-files -f "*.toml"
# Save current search as filter
tagr search -t rust -t tutorial -f "*.rs" --save-filter "my-rust-search"
# Save with description
tagr search -t rust -f "*.rs" --save-filter "rust-src" --filter-desc "All Rust source files"Share filters with your team or back them up:
# Export all filters to file
tagr filter export --output team-filters.toml
# Export specific filters
tagr filter export rust-tutorials prod-rust --output rust-filters.toml
# Export to stdout
tagr filter export rust-tutorials
# Import filters
tagr filter import team-filters.toml
# Import with conflict resolution
tagr filter import team-filters.toml --overwrite # Replace existing
tagr filter import team-filters.toml --skip-existing # Keep existingFilters are stored in TOML format at ~/.config/tagr/filters.toml:
[[filter]]
name = "rust-tutorials"
description = "Find Rust tutorial files"
created = "2025-11-10T14:30:00Z"
last_used = "2025-11-10T15:45:00Z"
use_count = 12
[filter.criteria]
tags = ["rust", "tutorial"]
tag_mode = "all"
file_patterns = ["*.rs"]
file_mode = "any"
excludes = []
regex_tag = false
regex_file = false
virtual_tags = []
virtual_mode = "all"Virtual tags are dynamically computed filters based on file metadata, requiring zero database storage. Query files by size, modification time, extension type, and more!
- No database overhead - Virtual tags are computed on-the-fly from filesystem metadata
- Always accurate - Reflects current file state without manual updates
- Powerful queries - Filter by properties that would be cumbersome to tag manually
- Combine with regular tags - Mix virtual tags with your tagged files seamlessly
Query files by their timestamps:
# Files modified today
tagr search -v modified:today
# Files modified in the last 7 days
tagr search -v modified:last-7-days
# Files created this week
tagr search -v created:this-week
# Files accessed in the last 24 hours
tagr search -v accessed:last-24-hours
# Files modified after a specific date
tagr search -v modified:after-2025-11-01
# Files modified before a date
tagr search -v modified:before-2025-10-01
# Files modified between dates
tagr search -v modified:2025-11-01..2025-11-10Filter by file size:
# Empty files
tagr search -v size:empty
# Size categories
tagr search -v size:tiny # < 1KB
tagr search -v size:small # 1KB - 100KB
tagr search -v size:medium # 100KB - 1MB
tagr search -v size:large # 1MB - 10MB
tagr search -v size:huge # > 10MB
# Specific sizes
tagr search -v "size:>1MB"
tagr search -v "size:<100KB"
tagr search -v "size:1MB-10MB"
# Exact size
tagr search -v size:1024Filter by file extension or type:
# Specific extension
tagr search -v ext:.rs
tagr search -v ext:.md
# Extension types
tagr search -v ext-type:source # .rs, .py, .js, .go, .cpp, .c, .java, .ts
tagr search -v ext-type:document # .md, .txt, .pdf, .doc, .docx, .org
tagr search -v ext-type:config # .toml, .yaml, .json, .ini, .conf
tagr search -v ext-type:image # .png, .jpg, .jpeg, .gif, .svg, .webp
tagr search -v ext-type:archive # .zip, .tar, .gz, .7z, .rar, .bz2Query by file location:
# Files in a specific directory
tagr search -v dir:src
# Files matching a path pattern
tagr search -v "path:src/**/*.rs"
tagr search -v "path:tests/*.rs"
# Files at a specific depth
tagr search -v depth:3
tagr search -v "depth:<5"Filter by file permissions:
# Executable files
tagr search -v perm:executable
# Read-only files
tagr search -v perm:readonly
# Writable files
tagr search -v perm:writableQuery by file content properties:
# Files with specific line count
tagr search -v "lines:>100"
tagr search -v "lines:<50"
tagr search -v lines:10-50Query by Git status (when in a repository):
# Tracked files
tagr search -v git:tracked
# Modified files
tagr search -v git:modified
# Staged files
tagr search -v git:staged
# Untracked files
tagr search -v git:untracked
# Stale files (not modified in 6+ months)
tagr search -v git:staleUse multiple virtual tags together with AND/OR logic:
# Find large Rust files (AND logic - default)
tagr search -v ext:.rs -v "size:>100KB"
# Find files that are either empty OR tiny (OR logic)
tagr search -v size:empty -v size:tiny --any-virtual
# Combine regular tags with virtual tags
tagr search -t rust -v "modified:this-week"
tagr search -t config -v ext:.toml -v "size:<10KB"
# Complex queries
tagr search -t documentation -v ext-type:document -v "modified:last-7-days"Virtual tags can be saved in filters for quick recall:
# Save a filter with virtual tags
tagr search -t rust -v ext:.rs -v "size:>1KB" \\
--save-filter "rust-source" \\
--filter-desc "Non-empty Rust source files"
# Use the saved filter
tagr search -F rust-source
# View the filter (shows virtual tags)
tagr filter show rust-source
# Combine saved filter with additional virtual tags
tagr search -F rust-source -v "modified:today"Customize virtual tag behavior in ~/.config/tagr/config.toml:
[virtual_tags]
enabled = true
cache_metadata = true
cache_ttl_seconds = 300
[virtual_tags.size_categories]
tiny = "1KB"
small = "100KB"
medium = "1MB"
large = "10MB"
huge = "100MB"
[virtual_tags.extension_types]
source = [".rs", ".py", ".js", ".go"]
document = [".md", ".txt", ".pdf"]
config = [".toml", ".yaml", ".json"]
[virtual_tags.time]
recent = 7 # days
stale = 180 # days
[virtual_tags.git]
enabled = true
detect_repo = true# Clean up missing files and untagged entries
tagr cleanup
tagr c
# Quiet mode (suppress informational output)
tagr -q <command>The cleanup command helps maintain database integrity by identifying and removing:
- Missing Files - Files in the database that no longer exist on the filesystem
- Untagged Files - Files with no tags assigned
tagr cleanupFor each problematic file, you can respond with:
yoryes- Delete this file from the databasenorno- Skip this fileaoryes-to-all- Delete this file and all remaining in this categoryqorno-to-all- Skip this file and all remaining in this category
# Delete all missing files and all untagged files
echo -e "a\na" | tagr cleanup
# Delete all missing files but skip untagged files
echo -e "a\nq" | tagr cleanuptagr uses multiple sled trees for efficient bidirectional lookups:
Key: file_path (UTF-8 string as bytes)
Value: Vec<String> (bincode-encoded list of tags)
Example:
"file1.txt" → ["rust", "programming", "tutorial"]
"file2.txt" → ["rust", "advanced"]
Key: tag (UTF-8 string as bytes)
Value: Vec<String> (bincode-encoded list of file paths)
Example:
"rust" → ["file1.txt", "file2.txt", "file4.txt"]
"programming" → ["file1.txt", "file3.txt", "file4.txt"]
| Operation | Before (Single Tree) | After (Multi-Tree) | Speedup |
|---|---|---|---|
find_by_tag("rust") |
O(n) - scan all files | O(1) - direct lookup | 100-1000x |
list_all_tags() |
O(n) - scan all files | O(k) - iterate tags | 100x |
find_by_all_tags(...) |
O(n) - scan all files | O(k) - set intersection | 100x |
Example: For 10,000 files with 100 unique tags:
- Old: 10,000 iterations per query (~50ms)
- New: 1 iteration per query (~0.1ms) - 500x faster!
src/
├── lib.rs # Library root, exports all modules
├── main.rs # CLI application entry point
├── cli.rs # Command line interface
├── config.rs # Configuration management
├── db/ # Database wrapper
│ ├── mod.rs # Database operations
│ ├── types.rs # Data types
│ └── error.rs # Error types
└── search/ # Interactive fuzzy finding
├── mod.rs # Browse functionality
├── browse.rs # Browse implementation
└── error.rs # Error types
tagr can be used as a library in your Rust projects:
use tagr::{db::Database, search, filters::FilterManager};
use std::path::PathBuf;
// Database operations
let db = Database::open("my_db").unwrap();
db.insert("file.txt", vec!["tag1".into(), "tag2".into()]).unwrap();
let files = db.find_by_tag("tag1").unwrap();
// Filter management
let filter_manager = FilterManager::new(PathBuf::from("~/.config/tagr/filters.toml"));
let filters = filter_manager.list().unwrap();
// Interactive browse
match search::browse(&db) {
Ok(Some(result)) => {
println!("Selected {} files", result.selected_files.len());
}
Ok(None) => println!("Cancelled"),
Err(e) => eprintln!("Error: {}", e),
}use tagr::filters::{FilterManager, FilterCriteria, TagMode};
use std::path::PathBuf;
let manager = FilterManager::new(PathBuf::from("~/.config/tagr/filters.toml"));
// Create a filter
let criteria = FilterCriteria {
tags: vec!["rust".to_string(), "tutorial".to_string()],
tag_mode: TagMode::All,
file_patterns: vec!["*.rs".to_string()],
..Default::default()
};
manager.create(
"rust-tutorials".to_string(),
"Find Rust tutorial files".to_string(),
criteria,
).unwrap();
// Use a filter
let filter = manager.get("rust-tutorials").unwrap();
println!("Filter: {} - {}", filter.name, filter.description);
// List all filters
let filters = manager.list().unwrap();
for filter in filters {
println!("{}: {}", filter.name, filter.description);
}
// Export/import filters
manager.export(&PathBuf::from("my-filters.toml"), &[]).unwrap();// Insert/Update
db.insert("file.txt", vec!["tag1".into()]).unwrap();
db.insert_pair(pair).unwrap();
// Retrieve
db.get_tags("file.txt").unwrap(); // Option<Vec<String>>
db.get_pair("file.txt").unwrap(); // Option<Pair>
// Add/Remove Tags
db.add_tags("file.txt", vec!["tag3".into()]).unwrap();
db.remove_tags("file.txt", &["tag1".into()]).unwrap();
// Delete
db.remove("file.txt").unwrap(); // bool (existed?)
// Query
db.find_by_tag("tag1").unwrap(); // Vec<PathBuf>
db.find_by_all_tags(&[...]).unwrap(); // Vec<PathBuf>
db.find_by_any_tag(&[...]).unwrap(); // Vec<PathBuf>
// List
db.list_all().unwrap(); // Vec<Pair>
db.list_all_tags().unwrap(); // Vec<String>
// Utility
db.contains("file.txt").unwrap(); // bool
db.count(); // usize
db.flush().unwrap();
db.clear().unwrap();Configuration file location: ~/.config/tagr/config.toml
default_database = "myfiles"
[databases]
myfiles = "/home/user/tags_db"
default = "/home/user/.local/share/tagr/default"- Linux:
~/.local/share/tagr/ - macOS:
~/Library/Application Support/tagr/ - Windows:
C:\Users\<username>\AppData\Local\tagr\
cargo run --example browse_demoThis creates a test database with 10 files and 13+ tags, then launches browse mode.
# Run tests
cargo test
# Run with test data
./test_browse.sh- sled - Embedded database for persistent storage
- skim - Fuzzy finder for interactive browsing
- bincode - Efficient binary serialization
- clap - Command-line argument parsing
- chrono - Date/time handling for filter timestamps
- thiserror - Error handling
- Tag lookups are O(1) with reverse indexing
- Storage overhead is ~50% (files tree + tags tree)
- Auto-flush on drop ensures data durability
- Efficient for 10,000+ files with 100+ tags
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License.
Potential improvements:
- Filter storage infrastructure with
FilterManager - Filter CRUD operations (create, get, update, delete, rename, list)
- Export/import functionality with conflict resolution
- Usage statistics tracking
- CLI commands for filter management (
tagr filter list,show,create, etc.) -
--save-filterflag for search/browse commands -
--filter/-Fflag to load and apply saved filters - Filter test command to preview matches
- Filter statistics command
- Interactive filter builder wizard
- Filter configuration options in config.toml
- Preview pane - Show file content in skim preview
- Tag statistics - Show file count per tag
- Recent selections - Remember last used tags
- Custom search queries - Complex tag expressions
- Export results - Save selections to file
- Actions on selection - Open, copy, delete files directly
- Tag counts - Store tag→count mapping for statistics
- Prefix search - Use key prefixes for tag autocomplete
- LRU cache - In-memory cache for hot tags