diff --git a/README.md b/README.md index 448f69c..aa52c13 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,8 @@ The loader sources every readable `bashrc.d/*.bash` file in lexical order: - `50-completions.bash`: command-specific completion setup. - `60-directory-notes.bash`: `TODO.md` and `.autonote.md` display after `cd`. - `70-development.bash`: OpenCode and Pyenv initialization. -- `90-memory-question.bash`: daily shell-memory question and `answerpls`. +- `90-memory-question.bash`: persisted shuffled question deck, `answerpls` and + on-demand `refreshpls` rotation. Keep cross-module dependencies explicit through this numbering. New modules should own one coherent area rather than becoming another general-purpose RC. diff --git a/bashrc.d/90-memory-question.bash b/bashrc.d/90-memory-question.bash index 5a1c660..bd23740 100644 --- a/bashrc.d/90-memory-question.bash +++ b/bashrc.d/90-memory-question.bash @@ -1,6 +1,8 @@ # shellcheck shell=bash _memory_file="${XDG_CONFIG_HOME:-$HOME/.config}/bash/memory.tsv" +_memory_state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/bash" +_memory_state_file="$_memory_state_dir/memory-question" _memory_question_color='\033[38;5;11m' _memory_names=() _memory_questions=() @@ -40,18 +42,141 @@ else fi if [[ $_memory_invalid -eq 0 && ${#_memory_names[@]} -gt 0 ]]; then - _memory_day=$(date -d "$(date +%F) 00:00" +%s) - _memory_index=$(( (_memory_day / 86400) % ${#_memory_names[@]} )) - _MEMORY_ANSWER=${_memory_answers[$_memory_index]} + _memory_load_state() { + local -a fields=() state_deck=() + local state_date state_index item + local -A state_seen=() + + [[ -r "$_memory_state_file" ]] || return 1 + IFS=$'\t' read -r -a fields < "$_memory_state_file" + [[ ${#fields[@]} -eq $((${#_memory_names[@]} + 2)) ]] || return 1 + + state_date=${fields[0]} + state_index=${fields[1]} + state_deck=("${fields[@]:2}") + [[ $state_index =~ ^[0-9]+$ && $state_index -lt ${#state_deck[@]} ]] || return 1 + + for item in "${state_deck[@]}"; do + [[ -n ${_memory_seen[$item]:-} && -z ${state_seen[$item]:-} ]] || return 1 + state_seen[$item]=1 + done + + _memory_state_date=$state_date + _memory_index=$state_index + _memory_deck=("${state_deck[@]}") + } + + _memory_shuffle_deck() { + local previous_name=$1 + local i j swap + + _memory_deck=("${_memory_names[@]}") + for ((i = ${#_memory_deck[@]} - 1; i > 0; i--)); do + j=$((RANDOM % (i + 1))) + swap=${_memory_deck[$i]} + _memory_deck[i]=${_memory_deck[j]} + _memory_deck[j]=$swap + done + + if [[ ${#_memory_deck[@]} -gt 1 && ${_memory_deck[0]} == "$previous_name" ]]; then + swap=${_memory_deck[0]} + _memory_deck[0]=${_memory_deck[1]} + _memory_deck[1]=$swap + fi + + _memory_index=0 + } + + _memory_save_state() { + local state_tmp="${_memory_state_file}.$$" + local item + + mkdir -p "$_memory_state_dir" || return 1 + { + printf '%s\t%s' "$_memory_state_date" "$_memory_index" + for item in "${_memory_deck[@]}"; do + printf '\t%s' "$item" + done + printf '\n' + } > "$state_tmp" || return 1 + mv -f -- "$state_tmp" "$_memory_state_file" + } + + _memory_select_current() { + local current_name=${_memory_deck[$_memory_index]} + local i + + for ((i = 0; i < ${#_memory_names[@]}; i++)); do + if [[ ${_memory_names[$i]} == "$current_name" ]]; then + _MEMORY_ANSWER=${_memory_answers[$i]} + _MEMORY_QUESTION=${_memory_questions[$i]} + return + fi + done + + return 1 + } + + _memory_advance() { + local previous_name=${_memory_deck[$_memory_index]} + + ((_memory_index++)) + if [[ $_memory_index -ge ${#_memory_deck[@]} ]]; then + _memory_shuffle_deck "$previous_name" + fi + + _memory_select_current + _memory_save_state + } + + _memory_sync_day() { + local today + + today=$(date +%F) + [[ $_memory_state_date != "$today" ]] || return 0 + + _memory_state_date=$today + _memory_advance + } + + _memory_print_question() { + printf '%b%s%b (answerpls, refreshpls)\n\n' \ + "$_memory_question_color" "$_MEMORY_QUESTION" "$_NC" + } + + _memory_today=$(date +%F) + if _memory_load_state; then + _memory_select_current + _memory_sync_day + else + _memory_state_date=$_memory_today + _memory_shuffle_deck '' + _memory_select_current + _memory_save_state + fi answerpls() { printf '%s\n' "$_MEMORY_ANSWER" } - printf '%b%s%b (answerpls)\n\n' \ - "$_memory_question_color" "${_memory_questions[$_memory_index]}" "$_NC" + refreshpls() { + if _memory_load_state; then + _memory_select_current + else + _memory_state_date=$(date +%F) + _memory_shuffle_deck "${_memory_deck[$_memory_index]:-}" + _memory_select_current + _memory_save_state + fi + + _memory_sync_day + _memory_advance + _memory_print_question + } + + _memory_print_question fi -unset _memory_file _memory_question_color _memory_names _memory_questions _memory_answers -unset _memory_seen _memory_invalid _memory_line_number _memory_name _memory_question -unset _memory_answer _memory_extra _memory_day _memory_index +unset _memory_file +unset _memory_invalid _memory_line_number _memory_name _memory_question +unset _memory_answer _memory_extra _memory_today diff --git a/memory.README.md b/memory.README.md index addc974..ed6b2ca 100644 --- a/memory.README.md +++ b/memory.README.md @@ -14,10 +14,18 @@ name question answer - `question` is displayed on shell startup. - `answer` is displayed by running `answerpls` in that terminal. +Run `refreshpls` to advance to another question before the next calendar day. +`answerpls` then displays the answer to the newly shown question. Refreshing is +persisted under `$XDG_STATE_HOME/bash` (or `~/.local/state/bash` when that +variable is unset), so new terminals show the refreshed question for the rest +of that calendar day. The normal daily selection takes over again the following +day. + The file has no header or comments. Empty fields, duplicate names and malformed rows are errors. -Bash selects one entry deterministically from the local calendar date. Every -terminal opened on the same day therefore shows the same question without -requiring a state file. Editing the bank may change which question maps to a -given date. +Bash works through a persisted, randomly shuffled deck. Each question appears +once before the deck is shuffled again, and a new deck does not begin with the +question that ended the previous one. The deck advances on a new calendar day +or when `refreshpls` is run, while every terminal opened on the same day shows +the current persisted question. Editing the bank starts a newly shuffled deck.