Files
git-er-done/ged
Storm Dragon fff22dd093 First commit.
2025-08-07 16:51:08 -04:00

434 lines
12 KiB
Bash
Executable File

#!/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 <task_number> - Mark task as done
# ged -r <task_number> <new_date> - 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