WizardKit/scripts/wk/clone/menus.py

265 lines
7.4 KiB
Python

"""WizardKit: ddrescue TUI - Menus"""
# vim: sts=2 sw=2 ts=2
import logging
import pathlib
from wk.cfg.ddrescue import DDRESCUE_SETTINGS
from wk.hw.disk import Disk, get_disks
from wk.std import GenericAbort, PLATFORM, bytes_to_string
from wk.ui import ansi, cli
# STATIC VARIABLES
LOG = logging.getLogger(__name__)
CLONE_SETTINGS = {
'Source': None,
'Destination': None,
'Create Boot Partition': False,
'First Run': True,
'Needs Format': False,
'Table Type': None,
'Partition Mapping': [
# (5, 1) ## Clone source partition #5 to destination partition #1
],
}
if PLATFORM == 'Darwin':
# TODO: Direct I/O needs more testing under macOS
DDRESCUE_SETTINGS['Default']['--idirect'] = {'Selected': False, 'Hidden': True}
DDRESCUE_SETTINGS['Default']['--odirect'] = {'Selected': False, 'Hidden': True}
MENU_ACTIONS = (
'Start',
f'Change settings {ansi.color_string("(experts only)", "YELLOW")}',
f'Detect drives {ansi.color_string("(experts only)", "YELLOW")}',
'Quit')
MENU_TOGGLES = {
'Auto continue (if recovery % over threshold)': True,
'Retry (mark non-rescued sectors "non-tried")': False,
}
SETTING_PRESETS = (
'Default',
'Fast',
'Safe',
)
# Functions
def main() -> cli.Menu:
"""Main menu, returns wk.ui.cli.Menu."""
menu = cli.Menu(title=ansi.color_string('ddrescue TUI: Main Menu', 'GREEN'))
menu.separator = ' '
# Add actions, options, etc
for action in MENU_ACTIONS:
if not (PLATFORM == 'Darwin' and 'Detect drives' in action):
menu.add_action(action)
for toggle, selected in MENU_TOGGLES.items():
menu.add_toggle(toggle, {'Selected': selected})
# Done
return menu
def settings(silent: bool = True) -> cli.Menu:
"""Settings menu, returns wk.ui.cli.Menu."""
title_text = [
ansi.color_string('ddrescue TUI: Expert Settings', 'GREEN'),
' ',
ansi.color_string(
['These settings can cause', 'MAJOR DAMAGE', 'to drives'],
['YELLOW', 'RED', 'YELLOW'],
),
'Please read the manual before making changes',
]
menu = cli.Menu(title='\n'.join(title_text))
menu.separator = ' '
preset = 'Default'
if not silent:
# Ask which preset to use
cli.print_standard(
f'Available ddrescue presets: {" / ".join(SETTING_PRESETS)}'
)
preset = cli.choice('Please select a preset:', SETTING_PRESETS)
# Fix selection
for _p in SETTING_PRESETS:
if _p.startswith(preset):
preset = _p
# Add default settings
menu.add_action('Load Preset')
menu.add_action('Main Menu')
for name, details in DDRESCUE_SETTINGS['Default'].items():
menu.add_option(name, details.copy())
# Update settings using preset
if preset != 'Default':
for name, details in DDRESCUE_SETTINGS[preset].items():
menu.options[name].update(details.copy())
# Done
return menu
def disks() -> cli.Menu:
"""Disk menu, returns wk.ui.cli.Menu()."""
cli.print_info('Scanning disks...')
available_disks = get_disks()
menu = cli.Menu('ddrescue TUI: Disk selection')
menu.disabled_str = 'Already selected'
menu.separator = ' '
menu.add_action('Quit')
for disk in available_disks:
menu.add_option(
name=(
f'{str(disk.path):<12} '
f'{disk.bus:<5} '
f'{bytes_to_string(disk.size, decimals=1, use_binary=False):<8} '
f'{disk.model} '
f'{disk.serial}'
),
details={'Object': disk},
)
# Done
return menu
def select_disk(prompt_msg: str, menu: cli.Menu) -> Disk:
"""Select disk from provided Menu, returns Disk()."""
menu.title = ansi.color_string(
f'ddrescue TUI: {prompt_msg} Selection', 'GREEN',
)
# Get selection
selection = menu.simple_select()
if 'Quit' in selection:
raise GenericAbort()
# Disable selected disk's menu entry
menu.options[selection[0]]['Disabled'] = True
# Update details to include child devices
selected_disk = selection[-1]['Object']
selected_disk.update_details(skip_children=False)
# Done
return selected_disk
def select_disk_parts(prompt_msg, disk) -> list[Disk]:
"""Select disk parts from list, returns list of Disk()."""
title = ansi.color_string('ddrescue TUI: Partition Selection', 'GREEN')
title += f'\n\nDisk: {disk.path} {disk.description}'
menu = cli.Menu(title)
menu.separator = ' '
menu.add_action('All')
menu.add_action('None')
menu.add_action('Proceed', {'Separator': True})
menu.add_action('Quit')
object_list = []
def _select_parts(menu) -> None:
"""Loop over selection menu until at least one partition selected."""
while True:
selection = menu.advanced_select(
f'Please select the parts to {prompt_msg.lower()}: ',
)
if 'All' in selection:
for option in menu.options.values():
option['Selected'] = True
elif 'None' in selection:
for option in menu.options.values():
option['Selected'] = False
elif 'Proceed' in selection:
if any(option['Selected'] for option in menu.options.values()):
# At least one partition/device selected/device selected
break
elif 'Quit' in selection:
raise GenericAbort()
# Bail early if running under macOS
if PLATFORM == 'Darwin':
return [disk]
# Bail early if child device selected
if disk.parent:
return [disk]
# Add parts
whole_disk_str = f'{str(disk.path):<14} (Whole device)'
for part in disk.children:
size = part["size"]
name = (
f'{str(part["path"]):<14} '
f'({bytes_to_string(size, decimals=1, use_binary=False):>6})'
)
menu.add_option(name, details={'Selected': True, 'pathlib.Path': part['path']})
# Add whole disk if necessary
if not menu.options:
menu.add_option(whole_disk_str, {'Selected': True, 'pathlib.Path': disk.path})
menu.title += '\n\n'
menu.title += ansi.color_string(' No partitions detected.', 'YELLOW')
# Get selection
_select_parts(menu)
# Build list of Disk() object_list
for option in menu.options.values():
if option['Selected']:
object_list.append(option['pathlib.Path'])
# Check if whole disk selected
if len(object_list) == len(disk.children):
# NOTE: This is not true if the disk has no partitions
msg = f'Preserve partition table and unused space in {prompt_msg.lower()}?'
if cli.ask(msg):
# Replace part list with whole disk obj
object_list = [disk.path]
# Convert object_list to Disk() objects
cli.print_standard(' ')
cli.print_info('Getting disk/partition details...')
object_list = [Disk(path) for path in object_list]
# Done
return object_list
def select_path(prompt_msg) -> pathlib.Path:
"""Select path, returns pathlib.Path."""
invalid = False
menu = cli.Menu(
title=ansi.color_string(f'ddrescue TUI: {prompt_msg} Path Selection', 'GREEN'),
)
menu.separator = ' '
menu.add_action('Quit')
menu.add_option('Current directory')
menu.add_option('Enter manually')
path = pathlib.Path.cwd()
# Make selection
selection = menu.simple_select()
if 'Current directory' in selection:
pass
elif 'Enter manually' in selection:
path = pathlib.Path(cli.input_text('Please enter path: '))
elif 'Quit' in selection:
raise GenericAbort()
# Check
try:
path = path.resolve()
except TypeError:
invalid = True
if invalid or not path.is_dir():
cli.print_error(f'Invalid path: {path}')
raise GenericAbort()
# Done
return path
if __name__ == '__main__':
print("This file is not meant to be called directly.")