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()