#!/usr/bin/env python3 '''....................................................................... | | Cleaning House (Personal Inventory) | | program: cleaning_house | date...: 31 Jan 2025 | by.....: yetimach | | This is a tool to help work through Step 4 of the 12 steps | of Alcoholics Anonymous. The idea is to make an inventory | of resentments against people, institutions and principles. | After identifying the resentment, it is then described and | finally the affected aspects of life are named. Some people | may prefer to use a notebook, old-school style, but I like | this method. I share this program here in case anybody else | may benefit from it as well. | | There is a password feature, just for fun. It is just to make | the process feel more private and personal. | | -w flag will write a file called my_resentments.txt in | ~/cleaning_house/ | -n will skip printing the text at the beginning | .......................................................................''' import json import os import getpass import textwrap import argparse # Added for flag handling # Configuration HOME_DIR = os.path.expanduser('~') DATA_DIR = os.path.join(HOME_DIR, '.cleaning_house') os.makedirs(DATA_DIR, exist_ok=True) DATA_FILE = os.path.join(DATA_DIR, 'resentments.json') PW_FILE = os.path.join(DATA_DIR, '.pw') # Hidden file for password OUTPUT_FILE = os.path.join(DATA_DIR, 'my_resentments.txt') # New output file WRAP_WIDTH = 50 # Fixed lists from the Big Book ASPECTS = [ "Self-esteem", "Personal/sexual relationships", "Ambitions", "Security" ] CATEGORIES = ["people", "institutions", "principles"] CATEGORY_NAMES = ["People", "Institutions", "Principles"] CATEGORY_PROMPTS = [ "Enter the name of the person", "Enter the name of the institution", "Enter the principle" ] # Helper to wrap text def wrap_print(text): for line in textwrap.wrap(text, width=WRAP_WIDTH): print(line) # Password handling def get_password(): print("Note: For privacy, nothing will appear on screen as you type your password (not even asterisks). Just type and press Enter.") return getpass.getpass("Enter password: ").strip() def setup_password(): print("Welcome! This appears to be your first time using Cleaning House.") print("Let's set up a password to protect your inventory (like a lock on a journal).") print("Note: For privacy, nothing will appear on screen as you type your password (not even asterisks). Just type and press Enter.") while True: pw1 = getpass.getpass("Enter your new password: ").strip() pw2 = getpass.getpass("Confirm password: ").strip() if pw1 == pw2: if not pw1: print("Password cannot be blank. Try again.") continue with open(PW_FILE, 'w') as f: f.write(pw1) print("Password set successfully.\n") return pw1 else: print("Passwords do not match. Try again.") def check_password(max_attempts=3): if not os.path.exists(PW_FILE): return setup_password() stored_pw = open(PW_FILE, 'r').read().strip() for attempt in range(1, max_attempts + 1): entered = get_password() if entered == stored_pw: print("Access granted.\n") return stored_pw else: remaining = max_attempts - attempt if remaining > 0: print(f"Incorrect password. {remaining} attempt{'s' if remaining > 1 else ''} left.") else: print("Too many incorrect attempts. Goodbye.") exit(0) # Load and save data def load_data(): if os.path.exists(DATA_FILE): with open(DATA_FILE, 'r') as f: return json.load(f) return {cat: [] for cat in CATEGORIES} def save_data(data): with open(DATA_FILE, 'w') as f: json.dump(data, f, indent=4) # Initial explanation (only shown if -n not used) def print_explanation(): print("\n" + "=" * WRAP_WIDTH) wrap_print("Cleaning House: AA 4th Step Resentment Inventory") print("=" * WRAP_WIDTH) wrap_print( "From 'How It Works' in the Big Book (pp. 63-67): Resentment is the 'number one' offender. " "It destroys more alcoholics than anything else. From it stem all forms of spiritual disease." ) print() wrap_print( "We made a searching and fearless moral inventory of ourselves. We set on paper a list of " "people, institutions, or principles with whom we were angry. We asked ourselves why we were angry. " "In most cases we found that our self-esteem, our pocketbooks, our ambitions, or our personal " "relationships (including sex) were hurt or threatened." ) print() wrap_print( "Recommended process: First, list everything that comes to mind (just the names). " "When the list feels complete, go back and fill in the causes and what was affected." ) print("\n" + "-" * WRAP_WIDTH + "\n") # Print header that is always shown def print_header(): print("\n" + "=" * 51) print("Cleaning House: AA 4th Step Resentment Inventory") print("=" * 51) print("\nMenu:\n") # List everything (used both for display and file output) def list_resentments(data, to_file=False): output_lines = [] output_lines.append("=" * WRAP_WIDTH) output_lines.append("Current Resentment Inventory") output_lines.append("=" * WRAP_WIDTH) output_lines.append("") total = 0 incomplete_count = 0 for cat, cat_name in zip(CATEGORIES, CATEGORY_NAMES): entries = data[cat] if not entries: continue output_lines.append(f"\n--- {cat_name} ---") output_lines.append("") for res in entries: total += 1 status = " (incomplete)" if not res["description"] else "" output_lines.append(f"• {res['name']}{status}") if res["description"]: wrapped_desc = textwrap.wrap(f"Cause: {res['description']}", width=WRAP_WIDTH) for line in wrapped_desc: output_lines.append(f" {line}") affected = ", ".join(res["affected"]) if res["affected"] else "None" wrapped_affected = textwrap.wrap(f"Affects: {affected}", width=WRAP_WIDTH) for line in wrapped_affected: output_lines.append(f" {line}") else: incomplete_count += 1 output_lines.append("") output_lines.append(f"Total entries: {total}") if incomplete_count: output_lines.append(f"Still need details for: {incomplete_count}") else: output_lines.append("All entries complete — powerful work!") if to_file: with open(OUTPUT_FILE, 'w', encoding='utf-8') as f: for line in output_lines: f.write(line + "\n") print(f"\nInventory written to {OUTPUT_FILE}") else: for line in output_lines: print(line) # Quick add with loop def quick_add_names_loop(data): print("\nSelect category:") for i, name in enumerate(CATEGORY_NAMES, 1): print(f"{i}. {name}") try: cat_idx = int(input("Enter number (1-3): ").strip()) - 1 category = CATEGORIES[cat_idx] category_name = CATEGORY_NAMES[cat_idx] prompt = CATEGORY_PROMPTS[cat_idx] except (ValueError, IndexError): print("Invalid category.") return print(f"\nAdding to {category_name}. Leave blank and press Enter to finish this category.") while True: name = input(f"{prompt}: ").strip() if not name: print("Done with this category.") break resentment = { "name": name, "description": "", "affected": [] } data[category].append(resentment) save_data(data) print(f"Added: {name}") print("Returning to main menu.\n") # Complete details def complete_resentment(data): incomplete = [] for cat in CATEGORIES: for i, res in enumerate(data[cat]): if not res["description"]: incomplete.append((cat, i, res["name"])) if not incomplete: print("\nAll resentments have details filled in. Great progress!") return print("\nIncomplete resentments:") for idx, (cat, i, name) in enumerate(incomplete, 1): cat_name = CATEGORY_NAMES[CATEGORIES.index(cat)] print(f"{idx}. [{cat_name}] {name}") try: choice = int(input("\nEnter number to complete (or 0 to cancel): ").strip()) if choice == 0: return cat, i, _ = incomplete[choice - 1] except (ValueError, IndexError): print("Invalid selection.") return res = data[cat][i] cat_name = CATEGORY_NAMES[CATEGORIES.index(cat)] print(f"\nCompleting: {res['name']} [{cat_name}]") desc = input("Enter the cause/description of the resentment: ").strip() if not desc: print("Description required to complete.") return print("\nSelect affected aspects (space-separated numbers, e.g., '1 3', or leave blank for none):") for num, asp in enumerate(ASPECTS, 1): print(f"{num}. {asp}") asp_input = input("Your choices: ").strip() selected = [] if asp_input: try: indices = [int(x) - 1 for x in asp_input.split()] selected = [ASPECTS[j] for j in indices if 0 <= j < len(ASPECTS)] except ValueError: print("Invalid input — no aspects recorded.") res["description"] = desc res["affected"] = selected save_data(data) print("Details completed and saved.") # Main program def main(write_file=False, no_instructions=False): check_password() # This blocks until correct password or exits data = load_data() if not no_instructions: print_explanation() else: print_header() while True: if no_instructions: print("Menu:") print("1. Quickly add names (build the list)") print("2. Complete details for a resentment") print("3. List all resentments") print("4. Quit") choice = input("\nEnter choice (1-4): ").strip() if choice == '1': quick_add_names_loop(data) elif choice == '2': complete_resentment(data) elif choice == '3': list_resentments(data) elif choice == '4': save_data(data) if write_file: list_resentments(data, to_file=True) print("\nProgress saved. Take care.") break else: print("Invalid choice — please try again.") if __name__ == "__main__": parser = argparse.ArgumentParser(description="Cleaning House: AA 4th Step Resentment Inventory") parser.add_argument("-w", action="store_true", help="Write full inventory to my_resentments.txt on exit") parser.add_argument("-n", action="store_true", help="Skip initial instructions (start directly with menu)") args = parser.parse_args() main(write_file=args.w, no_instructions=args.n)