First commit.

This commit is contained in:
Storm Dragon
2025-08-07 16:51:08 -04:00
commit fff22dd093
2 changed files with 452 additions and 0 deletions

19
LICENSE Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2025 Stormux
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.

433
ged Executable file
View File

@@ -0,0 +1,433 @@
#!/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