Today I configured my Debian Linux machine to log my activity locally and then post my daily active time to a Beeminder goal. I used Warp and Codebuff to help me set everything up and write out the following post detailing what it did.
Everything beyond this point is AI-generated
Tracking Computer Activity with Beeminder: A Linux Automation Story
Have you ever wondered how much time you actually spend actively using your computer each day? I wanted to answer this question for myself, and being a fan of Beeminder for habit tracking, I decided to build a system that would automatically track my computer usage and post it to Beeminder. In this post, I’ll walk through how I set up this automation using bash scripts, systemd, and cron.
System Architecture Overview
My activity tracking system consists of four key components:
- Activity Monitor Script: Collects data about my computer usage every minute
- Activity Log File: Stores the collected data with timestamps
- Daily Active Time Script: Parses the logs to calculate my active time
- Beeminder Posting Script: Sends the calculated time to Beeminder
The system is fully automated using systemd to run the activity monitor every minute and cron to trigger the Beeminder update script hourly.
The Activity Monitor Script
The heart of the system is activity_monitor.sh
, which captures what I’m doing on my computer:
#!/bin/bash
LOG_FILE="$HOME/.activity_log"
DATE=$(date "+%Y-%m-%d %H:%M:%S")
# Get current active window information using xdotool
WINDOW_ID=$(xdotool getwindowfocus)
WINDOW_TITLE=$(xdotool getwindowfocus getwindowname)
# Get PID and application name of the window
WINDOW_PID=$(xprop -id $WINDOW_ID _NET_WM_PID 2>/dev/null | grep -o '[0-9]*$')
if [ -n "$WINDOW_PID" ]; then
APP_NAME=$(ps -p $WINDOW_PID -o comm= 2>/dev/null || echo "Unknown")
else
APP_NAME="Unknown"
fi
# Get idle time in milliseconds if xprintidle is installed
if command -v xprintidle &> /dev/null; then
IDLE_TIME=$(xprintidle)
IDLE_SEC=$((IDLE_TIME/1000))
else
IDLE_SEC="xprintidle not installed"
fi
# Get system load
LOAD=$(cat /proc/loadavg | cut -d " " -f 1-3)
# Log the information
echo "$DATE | App: $APP_NAME | Window: $WINDOW_TITLE | Idle: $IDLE_SEC sec | Load: $LOAD" >> "$LOG_FILE"
This script uses xdotool
to detect the currently focused window and xprintidle
to track how long I’ve been idle. Every minute, it logs:
- The current date and time
- The application name
- The window title
- How long I’ve been idle (in seconds)
- The system load
Each entry is appended to ~/.activity_log
with a timestamp, creating a detailed record of my computer usage throughout the day.
The Activity Log File
The log file (~/.activity_log
) is a simple text file that accumulates entries like this:
2023-11-08 14:30:45 | App: firefox | Window: Gmail - Mozilla Firefox | Idle: 12 sec | Load: 0.52 0.48 0.45
2023-11-08 14:31:45 | App: code | Window: activity_monitor.sh - VS Code | Idle: 3 sec | Load: 0.67 0.52 0.47
This format makes it easy to parse and analyze later. While I’m currently only using it for daily active time calculations, having this detailed log opens up possibilities for deeper analysis in the future.
Calculating Daily Active Time
The daily_active_time.sh
script processes the log file to determine how much time I’ve spent actively using my computer:
#!/bin/bash
# Script to calculate daily active time from ~/.activity_log
# Active time is defined as periods when idle time is less than 300 seconds
# This script identifies contiguous active sessions and sums their durations
# Get today's date in YYYY-MM-DD format
TODAY=$(date "+%Y-%m-%d")
# Define activity log path
ACTIVITY_LOG=~/.activity_log
# Define idle threshold in seconds (user is considered active if idle time is below this)
IDLE_THRESHOLD=300
# Enable or disable debug output
DEBUG=true
# Function to output debug information
debug_log() {
if [ "$DEBUG" = true ]; then
echo "[DEBUG] $1"
fi
}
# Check if the activity log exists
if [ ! -f "$ACTIVITY_LOG" ]; then
echo "Error: Activity log ($ACTIVITY_LOG) not found."
exit 1
fi
debug_log "Processing activity log for $TODAY"
# Create a temporary file with today's entries to avoid pipe issues
TMP_FILE=$(mktemp)
grep "$TODAY" "$ACTIVITY_LOG" > "$TMP_FILE"
# Check if there are any entries for today
if [ ! -s "$TMP_FILE" ]; then
echo "No activity logged for today ($TODAY)."
rm "$TMP_FILE"
exit 0
fi
# Initialize variables
FIRST_ACTIVE_TIMESTAMP=""
LAST_ACTIVE_TIMESTAMP=""
TOTAL_ACTIVE_ENTRIES=0
TOTAL_ACTIVE_SECONDS=0
ACTIVE_SESSIONS=0
# Store all active timestamps for processing
declare -a ACTIVE_TIMESTAMPS
declare -a UNIX_ACTIVE_TIMESTAMPS
debug_log "Parsing log entries..."
# Process each line in today's log entries
while read -r line; do
# Extract timestamp (assuming format like "2023-11-08 14:30:45")
TIMESTAMP=$(echo "$line" | grep -o '[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}')
# Extract idle time (assuming format like "Idle: 123 sec")
IDLE_TIME=$(echo "$line" | grep -o 'Idle: [0-9]\+ sec' | grep -o '[0-9]\+')
# Skip if we couldn't parse the line properly
if [ -z "$TIMESTAMP" ] || [ -z "$IDLE_TIME" ]; then
debug_log "Skipping malformed line: $line"
continue
fi
# Convert timestamp to Unix time for calculations
UNIX_TIMESTAMP=$(date -d "$TIMESTAMP" "+%s")
# Check if user was active during this entry
if [ "$IDLE_TIME" -lt "$IDLE_THRESHOLD" ]; then
# User was active
TOTAL_ACTIVE_ENTRIES=$((TOTAL_ACTIVE_ENTRIES + 1))
# Store timestamp for processing and debugging
ACTIVE_TIMESTAMPS+=("$TIMESTAMP (Idle: ${IDLE_TIME}s)")
UNIX_ACTIVE_TIMESTAMPS+=($UNIX_TIMESTAMP)
fi
done < "$TMP_FILE"
# Clean up temporary file
rm "$TMP_FILE"
debug_log "Total active entries: $TOTAL_ACTIVE_ENTRIES"
# Calculate total active time based on contiguous sessions
if [ ${#UNIX_ACTIVE_TIMESTAMPS[@]} -gt 0 ]; then
# Sort timestamps to ensure chronological order
IFS=$'\n' SORTED_TIMESTAMPS=($(sort -n <<<"${UNIX_ACTIVE_TIMESTAMPS[*]}"))
unset IFS
debug_log "Processing ${#SORTED_TIMESTAMPS[@]} active timestamps to identify sessions..."
# Log all active timestamps for debugging
if [ "$DEBUG" = true ]; then
echo "[DEBUG] All active timestamps:"
for i in "${!ACTIVE_TIMESTAMPS[@]}"; do
echo " - ${ACTIVE_TIMESTAMPS[$i]}"
done
fi
# Initialize session tracking
SESSION_START=${SORTED_TIMESTAMPS[0]}
PREV_TIMESTAMP=${SORTED_TIMESTAMPS[0]}
FIRST_ACTIVE_TIMESTAMP=$(date -d @${SORTED_TIMESTAMPS[0]} "+%Y-%m-%d %H:%M:%S")
LAST_ACTIVE_TIMESTAMP=$(date -d @${SORTED_TIMESTAMPS[-1]} "+%Y-%m-%d %H:%M:%S")
ACTIVE_SESSIONS=1
debug_log "Started session #1 at $(date -d @$SESSION_START "+%Y-%m-%d %H:%M:%S")"
# Process each timestamp to identify sessions
for ((i=1; i<${#SORTED_TIMESTAMPS[@]}; i++)); do
CURRENT_TIMESTAMP=${SORTED_TIMESTAMPS[$i]}
TIME_DIFF=$((CURRENT_TIMESTAMP - PREV_TIMESTAMP))
# If gap is larger than threshold, end current session and start new one
if [ $TIME_DIFF -gt $IDLE_THRESHOLD ]; then
# Calculate duration of the completed session
SESSION_DURATION=$((PREV_TIMESTAMP - SESSION_START))
TOTAL_ACTIVE_SECONDS=$((TOTAL_ACTIVE_SECONDS + SESSION_DURATION))
debug_log "Ended session #$ACTIVE_SESSIONS at $(date -d @$PREV_TIMESTAMP "+%Y-%m-%d %H:%M:%S") (duration: $(date -u -d @$SESSION_DURATION "+%H:%M:%S"))"
# Start a new session
SESSION_START=$CURRENT_TIMESTAMP
ACTIVE_SESSIONS=$((ACTIVE_SESSIONS + 1))
debug_log "Started session #$ACTIVE_SESSIONS at $(date -d @$CURRENT_TIMESTAMP "+%Y-%m-%d %H:%M:%S")"
fi
PREV_TIMESTAMP=$CURRENT_TIMESTAMP
done
# Calculate duration of the last session
SESSION_DURATION=$((PREV_TIMESTAMP - SESSION_START))
TOTAL_ACTIVE_SECONDS=$((TOTAL_ACTIVE_SECONDS + SESSION_DURATION))
debug_log "Ended session #$ACTIVE_SESSIONS at $(date -d @$PREV_TIMESTAMP "+%Y-%m-%d %H:%M:%S") (duration: $(date -u -d @$SESSION_DURATION "+%H:%M:%S"))"
debug_log "Total active sessions: $ACTIVE_SESSIONS"
debug_log "First active timestamp: $FIRST_ACTIVE_TIMESTAMP"
debug_log "Last active timestamp: $LAST_ACTIVE_TIMESTAMP"
debug_log "Total active time in seconds: $TOTAL_ACTIVE_SECONDS"
else
debug_log "No active periods found based on idle threshold"
TOTAL_ACTIVE_SECONDS=0
FIRST_ACTIVE_TIMESTAMP=""
LAST_ACTIVE_TIMESTAMP=""
fi
# Convert seconds to hours and minutes
HOURS=$((TOTAL_ACTIVE_SECONDS / 3600))
MINUTES=$(( (TOTAL_ACTIVE_SECONDS % 3600) / 60 ))
# Print summary
echo "=== Activity Summary for $TODAY ==="
echo "Total active time today: $HOURS hours $MINUTES minutes"
echo "Based on idle threshold of $IDLE_THRESHOLD seconds"
if [ -n "$FIRST_ACTIVE_TIMESTAMP" ] && [ -n "$LAST_ACTIVE_TIMESTAMP" ]; then
echo "Activity span: $FIRST_ACTIVE_TIMESTAMP to $LAST_ACTIVE_TIMESTAMP"
fi
echo "Total entries counted as active: $TOTAL_ACTIVE_ENTRIES"
echo "Contiguous active sessions: $ACTIVE_SESSIONS"
The script works by:
- Filtering out entries from today’s date
- Considering me “active” only when my idle time is less than 300 seconds (5 minutes)
- Finding the first and last active timestamps of the day
- Calculating the total span between these timestamps
- Converting this span to hours and minutes
This approach means I’m counting the entire span from when I first start using my computer until the last time I use it, which works well for my purposes.
Posting to Beeminder
The final piece is post_active_time_to_beeminder.sh
, which takes the calculated active time and sends it to Beeminder:
#!/bin/bash
#
# post_active_time_to_beeminder.sh
#
# Description: This script parses active time from daily_active_time.sh
# and submits it to Beeminder. It extracts the total hours and minutes,
# converts them to a decimal format, and posts to the specified Beeminder goal.
# Function to log error messages and exit
log_error() {
echo "ERROR: $1" >&2
exit 1
}
# Function to log informational messages
log_info() {
echo "INFO: $1"
}
# Get Beeminder goal name from command-line argument or use default
GOAL_NAME="${1:-bizsys}"
log_info "Using Beeminder goal: $GOAL_NAME"
# Check for auth token
if [ -z "$BM_AUTH_TOKEN" ]; then
log_error "Beeminder authentication token not found. Please set the BM_AUTH_TOKEN environment variable."
fi
# Check for Beeminder username
if [ -z "$BM_USERNAME" ]; then
log_error "Beeminder username not found. Please set the BM_USERNAME environment variable."
fi
# Execute daily_active_time.sh and capture output
if ! OUTPUT=$(~/.scripts/daily_active_time.sh); then
log_error "Failed to execute daily_active_time.sh"
fi
# Check if output is empty
if [ -z "$OUTPUT" ]; then
log_error "No output received from daily_active_time.sh"
fi
log_info "Successfully captured output from daily_active_time.sh"
# Parse the output to find total hours and minutes
# Expected format: "Total active time today: X hours Y minutes"
HOURS=$(echo "$OUTPUT" | grep -oP "Total active time today: \K[0-9]+ (?=hours)" || echo "0")
MINUTES=$(echo "$OUTPUT" | grep -oP "hours \K[0-9]+ (?=minutes)" || echo "0")
# Trim whitespace
HOURS=$(echo "$HOURS" | tr -d ' ')
MINUTES=$(echo "$MINUTES" | tr -d ' ')
# Validate that we got numeric values
if ! [[ "$HOURS" =~ ^[0-9]+$ ]]; then
log_error "Failed to parse hours from the output: $OUTPUT"
fi
if ! [[ "$MINUTES" =~ ^[0-9]+$ ]]; then
log_error "Failed to parse minutes from the output: $OUTPUT"
fi
log_info "Parsed active time: $HOURS hours and $MINUTES minutes"
# Convert to decimal format
if ! DECIMAL_VALUE=$(echo "scale=2; $HOURS + $MINUTES/60" | bc); then
log_error "Failed to convert hours and minutes to decimal format"
fi
log_info "Converted to decimal format: $DECIMAL_VALUE hours"
# Prepare the Beeminder API request
API_URL="https://www.beeminder.com/api/v1/users/$BM_USERNAME/goals/$GOAL_NAME/datapoints.json"
COMMENT="Automated daily active time submission"
TIMESTAMP=$(date +%s)
REQUEST_ID=$(date +"%Y%m%d")
log_info "Preparing to send data to Beeminder: $DECIMAL_VALUE hours"
# Send the request to Beeminder API using curl
RESPONSE=$(curl -s -w "%{http_code}" -X POST \
-d "auth_token=$BM_AUTH_TOKEN" \
-d "value=$DECIMAL_VALUE" \
-d "comment=$COMMENT" \
-d "timestamp=$TIMESTAMP" \
-d "requestid=$REQUEST_ID" \
"$API_URL")
# Extract HTTP status code
HTTP_STATUS="${RESPONSE: -3}"
RESPONSE_BODY="${RESPONSE:0:${#RESPONSE}-3}"
# Check if request was successful
if [ "$HTTP_STATUS" -eq 200 ] || [ "$HTTP_STATUS" -eq 201 ]; then
log_info "Successfully posted $DECIMAL_VALUE hours to Beeminder goal '$GOAL_NAME'"
else
log_error "Failed to post to Beeminder. HTTP Status: $HTTP_STATUS, Response: $RESPONSE_BODY"
fi
exit 0
This script:
- Runs
daily_active_time.sh
and captures its output - Parses the hours and minutes from the output
- Converts them to a decimal format (e.g., 7 hours and 30 minutes becomes 7.5)
- Sends a POST request to the Beeminder API with the value
The script requires two environment variables to be set in ~/.bashrc
:
BM_USERNAME
: Your Beeminder usernameBM_AUTH_TOKEN
: Your Beeminder API authentication token
It also includes robust error handling to log issues if anything goes wrong during the process.
Scheduling with Systemd and Cron
To automate everything, I use:
-
Systemd Timer: Runs
activity_monitor.sh
every minute to ensure near real-time logging# /etc/systemd/system/activity-monitor.service [Unit] Description=Monitor active window and idle time [Service] Type=oneshot ExecStart=/home/username/.scripts/activity_monitor.sh [Install] WantedBy=multi-user.target
# /etc/systemd/system/activity-monitor.timer [Unit] Description=Run activity monitor every minute [Timer] OnBootSec=1min OnUnitActiveSec=1min [Install] WantedBy=timers.target
-
Cron Job: Triggers
post_active_time_to_beeminder.sh
hourly to keep Beeminder updated# Add to crontab with: crontab -e 0 * * * * . /home/username/.bashrc && /home/username/.scripts/post_active_time_to_beeminder.sh
Managing Log Growth with Automatic Log Rotation
One challenge with any logging system is managing file size. Without proper maintenance, my ~/.activity_log
would grow indefinitely, potentially consuming significant disk space over time. To prevent this, I implemented a simple log rotation system using logrotate
, a standard Linux utility designed specifically for this purpose.
Setting Up Logrotate
I created a custom logrotate configuration file:
# /etc/logrotate.d/activity_log
/home/narthur/.activity_log {
weekly
rotate 4
compress
missingok
notifempty
size 10M
}
This configuration:
- Rotates the log file weekly or when it reaches 10MB in size (whichever comes first)
- Keeps 4 rotated log files before deleting older ones
- Compresses older logs to save space
- Skips rotation if the log file is missing (
missingok
) - Doesn’t rotate empty log files (
notifempty
)
This approach strikes a good balance between preserving historical data and managing disk space. The size-based trigger (10MB) ensures that even during periods of heavy computer usage, the log file won’t grow excessively large.
Accessing Historical Data
With log rotation in place, I can still access historical data when needed:
# View a previous activity log
zcat ~/.activity_log.1.gz | grep "App: firefox"
This makes it possible to perform retrospective analysis across multiple weeks while keeping the current log file manageable.
By implementing this log rotation strategy, I ensure that my activity tracking system remains sustainable over the long term without manual intervention or risk of filling up my disk space. The configuration is minimal but effective, which aligns with the overall philosophy of this tracking system: simple, reliable automation.
Benefits and Future Improvements
This system has several benefits:
- Automatic tracking: Once set up, it requires zero manual intervention
- Accurate measurement: It only counts time when I’m actively using my computer
- Accountability: Beeminder helps me stay aware of my computer usage patterns
Some potential improvements I’m considering:
- Adding application-specific tracking to see which programs I use most
- Creating weekly reports with visualizations of my usage patterns
- Implementing categorization to distinguish between work and leisure activities
- Adding a dashboard to view my activity in real-time
Key Takeaways
This project demonstrates how a few simple bash scripts can be combined to create a powerful self-tracking system. The modular design—with separate scripts for data collection, analysis, and reporting—makes it easy to maintain and extend.
The beauty of this approach is its simplicity and flexibility. You could easily adapt it to track different metrics or post to different services beyond Beeminder.
If you’re interested in tracking your own computer usage, feel free to use these scripts as a starting point.
Happy tracking!