#!/usr/bin/env bash # Git 'er Done - Simple Todo Manager # Usage: # ged [task description] [due_date] - Add a task # ged - List all tasks # ged -d - Mark task as done # ged -r - Reschedule task # Use XDG data directory or fallback XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}" TASKS_DIR="$XDG_DATA_HOME/git-er-done" TASKS_FILE="$TASKS_DIR/tasks" # Create directory if it doesn't exist mkdir -p "$TASKS_DIR" help() { echo "${0##*/}" echo "Git 'er Done - Simple Todo Manager" echo "" echo "Usage:" echo "With no arguments, list all tasks." for i in "${!command[@]}" ; do if [[ "$i" == *:* ]]; then local flag="${i%%:*}" local params="${i#*:}" params="${params//:/ ><}" echo "-${flag} <${params}>: ${command[${i}]}" else echo "-${i}: ${command[${i}]}" fi done | sort echo "" echo "When adding tasks:" echo " ged \"task description\" [due_date]" echo "" echo "Date formats: YYYY-MM-DD, 2025-08-10, tomorrow, next monday, etc." echo "" echo "Examples:" echo " ged \"Fix bug in project\"" echo " ged \"Call dentist\" tomorrow" echo " ged \"Submit report\" 2025-08-15" echo " ged -d 1" echo " ged -r 2 \"next friday\"" exit 0 } parse_date() { local inputDate="$1" if [[ -z "$inputDate" ]]; then echo "" return fi # If it's already in YYYY-MM-DD format, return it if [[ "$inputDate" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then echo "$inputDate" return fi # Use date command to parse natural language dates local parsedDate if parsedDate=$(date -d "$inputDate" +%Y-%m-%d 2>/dev/null); then echo "$parsedDate" else echo "" fi } format_date() { local dateStr="$1" if [[ -z "$dateStr" ]]; then echo "" return fi local formatted if formatted=$(date -d "$dateStr" "+%A, %B %d, %Y" 2>/dev/null); then echo "$formatted" else echo "$dateStr" fi } add_task() { local description="$1" local dueDate="$2" if [[ -z "$description" ]]; then echo "Error: Task description required" return 1 fi local parsedDue="" if [[ -n "$dueDate" ]]; then parsedDue=$(parse_date "$dueDate") if [[ -z "$parsedDue" ]]; then echo "Error: Invalid date format '$dueDate'" return 1 fi fi # Create tasks file if it doesn't exist touch "$TASKS_FILE" # Add task to file (format: description|due_date) echo "$description|$parsedDue" >> "$TASKS_FILE" if [[ -n "$parsedDue" ]]; then local formattedDate formattedDate=$(format_date "$parsedDue") echo "Task \"$description\" added, due by $formattedDate." else echo "Task \"$description\" added." fi } list_tasks() { if [[ ! -f "$TASKS_FILE" ]] || [[ ! -s "$TASKS_FILE" ]]; then echo "No tasks found. Add some tasks to get started!" return fi echo "Git 'er Done task list" # Read tasks, sort by due date (tasks with dates first, then tasks without) local tasksWithDates=() local tasksWithoutDates=() local lineNum=1 while IFS='|' read -r description dueDate; do if [[ -n "$dueDate" ]]; then tasksWithDates+=("$lineNum|$description|$dueDate") else tasksWithoutDates+=("$lineNum|$description|") fi ((lineNum++)) done < "$TASKS_FILE" # Sort tasks with dates by date if [[ ${#tasksWithDates[@]} -gt 0 ]]; then readarray -t tasksWithDates < <(printf '%s\n' "${tasksWithDates[@]}" | sort -t'|' -k3) fi local displayNum=1 # Display tasks with dates first for task in "${tasksWithDates[@]}"; do IFS='|' read -r _ description dueDate <<< "$task" local formattedDate formattedDate=$(format_date "$dueDate") echo "$displayNum. $description Due by $formattedDate" ((displayNum++)) done # Then display tasks without dates for task in "${tasksWithoutDates[@]}"; do IFS='|' read -r _ description _ <<< "$task" echo "$displayNum. $description" ((displayNum++)) done } mark_done() { local taskNum="$1" if [[ ! -f "$TASKS_FILE" ]] || [[ ! -s "$TASKS_FILE" ]]; then echo "No tasks found." return 1 fi if [[ ! "$taskNum" =~ ^[0-9]+$ ]]; then echo "Error: Task number must be a number" return 1 fi # Get the task description for confirmation local taskDescription taskDescription=$(get_task_by_display_number "$taskNum") if [[ -z "$taskDescription" ]]; then echo "Error: Task $taskNum not found" return 1 fi # Remove the task from the file local tempFile tempFile=$(mktemp) remove_task_by_display_number "$taskNum" > "$tempFile" mv "$tempFile" "$TASKS_FILE" echo "Task $taskNum \"$taskDescription\" marked as done and removed from list." } reschedule_task() { local taskNum="$1" local newDate="$2" if [[ ! -f "$TASKS_FILE" ]] || [[ ! -s "$TASKS_FILE" ]]; then echo "No tasks found." return 1 fi if [[ ! "$taskNum" =~ ^[0-9]+$ ]]; then echo "Error: Task number must be a number" return 1 fi local parsedDate parsedDate=$(parse_date "$newDate") if [[ -z "$parsedDate" ]]; then echo "Error: Invalid date format '$newDate'" return 1 fi # Get the task description local taskDescription taskDescription=$(get_task_by_display_number "$taskNum") if [[ -z "$taskDescription" ]]; then echo "Error: Task $taskNum not found" return 1 fi # Update the task local tempFile tempFile=$(mktemp) update_task_by_display_number "$taskNum" "$parsedDate" > "$tempFile" mv "$tempFile" "$TASKS_FILE" local formattedDate formattedDate=$(format_date "$parsedDate") echo "Task $taskNum \"$taskDescription\" rescheduled to $formattedDate." } get_task_by_display_number() { local targetNum="$1" local currentNum=1 # Build sorted list like in list_tasks local tasksWithDates=() local tasksWithoutDates=() local lineNum=1 while IFS='|' read -r description dueDate; do if [[ -n "$dueDate" ]]; then tasksWithDates+=("$lineNum|$description|$dueDate") else tasksWithoutDates+=("$lineNum|$description|") fi ((lineNum++)) done < "$TASKS_FILE" if [[ ${#tasksWithDates[@]} -gt 0 ]]; then readarray -t tasksWithDates < <(printf '%s\n' "${tasksWithDates[@]}" | sort -t'|' -k3) fi # Check tasks with dates first for task in "${tasksWithDates[@]}"; do if [[ $currentNum -eq $targetNum ]]; then IFS='|' read -r _ description _ <<< "$task" echo "$description" return fi ((currentNum++)) done # Then check tasks without dates for task in "${tasksWithoutDates[@]}"; do if [[ $currentNum -eq $targetNum ]]; then IFS='|' read -r _ description _ <<< "$task" echo "$description" return fi ((currentNum++)) done } remove_task_by_display_number() { local targetNum="$1" local currentNum=1 # Build sorted list and track original line numbers local tasksWithDates=() local tasksWithoutDates=() local lineNum=1 while IFS='|' read -r description dueDate; do if [[ -n "$dueDate" ]]; then tasksWithDates+=("$lineNum|$description|$dueDate") else tasksWithoutDates+=("$lineNum|$description|") fi ((lineNum++)) done < "$TASKS_FILE" if [[ ${#tasksWithDates[@]} -gt 0 ]]; then readarray -t tasksWithDates < <(printf '%s\n' "${tasksWithDates[@]}" | sort -t'|' -k3) fi local skipLine=0 # Find which original line to skip for task in "${tasksWithDates[@]}"; do if [[ $currentNum -eq $targetNum ]]; then IFS='|' read -r origLine _ _ <<< "$task" skipLine=$origLine break fi ((currentNum++)) done if [[ $skipLine -eq 0 ]]; then for task in "${tasksWithoutDates[@]}"; do if [[ $currentNum -eq $targetNum ]]; then IFS='|' read -r origLine _ _ <<< "$task" skipLine=$origLine break fi ((currentNum++)) done fi # Output all lines except the one to skip lineNum=1 while IFS='|' read -r description dueDate; do if [[ $lineNum -ne $skipLine ]]; then echo "$description|$dueDate" fi ((lineNum++)) done < "$TASKS_FILE" } update_task_by_display_number() { local targetNum="$1" local newDueDate="$2" local currentNum=1 # Build sorted list and track original line numbers local tasksWithDates=() local tasksWithoutDates=() local lineNum=1 while IFS='|' read -r description dueDate; do if [[ -n "$dueDate" ]]; then tasksWithDates+=("$lineNum|$description|$dueDate") else tasksWithoutDates+=("$lineNum|$description|") fi ((lineNum++)) done < "$TASKS_FILE" if [[ ${#tasksWithDates[@]} -gt 0 ]]; then readarray -t tasksWithDates < <(printf '%s\n' "${tasksWithDates[@]}" | sort -t'|' -k3) fi local updateLine=0 local targetDescription="" # Find which original line to update for task in "${tasksWithDates[@]}"; do if [[ $currentNum -eq $targetNum ]]; then IFS='|' read -r origLine description _ <<< "$task" updateLine=$origLine targetDescription="$description" break fi ((currentNum++)) done if [[ $updateLine -eq 0 ]]; then for task in "${tasksWithoutDates[@]}"; do if [[ $currentNum -eq $targetNum ]]; then IFS='|' read -r origLine description _ <<< "$task" updateLine=$origLine targetDescription="$description" break fi ((currentNum++)) done fi # Output all lines, updating the target line lineNum=1 while IFS='|' read -r description dueDate; do if [[ $lineNum -eq $updateLine ]]; then echo "$targetDescription|$newDueDate" else echo "$description|$dueDate" fi ((lineNum++)) done < "$TASKS_FILE" } # Array of command line arguments declare -A command=( [d:task_number]="Mark task as done and remove from list." [h]="This help screen." [r:task_number:new_date]="Reschedule task to new date." [l]="List all tasks (same as no arguments)." ) # Main script logic with improved help but simple case parsing case "$1" in "") list_tasks ;; "-h"|"--help") help ;; "-d") if [[ -z "$2" ]]; then echo "Error: Task number required for -d option" exit 1 fi mark_done "$2" ;; "-r") if [[ -z "$2" ]] || [[ -z "$3" ]]; then echo "Error: Task number and new date required for -r option" exit 1 fi reschedule_task "$2" "$3" ;; "-l") list_tasks ;; *) # Add new task add_task "$1" "$2" ;; esac exit 0