host.controller_config.main

Entry point for the controller configuration GUI tool.

Usage: python -m host.controller_config [config.yaml] python host/controller_config/main.py [config.yaml]

CLI export (no GUI): python -m host.controller_config config.yaml --export out.png python -m host.controller_config config.yaml --export out.pdf --orientation landscape

  1"""Entry point for the controller configuration GUI tool.
  2
  3Usage:
  4    python -m host.controller_config [config.yaml]
  5    python host/controller_config/main.py [config.yaml]
  6
  7CLI export (no GUI):
  8    python -m host.controller_config config.yaml --export out.png
  9    python -m host.controller_config config.yaml --export out.pdf --orientation landscape
 10"""
 11
 12import argparse
 13import sys
 14from pathlib import Path
 15
 16_HAS_TKINTER = True
 17try:
 18    import tkinter  # noqa: F401
 19except ImportError:
 20    _HAS_TKINTER = False
 21
 22
 23def _get_project_root() -> Path:
 24    """Return the project root, handling PyInstaller frozen bundles."""
 25    if getattr(sys, 'frozen', False):
 26        # PyInstaller extracts data to a temp dir stored in sys._MEIPASS
 27        return Path(sys._MEIPASS)
 28    return Path(__file__).resolve().parent.parent.parent
 29
 30
 31# Ensure project root is importable
 32_project_root = _get_project_root()
 33if str(_project_root) not in sys.path:
 34    sys.path.insert(0, str(_project_root))
 35
 36
 37def main():
 38    if not _HAS_TKINTER:
 39        print(
 40            "Error: tkinter is not installed.\n"
 41            "On macOS with Homebrew Python, run:\n"
 42            "    brew install python-tk\n"
 43            "Then retry.",
 44            file=sys.stderr,
 45        )
 46        sys.exit(1)
 47
 48    parser = argparse.ArgumentParser(
 49        description="FRC Controller Configuration Tool",
 50    )
 51    parser.add_argument(
 52        "config_file",
 53        nargs="?",
 54        default=None,
 55        help="Path to a YAML config file to open on launch",
 56    )
 57    parser.add_argument(
 58        "--export", metavar="OUTPUT",
 59        help="Export controller layout to PNG or PDF (no GUI)",
 60    )
 61    parser.add_argument(
 62        "--orientation", choices=["portrait", "landscape"],
 63        default="portrait",
 64        help="Page orientation for export (default: portrait)",
 65    )
 66    parser.add_argument(
 67        "--hide-unassigned", action="store_true",
 68        help="Hide inputs with no bindings in export",
 69    )
 70    args = parser.parse_args()
 71
 72    if args.export:
 73        # CLI export mode — no GUI
 74        if not args.config_file:
 75            parser.error("config_file is required when using --export")
 76
 77        from utils.controller.config_io import load_config
 78        from host.controller_config.app import load_settings
 79        from host.controller_config.print_render import export_pages
 80
 81        config_path = Path(args.config_file)
 82        if not config_path.exists():
 83            print(f"Error: config file not found: {config_path}",
 84                  file=sys.stderr)
 85            sys.exit(1)
 86
 87        config = load_config(config_path)
 88        settings = load_settings()
 89        label_positions = settings.get("label_positions", {})
 90
 91        output_path = Path(args.export)
 92        try:
 93            from host.controller_config.icon_loader import InputIconLoader
 94            icons_dir = _get_project_root() / "images" / "XboxControlIcons" / "Buttons Full Solid"
 95            cli_icon_loader = InputIconLoader(icons_dir) if icons_dir.exists() else None
 96            export_pages(config, args.orientation, output_path,
 97                         label_positions, args.hide_unassigned,
 98                         cli_icon_loader)
 99            print(f"Exported to {output_path}")
100        except Exception as e:
101            print(f"Error: {e}", file=sys.stderr)
102            sys.exit(1)
103    else:
104        # GUI mode
105        import signal
106        import tempfile
107        from utils.controller.config_io import save_config
108        from host.controller_config.app import ControllerConfigApp
109
110        app = ControllerConfigApp(initial_file=args.config_file)
111
112        def _sigint_handler(sig, frame):
113            """Handle Ctrl+C: save unsaved work to temp file and exit."""
114            if app._dirty:
115                try:
116                    app._sync_config_from_ui()
117                    tmp = tempfile.NamedTemporaryFile(
118                        prefix="controller_config_recovery_",
119                        suffix=".yaml",
120                        delete=False,
121                    )
122                    tmp.close()
123                    save_config(app._config, tmp.name)
124                    print(f"\nUnsaved changes saved to: {tmp.name}",
125                          file=sys.stderr)
126                except Exception as e:
127                    print(f"\nFailed to save recovery file: {e}",
128                          file=sys.stderr)
129            else:
130                print("\nNo unsaved changes.", file=sys.stderr)
131            app.destroy()
132            sys.exit(0)
133
134        signal.signal(signal.SIGINT, _sigint_handler)
135
136        # Tkinter mainloop blocks Python signal handling on Windows.
137        # Periodic after() calls let the interpreter check for signals.
138        def _poll_signals():
139            app.after(500, _poll_signals)
140        app.after(500, _poll_signals)
141
142        app.mainloop()
143
144
145if __name__ == "__main__":
146    main()
def main():
 38def main():
 39    if not _HAS_TKINTER:
 40        print(
 41            "Error: tkinter is not installed.\n"
 42            "On macOS with Homebrew Python, run:\n"
 43            "    brew install python-tk\n"
 44            "Then retry.",
 45            file=sys.stderr,
 46        )
 47        sys.exit(1)
 48
 49    parser = argparse.ArgumentParser(
 50        description="FRC Controller Configuration Tool",
 51    )
 52    parser.add_argument(
 53        "config_file",
 54        nargs="?",
 55        default=None,
 56        help="Path to a YAML config file to open on launch",
 57    )
 58    parser.add_argument(
 59        "--export", metavar="OUTPUT",
 60        help="Export controller layout to PNG or PDF (no GUI)",
 61    )
 62    parser.add_argument(
 63        "--orientation", choices=["portrait", "landscape"],
 64        default="portrait",
 65        help="Page orientation for export (default: portrait)",
 66    )
 67    parser.add_argument(
 68        "--hide-unassigned", action="store_true",
 69        help="Hide inputs with no bindings in export",
 70    )
 71    args = parser.parse_args()
 72
 73    if args.export:
 74        # CLI export mode — no GUI
 75        if not args.config_file:
 76            parser.error("config_file is required when using --export")
 77
 78        from utils.controller.config_io import load_config
 79        from host.controller_config.app import load_settings
 80        from host.controller_config.print_render import export_pages
 81
 82        config_path = Path(args.config_file)
 83        if not config_path.exists():
 84            print(f"Error: config file not found: {config_path}",
 85                  file=sys.stderr)
 86            sys.exit(1)
 87
 88        config = load_config(config_path)
 89        settings = load_settings()
 90        label_positions = settings.get("label_positions", {})
 91
 92        output_path = Path(args.export)
 93        try:
 94            from host.controller_config.icon_loader import InputIconLoader
 95            icons_dir = _get_project_root() / "images" / "XboxControlIcons" / "Buttons Full Solid"
 96            cli_icon_loader = InputIconLoader(icons_dir) if icons_dir.exists() else None
 97            export_pages(config, args.orientation, output_path,
 98                         label_positions, args.hide_unassigned,
 99                         cli_icon_loader)
100            print(f"Exported to {output_path}")
101        except Exception as e:
102            print(f"Error: {e}", file=sys.stderr)
103            sys.exit(1)
104    else:
105        # GUI mode
106        import signal
107        import tempfile
108        from utils.controller.config_io import save_config
109        from host.controller_config.app import ControllerConfigApp
110
111        app = ControllerConfigApp(initial_file=args.config_file)
112
113        def _sigint_handler(sig, frame):
114            """Handle Ctrl+C: save unsaved work to temp file and exit."""
115            if app._dirty:
116                try:
117                    app._sync_config_from_ui()
118                    tmp = tempfile.NamedTemporaryFile(
119                        prefix="controller_config_recovery_",
120                        suffix=".yaml",
121                        delete=False,
122                    )
123                    tmp.close()
124                    save_config(app._config, tmp.name)
125                    print(f"\nUnsaved changes saved to: {tmp.name}",
126                          file=sys.stderr)
127                except Exception as e:
128                    print(f"\nFailed to save recovery file: {e}",
129                          file=sys.stderr)
130            else:
131                print("\nNo unsaved changes.", file=sys.stderr)
132            app.destroy()
133            sys.exit(0)
134
135        signal.signal(signal.SIGINT, _sigint_handler)
136
137        # Tkinter mainloop blocks Python signal handling on Windows.
138        # Periodic after() calls let the interpreter check for signals.
139        def _poll_signals():
140            app.after(500, _poll_signals)
141        app.after(500, _poll_signals)
142
143        app.mainloop()