def monu(self): response = None while not (response and response == utils.TUIMenu.EXIT_MENU): in_between = [("Saved Data: {}" + Colors.RESET.value).format( Colors.GREEN.value + "Loaded" if self.json_path.exists() else (Colors.RED.value + "None"))] menu = utils.TUIMenu(f"USBToolBox {shared.VERSION}", "Select an option: ", in_between=in_between, top_level=True) menu_options = [ # ["H", "Print Historical", self.print_historical], ["D", "Discover Ports", self.discover_ports], ["S", "Select Ports and Build Kext", self.select_ports], ["C", "Change Settings", self.change_settings], ] if self.json_path.exists(): menu_options.insert( 0, ["P", "Delete Saved USB Data", self.remove_historical]) for i in menu_options: menu.add_menu_option(i[1], None, i[2], i[0]) response = menu.start() self.on_quit() self.utils.custom_quit()
def change_settings(self): def functionify(func): return lambda *args, **kwargs: lambda: func(*args, **kwargs) @functionify def color_status(name, variable): return f"{name}: {color('Enabled').green if self.settings[variable] else color('Disabled').red}" @functionify def toggle_setting(variable): self.settings[variable] = not self.settings[variable] def combination(name, variable): return color_status(name, variable), toggle_setting(variable) menu = utils.TUIMenu("Change Settings", "Toggle a setting: ", loop=True) for i in [ ["T", *combination("Show Friendly Types", "show_friendly_types"), ["Show friendly types (ie. 'USB 3 Type A') instead of numbers."]], ["N", *combination("Use Native Classes", "use_native"), ["Use native Apple classes (AppleUSBHostMergeProperties) instead of the USBToolBox kext."]], ["A", *combination("Add Comments to Map", "add_comments_to_map"), ["Add port comments inside the map."]], [ "C", *combination("Bind Companions", "auto_bind_companions"), ["Tie companion ports together. If one companion is enabled/disable/port type changed, the other companion will also be affected."], ], ]: menu.add_menu_option(name=i[1], function=i[2], key=i[0], description=i[3] if len(i) == 4 else None) menu.start() self.dump_settings()
def print_errors(self, errors): if not errors: return True utils.TUIMenu("Selection Validation", "Select an option: ", in_between=errors, loop=True).start() return False
def print_types(self): in_between = [f"{i}: {i.value}" for i in shared.USBPhysicalPortTypes] + [ "", textwrap.dedent( """\ The difference between connector types 9 and 10 is if you reverse the plug and the devices are connected to the same ports as before, they have a switch (type 9). If not, and they are connected to different ports, they do not have a switch (type 10).""" ), "", "For more information and pictures, go to https://github.com/USBToolBox/tool/blob/master/TYPES.md.", ] utils.TUIMenu("USB Types", "Select an option: ", in_between=in_between).start()
def validate_selections(self): errors = [] if not any(any(p["selected"] for p in c["ports"]) for c in self.controllers_historical): utils.TUIMenu("Selection Validation", "Select an option: ", in_between=["No ports are selected! Select some ports."], loop=True).start() return False for controller in self.controllers_historical: for port in controller["ports"]: if not port["selected"]: continue if port["type"] is None and port["guessed"] is None: errors.append(f"Port {port['selection_index']} is missing a connector type!") return self.print_errors(errors)
def build_kext(self): empty_controllers = [ c for c in self.controllers_historical if not any(p["selected"] for p in c["ports"]) ] response = None if empty_controllers: empty_menu = utils.TUIMenu( "Selection Validation", "Select an option: ", in_between=[ "The following controllers have no enabled ports:", "" ] + [controller["name"] for controller in empty_controllers] + [ "Select whether to ignore these controllers and exclude them from the map, or disable all ports on these controllers." ], add_quit=False, return_number=True, ) empty_menu.add_menu_option("Ignore", key="I") empty_menu.add_menu_option("Disable", key="D") response = empty_menu.start() model_identifier = None if self.settings["use_native"]: if platform.system() == "Darwin": model_identifier = plistlib.loads( subprocess.run( "system_profiler -detailLevel mini -xml SPHardwareDataType" .split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.strip() )[0]["_items"][0]["machine_model"] else: model_menu = utils.TUIOnlyPrint( "Enter Model Identifier", "Enter the model identifier: ", [ "You are seeing this as you have selected to use AppleUSBHostController. Model identifier autodetection is unavailable as you are not on macOS.", "Please enter the model identifier of the target system below. You can find it in System Information or with 'system_profiler -detailLevel mini SPHardwareDataType'.", ], ).start() model_identifier = model_menu.strip() ignore = response == "I" template = plistlib.load( (shared.resource_dir / Path("Info.plist")).open("rb")) menu = utils.TUIMenu("Building USBMap", "Select an option: ") menu.head() print("Generating Info.plist...") for controller in self.controllers_historical: if not any(i["selected"] for i in controller["ports"]) and ignore: continue # FIXME: ensure unique if controller["identifiers"].get("acpi_path"): if self.check_unique( lambda c: c["identifiers"]["acpi_path"].rpartition( ".")[2], lambda c: "acpi_path" in c["identifiers"], controller): personality_name: str = controller["identifiers"][ "acpi_path"].rpartition(".")[2] else: personality_name: str = controller["identifiers"][ "acpi_path"][1:] # Strip leading \ elif controller["identifiers"].get("bdf"): personality_name: str = ":".join( [str(i) for i in controller["identifiers"]["bdf"]]) else: personality_name: str = controller["name"] if self.settings["use_native"]: personality = { "CFBundleIdentifier": "com.apple.driver.AppleUSBHostMergeProperties", "IOClass": "AppleUSBHostMergeProperties", "IOProviderClass": "AppleUSBHostController", "IOParentMatch": self.choose_matching_key(controller), "model": model_identifier, } else: personality = { "CFBundleIdentifier": "com.dhinakg.USBToolBox.kext", "IOClass": "USBToolBox", "IOProviderClass": "IOPCIDevice", "IOMatchCategory": "USBToolBox", } | self.choose_matching_key(controller) # type: ignore personality["IOProviderMergeProperties"] = { "ports": {}, "port-count": None } port_name_index = {} highest_index = 0 for port in controller["ports"]: if not port["selected"]: continue if port["index"] > highest_index: highest_index = port["index"] if controller[ "class"] == shared.USBControllerTypes.XHCI and port[ "class"] == shared.USBDeviceSpeeds.SuperSpeed: prefix = "SS" elif controller[ "class"] == shared.USBControllerTypes.XHCI and port[ "class"] == shared.USBDeviceSpeeds.HighSpeed: prefix = "HS" else: prefix = "PRT" port_index = port_name_index.setdefault(prefix, 1) port_name = prefix + str(port_index).zfill(4 - len(prefix)) port_name_index[prefix] += 1 personality["IOProviderMergeProperties"]["ports"][ port_name] = { "port": binascii.a2b_hex( hexswap(hex(port["index"])[2:].zfill(8))), "UsbConnector": port["type"] or port["guessed"], } if self.settings["add_comments_to_map"] and port["comment"]: personality["IOProviderMergeProperties"]["ports"][ port_name]["#comment"] = port["comment"] personality["IOProviderMergeProperties"][ "port-count"] = binascii.a2b_hex( hexswap(hex(highest_index)[2:].zfill(8))) template["IOKitPersonalities"][personality_name] = personality if not self.settings["use_native"]: template["OSBundleLibraries"] = { "com.dhinakg.USBToolBox.kext": "1.0.0" } write_path = shared.current_dir / (Path("USBMap.kext") if self.settings["use_native"] else Path("UTBMap.kext")) if write_path.exists(): print("Removing existing kext...") shutil.rmtree(write_path) print("Writing kext and Info.plist...") (write_path / Path("Contents")).mkdir(parents=True) plistlib.dump(template, (write_path / Path("Contents/Info.plist")).open("wb"), sort_keys=True) print(f"Done. Saved to {write_path.resolve()}.\n") menu.print_options() menu.select() return True
def select_ports(self): if not self.controllers_historical: utils.TUIMenu("Select Ports and Build Kext", "Select an option: ", in_between=["No ports! Use the discovery mode."], loop=True).start() return selection_index = 1 by_port = [] for controller in self.controllers_historical: controller["selected_count"] = 0 for port in controller["ports"]: if "selected" not in port: port["selected"] = bool(port["devices"]) port["selected"] = port["selected"] or ( bool(self.get_companion_port(port)["devices"]) if self.get_companion_port(port) else False) controller["selected_count"] += 1 if port["selected"] else 0 port["selection_index"] = selection_index selection_index += 1 by_port.append(port) while True: self.dump_historical() for controller in self.controllers_historical: controller["selected_count"] = sum( 1 if port["selected"] else 0 for port in controller["ports"]) utils.header("Select Ports and Build Kext") print() for controller in self.controllers_historical: port_count_str = f"{controller['selected_count']}/{len(controller['ports'])}" port_count_str = color(port_count_str).red if controller[ "selected_count"] > 15 else color(port_count_str).green print( self.controller_to_str(controller) + f" | {port_count_str} ports") for port in controller["ports"]: port_info = f"[{'#' if port['selected'] else ' '}] {port['selection_index']}.{(len(str(selection_index)) - len(str(port['selection_index'])) + 1) * ' ' }" + self.port_to_str( port) companion = self.get_companion_port(port) if companion: port_info += f" | Companion to {companion['selection_index']}" if port["selected"]: print(color(port_info).green.bold) else: print(port_info) if port["comment"]: print( len(f"[{'#' if port['selected'] else ' '}] {port['selection_index']}.{(len(str(selection_index)) - len(str(port['selection_index'])) + 1) * ' ' }" ) * " " + color(port["comment"]).blue.bold) for device in port["devices"]: self.print_devices(device, indentation=" " + len(str(selection_index)) * " " * 2) print() print( f"Binding companions is currently {color('on').green if self.settings['auto_bind_companions'] else color('off').red}.\n" ) print( textwrap.dedent(f"""\ K. Build {'USBMap' if self.settings['use_native'] else 'UTBMap'}.kext A. Select All N. Select None P. Enable All Populated Ports D. Disable All Empty Ports T. Show Types B. Back - Select ports to toggle with comma-delimited lists (eg. 1,2,3,4,5) - Change types using this formula T:1,2,3,4,5:t where t is the type - Set custom names using this formula C:1:Name - Name = None to clear""" )) output = input("Select an option: ") if not output: continue elif output.upper() == "B": break elif output.upper() == "K": if not self.validate_selections(): continue self.build_kext() continue elif output.upper() in ("N", "A"): for port in by_port: port["selected"] = output.upper() == "A" elif output.upper() == "P": for port in by_port: if port["devices"] or ( self.get_companion_port(port)["devices"] if self.get_companion_port(port) else False): port["selected"] = True elif output.upper() == "D": for port in by_port: if not port["devices"] and not ( self.get_companion_port(port)["devices"] if self.get_companion_port(port) else False): port["selected"] = False elif output.upper() == "T": self.print_types() continue elif output[0].upper() == "T": # We should have a type if len(output.split(":")) != 3: continue try: port_nums, port_type = output.split(":")[1:] port_nums = port_nums.replace(" ", "").split(",") port_type = shared.USBPhysicalPortTypes(int(port_type)) for port_num in list(port_nums): if port_num not in port_nums: continue port_num = int(port_num) - 1 if port_num not in range(len(by_port)): continue companion = self.get_companion_port(by_port[port_num]) if self.settings["auto_bind_companions"] and companion: companion["type"] = port_type if str(companion["selection_index"]) in port_nums: port_nums.remove( str(companion["selection_index"])) by_port[port_num]["type"] = port_type except ValueError: continue elif output[0].upper() == "C": # We should have a new name if len(output.split(":")) < 2: continue try: port_nums = output.split(":")[1].replace(" ", "").split(",") port_comment = output.split(":", 2)[2:] for port_num in list(port_nums): if port_num not in port_nums: continue port_num = int(port_num) - 1 if port_num not in range(len(by_port)): continue by_port[port_num]["comment"] = port_comment[ 0] if port_comment else None except ValueError: continue else: try: port_nums = output.replace(" ", "").split(",") for port_num in list(port_nums): if port_num not in port_nums: continue port_num = int(port_num) - 1 if port_num not in range(len(by_port)): continue companion = self.get_companion_port(by_port[port_num]) if self.settings["auto_bind_companions"] and companion: companion[ "selected"] = not by_port[port_num]["selected"] if str(companion["selection_index"]) in port_nums: port_nums.remove( str(companion["selection_index"])) by_port[port_num][ "selected"] = not by_port[port_num]["selected"] except ValueError: continue
def print_historical(self): utils.TUIMenu("Print Historical (DEBUG)", "Select an option: ", in_between=lambda: self.print_controllers( self.controllers_historical), loop=True).start()