diff --git a/distro-packages/Arch-Linux/PKGBUILD b/distro-packages/Arch-Linux/PKGBUILD index 606b516..74b3893 100644 --- a/distro-packages/Arch-Linux/PKGBUILD +++ b/distro-packages/Arch-Linux/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Storm Dragon pkgname=cthulhu -pkgver=2025.08.14 +pkgver=2025.12.09 pkgrel=1 pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca" url="https://git.stormux.org/storm/cthulhu" @@ -60,6 +60,9 @@ optdepends=( # OCR plugin dependencies (optional) 'python-pillow: Image processing for OCR and AI Assistant' 'python-pytesseract: Python wrapper for Tesseract OCR engine' + 'python-pdf2image: PDF to image conversion for OCR' + 'python-scipy: Scientific computing for OCR color analysis' + 'python-webcolors: Color name lookup for OCR text decoration' 'tesseract: OCR engine for text recognition' 'tesseract-data-eng: English language data for Tesseract' ) diff --git a/meson.build b/meson.build index 385400b..b025b96 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('cthulhu', - version: '2025.08.11', + version: '2025.12.09', meson_version: '>= 1.0.0', ) @@ -54,6 +54,9 @@ optional_modules = { 'dasbus': 'D-Bus remote controller', 'psutil': 'system information commands', 'gi.repository.Wnck': 'mouse review', + 'pdf2image': 'PDF processing for OCR', + 'scipy': 'Scientific computing for OCR analysis', + 'webcolors': 'Color name resolution for OCR', } summary = {} diff --git a/src/cthulhu/cthulhuVersion.py b/src/cthulhu/cthulhuVersion.py index 565763c..6593769 100644 --- a/src/cthulhu/cthulhuVersion.py +++ b/src/cthulhu/cthulhuVersion.py @@ -23,5 +23,5 @@ # Fork of Orca Screen Reader (GNOME) # Original source: https://gitlab.gnome.org/GNOME/orca -version = "2025.08.22" +version = "2025.12.09" codeName = "testing" diff --git a/src/cthulhu/plugins/AIAssistant/plugin.py b/src/cthulhu/plugins/AIAssistant/plugin.py index 4f7ba9b..b2f3a1c 100644 --- a/src/cthulhu/plugins/AIAssistant/plugin.py +++ b/src/cthulhu/plugins/AIAssistant/plugin.py @@ -46,9 +46,7 @@ class AIAssistant(Plugin): def __init__(self, *args, **kwargs): """Initialize the AI Assistant plugin.""" super().__init__(*args, **kwargs) - - # Use print to ensure we see this message - print("DEBUG: AI ASSISTANT __init__ called") + logger.info("AI ASSISTANT: Plugin __init__ starting") logger.info("AI ASSISTANT: Plugin initialized successfully") @@ -79,51 +77,40 @@ class AIAssistant(Plugin): # Prevent multiple activations if self._enabled: logger.info("AI ASSISTANT: Already activated, skipping") - print("DEBUG: AI ASSISTANT already activated, skipping") return try: logger.info("AI ASSISTANT: === Plugin activation starting ===") - print("DEBUG: AI ASSISTANT activation starting") - + # Check if AI Assistant is enabled in settings enabled = self._settings_manager.getSetting('aiAssistantEnabled') logger.info(f"AI ASSISTANT: Enabled setting: {enabled}") - print(f"DEBUG: AI ASSISTANT enabled setting: {enabled}") if not enabled: logger.info("AI Assistant is disabled in settings, skipping activation") - print("DEBUG: AI Assistant disabled, skipping activation") return # Load AI settings self._load_ai_settings() - print(f"DEBUG: AI settings loaded - provider: {self._provider_type}") - + # Check if we have valid configuration config_valid = self._validate_configuration() logger.info(f"AI Assistant configuration valid: {config_valid}") - print(f"DEBUG: AI Assistant configuration valid: {config_valid}") - + # Initialize AI provider (may fail but we still want menu access) if config_valid: provider_init = self._initialize_ai_provider() - print(f"DEBUG: AI provider initialization: {provider_init}") else: logger.warning("AI Assistant configuration invalid, menu will show error messages") - print("DEBUG: AI Assistant configuration invalid, menu will show error messages") provider_init = False - + # Always register keybindings so menu is accessible even with config issues self._register_keybindings() - print("DEBUG: AI keybindings registered") - + self._enabled = True logger.info("AI Assistant plugin activated successfully") - print("DEBUG: AI Assistant plugin activated successfully") - + except Exception as e: logger.error(f"Error activating AI Assistant plugin: {e}") - print(f"DEBUG: Error activating AI Assistant plugin: {e}") import traceback logger.error(traceback.format_exc()) @@ -144,44 +131,35 @@ class AIAssistant(Plugin): """Refresh plugin settings and reinitialize provider. Called when settings change.""" try: logger.info("AI Assistant: Refreshing settings") - print("DEBUG: AI Assistant refreshing settings") - + # Reload settings self._load_ai_settings() - + # Validate new configuration config_valid = self._validate_configuration() - print(f"DEBUG: New configuration valid: {config_valid}") - + # Reinitialize provider if configuration is valid if config_valid: old_provider = self._ai_provider provider_init = self._initialize_ai_provider() - print(f"DEBUG: Provider reinitialization: {provider_init}") if provider_init: logger.info(f"AI Assistant provider changed to: {self._provider_type}") - print(f"DEBUG: Provider successfully changed to: {self._provider_type}") else: logger.warning("Failed to initialize new provider") - print("DEBUG: Failed to initialize new provider") self._ai_provider = None else: logger.warning("New configuration invalid, clearing provider") - print("DEBUG: New configuration invalid, clearing provider") self._ai_provider = None - + except Exception as e: logger.error(f"Error refreshing AI Assistant settings: {e}") - print(f"DEBUG: Error refreshing settings: {e}") def _load_ai_settings(self): """Load AI Assistant settings from Cthulhu configuration.""" try: # Get provider provider = self._settings_manager.getSetting('aiProvider') - print(f"DEBUG: Raw provider setting: '{provider}'") self._provider_type = provider or settings.AI_PROVIDER_CLAUDE_CODE - print(f"DEBUG: Final provider type: '{self._provider_type}'") # Load API key from file api_key_file = self._settings_manager.getSetting('aiApiKeyFile') @@ -308,10 +286,9 @@ class AIAssistant(Plugin): "Show AI Assistant menu", 'kb:cthulhu+shift+control+a' ) - + logger.info("AI Assistant menu keybinding registered") - print(f"DEBUG: AI Assistant menu keybinding registered: {self._kb_binding_menu}") - + except Exception as e: logger.error(f"Error registering AI menu keybinding: {e}") @@ -325,22 +302,18 @@ class AIAssistant(Plugin): """Show the AI Assistant menu.""" try: logger.info("AI ASSISTANT: _show_ai_menu called!") - print("DEBUG: AI ASSISTANT _show_ai_menu called!") - + # IMPORTANT: Capture screen data BEFORE showing menu # This ensures we get the actual screen content, not the menu itself self._pre_menu_screen_data = self._collect_ai_data() logger.info("Pre-captured screen data for menu actions") - print("DEBUG: Pre-captured screen data for menu actions") - + # Now show the menu self._menu_gui = AIAssistantMenu(self._handle_menu_selection) self._menu_gui.show_gui() - print("DEBUG: AI menu GUI shown") return True except Exception as e: logger.error(f"Error showing AI menu: {e}") - print(f"DEBUG: Error showing AI menu: {e}") import traceback traceback.print_exc() return False @@ -407,22 +380,17 @@ class AIAssistant(Plugin): """Handle browsing for an image file to analyze.""" try: logger.info("AI image file browsing requested") - print("DEBUG: _handle_browse_image_file called") - + if not self._enabled: - print("DEBUG: AI Assistant not enabled") self._present_message("AI Assistant is not enabled") return True - + if not self._ai_provider: - print("DEBUG: AI provider not available") self._present_message("AI provider not available. Check configuration.") return True - + # Show file chooser dialog - print("DEBUG: About to show file chooser") image_file = self._show_image_file_chooser() - print(f"DEBUG: File chooser returned: {image_file}") if image_file: provider_name = self._provider_type.replace('_', ' ').title() @@ -536,10 +504,8 @@ class AIAssistant(Plugin): """Handle main AI Assistant activation - now shows action dialog.""" try: logger.info("AI Assistant activation requested") - print("DEBUG: AI Assistant activation keybinding triggered!") - + if not self._enabled: - print("DEBUG: AI Assistant not enabled, presenting message") self._present_message("AI Assistant is not enabled") return True @@ -2077,13 +2043,9 @@ class AIAssistantMenu(Gtk.Dialog): # Connect keyboard events for Enter key handling self.connect("key-press-event", self._on_key_press) - - print("DEBUG: AIAssistantMenu dialog created with radio buttons") def _on_response(self, dialog, response_id): """Handler for dialog response.""" - print(f"DEBUG: Dialog response: {response_id}") - if response_id == Gtk.ResponseType.OK: # Determine which radio button is selected if self.radio_ask.get_active(): @@ -2098,9 +2060,8 @@ class AIAssistantMenu(Gtk.Dialog): action_id = "browse_image_file" else: action_id = None - + if action_id: - print(f"DEBUG: Selected action: {action_id}") self.on_option_selected(action_id) self.destroy() @@ -2116,15 +2077,12 @@ class AIAssistantMenu(Gtk.Dialog): def show_gui(self): """Shows the AI Assistant dialog.""" try: - print("DEBUG: Starting dialog show_gui") self.show_all() - print("DEBUG: Dialog show_all() called - should be visible and accessible now") - + # Present the dialog to ensure it gets focus self.present() - print("DEBUG: Dialog presented") - + except Exception as e: - print(f"DEBUG: Error in show_gui: {e}") + logger.error(f"Error in show_gui: {e}") import traceback traceback.print_exc() diff --git a/tools/generate_dbus_documentation.py b/tools/generate_dbus_documentation.py new file mode 100755 index 0000000..7dcfd2f --- /dev/null +++ b/tools/generate_dbus_documentation.py @@ -0,0 +1,477 @@ +#!/usr/bin/python +# generate_dbus_documentation.py +# +# Generate markdown documentation for Cthulhu's D-Bus remote controller. +# Must be run while Cthulhu is active with the D-Bus service enabled. +# +# Copyright 2025 Igalia, S.L. +# Author: Joanmarie Diggs +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. + +"""Generate markdown documentation for Cthulhu's D-Bus remote controller.""" + +import os +import sys +from dasbus.connection import SessionMessageBus +from dasbus.error import DBusError + + +SERVICE_NAME = "org.stormux.Cthulhu.Service" +SERVICE_PATH = "/org/stormux/Cthulhu/Service" + + +def get_system_commands(proxy): + """Get system-level commands from the main service interface.""" + try: + commands = proxy.ListCommands() + return sorted(commands, key=lambda x: x[0]) + except DBusError as e: + print(f"Error getting system commands: {e}", file=sys.stderr) + return [] + + +def get_modules(proxy): + """Get list of registered modules.""" + try: + modules = proxy.ListModules() + return sorted(modules) + except DBusError as e: + print(f"Error getting modules: {e}", file=sys.stderr) + return [] + + +def get_module_info(bus, module_name): + """Get all commands, parameterized commands, getters, and setters for a module.""" + object_path = f"{SERVICE_PATH}/{module_name}" + + try: + module_proxy = bus.get_proxy(SERVICE_NAME, object_path) + + commands = module_proxy.ListCommands() + parameterized_commands = module_proxy.ListParameterizedCommands() + getters = module_proxy.ListRuntimeGetters() + setters = module_proxy.ListRuntimeSetters() + + return { + "commands": sorted(commands, key=lambda x: x[0]), + "parameterized_commands": sorted(parameterized_commands, key=lambda x: x[0]), + "getters": sorted(getters, key=lambda x: x[0]), + "setters": sorted(setters, key=lambda x: x[0]) + } + except DBusError as e: + print(f"Error getting info for module {module_name}: {e}", file=sys.stderr) + return { + "commands": [], + "parameterized_commands": [], + "getters": [], + "setters": [] + } + + +def format_system_commands(commands): + """Format system-level commands as markdown.""" + lines = [] + lines.append("## Service-Level Commands") + lines.append("") + lines.append( + "These commands are available directly on the main service object " + "at `/org/stormux/Cthulhu/Service`." + ) + lines.append("") + + for name, description in commands: + lines.append(f"- **`{name}`:** {description}") + + lines.append("") + return "\n".join(lines) + + +def _group_structural_navigator_commands(commands): + """Group structural navigator commands by object type.""" + groups = {} + other = [] + + def normalize_obj_type(obj_type): # pylint: disable=too-many-return-statements + """Convert to a canonical form for grouping.""" + + # Group all heading commands together. Ditto for link commands. + if obj_type.startswith("Heading"): + return "Heading" + if "Link" in obj_type: + return "Link" + + # Common plural patterns + if obj_type.endswith("ies"): # Entries -> Entry + return obj_type[:-3] + "y" + if obj_type.endswith("xes"): # Checkboxes -> Checkbox + return obj_type[:-2] + if obj_type.endswith("shes") or obj_type.endswith("ches"): # Matches -> Match + return obj_type[:-2] + if obj_type.endswith("s") and not obj_type.endswith("ss"): + return obj_type[:-1] + return obj_type + + for name, description in commands: + # Extract the object type from command names like NextHeading, ListButtons, etc. + if name.startswith("Next") or name.startswith("Previous"): + prefix = "Next" if name.startswith("Next") else "Previous" + obj_type = name[len(prefix):] + normalized = normalize_obj_type(obj_type) + if normalized not in groups: + if normalized == "Heading": + display = "Headings" + elif not obj_type.endswith("s"): + display = obj_type + "s" + else: + display = obj_type + groups[normalized] = {"commands": [], "display_name": display} + groups[normalized]["commands"].append((name, description)) + elif name.startswith("List"): + obj_type = name[4:] + normalized = normalize_obj_type(obj_type) + if normalized not in groups: + display = "Headings" if normalized == "Heading" else obj_type + groups[normalized] = {"commands": [], "display_name": display} + else: + current_display = groups[normalized]["display_name"] + if not current_display.endswith("s") or normalized == "Heading": + new_display = "Headings" if normalized == "Heading" else obj_type + groups[normalized]["display_name"] = new_display + groups[normalized]["commands"].append((name, description)) + else: + # Other commands like ContainerStart, CycleMode + other.append((name, description)) + + return groups, other + +# pylint: disable-next=too-many-branches,too-many-statements,too-many-locals +def format_module_commands(module_name, info): + """Format module-level commands as markdown.""" + + lines = [] + lines.append(f"### {module_name}") + lines.append("") + lines.append(f"**Object Path:** `/org/stormux/Cthulhu/Service/{module_name}`") + lines.append("") + + # Commands - special handling for certain modules + if info["commands"]: + lines.append("#### Commands") + lines.append("") + lines.append("**Method:** `org.stormux.Cthulhu.Module.ExecuteCommand`") + lines.append("") + lines.append( + "**Parameters:** `CommandName` (string), " + "[`NotifyUser`](README-REMOTE-CONTROLLER.md#user-notification-applicability) (boolean)" + ) + lines.append("") + + if module_name == "SpeechAndVerbosityManager": + # Group related increase/decrease commands + def sort_speech_commands(cmd_tuple): + name, _ = cmd_tuple + # Define groups and their order + groups = { + "Rate": 0, "Pitch": 1, "Volume": 2, + } + # Check if it's an Increase/Decrease command + for group_name, group_order in groups.items(): + if group_name in name: + if name.startswith("Increase"): + return (group_order, 0, name) + if name.startswith("Decrease"): + return (group_order, 1, name) + # Other commands go at the end, sorted alphabetically + return (100, 0, name) + + sorted_commands = sorted(info["commands"], key=sort_speech_commands) + for name, description in sorted_commands: + lines.append(f"- **`{name}`:** {description}") + lines.append("") + + elif module_name == "FlatReviewPresenter": + # Group Go commands at the top + def sort_flat_review_commands(cmd_tuple): + name, _ = cmd_tuple + if name.startswith("Go"): + return (0, name) + return (1, name) + + sorted_commands = sorted(info["commands"], key=sort_flat_review_commands) + for name, description in sorted_commands: + lines.append(f"- **`{name}`:** {description}") + lines.append("") + + elif module_name == "CaretNavigator": + # Group related navigation commands + def sort_caret_commands(cmd_tuple): + name, _ = cmd_tuple + # Define groups for Character, Word, Line, File + if "Character" in name: + group = 0 + elif "Word" in name: + group = 1 + elif "Line" in name: + group = 2 + elif "File" in name: + group = 3 + else: + group = 100 + + # Within each group: Next, Previous, Start, End, Toggle + if name.startswith("Next"): + order = 0 + elif name.startswith("Previous"): + order = 1 + elif name.startswith("Start"): + order = 0 + elif name.startswith("End"): + order = 1 + elif name.startswith("Toggle"): + order = 2 + else: + order = 99 + + return (group, order, name) + + sorted_commands = sorted(info["commands"], key=sort_caret_commands) + for name, description in sorted_commands: + lines.append(f"- **`{name}`:** {description}") + lines.append("") + + elif module_name == "StructuralNavigator": + groups, other = _group_structural_navigator_commands(info["commands"]) + + # Show grouped commands by object type first + for obj_type in sorted(groups.keys()): + cmds = groups[obj_type] + display_name = cmds["display_name"] + lines.append(f"##### {display_name}") + lines.append("") + + # Sort commands in a specific order: Next, Previous, List, grouped by variant + def sort_key(cmd_tuple): + name, _ = cmd_tuple + # Extract base command and any suffix (like Level1, UnvisitedLink, etc.) + if name.startswith("Next"): + prefix_order = 0 + suffix = name[4:] # Remove "Next" + elif name.startswith("Previous"): + prefix_order = 1 + suffix = name[8:] # Remove "Previous" + elif name.startswith("List"): + prefix_order = 2 + suffix = name[4:] # Remove "List" + else: + prefix_order = 3 + suffix = name + + # Extract level number or variant for proper ordering + level_or_variant = 0 + + if "Level" in suffix: + # For headings: extract level number + try: + level_or_variant = int(suffix.split("Level")[1]) + except (IndexError, ValueError): + pass + elif "Unvisited" in suffix: + # For links: Unvisited comes after base + level_or_variant = 1 + elif "Visited" in suffix: + # For links: Visited comes after Unvisited + level_or_variant = 2 + + return (level_or_variant, prefix_order, name) + + sorted_commands = sorted(cmds["commands"], key=sort_key) + for name, desc in sorted_commands: + lines.append(f"- **`{name}`:** {desc}") + lines.append("") + + # Show uncategorized commands at the end + if other: + lines.append("##### Other") + lines.append("") + for name, description in other: + lines.append(f"- **`{name}`:** {description}") + lines.append("") + else: + for name, description in info["commands"]: + lines.append(f"- **`{name}`:** {description}") + lines.append("") + + # Parameterized Commands + if info["parameterized_commands"]: + lines.append("#### Parameterized Commands") + lines.append("") + lines.append("**Method:** `org.stormux.Cthulhu.Module.ExecuteParameterizedCommand`") + lines.append("") + for name, description, parameters in info["parameterized_commands"]: + param_list = ", ".join([f"`{pname}` ({ptype})" for pname, ptype in parameters]) + if param_list: + lines.append(f"- **`{name}`:** {description} Parameters: {param_list}") + else: + lines.append(f"- **`{name}`:** {description}") + lines.append("") + + # Runtime Settings (combine getters and setters) + if info["getters"] or info["setters"]: + lines.append("#### Settings") + lines.append("") + lines.append("**Methods:** `org.stormux.Cthulhu.Module.ExecuteRuntimeGetter` / `org.stormux.Cthulhu.Module.ExecuteRuntimeSetter`") + lines.append("") + lines.append( + "**Parameters:** `PropertyName` (string), " + "`Value` (variant, setter only)" + ) + lines.append("") + + # Build a merged dictionary of properties + # Prefer setter descriptions as they may contain range/default info + properties = {} + for name, description in info["getters"]: + properties[name] = {"description": description, "getter": True, "setter": False} + for name, description in info["setters"]: + if name in properties: + properties[name]["setter"] = True + properties[name]["description"] = description + else: + properties[name] = {"description": description, "getter": False, "setter": True} + + # Output sorted properties with annotations + for name in sorted(properties.keys()): + prop = properties[name] + description = prop["description"] + annotation = "" + + if prop["getter"] and not prop["setter"]: + annotation = " (getter only)" + elif prop["setter"] and not prop["getter"]: + annotation = " (setter only)" + else: + # Both getter and setter - change "Returns" or "Sets" to "Gets/Sets" + if description.startswith("Returns "): + description = description.replace("Returns ", "Gets/Sets ", 1) + elif description.startswith("Sets "): + description = description.replace("Sets ", "Gets/Sets ", 1) + + lines.append(f"- **`{name}`:** {description}{annotation}") + lines.append("") + + return "\n".join(lines) + + +def generate_documentation(): + """Generate the complete documentation.""" + try: + bus = SessionMessageBus() + except DBusError as e: + print(f"Error connecting to D-Bus: {e}", file=sys.stderr) + return None + + try: + proxy = bus.get_proxy(SERVICE_NAME, SERVICE_PATH) + except DBusError as e: + print(f"Error connecting to Cthulhu service: {e}", file=sys.stderr) + print("Make sure Cthulhu is running with the D-Bus service enabled.", file=sys.stderr) + return None + + system_commands = get_system_commands(proxy) + modules = get_modules(proxy) + module_infos = {} + for module_name in modules: + module_infos[module_name] = get_module_info(bus, module_name) + + total_commands = len(system_commands) + total_commands += sum(len(info["commands"]) for info in module_infos.values()) + total_commands += sum(len(info["parameterized_commands"]) for info in module_infos.values()) + total_getters = sum(len(info["getters"]) for info in module_infos.values()) + total_setters = sum(len(info["setters"]) for info in module_infos.values()) + + lines = [] + lines.append("# Cthulhu D-Bus Service Commands Reference") + lines.append("") + lines.append( + f"This document lists all commands ({total_commands}), " + f"runtime getters ({total_getters}), and runtime setters ({total_setters}) available" + ) + lines.append("via Cthulhu's D-Bus Remote Controller interface.") + lines.append("") + lines.append("The service can be accessed at:") + lines.append("") + lines.append("- **Service Name:** `org.stormux.Cthulhu.Service`") + lines.append("- **Main Object Path:** `/org/stormux/Cthulhu/Service`") + lines.append("- **Module Object Paths:** `/org/stormux/Cthulhu/Service/ModuleName`") + lines.append("") + lines.append( + "Additional information about using the remote controller can be found in " + "[README-REMOTE-CONTROLLER.md](README-REMOTE-CONTROLLER.md)." + ) + lines.append("") + lines.append("---") + lines.append("") + + # System commands + lines.append(format_system_commands(system_commands)) + lines.append("---") + lines.append("") + + # Module commands + lines.append("## Modules") + lines.append("") + lines.append( + "Each module exposes commands, getters, and setters on its object " + "at `/org/stormux/Cthulhu/Service/ModuleName`." + ) + lines.append("") + + for module_name in modules: + lines.append(format_module_commands(module_name, module_infos[module_name])) + lines.append("---") + lines.append("") + + return "\n".join(lines) + + +def main(): + """Main entry point.""" + + # Write to parent directory since script is in tools/ + script_dir = os.path.dirname(os.path.abspath(__file__)) + parent_dir = os.path.dirname(script_dir) + output_file = os.path.join(parent_dir, "REMOTE-CONTROLLER-COMMANDS.md") + + print("Generating D-Bus documentation...", file=sys.stderr) + documentation = generate_documentation() + + if documentation is None: + print("Failed to generate documentation.", file=sys.stderr) + return 1 + + try: + with open(output_file, "w", encoding="utf-8") as f: + f.write(documentation) + print(f"Documentation written to {output_file}", file=sys.stderr) + return 0 + except IOError as e: + print(f"Error writing to {output_file}: {e}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main())