Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
path: root/archinstall/lib
diff options
context:
space:
mode:
Diffstat (limited to 'archinstall/lib')
-rw-r--r--archinstall/lib/menu/global_menu.py16
-rw-r--r--archinstall/lib/menu/list_manager.py100
-rw-r--r--archinstall/lib/menu/menu.py21
-rw-r--r--archinstall/lib/models/network_configuration.py67
-rw-r--r--archinstall/lib/user_interaction/manage_users_conf.py42
-rw-r--r--archinstall/lib/user_interaction/network_conf.py96
-rw-r--r--archinstall/lib/user_interaction/subvolume_config.py24
7 files changed, 210 insertions, 156 deletions
diff --git a/archinstall/lib/menu/global_menu.py b/archinstall/lib/menu/global_menu.py
index b73fb48f..1a292476 100644
--- a/archinstall/lib/menu/global_menu.py
+++ b/archinstall/lib/menu/global_menu.py
@@ -163,7 +163,8 @@ class GlobalMenu(GeneralMenu):
Selector(
_('Network configuration'),
ask_to_configure_network,
- display_func=lambda x: self._prev_network_configuration(x),
+ display_func=lambda x: self._display_network_conf(x),
+ preview_func=self._prev_network_config,
default={})
self._menu_options['timezone'] = \
Selector(
@@ -226,16 +227,23 @@ class GlobalMenu(GeneralMenu):
return _('Install ({} config(s) missing)').format(missing)
return _('Install')
- def _prev_network_configuration(self, cur_value: Union[NetworkConfiguration, List[NetworkConfiguration]]) -> str:
+ def _display_network_conf(self, cur_value: Union[NetworkConfiguration, List[NetworkConfiguration]]) -> str:
if not cur_value:
return _('Not configured, unavailable unless setup manually')
else:
if isinstance(cur_value, list):
- ifaces = [x.iface for x in cur_value]
- return f'Configured ifaces: {ifaces}'
+ return str(_('Configured {} interfaces')).format(len(cur_value))
else:
return str(cur_value)
+ def _prev_network_config(self) -> Optional[str]:
+ selector = self._menu_options['nic']
+ if selector.has_selection():
+ ifaces = selector.current_selection
+ if isinstance(ifaces, list):
+ return FormattedOutput.as_table(ifaces)
+ return None
+
def _prev_harddrives(self) -> Optional[str]:
selector = self._menu_options['harddrives']
if selector.has_selection():
diff --git a/archinstall/lib/menu/list_manager.py b/archinstall/lib/menu/list_manager.py
index 40d01ce3..fe491caa 100644
--- a/archinstall/lib/menu/list_manager.py
+++ b/archinstall/lib/menu/list_manager.py
@@ -86,7 +86,7 @@ The contents in the base class of this methods serve for a very basic usage, and
"""
import copy
from os import system
-from typing import Union, Any, TYPE_CHECKING, Dict, Optional
+from typing import Union, Any, TYPE_CHECKING, Dict, Optional, Tuple, List
from .text_input import TextInput
from .menu import Menu
@@ -135,9 +135,9 @@ class ListManager:
elif isinstance(default_action,(list,tuple)):
self._default_action = default_action
else:
- self._default_action = [str(default_action),]
+ self._default_action = [str(default_action)]
- self._header = header if header else None
+ self._header = header if header else ''
self._cancel_action = str(_('Cancel'))
self._confirm_action = str(_('Confirm and exit'))
self._separator = ''
@@ -155,61 +155,81 @@ class ListManager:
while True:
# this will return a dictionary with the key as the menu entry to be displayed
# and the value is the original value from the self._data container
-
data_formatted = self.reformat(self._data)
- options = list(data_formatted.keys())
-
- if len(options) > 0:
- options.append(self._separator)
+ options, header = self._prepare_selection(data_formatted)
- if self._default_action:
- options += self._default_action
+ menu_header = self._header
- options += self._bottom_list
+ if header:
+ menu_header += header
system('clear')
- target = Menu(
+ choice = Menu(
self._prompt,
options,
sort=False,
clear_screen=False,
clear_menu_on_exit=False,
- header=self._header,
+ header=header,
skip_empty_entries=True,
- skip=False
+ skip=False,
+ show_search_hint=False
).run()
- if not target.value or target.value in self._bottom_list:
- self.action = target
+ if not choice.value or choice.value in self._bottom_list:
+ self.action = choice
break
- if target.value and target.value in self._default_action:
- self.action = target.value
+ if choice.value and choice.value in self._default_action:
+ self.action = choice.value
self.target = None
self._data = self.exec_action(self._data)
continue
- if isinstance(self._data,dict):
- data_key = data_formatted[target.value]
+ if isinstance(self._data, dict):
+ data_key = data_formatted[choice.value]
key = self._data[data_key]
self.target = {data_key: key}
elif isinstance(self._data, list):
- self.target = [d for d in self._data if d == data_formatted[target.value]][0]
+ self.target = [d for d in self._data if d == data_formatted[choice.value]][0]
else:
- self.target = self._data[data_formatted[target.value]]
+ self.target = self._data[data_formatted[choice.value]]
# Possible enhancement. If run_actions returns false a message line indicating the failure
- self.run_actions(target.value)
+ self.run_actions(choice.value)
- if target.value == self._cancel_action: # TODO dubious
+ if choice.value == self._cancel_action:
return self._original_data # return the original list
else:
return self._data
- def run_actions(self,prompt_data=None):
+ def _prepare_selection(self, data_formatted: Dict[str, Any]) -> Tuple[List[str], str]:
+ # header rows are mapped to None so make sure
+ # to exclude those from the selectable data
+ options: List[str] = [key for key, val in data_formatted.items() if val is not None]
+ header = ''
+
+ if len(options) > 0:
+ table_header = [key for key, val in data_formatted.items() if val is None]
+ header = '\n'.join(table_header)
+
+ if len(options) > 0:
+ options.append(self._separator)
+
+ if self._default_action:
+ # done only for mypy -> todo fix the self._default_action declaration
+ options += [action for action in self._default_action if action]
+
+ options += self._bottom_list
+ return options, header
+
+ def run_actions(self,prompt_data=''):
options = self.action_list() + self._bottom_item
- prompt = _("Select an action for < {} >").format(prompt_data if prompt_data else self.target)
+ display_value = self.selected_action_display(self.target) if self.target else prompt_data
+
+ prompt = _("Select an action for '{}'").format(display_value)
+
choice = Menu(
prompt,
options,
@@ -225,26 +245,28 @@ class ListManager:
if self.action and self.action != self._cancel_action:
self._data = self.exec_action(self._data)
- """
- The following methods are expected to be overwritten by the user if the needs of the list are beyond the simple case
- """
+ def selected_action_display(self, selection: Any) -> str:
+ # this will return the value to be displayed in the
+ # "Select an action for '{}'" string
+ raise NotImplementedError('Please implement me in the child class')
- def reformat(self, data: Any) -> Dict[str, Any]:
- """
- method to get the data in a format suitable to be shown
- It is executed once for run loop and processes the whole self._data structure
- """
- if isinstance(data,dict):
- return {f'{k}: {v}': k for k, v in data.items()}
- else:
- return {str(k): k for k in data}
+ def reformat(self, data: List[Any]) -> Dict[str, Any]:
+ # this should return a dictionary of display string to actual data entry
+ # mapping; if the value for a given display string is None it will be used
+ # in the header value (useful when displaying tables)
+ raise NotImplementedError('Please implement me in the child class')
def action_list(self):
"""
can define alternate action list or customize the list for each item.
Executed after any item is selected, contained in self.target
"""
- return self._base_actions
+ active_entry = self.target if self.target else None
+
+ if active_entry is None:
+ return [self._base_actions[0]]
+ else:
+ return self._base_actions[1:]
def exec_action(self, data: Any):
"""
diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py
index 3a26f6e7..80982db0 100644
--- a/archinstall/lib/menu/menu.py
+++ b/archinstall/lib/menu/menu.py
@@ -1,5 +1,6 @@
from dataclasses import dataclass
from enum import Enum, auto
+from os import system
from typing import Dict, List, Union, Any, TYPE_CHECKING, Optional
from archinstall.lib.menu.simple_menu import TerminalMenu
@@ -57,7 +58,11 @@ class Menu(TerminalMenu):
header :Union[List[str],str] = None,
explode_on_interrupt :bool = False,
explode_warning :str = '',
- **kwargs
+ clear_screen: bool = True,
+ show_search_hint: bool = True,
+ cycle_cursor: bool = True,
+ clear_menu_on_exit: bool = True,
+ skip_empty_entries: bool = False
):
"""
Creates a new menu
@@ -153,8 +158,7 @@ class Menu(TerminalMenu):
if header:
if not isinstance(header,(list,tuple)):
header = [header]
- header = '\n'.join(header)
- menu_title += f'\n{header}\n'
+ menu_title += '\n'.join(header)
action_info = ''
if skip:
@@ -178,10 +182,6 @@ class Menu(TerminalMenu):
cursor = "> "
main_menu_cursor_style = ("fg_cyan", "bold")
main_menu_style = ("bg_blue", "fg_gray")
- # defaults that can be changed up the stack
- kwargs['clear_screen'] = kwargs.get('clear_screen',True)
- kwargs['show_search_hint'] = kwargs.get('show_search_hint',True)
- kwargs['cycle_cursor'] = kwargs.get('cycle_cursor',True)
super().__init__(
menu_entries=self._menu_options,
@@ -200,7 +200,11 @@ class Menu(TerminalMenu):
preview_title=preview_title,
explode_on_interrupt=self._explode_on_interrupt,
multi_select_select_on_accept=False,
- **kwargs,
+ clear_screen=clear_screen,
+ show_search_hint=show_search_hint,
+ cycle_cursor=cycle_cursor,
+ clear_menu_on_exit=clear_menu_on_exit,
+ skip_empty_entries=skip_empty_entries
)
def _show(self) -> MenuSelection:
@@ -238,6 +242,7 @@ class Menu(TerminalMenu):
return self.run()
if ret.type_ is not MenuSelectionType.Selection and not self._skip:
+ system('clear')
return self.run()
return ret
diff --git a/archinstall/lib/models/network_configuration.py b/archinstall/lib/models/network_configuration.py
index 4f135da5..e026e97b 100644
--- a/archinstall/lib/models/network_configuration.py
+++ b/archinstall/lib/models/network_configuration.py
@@ -39,8 +39,22 @@ class NetworkConfiguration:
else:
return 'Unknown type'
- # for json serialization when calling json.dumps(...) on this class
- def json(self):
+ def as_json(self) -> Dict:
+ exclude_fields = ['type']
+ data = {}
+ for k, v in self.__dict__.items():
+ if k not in exclude_fields:
+ if isinstance(v, list) and len(v) == 0:
+ v = ''
+ elif v is None:
+ v = ''
+
+ data[k] = v
+
+ return data
+
+ def json(self) -> Dict:
+ # for json serialization when calling json.dumps(...) on this class
return self.__dict__
def is_iso(self) -> bool:
@@ -111,19 +125,10 @@ class NetworkConfigurationHandler:
else: # not recognized
return None
- def _parse_manual_config(self, config: Dict[str, Any]) -> Union[None, List[NetworkConfiguration]]:
- manual_configs: List = config.get('config', [])
-
- if not manual_configs:
- return None
-
- if not isinstance(manual_configs, list):
- log(_('Manual configuration setting must be a list'))
- exit(1)
-
+ def _parse_manual_config(self, configs: List[Dict[str, Any]]) -> Optional[List[NetworkConfiguration]]:
configurations = []
- for manual_config in manual_configs:
+ for manual_config in configs:
iface = manual_config.get('iface', None)
if iface is None:
@@ -135,7 +140,7 @@ class NetworkConfigurationHandler:
NetworkConfiguration(NicType.MANUAL, iface=iface)
)
else:
- ip = config.get('ip', '')
+ ip = manual_config.get('ip', '')
if not ip:
log(_('Manual nic configuration with no auto DHCP requires an IP address'), fg='red')
exit(1)
@@ -145,32 +150,34 @@ class NetworkConfigurationHandler:
NicType.MANUAL,
iface=iface,
ip=ip,
- gateway=config.get('gateway', ''),
- dns=config.get('dns', []),
+ gateway=manual_config.get('gateway', ''),
+ dns=manual_config.get('dns', []),
dhcp=False
)
)
return configurations
- def parse_arguments(self, config: Any):
- nic_type = config.get('type', None)
-
- if not nic_type:
- # old style definitions
- network_config = self._backwards_compability_config(config)
- if network_config:
- return network_config
- return None
-
+ def _parse_nic_type(self, nic_type: str) -> NicType:
try:
- type_ = NicType(nic_type)
+ return NicType(nic_type)
except ValueError:
options = [e.value for e in NicType]
log(_('Unknown nic type: {}. Possible values are {}').format(nic_type, options), fg='red')
exit(1)
- if type_ != NicType.MANUAL:
- self._configuration = NetworkConfiguration(type_)
- else: # manual configuration settings
+ def parse_arguments(self, config: Any):
+ if isinstance(config, list): # new data format
self._configuration = self._parse_manual_config(config)
+ elif nic_type := config.get('type', None): # new data format
+ type_ = self._parse_nic_type(nic_type)
+
+ if type_ != NicType.MANUAL:
+ self._configuration = NetworkConfiguration(type_)
+ else: # manual configuration settings
+ self._configuration = self._parse_manual_config([config])
+ else: # old style definitions
+ network_config = self._backwards_compability_config(config)
+ if network_config:
+ return network_config
+ return None
diff --git a/archinstall/lib/user_interaction/manage_users_conf.py b/archinstall/lib/user_interaction/manage_users_conf.py
index 567a2964..33c31342 100644
--- a/archinstall/lib/user_interaction/manage_users_conf.py
+++ b/archinstall/lib/user_interaction/manage_users_conf.py
@@ -7,6 +7,7 @@ from .utils import get_password
from ..menu import Menu
from ..menu.list_manager import ListManager
from ..models.users import User
+from ..output import FormattedOutput
if TYPE_CHECKING:
_: Any
@@ -18,13 +19,6 @@ class UserList(ListManager):
"""
def __init__(self, prompt: str, lusers: List[User]):
- """
- param: prompt
- type: str
- param: lusers dict with the users already defined for the system
- type: Dict
- param: sudo. boolean to determine if we handle superusers or users. If None handles both types
- """
self._actions = [
str(_('Add a user')),
str(_('Change password')),
@@ -34,21 +28,25 @@ class UserList(ListManager):
super().__init__(prompt, lusers, self._actions, self._actions[0])
def reformat(self, data: List[User]) -> Dict[str, User]:
- return {e.display(): e for e in data}
+ table = FormattedOutput.as_table(data)
+ rows = table.split('\n')
- def action_list(self):
- active_user = self.target if self.target else None
+ # these are the header rows of the table and do not map to any User obviously
+ # we're adding 2 spaces as prefix because the menu selector '> ' will be put before
+ # the selectable rows so the header has to be aligned
+ display_data = {f' {rows[0]}': None, f' {rows[1]}': None}
+
+ for row, user in zip(rows[2:], data):
+ row = row.replace('|', '\\|')
+ display_data[row] = user
- if active_user is None:
- return [self._actions[0]]
- else:
- return self._actions[1:]
+ return display_data
+
+ def selected_action_display(self, user: User) -> str:
+ return user.username
def exec_action(self, data: List[User]) -> List[User]:
- if self.target:
- active_user = self.target
- else:
- active_user = None
+ active_user = self.target if self.target else None
if self.action == self._actions[0]: # add
new_user = self._add_user()
@@ -77,8 +75,7 @@ class UserList(ListManager):
return False
def _add_user(self) -> Optional[User]:
- print(_('\nDefine a new user\n'))
- prompt = str(_('Enter username (leave blank to skip): '))
+ prompt = '\n\n' + str(_('Enter username (leave blank to skip): '))
while True:
username = input(prompt).strip(' ')
@@ -94,7 +91,9 @@ class UserList(ListManager):
choice = Menu(
str(_('Should "{}" be a superuser (sudo)?')).format(username), Menu.yes_no(),
skip=False,
- default_option=Menu.no()
+ default_option=Menu.no(),
+ clear_screen=False,
+ show_search_hint=False
).run()
sudo = True if choice.value == Menu.yes() else False
@@ -102,6 +101,5 @@ class UserList(ListManager):
def ask_for_additional_users(prompt: str = '', defined_users: List[User] = []) -> List[User]:
- prompt = prompt if prompt else _('Enter username (leave blank to skip): ')
users = UserList(prompt, defined_users).run()
return users
diff --git a/archinstall/lib/user_interaction/network_conf.py b/archinstall/lib/user_interaction/network_conf.py
index 5154d8b1..4f22b790 100644
--- a/archinstall/lib/user_interaction/network_conf.py
+++ b/archinstall/lib/user_interaction/network_conf.py
@@ -2,7 +2,7 @@ from __future__ import annotations
import ipaddress
import logging
-from typing import Any, Optional, TYPE_CHECKING, List, Union
+from typing import Any, Optional, TYPE_CHECKING, List, Union, Dict
from ..menu.menu import MenuSelectionType
from ..menu.text_input import TextInput
@@ -10,7 +10,7 @@ from ..models.network_configuration import NetworkConfiguration, NicType
from ..networking import list_interfaces
from ..menu import Menu
-from ..output import log
+from ..output import log, FormattedOutput
from ..menu.list_manager import ListManager
if TYPE_CHECKING:
@@ -19,55 +19,57 @@ if TYPE_CHECKING:
class ManualNetworkConfig(ListManager):
"""
- subclass of ListManager for the managing of network configuration accounts
+ subclass of ListManager for the managing of network configurations
"""
- def __init__(self, prompt: str, ifaces: Union[None, NetworkConfiguration, List[NetworkConfiguration]]):
- """
- param: prompt
- type: str
- param: ifaces already defined previously
- type: Dict
- """
+ def __init__(self, prompt: str, ifaces: List[NetworkConfiguration]):
+ self._actions = [
+ str(_('Add interface')),
+ str(_('Edit interface')),
+ str(_('Delete interface'))
+ ]
- if ifaces is not None and isinstance(ifaces, list):
- display_values = {iface.iface: iface for iface in ifaces}
- else:
- display_values = {}
+ super().__init__(prompt, ifaces, self._actions, self._actions[0])
+
+ def reformat(self, data: List[NetworkConfiguration]) -> Dict[str, Optional[NetworkConfiguration]]:
+ table = FormattedOutput.as_table(data)
+ rows = table.split('\n')
- self._action_add = str(_('Add interface'))
- self._action_edit = str(_('Edit interface'))
- self._action_delete = str(_('Delete interface'))
+ # these are the header rows of the table and do not map to any User obviously
+ # we're adding 2 spaces as prefix because the menu selector '> ' will be put before
+ # the selectable rows so the header has to be aligned
+ display_data: Dict[str, Optional[NetworkConfiguration]] = {f' {rows[0]}': None, f' {rows[1]}': None}
- self._iface_actions = [self._action_edit, self._action_delete]
+ for row, iface in zip(rows[2:], data):
+ row = row.replace('|', '\\|')
+ display_data[row] = iface
- super().__init__(prompt, display_values, self._iface_actions, self._action_add)
+ return display_data
- def run_manual(self) -> List[NetworkConfiguration]:
- ifaces = super().run()
- if ifaces is not None:
- return list(ifaces.values())
- return []
+ def selected_action_display(self, iface: NetworkConfiguration) -> str:
+ return iface.iface if iface.iface else ''
- def exec_action(self, data: Any):
- if self.action == self._action_add:
- iface_name = self._select_iface(data.keys())
+ def exec_action(self, data: List[NetworkConfiguration]):
+ active_iface: Optional[NetworkConfiguration] = self.target if self.target else None
+
+ if self.action == self._actions[0]: # add
+ iface_name = self._select_iface(data)
if iface_name:
iface = NetworkConfiguration(NicType.MANUAL, iface=iface_name)
- data[iface_name] = self._edit_iface(iface)
- elif self.target:
- iface_name = list(self.target.keys())[0]
- iface = data[iface_name]
-
- if self.action == self._action_edit:
- data[iface_name] = self._edit_iface(iface)
- elif self.action == self._action_delete:
- del data[iface_name]
+ iface = self._edit_iface(iface)
+ data += [iface]
+ elif active_iface:
+ if self.action == self._actions[1]: # edit interface
+ data = [d for d in data if d.iface != active_iface.iface]
+ data.append(self._edit_iface(active_iface))
+ elif self.action == self._actions[2]: # delete
+ data = [d for d in data if d != active_iface]
return data
- def _select_iface(self, existing_ifaces: List[str]) -> Optional[Any]:
+ def _select_iface(self, data: List[NetworkConfiguration]) -> Optional[Any]:
all_ifaces = list_interfaces().values()
+ existing_ifaces = [d.iface for d in data]
available = set(all_ifaces) - set(existing_ifaces)
choice = Menu(str(_('Select interface to add')), list(available), skip=True).run()
@@ -76,7 +78,7 @@ class ManualNetworkConfig(ListManager):
return choice.value
- def _edit_iface(self, edit_iface :NetworkConfiguration):
+ def _edit_iface(self, edit_iface: NetworkConfiguration):
iface_name = edit_iface.iface
modes = ['DHCP (auto detect)', 'IP (static)']
default_mode = 'DHCP (auto detect)'
@@ -99,11 +101,13 @@ class ManualNetworkConfig(ListManager):
gateway = None
while 1:
- gateway_input = TextInput(_('Enter your gateway (router) IP address or leave blank for none: '),
- edit_iface.gateway).run().strip()
+ gateway = TextInput(
+ _('Enter your gateway (router) IP address or leave blank for none: '),
+ edit_iface.gateway
+ ).run().strip()
try:
- if len(gateway_input) > 0:
- ipaddress.ip_address(gateway_input)
+ if len(gateway) > 0:
+ ipaddress.ip_address(gateway)
break
except ValueError:
log("You need to enter a valid gateway (router) IP address.", level=logging.WARNING, fg='red')
@@ -124,7 +128,9 @@ class ManualNetworkConfig(ListManager):
return NetworkConfiguration(NicType.MANUAL, iface=iface_name)
-def ask_to_configure_network(preset: Union[None, NetworkConfiguration, List[NetworkConfiguration]]) -> Optional[Union[List[NetworkConfiguration], NetworkConfiguration]]:
+def ask_to_configure_network(
+ preset: Union[NetworkConfiguration, List[NetworkConfiguration]]
+) -> Optional[NetworkConfiguration | List[NetworkConfiguration]]:
"""
Configure the network on the newly installed system
"""
@@ -165,7 +171,7 @@ def ask_to_configure_network(preset: Union[None, NetworkConfiguration, List[Netw
elif choice.value == network_options['network_manager']:
return NetworkConfiguration(NicType.NM)
elif choice.value == network_options['manual']:
- manual = ManualNetworkConfig('Configure interfaces', preset)
- return manual.run_manual()
+ preset_ifaces = preset if isinstance(preset, list) else []
+ return ManualNetworkConfig('Configure interfaces', preset_ifaces).run()
return preset
diff --git a/archinstall/lib/user_interaction/subvolume_config.py b/archinstall/lib/user_interaction/subvolume_config.py
index a54ec891..e2797bba 100644
--- a/archinstall/lib/user_interaction/subvolume_config.py
+++ b/archinstall/lib/user_interaction/subvolume_config.py
@@ -5,6 +5,7 @@ from ..menu.menu import MenuSelectionType
from ..menu.text_input import TextInput
from ..menu import Menu
from ..models.subvolume import Subvolume
+from ... import FormattedOutput
if TYPE_CHECKING:
_: Any
@@ -19,16 +20,23 @@ class SubvolumeList(ListManager):
]
super().__init__(prompt, current_volumes, self._actions, self._actions[0])
- def reformat(self, data: List[Subvolume]) -> Dict[str, Subvolume]:
- return {e.display(): e for e in data}
+ def reformat(self, data: List[Subvolume]) -> Dict[str, Optional[Subvolume]]:
+ table = FormattedOutput.as_table(data)
+ rows = table.split('\n')
- def action_list(self):
- active_user = self.target if self.target else None
+ # these are the header rows of the table and do not map to any User obviously
+ # we're adding 2 spaces as prefix because the menu selector '> ' will be put before
+ # the selectable rows so the header has to be aligned
+ display_data: Dict[str, Optional[Subvolume]] = {f' {rows[0]}': None, f' {rows[1]}': None}
- if active_user is None:
- return [self._actions[0]]
- else:
- return self._actions[1:]
+ for row, subvol in zip(rows[2:], data):
+ row = row.replace('|', '\\|')
+ display_data[row] = subvol
+
+ return display_data
+
+ def selected_action_display(self, subvolume: Subvolume) -> str:
+ return subvolume.name
def _prompt_options(self, editing: Optional[Subvolume] = None) -> List[str]:
preset_options = []