Complete documentation of EVERY function, menu, option, and workflow in Capcat's interactive mode.
Source: Application/core/interactive.py
#d75f00 - Orange (RGB: 215, 95, 0)
Location: Application/core/interactive.py:39
menu_lines (int) - Number of lines menu will occupy (default: 10)shutil.get_terminal_size()\033[2Jmax(0, terminal_height - menu_lines - logo_lines - 1)Source location: Application/core/interactive.py:54-64
\033[2J - Clear entire screen\033[H - Move cursor to home (0,0)\033[38;5;202m - Orange foreground color\033[0m - Reset formatting\033[F - Move cursor up one line\033[K - Clear line from cursor to endprint('\033[2J\033[H', end='')
Location: Application/core/interactive.py:28
@contextlib.contextmanager
def suppress_logging():
logger = logging.getLogger()
original_level = logger.level
logger.setLevel(logging.CRITICAL) # Suppress everything below CRITICAL
try:
yield
finally:
logger.setLevel(original_level) # Restore original level
with suppress_logging():
response = questionary.select(...).ask()
Location: Application/core/interactive.py:78
first_run = True flagNone from .ask())# Clear questionary's selection echo and show custom message
print('\033[F\033[K', end='') # Move up, clear line
print(f" Selected option: {action_names.get(action, action)}")
action is Noneaction == 'exit'Result: Prints "Exiting interactive mode." and returns to shell.
Location: Application/core/interactive.py:132
while True:
# Show menu with suppress_logging()
action = questionary.select(...).ask()
if not action or action == 'back':
return # Exit to main menu
# Call handler based on action
if action == 'add_rss':
_handle_add_source_from_rss()
# ... etc
Location: Application/core/interactive.py:171
print(" (Use Ctrl+C to go back)")
url = questionary.text(
" Enter the RSS feed URL:",
style=custom_style,
qmark="",
).ask()
url is None or empty: Print "No URL provided. Returning to menu." and returnfrom cli import add_source, get_available_sources
add_source(url) # Calls Application/cli.py:200
RssFeedIntrospector(url)sources = get_available_sources()
print(f"\n[OK] Active sources: {len(sources)}")
input("\nPress Enter to continue...")
except Exception as e:
print(f"Error adding source: {e}")
input("\nPress Enter to continue...")
(Use Ctrl+C to go back)
Enter the RSS feed URL: https://techcrunch.com/feed/
Attempting to add new source from: https://techcrunch.com/feed/
Inspecting RSS feed...
[OK] Feed 'TechCrunch' found.
--- Configure New Source ---
Source ID (alphanumeric): techcrunch
Select category: tech
Add to bundle? Yes
Select bundle: tech
[OK] Added 'techcrunch' to bundle 'tech'.
--- Running Test Fetch ---
Test fetch? (recommended) Yes
[OK] Source added and verified successfully!
[OK] Active sources: 16
Press Enter to continue...
Location: Application/core/interactive.py:200
print("\n--- Generate Custom Source Configuration ---")
print("This will launch the interactive config generator.\n")
confirm = questionary.confirm(
" Continue?",
default=True,
style=custom_style,
qmark="",
).ask()
import subprocess
script_path = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
"scripts",
"generate_source_config.py"
)
result = subprocess.run([sys.executable, script_path], check=False)
if result.returncode != 0:
print(f"\nConfig generation exited with code: {result.returncode}")
input("\nPress Enter to continue...")
except Exception as e:
print(f"Error launching config generator: {e}")
input("\nPress Enter to continue...")
--- Generate Custom Source Configuration ---
This will launch the interactive config generator.
Continue? Yes
[Interactive config generator launches]
[User goes through wizard...]
[Config saved to sources/active/config_driven/configs/newsource.yaml]
Press Enter to continue...
Location: Application/core/interactive.py:237
from core.source_system.remove_source_service import create_remove_source_service
from core.source_system.enhanced_remove_command import RemovalOptions
service = create_remove_source_service()
command = service._create_remove_source_command()
options = RemovalOptions(
dry_run=False, # No preview mode in interactive
create_backup=True, # Always create backup
show_analytics=True, # Show usage statistics
batch_file=None, # No batch file in interactive
force=False # Always confirm
)
from core.source_system.removal_ui import QuestionaryRemovalUI
from core.source_system.enhanced_remove_command import EnhancedRemoveCommand
from core.source_system.source_backup_manager import SourceBackupManager
from core.source_system.source_analytics import SourceAnalytics
enhanced_command = EnhancedRemoveCommand(
base_command=command,
backup_manager=SourceBackupManager(),
analytics=SourceAnalytics(),
ui=QuestionaryRemovalUI(),
logger=get_logger(__name__)
)
enhanced_command.execute_with_options(options)
from cli import get_available_sources
sources = get_available_sources()
print(f"\n[OK] Active sources: {len(sources)}")
input("\nPress Enter to continue...")
--- Remove Sources ---
This will launch the interactive source removal tool.
Select sources to remove (Space to select, Enter to confirm):
[ ] hn Hacker News
[x] oldsite Old Site
[x] discontinued Discontinued Source
[ ] bbc BBC News
Usage Analytics:
oldsite: Last used 90 days ago, 5 articles
discontinued: Last used 180 days ago, 0 articles
Confirm removal? Yes
[OK] Backup created: .capcat-backups/backup_20251125_143022/
[OK] Removed 2 sources successfully
[OK] Active sources: 14
Press Enter to continue...
Location: Application/core/interactive.py:287
from cli import get_available_sources
from core.source_system.source_registry import get_source_registry
sources = get_available_sources() # Dict[source_id, display_name]
registry = get_source_registry()
categories = {} # Dict[category, List[Tuple[source_id, display_name]]]
for source_id, display_name in sorted(sources.items()):
try:
config = registry.get_source_config(source_id)
category = config.category if config and hasattr(config, 'category') else 'other'
except:
category = 'other'
if category not in categories:
categories[category] = []
categories[category].append((source_id, display_name))
if selected and selected != 'back':
_show_source_details(selected, registry)
_handle_list_sources() # Recursive call to return to listing
Location: Application/core/interactive.py:351
source_id (str) - Source identifierregistry (SourceRegistry) - Registry instance if hasattr(config, 'base_url'):
print(f" \033[1mBase URL:\033[0m {config.base_url}")
if hasattr(config, 'discovery') and hasattr(config.discovery, 'method'):
print(f" \033[1mDiscovery:\033[0m {config.discovery.method}")
# RSS URLs if available
if config.discovery.method == 'rss' and hasattr(config.discovery, 'rss_urls'):
if hasattr(rss_urls, 'primary'):
print(f" \033[1mRSS Feed:\033[0m {rss_urls.primary}")
source_type = "Config-driven (YAML)" if hasattr(config, 'article_selectors') else "Custom (Python)"
print(f" \033[1mType:\033[0m {source_type}")
except Exception as e:
print(f"\n Error loading source details: {e}")
input("\n Press Enter to continue...")
Location: Application/core/interactive.py:396
print(f"\n--- Testing Source: {source_id} ---")
print("Fetching 3 articles...\n")
from capcat import run_app
args = ['fetch', source_id, '--count', '3']
run_app(args)
print(f"\n[OK] Source '{source_id}' test completed")
except SystemExit as e:
if e.code != 0:
print(f"\n✗ Source test failed with code: {e.code}")
except Exception as e:
print(f"\n✗ Error testing source: {e}")
input("\nPress Enter to continue...")
Select source to test:
> Hacker News
Lobsters
BBC News
Back
--- Testing Source: hn ---
Fetching 3 articles...
Processing hn articles...
[Article processing output...]
Successfully processed 3 articles
[OK] Source 'hn' test completed
Press Enter to continue...
Location: Application/core/interactive.py:436
# Bundle menu can be long
position_menu_at_bottom(menu_lines=15)
from cli import get_available_bundles, get_available_sources
from core.source_system.source_registry import get_source_registry
bundles = get_available_bundles()
all_sources_map = get_available_sources()
registry = get_source_registry()
bundle_choices = []
for name, data in bundles.items():
description = data.get("description", "")
# Special handling for 'all' bundle
if name == "all":
full_description = description
else:
# Explicit sources from bundles.yml
bundle_sources = data.get("sources", [])
# Auto-discover sources with matching category
category_sources = registry.get_sources_by_category(name)
for source_id in category_sources:
if source_id not in bundle_sources:
bundle_sources.append(source_id)
# Format with source names
source_names = [all_sources_map.get(sid, sid) for sid in bundle_sources]
sources_str = f"\n ({', '.join(source_names)})" if source_names else ""
full_description = f"{description}{sources_str}"
bundle_choices.append(questionary.Choice(f"{name} - {full_description}", name))
if bundle and bundle != 'back':
_prompt_for_html('bundle', bundle)
Select a news bundle and hit Enter for activation.
> tech - Technology News
(IEEE Spectrum, Mashable, Gizmodo)
techpro - Advanced Technology
(Hacker News, Lobsters, InfoQ)
news - General News
(BBC News, The Guardian)
science - Science News
(Nature News, Scientific American)
all - All available sources
Back to Main Menu
(Use arrow keys to navigate)
Location: Application/core/interactive.py:492
position_menu_at_bottom(menu_lines=15)
from cli import get_available_sources
sources = get_available_sources()
source_choices = [questionary.Choice(name, sid) for sid, name in sources.items()]
source_choices.append(questionary.Separator())
source_choices.append(questionary.Choice("Back to Main Menu", "back"))
if selected_sources and 'back' not in selected_sources:
_prompt_for_html('fetch', selected_sources)
Select sources (Space to select, Enter to confirm):
[ ] hn Hacker News
[x] lb Lobsters
[x] iq InfoQ
[ ] bbc BBC News
[ ] guardian The Guardian
Back to Main Menu
(Use Space to select multiple sources, Enter to confirm)
Location: Application/core/interactive.py:520
position_menu_at_bottom(menu_lines=15)
if source and source != 'back':
# For single source, call fetch with just one source
_prompt_for_html('fetch', [source])
fetch action with single-item list, not separate single_source action.
Location: Application/core/interactive.py:549
position_menu_at_bottom(menu_lines=5)
print(" (Use Ctrl+C to go to the Main Menu)")
url = questionary.text(
" Please enter the article URL:",
style=custom_style,
qmark="",
).ask()
if url:
_prompt_for_html('single', url)
else:
repeat = questionary.confirm(
" No URL entered. Would you like to try again?",
default=True,
style=custom_style,
qmark="",
).ask()
if repeat:
_handle_single_url_flow() # Recursive call
(Use Ctrl+C to go to the Main Menu)
Please enter the article URL: https://example.com/article
[Proceeds to HTML prompt]
Please enter the article URL: [Enter pressed]
No URL entered. Would you like to try again? Yes
Please enter the article URL:
Location: Application/core/interactive.py:578
action (str) - Action type: 'bundle', 'fetch', or 'single'selection - Bundle name, source list, or URL depending on actionposition_menu_at_bottom(menu_lines=8)
if response and response != 'back':
generate_html = response == "yes"
_confirm_and_execute(action, selection, generate_html)
Generate HTML for web browsing?
> Yes
No
Back to Main Menu
(Use arrow keys to navigate)
Location: Application/core/interactive.py:604
action (str) - Action type: 'bundle', 'fetch', or 'single'selection - Action-specific datagenerate_html (bool) - HTML generation flagsummary = f"Action: {action}\n"
if action == 'bundle':
summary += f"Bundle: {selection}\n"
elif action == 'fetch':
summary += f"Sources: {', '.join(selection)}\n"
elif action == 'single':
summary += f"URL: {selection}\n"
summary += f"Generate HTML: {generate_html}\n"
print("--------------------")
print("SUMMARY")
print(summary)
print("--------------------")
args = [action]
if action == 'bundle':
args.append(selection)
elif action == 'fetch':
args.append(','.join(selection))
elif action == 'single':
args.append(selection)
if generate_html:
args.append('--html')
try:
print("Executing command...")
run_app(args) # From Application/capcat.py:run_app()
except SystemExit as e:
if e.code != 0:
print(f"Command finished with error code: {e.code}")
sys.exit(e.code) # Re-raise error for wrapper
# On success (code 0), continue without exiting
input("\nPress Enter to return to main menu...")
--------------------
SUMMARY
Action: bundle
Bundle: tech
Generate HTML: true
--------------------
Executing command...
Processing ieee articles...
Processing mashable articles...
[Article processing output...]
Successfully processed 2 sources
Press Enter to return to main menu...
Location: Application/core/interactive.py:649
from pathlib import Path
from core.source_system.bundle_service import BundleService
bundles_path = Path(__file__).parent.parent / "sources" / "active" / "bundles.yml"
service = BundleService(bundles_path)
while True:
action = service.ui.show_bundle_menu()
if not action or action == 'back':
return
# Execute action
if action == 'create':
service.execute_create_bundle()
elif action == 'edit':
service.execute_edit_bundle()
elif action == 'delete':
service.execute_delete_bundle()
elif action == 'add_sources':
service.execute_add_sources()
elif action == 'remove_sources':
service.execute_remove_sources()
elif action == 'move_sources':
service.execute_move_source()
elif action == 'list':
service.execute_list_bundles()
create - Create new bundleedit - Edit bundle name/descriptiondelete - Delete bundleadd_sources - Add sources to bundleremove_sources - Remove sources from bundlemove_sources - Move sources between bundleslist - List all bundlesLocation: Application/capcat.py
from capcat import run_app
# Example: Bundle execution
args = ['bundle', 'tech', '--html']
run_app(args)
# Bundle
args = ['bundle', bundle_name, '--html'] # or without --html
# Fetch
args = ['fetch', 'source1,source2,source3', '--html']
# Single
args = ['single', url, '--html']
try:
run_app(args)
except SystemExit as e:
if e.code != 0:
# Handle error
print(f"Error: {e.code}")
sys.exit(e.code) # Re-raise for wrapper
# Success: Continue interactive mode
run_app() calls sys.exit() on completion, which raises SystemExit exception. Interactive mode catches this to prevent exiting.
try:
run_app(args)
except SystemExit as e:
if e.code != 0:
print(f"Command finished with error code: {e.code}")
if response is None: # User pressed Ctrl+C
return # Go back to previous menu
if not url:
print(" No URL provided. Returning to menu.")
return
except Exception as e:
print(f"Error: {e}")
input("\nPress Enter to continue...")
capcat.yml and environment variablesFunction reference:
start_interactive_mode() - Application/core/interactive.py:78position_menu_at_bottom() - Application/core/interactive.py:39suppress_logging() - Application/core/interactive.py:28_handle_manage_sources_flow() - Application/core/interactive.py:132_handle_bundle_flow() - Application/core/interactive.py:436_handle_fetch_flow() - Application/core/interactive.py:492_handle_single_source_flow() - Application/core/interactive.py:520_handle_single_url_flow() - Application/core/interactive.py:549_prompt_for_html() - Application/core/interactive.py:578_confirm_and_execute() - Application/core/interactive.py:604_show_source_details() - Application/core/interactive.py:351