From fff22dd09301b42b90d9d85d16590397fe17f839 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 7 Aug 2025 16:51:08 -0400 Subject: [PATCH] First commit. --- LICENSE | 19 +++ ged | 433 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 452 insertions(+) create mode 100644 LICENSE create mode 100755 ged diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3a83b87 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/ged b/ged new file mode 100755 index 0000000..af0cbc4 --- /dev/null +++ b/ged @@ -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 - 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