diff --git a/checklist.sh b/checklist.sh index abd77f1..43f5d09 100755 --- a/checklist.sh +++ b/checklist.sh @@ -1,5 +1,10 @@ #!/bin/bash +# Set locale to handle UTF-8 characters +export LC_ALL=en_US.UTF-8 +export LANG=en_US.UTF-8 +export LANGUAGE=en_US.UTF-8 + # Check if file argument is provided if [ $# -ne 1 ]; then echo "Usage: $0 " @@ -32,6 +37,17 @@ clear_screen() { printf '\033[H' } +get_terminal_size() { + local size=$(stty size 2>/dev/null) + if [ -n "$size" ]; then + TERM_ROWS=${size% *} + TERM_COLS=${size#* } + else + TERM_ROWS=24 + TERM_COLS=80 + fi +} + cleanup() { show_cursor clear_screen @@ -45,109 +61,219 @@ trap cleanup EXIT INT TERM # Hide cursor at start hide_cursor +# Get terminal size +get_terminal_size + # Parse the file and store tasks declare -a categories=() declare -A tasks=() declare -A checked=() -declare -a display_items=() # Store display items in order +declare -a display_items=() +declare -A item_line_counts=() # Track how many lines each item takes current_category="" task_counter=0 +current_task="" while IFS= read -r line; do + # Don't trim leading whitespace yet - we need to detect indentation + trimmed_line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + if [[ $line =~ ^@(.+)$ ]]; then + # Save any pending task before starting new category + if [[ -n "$current_task" && -n "$current_category" ]]; then + task_key="task_${task_counter}" + tasks["$task_key"]="$current_task" + checked["$task_key"]=false + display_items+=("TASK:$task_key") + # Count lines in the task (plus 1 for empty line after) + line_count=$(echo "$current_task" | wc -l) + item_line_counts["TASK:$task_key"]=$((line_count + 1)) + ((task_counter++)) + current_task="" + fi + + # This is a new category current_category="${BASH_REMATCH[1]}" categories+=("$current_category") display_items+=("CATEGORY:$current_category") - elif [[ -n "$line" && -n "$current_category" ]]; then - task_key="${current_category}_${task_counter}" - tasks["$task_key"]="$line" - checked["$task_key"]=false - display_items+=("TASK:$task_key") - ((task_counter++)) + # Categories take 2 lines (category + empty line) + item_line_counts["CATEGORY:$current_category"]=2 + + elif [[ -n "$trimmed_line" && -n "$current_category" ]]; then + # Check if line starts with whitespace (continuation) + if [[ $line =~ ^[[:space:]] && -n "$current_task" ]]; then + # This is a continuation of the previous task + current_task="$current_task"$'\n'"$trimmed_line" + else + # Save previous task if exists + if [[ -n "$current_task" ]]; then + task_key="task_${task_counter}" + tasks["$task_key"]="$current_task" + checked["$task_key"]=false + display_items+=("TASK:$task_key") + # Count lines in the task (plus 1 for empty line after) + line_count=$(echo "$current_task" | wc -l) + item_line_counts["TASK:$task_key"]=$((line_count + 1)) + ((task_counter++)) + fi + # Start new task + current_task="$trimmed_line" + fi fi done < "$FILE" -# Current selection +# Don't forget the last task +if [[ -n "$current_task" && -n "$current_category" ]]; then + task_key="task_${task_counter}" + tasks["$task_key"]="$current_task" + checked["$task_key"]=false + display_items+=("TASK:$task_key") + # Count lines in the task (plus 1 for empty line after) + line_count=$(echo "$current_task" | wc -l) + item_line_counts["TASK:$task_key"]=$((line_count + 1)) + ((task_counter++)) +fi + +# Current selection and scrolling current_selection=0 total_items=${#display_items[@]} -header_lines=3 +header_lines=4 +scroll_offset=0 -# Initial display -initial_display() { +# Calculate how many items can fit on screen based on their actual line counts +calculate_visible_items() { + local available_lines=$((TERM_ROWS - header_lines - 1)) + local used_lines=0 + local count=0 + + for (( i=scroll_offset; i=0; i-- )); do + item="${display_items[$i]}" + lines_needed=${item_line_counts["$item"]} + + if (( used_lines + lines_needed <= available_lines )); then + used_lines=$((used_lines + lines_needed)) + scroll_offset=$i + else + break + fi + done + + calculate_visible_items + fi +} + +# Function to display a task (handles multi-line) +display_task() { + local task_key="$1" + local is_selected="$2" + local task_content="${tasks[$task_key]}" + + checkbox="[ ]" + if [ "${checked[$task_key]}" = true ]; then + checkbox="[✓]" + fi + + # Split task into lines + local first_line=true + while IFS= read -r task_line; do + if [ "$first_line" = true ]; then + # First line includes checkbox + local full_line=" $checkbox $task_line" + first_line=false + else + # Continuation lines are indented + local full_line=" $task_line" + fi + + if [ "$is_selected" = true ]; then + printf '\033[7m%s\033[0m\n' "$full_line" + else + printf '%s\n' "$full_line" + fi + done <<< "$task_content" +} + +# Full display refresh +full_display() { clear_screen echo "Checklist - Use ↑/↓ to navigate, SPACE to toggle, q to quit" echo "File: $FILE" + + # Show scroll indicator + if [ $total_items -gt $max_visible_items ]; then + end_item=$((scroll_offset + max_visible_items - 1)) + if [ $end_item -ge $total_items ]; then + end_item=$((total_items - 1)) + fi + echo "Items $((scroll_offset + 1))-$((end_item + 1)) of $total_items" + else + echo "All items shown" + fi + echo "==================================================" - for i in "${!display_items[@]}"; do + # Display visible items + visible_end=$((scroll_offset + max_visible_items)) + if [ $visible_end -gt $total_items ]; then + visible_end=$total_items + fi + + for (( i=scroll_offset; i