diff --git a/src/maildirclean/cli.py b/src/maildirclean/cli.py index cd403f4..e2c3792 100644 --- a/src/maildirclean/cli.py +++ b/src/maildirclean/cli.py @@ -2,7 +2,7 @@ import argparse import sys from pathlib import Path -from .maildir import parse_maildir +from .maildir import MailDir, TopSender, parse_maildir def parse_arguments() -> argparse.Namespace: @@ -43,30 +43,118 @@ def cli(): if args.verbose: print(f"Analyzing emails in {maildir_path}...") + run_loop(args, maildir_path) + + return 0 + + +def run_loop(args: argparse.Namespace, maildir_path: str | Path): + + # Set up maildir maildir = parse_maildir(maildir_path) if args.verbose: print(f"Found {len(maildir._df)} emails") - top_senders = maildir.get_top_n_senders(args.top) + # Main running loop + while True: + top_senders = maildir.get_top_n_senders(args.top) - if not top_senders: - print("No senders found in the maildir", file=sys.stderr) - return 0 + if not top_senders: + print("No senders found in the maildir", file=sys.stderr) + return 0 - result = [] - for i, sender in enumerate(top_senders, 1): - names_str = ", ".join(sender.names[:5]) # Limit to first 5 names - if len(sender.names) > 5: - names_str += f" and {len(sender.names) - 5} more" + result = [] + for i, sender in enumerate(top_senders, 1): + names_str = ", ".join(sender.names[:5]) # Limit to first 5 names + if len(sender.names) > 5: + names_str += f" and {len(sender.names) - 5} more" - result.append( - f"{i}. {sender.email} - Email count: {sender.count} - Names used: {names_str}" + result.append( + f"{i}. {sender.email} - Email count: {sender.count} - Names used: {names_str}" + ) + + output = "\n".join( + [f"Top {len(top_senders)} senders in {maildir_path}:", "=" * 40, *result] ) - output = "\n".join( - [f"Top {len(top_senders)} senders in {maildir_path}:", "=" * 40, *result] - ) + print(output) + if not user_input_loop(top_senders, maildir): + break - print(output) - return 0 +def user_input_loop(top_senders: list[TopSender], maildir: MailDir) -> bool: + user_input = input("> ").strip() + while handle_user_input(user_input, top_senders, maildir): + user_input = input("> ").strip() + + +def parse_selections(user_input, max_selection): + """ + Parse user input into a set of valid selections. + + Args: + user_input (str): User input string with numbers, comma-separated lists, or ranges + max_selection (int): Maximum allowed selection number + + Returns: + set: Set of valid selection numbers + + Raises: + ValueError: If any input is invalid or out of range + """ + selections = set() + + # Clean up the input + user_input = user_input.strip() + + # Split by comma + items = [item.strip() for item in user_input.split(",")] + + for item in items: + if "-" in item: + # Handle range (e.g., "1-3" or "4 - 5") + range_parts = [part.strip() for part in item.split("-")] + if ( + len(range_parts) != 2 + or not range_parts[0].isdigit() + or not range_parts[1].isdigit() + ): + raise ValueError(f"Invalid range format: {item}") + + start = int(range_parts[0]) + end = int(range_parts[1]) + + if start > end: + raise ValueError( + f"Invalid range: {item}. Start must be less than or equal to end." + ) + + selections.update(range(start, end + 1)) + elif item.isdigit(): + # Handle single number + selections.add(int(item)) + else: + raise ValueError(f"Invalid input: {item}") + + # Check if any selection is out of range + out_of_range = [s for s in selections if s < 1 or s > max_selection] + if out_of_range: + raise ValueError( + f"Selection(s) out of range: {', '.join(map(str, out_of_range))}. Valid range is 1-{max_selection}" + ) + + return selections + + +def handle_user_input(user_input, top_senders, maildir): + if user_input.lower() == "q": + return False + + try: + selections = parse_selections(user_input, len(top_senders)) + for selection in selections: + selected_sender = top_senders[selection - 1] + print(f"Selected {selected_sender.email}") + return True + except ValueError: + print("Please enter a valid number or 'q' to quit") diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..001ef94 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,87 @@ +import pytest +from pathlib import Path + +# Import the function to test - assuming it's in a module called 'maildir_analyzer' +# Update this import to match your actual module structure +from maildirclean.cli import parse_selections + + +def test_single_number(): + result = parse_selections("3", 5) + assert result == {3} + + +def test_comma_separated_list(): + result = parse_selections("1,3,5", 5) + assert result == {1, 3, 5} + + +def test_range(): + result = parse_selections("2-4", 5) + assert result == {2, 3, 4} + + +def test_range_with_spaces(): + result = parse_selections("1 - 3", 5) + assert result == {1, 2, 3} + + +def test_combined_input(): + result = parse_selections("1,3-5,7", 10) + assert result == {1, 3, 4, 5, 7} + + +def test_duplicate_numbers(): + result = parse_selections("1,1,2,2-4,3", 5) + assert result == {1, 2, 3, 4} # Set removes duplicates + + +def test_invalid_format(): + with pytest.raises(ValueError, match="Invalid input: abc"): + parse_selections("abc", 5) + + +def test_invalid_range_format(): + with pytest.raises(ValueError, match="Invalid range format: 2-a"): + parse_selections("2-a", 5) + + +def test_inverted_range(): + with pytest.raises(ValueError, match="Invalid range: 5-2"): + parse_selections("5-2", 5) + + +def test_out_of_range(): + with pytest.raises(ValueError, match="Selection.*out of range"): + parse_selections("3,6,8", 5) + + +def test_mix_valid_and_invalid(): + with pytest.raises(ValueError): + parse_selections("1,abc,3", 5) + + +def test_empty_input(): + with pytest.raises(ValueError): + parse_selections("", 5) + + +def test_whitespace_only(): + with pytest.raises(ValueError): + parse_selections(" ", 5) + + +def test_max_boundary(): + # Test the boundary case + result = parse_selections("5", 5) + assert result == {5} + + +def test_complex_input(): + result = parse_selections("1-2, 4, 6-8", 10) + assert result == {1, 2, 4, 6, 7, 8} + + +def test_negative_numbers(): + with pytest.raises(ValueError, match="Invalid range format:*"): + parse_selections("-1,2", 5)