Every journaling app I tried either suffered from feature bloat or I'd simply forget to use it. After trying and abandoning several apps, I realized the problem wasn't motivation; it was friction. So I built journalot, a terminal-based journaling tool that leverages git for version control and syncing. Here's how it works under the hood.
The biggest barrier to digital journaling isn't lack of discipline, it's context switching. Opening a separate app, waiting for sync, dealing with unfamiliar keybindings. Each friction point compounds.
My solution: meet developers where they already are. In the terminal, with their preferred editor, using tools they already trust.
Rather than forcing a specific editor, journalot follows a priority chain:
get_editor() { if [ -n "$EDITOR" ]; then echo "$EDITOR" elif command -v code &> /dev/null; then echo "code" elif command -v vim &> /dev/null; then echo "vim" elif command -v nano &> /dev/null; then echo "nano" else error_exit "No suitable editor found. Please set EDITOR environment variable." fi }
This respects the user's $EDITOR environment variable first (standard Unix convention), then falls back to common editors in order of popularity. No configuration required for 90% of users.
The biggest technical challenge was detecting whether the user actually wrote anything. Simply opening and closing an editor shouldn't create a commit.
# Capture file hash before editing BEFORE_HASH=$(md5sum "$FILENAME" 2>/dev/null || md5 -q "$FILENAME" 2>/dev/null) # Open editor (blocks until closed) $EDITOR_CMD "$FILENAME" # Check if content changed AFTER_HASH=$(md5sum "$FILENAME" 2>/dev/null || md5 -q "$FILENAME" 2>/dev/null) if [ "$BEFORE_HASH" != "$AFTER_HASH" ]; then # Only prompt for commit if file was modified git add "$FILENAME" git commit -m "Journal entry for $ENTRY_DATE" fi
This uses MD5 hashing to detect actual changes. The dual command syntax (md5sum for Linux, md5 -q for macOS) ensures cross-platform compatibility without external dependencies.
For fleeting thoughts, opening an editor is still too much friction. The quick capture feature lets you append directly:
journal "Had a breakthrough on the authentication bug"
Implementation:
quick_capture() { local text="$1" local timestamp=$(date '+%H:%M') local filename="$JOURNAL_DIR/entries/$ENTRY_DATE.md" # Create file with header if it doesn't exist if [ ! -f "$filename" ]; then echo "# $ENTRY_DATE" > "$filename" echo "" >> "$filename" fi # Append timestamped entry echo "" >> "$filename" echo "**$timestamp** - $text" >> "$filename" }
This creates the file if needed, ensures proper markdown formatting, and timestamps each quick capture. Multiple quick captures on the same day append to the same file.
Instead of building a sync service, journalot leverages git. Every journal entry is a commit. Syncing across devices is just git push and git pull.
Auto-sync on entry open:
if git remote get-url origin &> /dev/null; then echo "Syncing with remote..." if ! git pull origin main --rebase 2>/dev/null; then warn_msg "Failed to pull from remote. Continuing with local changes..." fi fi
This pulls before opening an entry to minimize conflicts. Using --rebase keeps history linear. If the pull fails (no internet, conflicts), it warns but continues; offline-first by default.
For push, there's an optional AUTOSYNC=true config flag. Without it, you're prompted after saving:
if [ "$AUTOSYNC" = "true" ]; then git add "$FILENAME" git commit -m "Journal entry for $ENTRY_DATE" git push origin main else echo -n "Commit changes? (y/n): " read -r commit_response # ... prompt for commit and push fi
No indexing. No database. Just grep:
search_entries() { local query="$1" grep -i -n -H "$query" "$JOURNAL_DIR/entries"/*.md 2>/dev/null | while IFS=: read -r file line content; do local filename=$(basename "$file") echo "$filename:$line: $content" done }
grep -i for case-insensitive search, -n for line numbers, -H to show filenames. Results are formatted as filename:line: content, familiar to anyone who's used grep in a codebase.
For 1000+ entries, this is still near-instant on modern hardware. And since entries are dated (YYYY-MM-DD.md), you can narrow searches: grep "bug fix" entries/2025-*.md.
Opening yesterday's entry is a single flag:
journal --yesterday
Implementation handles macOS/Linux differences:
# macOS uses -v flag, Linux uses -d ENTRY_DATE=$(date -v-1d '+%Y-%m-%d' 2>/dev/null || date -d 'yesterday' '+%Y-%m-%d' 2>/dev/null)
The 2>/dev/null silences errors from the wrong syntax, letting the || fallback succeed. For specific dates:
journal --date 2025-01-15
This just sets ENTRY_DATE to the provided string. Date validation happens naturally; if the date format is wrong, the filename is malformed and the user notices immediately.
/usr/local/bingrep, wc, sed)~/journalot/ ├── entries/ │ ├── 2025-01-01.md │ ├── 2025-01-02.md │ └── 2025-01-03.md └── .git/
Each entry is a separate file named by date (YYYY-MM-DD.md). This means:
ls shows your journaling frequency at a glanceI've maintained a daily journaling habit for the first time (aside from my physical journal). The key metrics:
journal, hit enter, start typing)What made it stick:
git clone https://github.com/jtaylortech/journalot.git cd journalot sudo ./install.sh journal
GitHub: https://github.com/jtaylortech/journalot
The entire codebase is 638 lines of bash, MIT licensed. No telemetry, no accounts, no cloud lock-in.
What I'm adding next (feedback welcome):
What would make this more useful for you?

