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. journalot The Core Problem: Reducing Friction to Zero 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. Implementation Details 1. Smart Editor Detection 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 } 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. $EDITOR No configuration required for 90% of users. 2. Change Detection: Only Commit When Necessary 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 # 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. md5sum md5 -q 3. Quick Capture: Appending Without Opening an Editor 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" 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" } 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. 4. Git-Based Sync: Cross-Device Without a Backend 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. git push 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 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. --rebase offline-first by default For push, there's an optional AUTOSYNC=true config flag. Without it, you're prompted after saving: AUTOSYNC=true 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 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 5. Search: Just Grep 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 } 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. grep -i -n -H filename:line: content 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. YYYY-MM-DD.md grep "bug fix" entries/2025-*.md 6. Date Parsing for Flexibility Opening yesterday's entry is a single flag: journal --yesterday 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) # 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: 2>/dev/null || journal --date 2025-01-15 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. ENTRY_DATE Architecture Decisions Why Bash? Zero dependencies: Works on any Unix-like system with git No installation friction: Just copy to /usr/local/bin Transparent: Users can read the entire implementation in 638 lines Fast startup: No runtime initialization, no package loading Zero dependencies: Works on any Unix-like system with git Zero dependencies No installation friction: Just copy to /usr/local/bin No installation friction /usr/local/bin Transparent: Users can read the entire implementation in 638 lines Transparent Fast startup: No runtime initialization, no package loading Fast startup Why Plain Markdown? Longevity: Markdown will outlive any proprietary format Composability: Pipe to other tools (grep, wc, sed) Portability: Open in any editor, render on GitHub, convert with pandoc Version control: Git diffs on plain text work beautifully Longevity: Markdown will outlive any proprietary format Longevity Composability: Pipe to other tools (grep, wc, sed) Composability grep wc sed Portability: Open in any editor, render on GitHub, convert with pandoc Portability Version control: Git diffs on plain text work beautifully Version control File Structure ~/journalot/ ├── entries/ │ ├── 2025-01-01.md │ ├── 2025-01-02.md │ └── 2025-01-03.md └── .git/ ~/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: YYYY-MM-DD.md Easy to locate any day's entry ls shows your journaling frequency at a glance Week/month views are just shell globs Archive old years by moving files Easy to locate any day's entry ls shows your journaling frequency at a glance ls Week/month views are just shell globs Archive old years by moving files My Results After 9 Months I've maintained a daily journaling habit for the first time (aside from my physical journal). The key metrics: Average time to first word written: ~3 seconds (type journal, hit enter, start typing) Entries missed: ~15 days out of 270 (mostly travel without laptop) Total words written: 47,000+ Average time to first word written: ~3 seconds (type journal, hit enter, start typing) Average time to first word written first word journal Entries missed: ~15 days out of 270 (mostly travel without laptop) Entries missed Total words written: 47,000+ Total words written What made it stick: What made it stick: Zero cognitive load: No app to open, no account to log into, no "should I journal?" friction Editor mastery: Vim keybindings, spell-check, snippets: everything I've already trained my fingers for Trust in durability: Git history means nothing is lost, entries are future-proof plain text Zero cognitive load: No app to open, no account to log into, no "should I journal?" friction Zero cognitive load Editor mastery: Vim keybindings, spell-check, snippets: everything I've already trained my fingers for Editor mastery Trust in durability: Git history means nothing is lost, entries are future-proof plain text Trust in durability Try It git clone https://github.com/jtaylortech/journalot.git cd journalot sudo ./install.sh journal git clone https://github.com/jtaylortech/journalot.git cd journalot sudo ./install.sh journal GitHub: https://github.com/jtaylortech/journalot 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 I'm adding next End-to-end encryption for git remotes (currently relies on repo privacy) Conflict resolution helper for simultaneous edits on multiple devices iOS companion app with SSH git push (for journaling without a laptop) End-to-end encryption for git remotes (currently relies on repo privacy) Conflict resolution helper for simultaneous edits on multiple devices iOS companion app with SSH git push (for journaling without a laptop) What would make this more useful for you? What would make this more useful for you?