First commit.
This commit is contained in:
19
LICENSE
Normal file
19
LICENSE
Normal 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
433
ged
Executable 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
|
Reference in New Issue
Block a user