"""Implement Mach5 menus.""" import os import errno import yaml from collections import OrderedDict import subprocess def get_data_from(menu_file): """Get menu data from a yaml menu definition file.""" menu_file = os.path.expanduser(menu_file) if not os.path.exists(menu_file): raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), menu_file) with open(menu_file, 'r') as yaml_file: menu_data = yaml.safe_load(yaml_file) ####################################################################### # WARNING # # yaml.safe_load does NOT return and OrderedDict # # so far, dict in Python are ordered, but IT CAN CHANGE # ####################################################################### if 'entries' not in menu_data: raise ValueError(f"Malformed file {menu_file}.\ First entry is not a menu") return menu_data class Command: """A Mach5 command.""" def __init__(self, cmd_data): """Construct a Mach5 command object.""" if len(cmd_data['sc']) != 1: raise ValueError(f"Shortcut must be only one character.\ {cmd_data['sc']} is invalid.") self._name = cmd_data['name'] self._cmd = cmd_data['cmd'] self._sc = cmd_data['sc'] def show(self): """Return string for Command.""" return f"{self._sc} - {self._name}" def __repr__(self): """Return representation string for Command object.""" return f"{self._sc!r} - {self._name!r} - {self._cmd!r}" def __str__(self): """Return string conversion for command.""" return f"{self._sc} - {self._name}" def execute(self): """Execute command.""" print(f"Execute command {self._name}") subprocess.Popen(os.path.expanduser(self._cmd)) class Menu: """A Mach5 menu class.""" def __init__(self, menu_data): """Init a menu object instance from a menu data dict.""" self._name = menu_data['name'] self._entries = OrderedDict() self._sc = menu_data['sc'] for e in menu_data['entries']: if e['sc'] in self._entries: raise ValueError(f"Repeated shortcut {e['sc']}") if 'entries' in e: self._entries[e['sc']] = Menu(e) else: self._entries[e['sc']] = Command(e) def __len__(self): """Return number of items in menu.""" return len(self._entries) def __iter__(self): return MenuIter(self) def from_file(path): """Return a Menu instance from menu definition file.""" return Menu(get_data_from(path)) def __repr__(self): """Return repr string for menu object.""" return f"{self._sc!r} - {self._name!r}" def __str__(self): """Return string conversion for menu.""" if self._sc: return f"{self._sc} - {self._name}" else: return f"{self._name}" def get_entries(self): """Return entries for menu instance.""" return self._entries class MenuIter: """A helper class to make iterables Menu objects""" def __init__(self, menu): """Return an iterable menu""" self._entries = menu.get_entries() self._current = 0 def __iter__(self): return self def __next__(self): if self._current < len(self._entries): item = self._entries[self._current] self._current += 1 return item raise StopIteration