🌰 Hazelnut
6 min read

CLI file autosorter inspired by macOS Hazel. Watches directories and automatically moves, renames, or processes files based on configurable rules (name patterns, extensions, dates, etc.).

Architecture

Hazelnut consists of two main components:

β€’ hazelnut - The TUI (Terminal User Interface) for managing rules, viewing logs, and monitoring file organization activity.
β€’ hazelnutd - The background daemon that watches folders and applies rules in real-time. Communicates with the TUI via Unix socket.

Quick Start

Install & Configure

Edit ~/.config/hazelnut/config.toml:

toml
# Watch your Downloads folder
[[watch]]
path = "/home/youruser/Downloads"

# Move PDFs to Documents
[[rule]]
name = "Organize PDFs"

[rule.condition]
extension = "pdf"

[rule.action]
type = "move"
destination = "/home/youruser/Documents/PDFs"

Run the Daemon

The daemon runs in the background, watching folders and applying rules:

sh
hazelnutd start

Use the TUI

Open the terminal interface to manage rules and monitor activity:

sh
hazelnut

Status Output

Check the daemon status with:

sh
$ hazelnutd status
🌰 Hazelnut daemon is running
PID: 12345
PID file: ~/.local/state/hazelnut/hazelnutd.pid
Log file: ~/.local/state/hazelnut/hazelnutd.log
Uptime: 2h 15m 30s

Configuration

Full Example Configuration

toml
[general]
log_level = "info"
log_file = "~/.local/share/hazelnut/hazelnut.log"
debounce_seconds = 2

# Watch folders
[[watch]]
path = "~/Downloads"
recursive = false

[[watch]]
path = "~/Desktop"
recursive = false

# Rules
[[rule]]
name = "Move PDFs"
enabled = true

[rule.condition]
extension = "pdf"

[rule.action]
type = "move"
destination = "~/Documents/PDFs"

General Settings

| Setting | Type | Default | Description |
|------|------|--------|--------| ------ | -------- | ----------------------------------------------- |
| log_level | string | "info" | Logging level (trace, debug, info, warn, error) |
| log_file | string | none | Path to log file |
| debounce_seconds | int | 2 | Wait time before processing a file |
| polling_interval_secs | int | 5 | How often to check for file changes |
| log_retention | int | 500 | Maximum activity log entries to keep |
| start_daemon_on_launch | bool | false | Auto-start daemon when TUI opens |
| notifications_enabled | bool | false | Desktop notifications on errors |

πŸ”” Desktop Notifications: Enable notifications_enabled to get alerted when something goes wrong (watch errors, rule failures, command errors). Works on Linux, macOS, and Windows.

Watch Folders

Configure which directories Hazelnut monitors for changes:

toml
[[watch]]
path = "/home/user/Downloads" # Use full paths
recursive = false
rules = [] # Empty = all rules apply

πŸ’‘ TUI Tip: You can manage watches directly in the terminal interface! Press a to add, e to edit, d to delete.

Watch Fields

| Field | Type | Default | Description |
| ----------- | ------ | -------- |------|------|--------|-----------------------------------------------|
| path | string | required | Directory to watch (use full paths, ~ not expanded) |
| recursive | bool | false | Watch subdirectories |
| rules | array | [] | Rule names to apply (empty = all rules apply) |

Watch Editor Keybindings

KeyAction
a / nAdd new watch
eEdit selected
dDelete selected

Conditions

All conditions must match for a rule to apply. Omit conditions you don't need.

File Name

toml
[rule.condition]
name_matches = "Screenshot*.png" # Glob pattern
name_regex = "^invoice_\\d+\\.pdf$" # Regex pattern

File Type

toml
[rule.condition]
extension = "pdf" # Single extension
extensions = ["jpg", "jpeg", "png", "gif"] # Multiple
is_directory = false
is_hidden = true # Files starting with .

File Size

toml
[rule.condition]
size_greater_than = 10485760 # > 10 MB (in bytes)
size_less_than = 1048576 # < 1 MB

File Age

toml
[rule.condition]
age_days_greater_than = 30 # Older than 30 days
age_days_less_than = 7 # Newer than 7 days

Actions

Move

toml
[rule.action]
type = "move"
destination = "~/Documents/PDFs"
create_destination = true # Create folder if missing
overwrite = false # Don't overwrite existing files

Works across filesystems β€” automatically falls back to copy + delete when needed. Supports both files and directories.

Copy

toml
[rule.action]
type = "copy"
destination = "~/Backup"
create_destination = true
overwrite = false


Rename

toml
[rule.action]
type = "rename"
pattern = "{date}_{name}.{ext}"

Available pattern variables:

β€’ {name} - Filename without extension
β€’ {filename} - Full filename
β€’ {ext} - Extension (empty string if none)
β€’ {path} - Full path
β€’ {dir} - Parent directory
β€’ {date} - Current date (YYYY-MM-DD)
β€’ {datetime} - Current datetime
β€’ {date:FORMAT} - Custom date format

Trash / Delete

toml
[rule.action]
type = "trash" # Safe, recoverable (uses native OS trash)

toml
# Or permanently delete (⚠️ dangerous!)
[rule.action]
type = "delete"

Run Command

toml
[rule.action]
type = "run"
command = "convert"
args = ["{path}", "-resize", "50%", "{dir}/{name}_small.{ext}"]

Pattern variables are automatically shell-escaped for security. Commands have a 60-second timeout.

Archive

toml
[rule.action]
type = "archive"
destination = "~/Archives"
delete_original = false

Supports both files and directories (directories are recursively archived).

Examples

Organize Screenshots

toml
[[rule]]
name = "Screenshots"

[rule.condition]
name_matches = "Screenshot*.png"

[rule.action]
type = "move"
destination = "~/Pictures/Screenshots"

Clean Old Downloads

toml
[[rule]]
name = "Clean old downloads"

[rule.condition]
age_days_greater_than = 30
extensions = ["tmp", "log", "bak"]

[rule.action]
type = "trash"

Sort Media Files

toml
[[rule]]
name = "Sort Images"

[rule.condition]
extensions = ["jpg", "jpeg", "png", "gif", "webp"]

[rule.action]
type = "move"
destination = "~/Pictures/Unsorted"

[[rule]]
name = "Sort Videos"

[rule.condition]
extensions = ["mp4", "mov", "avi", "mkv"]

[rule.action]
type = "move"
destination = "~/Videos/Unsorted"

Date-Prefix Invoices

toml
[[rule]]
name = "Date prefix invoices"

[rule.condition]
name_matches = "invoice*.pdf"

[rule.action]
type = "rename"
pattern = "{date:YYYY-MM-DD}_{name}.{ext}"

TUI Overview

The TUI provides a beautiful interface for managing Hazelnut. Launch it with:

sh
hazelnut

Views

β€’ Dashboard - Overview of daemon status and recent activity
β€’ Rules - Manage your file organization rules
β€’ Log - View recent file operations
β€’ Watches - Manage watched directories

Keyboard Shortcuts


| Key | Action |
|--|----| --------------------- |
| ? | Show help |
| q | Quit |
| 1-4 | Switch tabs |
| Tab | Next tab |
| j/k or ↑/↓ | Navigate list |
| g / G | Go to first/last item |

Rules View

KeyAction
nCreate new rule
eEdit selected rule
dDelete selected rule
Enter / SpaceToggle rule enabled

General

KeyAction
tOpen theme picker
sOpen settings
AAbout dialog

Watches View

KeyAction
a / nAdd new watch
eEdit selected
dDelete selected

Rule Editor / Watch Editor

KeyAction
TabNext field
Shift+TabPrevious
EnterSave
EscCancel