Update checklist

This commit is contained in:
shutkin.k 2025-05-25 13:35:32 +03:00
parent f566f8d9d0
commit d3acca7e66
1 changed files with 238 additions and 84 deletions

View File

@ -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 <path_to_file>"
@ -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"
# 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<total_items; i++ )); do
local item="${display_items[$i]}"
local lines_needed=${item_line_counts["$item"]}
if (( used_lines + lines_needed <= available_lines )); then
used_lines=$((used_lines + lines_needed))
count=$((count + 1))
else
break
fi
done
max_visible_items=$count
if [ $max_visible_items -lt 1 ]; then
max_visible_items=1
fi
}
# Calculate scroll position to keep current selection visible
update_scroll() {
# If current selection is before scroll window, scroll up
if [ $current_selection -lt $scroll_offset ]; then
scroll_offset=$current_selection
calculate_visible_items
return
fi
# Calculate visible items from current scroll position
calculate_visible_items
# If current selection is after scroll window, scroll down
visible_end=$((scroll_offset + max_visible_items))
if [ $current_selection -ge $visible_end ]; then
# Find the scroll position where current_selection is the last visible item
target_end=$((current_selection + 1))
available_lines=$((TERM_ROWS - header_lines - 1))
used_lines=0
# Work backwards from current_selection to find scroll_offset
for (( i=current_selection; 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<visible_end; i++ )); do
item="${display_items[$i]}"
if [[ $item =~ ^CATEGORY:(.+)$ ]]; then
category="${BASH_REMATCH[1]}"
echo "@$category"
if [ $i -eq $current_selection ]; then
printf '\033[7m@%s\033[0m\n' "$category"
else
printf '@%s\n' "$category"
fi
elif [[ $item =~ ^TASK:(.+)$ ]]; then
task_key="${BASH_REMATCH[1]}"
checkbox="[ ]"
if [ "${checked[$task_key]}" = true ]; then
checkbox="[✓]"
fi
echo " $checkbox ${tasks[$task_key]}"
display_task "$task_key" $([ $i -eq $current_selection ] && echo "true" || echo "false")
fi
echo # Empty line after each item
done
}
# Update single line
update_line() {
local line_num=$((header_lines + 1 + ($1 * 2))) # *2 because of empty lines
move_cursor $line_num 1
printf '\033[K' # Clear line
item="${display_items[$1]}"
if [[ $item =~ ^CATEGORY:(.+)$ ]]; then
category="${BASH_REMATCH[1]}"
if [ $1 -eq $current_selection ]; then
printf '\033[7m@%s\033[0m' "$category"
else
printf '@%s' "$category"
fi
elif [[ $item =~ ^TASK:(.+)$ ]]; then
task_key="${BASH_REMATCH[1]}"
checkbox="[ ]"
if [ "${checked[$task_key]}" = true ]; then
checkbox="[✓]"
fi
task_line=" $checkbox ${tasks[$task_key]}"
if [ $1 -eq $current_selection ]; then
printf '\033[7m%s\033[0m' "$task_line"
else
printf '%s' "$task_line"
fi
fi
}
# Update only the checkbox part of a task line
update_checkbox() {
local line_num=$((header_lines + 1 + ($1 * 2)))
local task_key="${BASH_REMATCH[1]}"
item="${display_items[$1]}"
if [[ $item =~ ^TASK:(.+)$ ]]; then
task_key="${BASH_REMATCH[1]}"
checkbox="[ ]"
if [ "${checked[$task_key]}" = true ]; then
checkbox="[✓]"
fi
# Move to checkbox position (column 3) and update just the checkbox
move_cursor $line_num 3
if [ $1 -eq $current_selection ]; then
printf '\033[7m%s\033[0m' "$checkbox"
else
printf '%s' "$checkbox"
fi
fi
}
# Get the task key at current selection
get_current_task_key() {
item="${display_items[$current_selection]}"
@ -158,14 +284,11 @@ get_current_task_key() {
fi
}
# Show initial display
initial_display
# Highlight first item
update_line 0
# Initial display
update_scroll
full_display
# Main loop
prev_selection=0
while true; do
# Read single character
IFS= read -rsn1 input
@ -177,18 +300,42 @@ while true; do
case $input in
'[A') # Up arrow
if [ $current_selection -gt 0 ]; then
prev_selection=$current_selection
((current_selection--))
update_line $prev_selection
update_line $current_selection
prev_scroll=$scroll_offset
update_scroll
# Always do full refresh for multi-line content
full_display
fi
;;
'[B') # Down arrow
if [ $current_selection -lt $((total_items - 1)) ]; then
prev_selection=$current_selection
((current_selection++))
update_line $prev_selection
update_line $current_selection
prev_scroll=$scroll_offset
update_scroll
# Always do full refresh for multi-line content
full_display
fi
;;
'[5~') # Page Up
if [ $current_selection -gt 0 ]; then
current_selection=$((current_selection - max_visible_items))
if [ $current_selection -lt 0 ]; then
current_selection=0
fi
update_scroll
full_display
fi
;;
'[6~') # Page Down
if [ $current_selection -lt $((total_items - 1)) ]; then
current_selection=$((current_selection + max_visible_items))
if [ $current_selection -ge $total_items ]; then
current_selection=$((total_items - 1))
fi
update_scroll
full_display
fi
;;
esac
@ -201,11 +348,18 @@ while true; do
else
checked["$task_key"]=true
fi
# Only update the checkbox, not the entire line
update_checkbox $current_selection
# Full refresh to update checkbox properly
full_display
fi
elif [[ $input == 'q' ]] || [[ $input == 'Q' ]]; then
# Quit - cleanup will be called by trap
exit 0
elif [[ $input == 'r' ]] || [[ $input == 'R' ]]; then
# Refresh display
get_terminal_size
update_scroll
full_display
fi
done