Update checklist
This commit is contained in:
parent
f566f8d9d0
commit
d3acca7e66
316
checklist.sh
316
checklist.sh
|
@ -1,5 +1,10 @@
|
||||||
#!/bin/bash
|
#!/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
|
# Check if file argument is provided
|
||||||
if [ $# -ne 1 ]; then
|
if [ $# -ne 1 ]; then
|
||||||
echo "Usage: $0 <path_to_file>"
|
echo "Usage: $0 <path_to_file>"
|
||||||
|
@ -32,6 +37,17 @@ clear_screen() {
|
||||||
printf '\033[H'
|
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() {
|
cleanup() {
|
||||||
show_cursor
|
show_cursor
|
||||||
clear_screen
|
clear_screen
|
||||||
|
@ -45,109 +61,219 @@ trap cleanup EXIT INT TERM
|
||||||
# Hide cursor at start
|
# Hide cursor at start
|
||||||
hide_cursor
|
hide_cursor
|
||||||
|
|
||||||
|
# Get terminal size
|
||||||
|
get_terminal_size
|
||||||
|
|
||||||
# Parse the file and store tasks
|
# Parse the file and store tasks
|
||||||
declare -a categories=()
|
declare -a categories=()
|
||||||
declare -A tasks=()
|
declare -A tasks=()
|
||||||
declare -A checked=()
|
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=""
|
current_category=""
|
||||||
task_counter=0
|
task_counter=0
|
||||||
|
current_task=""
|
||||||
|
|
||||||
while IFS= read -r line; do
|
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
|
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]}"
|
current_category="${BASH_REMATCH[1]}"
|
||||||
categories+=("$current_category")
|
categories+=("$current_category")
|
||||||
display_items+=("CATEGORY:$current_category")
|
display_items+=("CATEGORY:$current_category")
|
||||||
elif [[ -n "$line" && -n "$current_category" ]]; then
|
# Categories take 2 lines (category + empty line)
|
||||||
task_key="${current_category}_${task_counter}"
|
item_line_counts["CATEGORY:$current_category"]=2
|
||||||
tasks["$task_key"]="$line"
|
|
||||||
|
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
|
checked["$task_key"]=false
|
||||||
display_items+=("TASK:$task_key")
|
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++))
|
((task_counter++))
|
||||||
fi
|
fi
|
||||||
|
# Start new task
|
||||||
|
current_task="$trimmed_line"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
done < "$FILE"
|
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
|
current_selection=0
|
||||||
total_items=${#display_items[@]}
|
total_items=${#display_items[@]}
|
||||||
header_lines=3
|
header_lines=4
|
||||||
|
scroll_offset=0
|
||||||
|
|
||||||
# Initial display
|
# Calculate how many items can fit on screen based on their actual line counts
|
||||||
initial_display() {
|
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
|
clear_screen
|
||||||
echo "Checklist - Use ↑/↓ to navigate, SPACE to toggle, q to quit"
|
echo "Checklist - Use ↑/↓ to navigate, SPACE to toggle, q to quit"
|
||||||
echo "File: $FILE"
|
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 "=================================================="
|
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]}"
|
item="${display_items[$i]}"
|
||||||
if [[ $item =~ ^CATEGORY:(.+)$ ]]; then
|
if [[ $item =~ ^CATEGORY:(.+)$ ]]; then
|
||||||
category="${BASH_REMATCH[1]}"
|
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
|
elif [[ $item =~ ^TASK:(.+)$ ]]; then
|
||||||
task_key="${BASH_REMATCH[1]}"
|
task_key="${BASH_REMATCH[1]}"
|
||||||
checkbox="[ ]"
|
display_task "$task_key" $([ $i -eq $current_selection ] && echo "true" || echo "false")
|
||||||
if [ "${checked[$task_key]}" = true ]; then
|
|
||||||
checkbox="[✓]"
|
|
||||||
fi
|
|
||||||
echo " $checkbox ${tasks[$task_key]}"
|
|
||||||
fi
|
fi
|
||||||
echo # Empty line after each item
|
echo # Empty line after each item
|
||||||
done
|
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 the task key at current selection
|
||||||
get_current_task_key() {
|
get_current_task_key() {
|
||||||
item="${display_items[$current_selection]}"
|
item="${display_items[$current_selection]}"
|
||||||
|
@ -158,14 +284,11 @@ get_current_task_key() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show initial display
|
# Initial display
|
||||||
initial_display
|
update_scroll
|
||||||
|
full_display
|
||||||
# Highlight first item
|
|
||||||
update_line 0
|
|
||||||
|
|
||||||
# Main loop
|
# Main loop
|
||||||
prev_selection=0
|
|
||||||
while true; do
|
while true; do
|
||||||
# Read single character
|
# Read single character
|
||||||
IFS= read -rsn1 input
|
IFS= read -rsn1 input
|
||||||
|
@ -177,18 +300,42 @@ while true; do
|
||||||
case $input in
|
case $input in
|
||||||
'[A') # Up arrow
|
'[A') # Up arrow
|
||||||
if [ $current_selection -gt 0 ]; then
|
if [ $current_selection -gt 0 ]; then
|
||||||
prev_selection=$current_selection
|
|
||||||
((current_selection--))
|
((current_selection--))
|
||||||
update_line $prev_selection
|
prev_scroll=$scroll_offset
|
||||||
update_line $current_selection
|
update_scroll
|
||||||
|
|
||||||
|
# Always do full refresh for multi-line content
|
||||||
|
full_display
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
'[B') # Down arrow
|
'[B') # Down arrow
|
||||||
if [ $current_selection -lt $((total_items - 1)) ]; then
|
if [ $current_selection -lt $((total_items - 1)) ]; then
|
||||||
prev_selection=$current_selection
|
|
||||||
((current_selection++))
|
((current_selection++))
|
||||||
update_line $prev_selection
|
prev_scroll=$scroll_offset
|
||||||
update_line $current_selection
|
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
|
fi
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
@ -201,11 +348,18 @@ while true; do
|
||||||
else
|
else
|
||||||
checked["$task_key"]=true
|
checked["$task_key"]=true
|
||||||
fi
|
fi
|
||||||
# Only update the checkbox, not the entire line
|
# Full refresh to update checkbox properly
|
||||||
update_checkbox $current_selection
|
full_display
|
||||||
fi
|
fi
|
||||||
elif [[ $input == 'q' ]] || [[ $input == 'Q' ]]; then
|
elif [[ $input == 'q' ]] || [[ $input == 'Q' ]]; then
|
||||||
# Quit - cleanup will be called by trap
|
# Quit - cleanup will be called by trap
|
||||||
exit 0
|
exit 0
|
||||||
|
elif [[ $input == 'r' ]] || [[ $input == 'R' ]]; then
|
||||||
|
# Refresh display
|
||||||
|
get_terminal_size
|
||||||
|
update_scroll
|
||||||
|
full_display
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue