Move ddrescue-tui menus to a separate file
This commit is contained in:
parent
4feb15182e
commit
986c870090
4 changed files with 278 additions and 244 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
"""WizardKit: ddrescue-tui module init"""
|
"""WizardKit: ddrescue-tui module init"""
|
||||||
|
|
||||||
from . import ddrescue
|
from . import ddrescue
|
||||||
|
from . import menus
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,9 @@ from docopt import docopt
|
||||||
from wk import cfg, debug, exe, io, log, net, std
|
from wk import cfg, debug, exe, io, log, net, std
|
||||||
from wk.cfg.ddrescue import (
|
from wk.cfg.ddrescue import (
|
||||||
DDRESCUE_MAP_TEMPLATE,
|
DDRESCUE_MAP_TEMPLATE,
|
||||||
DDRESCUE_SETTINGS,
|
|
||||||
DDRESCUE_SPECIFIC_PASS_SETTINGS,
|
DDRESCUE_SPECIFIC_PASS_SETTINGS,
|
||||||
)
|
)
|
||||||
|
from wk.clone import menus
|
||||||
from wk.hw import disk as hw_disk
|
from wk.hw import disk as hw_disk
|
||||||
from wk.hw.smart import (
|
from wk.hw.smart import (
|
||||||
check_attributes,
|
check_attributes,
|
||||||
|
|
@ -38,6 +38,7 @@ from wk.ui import ansi, cli, tmux, tui
|
||||||
|
|
||||||
|
|
||||||
# STATIC VARIABLES
|
# STATIC VARIABLES
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
DOCSTRING = f'''{cfg.main.KIT_NAME_FULL}: ddrescue TUI
|
DOCSTRING = f'''{cfg.main.KIT_NAME_FULL}: ddrescue TUI
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
@ -68,9 +69,6 @@ CLONE_SETTINGS = {
|
||||||
# (5, 1) ## Clone source partition #5 to destination partition #1
|
# (5, 1) ## Clone source partition #5 to destination partition #1
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
if std.PLATFORM == 'Darwin':
|
|
||||||
DDRESCUE_SETTINGS['Default']['--idirect'] = {'Selected': False, 'Hidden': True}
|
|
||||||
DDRESCUE_SETTINGS['Default']['--odirect'] = {'Selected': False, 'Hidden': True}
|
|
||||||
DDRESCUE_LOG_REGEX = re.compile(
|
DDRESCUE_LOG_REGEX = re.compile(
|
||||||
r'^\s*(?P<key>\S+):\s+'
|
r'^\s*(?P<key>\S+):\s+'
|
||||||
r'(?P<size>\d+)\s+'
|
r'(?P<size>\d+)\s+'
|
||||||
|
|
@ -89,16 +87,6 @@ REGEX_REMAINING_TIME = re.compile(
|
||||||
r'\s*(?P<na>n/a)?',
|
r'\s*(?P<na>n/a)?',
|
||||||
re.IGNORECASE
|
re.IGNORECASE
|
||||||
)
|
)
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
PANE_RATIOS = (
|
PANE_RATIOS = (
|
||||||
12, # SMART
|
12, # SMART
|
||||||
22, # ddrescue progress
|
22, # ddrescue progress
|
||||||
|
|
@ -111,11 +99,6 @@ if PLATFORM == 'Darwin':
|
||||||
RECOMMENDED_MAP_FSTYPES = re.compile(
|
RECOMMENDED_MAP_FSTYPES = re.compile(
|
||||||
r'^(apfs|cifs|ext[234]|hfs.?|ntfs|smbfs|vfat|xfs)$'
|
r'^(apfs|cifs|ext[234]|hfs.?|ntfs|smbfs|vfat|xfs)$'
|
||||||
)
|
)
|
||||||
SETTING_PRESETS = (
|
|
||||||
'Default',
|
|
||||||
'Fast',
|
|
||||||
'Safe',
|
|
||||||
)
|
|
||||||
STATUS_COLORS = {
|
STATUS_COLORS = {
|
||||||
'Passed': 'GREEN',
|
'Passed': 'GREEN',
|
||||||
'Aborted': 'YELLOW',
|
'Aborted': 'YELLOW',
|
||||||
|
|
@ -450,7 +433,7 @@ class State():
|
||||||
)
|
)
|
||||||
self._add_block_pair(bp_source, bp_dest)
|
self._add_block_pair(bp_source, bp_dest)
|
||||||
else:
|
else:
|
||||||
source_parts = select_disk_parts('Clone', self.source)
|
source_parts = menus.select_disk_parts('Clone', self.source)
|
||||||
if self.source.path.samefile(source_parts[0].path):
|
if self.source.path.samefile(source_parts[0].path):
|
||||||
# Whole disk (or single partition via args), skip settings
|
# Whole disk (or single partition via args), skip settings
|
||||||
bp_dest = self.destination.path
|
bp_dest = self.destination.path
|
||||||
|
|
@ -645,6 +628,7 @@ class State():
|
||||||
def init_recovery(self, docopt_args: dict[str, Any]) -> None:
|
def init_recovery(self, docopt_args: dict[str, Any]) -> None:
|
||||||
"""Select source/dest and set env."""
|
"""Select source/dest and set env."""
|
||||||
cli.clear_screen()
|
cli.clear_screen()
|
||||||
|
disk_menu = menus.disks()
|
||||||
source_parts = []
|
source_parts = []
|
||||||
|
|
||||||
# Set log
|
# Set log
|
||||||
|
|
@ -662,16 +646,16 @@ class State():
|
||||||
# Select source
|
# Select source
|
||||||
self.source = get_object(docopt_args['<source>'])
|
self.source = get_object(docopt_args['<source>'])
|
||||||
if not self.source:
|
if not self.source:
|
||||||
self.source = select_disk('Source')
|
self.source = menus.select_disk('Source', disk_menu)
|
||||||
self.ui.set_title('Source', self.source.name)
|
self.ui.set_title('Source', self.source.name)
|
||||||
|
|
||||||
# Select destination
|
# Select destination
|
||||||
self.destination = get_object(docopt_args['<destination>'])
|
self.destination = get_object(docopt_args['<destination>'])
|
||||||
if not self.destination:
|
if not self.destination:
|
||||||
if self.mode == 'Clone':
|
if self.mode == 'Clone':
|
||||||
self.destination = select_disk('Destination', self.source)
|
self.destination = menus.select_disk('Destination', disk_menu)
|
||||||
elif self.mode == 'Image':
|
elif self.mode == 'Image':
|
||||||
self.destination = select_path('Destination')
|
self.destination = menus.select_path('Destination')
|
||||||
self.ui.add_title_pane('Destination', self.destination.name)
|
self.ui.add_title_pane('Destination', self.destination.name)
|
||||||
|
|
||||||
# Update details
|
# Update details
|
||||||
|
|
@ -702,7 +686,7 @@ class State():
|
||||||
if self.mode == 'Clone':
|
if self.mode == 'Clone':
|
||||||
source_parts = self.add_clone_block_pairs()
|
source_parts = self.add_clone_block_pairs()
|
||||||
else:
|
else:
|
||||||
source_parts = select_disk_parts(self.mode, self.source)
|
source_parts = menus.select_disk_parts(self.mode, self.source)
|
||||||
self.add_image_block_pairs(source_parts)
|
self.add_image_block_pairs(source_parts)
|
||||||
|
|
||||||
# Update SMART data
|
# Update SMART data
|
||||||
|
|
@ -1325,22 +1309,6 @@ def build_disk_report(dev) -> list[str]:
|
||||||
return report
|
return report
|
||||||
|
|
||||||
|
|
||||||
def build_main_menu() -> cli.Menu:
|
|
||||||
"""Build 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 build_object_report(obj) -> list[str]:
|
def build_object_report(obj) -> list[str]:
|
||||||
"""Build object report, returns list."""
|
"""Build object report, returns list."""
|
||||||
report = []
|
report = []
|
||||||
|
|
@ -1357,47 +1325,6 @@ def build_object_report(obj) -> list[str]:
|
||||||
return report
|
return report
|
||||||
|
|
||||||
|
|
||||||
def build_settings_menu(silent=True) -> cli.Menu:
|
|
||||||
"""Build 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 build_sfdisk_partition_line(table_type, dev_path, size, details) -> str:
|
def build_sfdisk_partition_line(table_type, dev_path, size, details) -> str:
|
||||||
"""Build sfdisk partition line using passed details, returns str."""
|
"""Build sfdisk partition line using passed details, returns str."""
|
||||||
line = f'{dev_path} : size={size}'
|
line = f'{dev_path} : size={size}'
|
||||||
|
|
@ -1826,8 +1753,8 @@ def main() -> None:
|
||||||
raise RuntimeError('tmux session not found')
|
raise RuntimeError('tmux session not found')
|
||||||
|
|
||||||
# Init
|
# Init
|
||||||
main_menu = build_main_menu()
|
main_menu = menus.main()
|
||||||
settings_menu = build_settings_menu()
|
settings_menu = menus.settings()
|
||||||
state = State()
|
state = State()
|
||||||
try:
|
try:
|
||||||
state.init_recovery(args)
|
state.init_recovery(args)
|
||||||
|
|
@ -1845,7 +1772,7 @@ def main() -> None:
|
||||||
selection = settings_menu.settings_select()
|
selection = settings_menu.settings_select()
|
||||||
if 'Load Preset' in selection:
|
if 'Load Preset' in selection:
|
||||||
# Rebuild settings menu using preset
|
# Rebuild settings menu using preset
|
||||||
settings_menu = build_settings_menu(silent=False)
|
settings_menu = menus.settings(silent=False)
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -2179,165 +2106,6 @@ def run_recovery(state: State, main_menu, settings_menu, dry_run=True) -> None:
|
||||||
state.update_progress_pane('Idle')
|
state.update_progress_pane('Idle')
|
||||||
|
|
||||||
|
|
||||||
def select_disk(prompt_msg, skip_disk=None) -> hw_disk.Disk:
|
|
||||||
"""Select disk from list, returns Disk()."""
|
|
||||||
cli.print_info('Scanning disks...')
|
|
||||||
disks = hw_disk.get_disks()
|
|
||||||
menu = cli.Menu(
|
|
||||||
title=ansi.color_string(f'ddrescue TUI: {prompt_msg} Selection', 'GREEN'),
|
|
||||||
)
|
|
||||||
menu.disabled_str = 'Already selected'
|
|
||||||
menu.separator = ' '
|
|
||||||
menu.add_action('Quit')
|
|
||||||
for disk in disks:
|
|
||||||
disable_option = False
|
|
||||||
size = disk.size
|
|
||||||
|
|
||||||
# Check if option should be disabled
|
|
||||||
if skip_disk:
|
|
||||||
if (disk.path.samefile(skip_disk.path)
|
|
||||||
or (skip_disk.parent and disk.path.samefile(skip_disk.parent))):
|
|
||||||
disable_option = True
|
|
||||||
|
|
||||||
# Add to menu
|
|
||||||
menu.add_option(
|
|
||||||
name=(
|
|
||||||
f'{str(disk.path):<12} '
|
|
||||||
f'{disk.bus:<5} '
|
|
||||||
f'{std.bytes_to_string(size, decimals=1, use_binary=False):<8} '
|
|
||||||
f'{disk.model} '
|
|
||||||
f'{disk.serial}'
|
|
||||||
),
|
|
||||||
details={'Disabled': disable_option, 'Object': disk},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get selection
|
|
||||||
selection = menu.simple_select()
|
|
||||||
if 'Quit' in selection:
|
|
||||||
raise std.GenericAbort()
|
|
||||||
|
|
||||||
# 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[hw_disk.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 std.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'({std.bytes_to_string(size, decimals=1, use_binary=False):>6})'
|
|
||||||
)
|
|
||||||
menu.add_option(name, details={'Selected': True, 'Path': part['path']})
|
|
||||||
|
|
||||||
# Add whole disk if necessary
|
|
||||||
if not menu.options:
|
|
||||||
menu.add_option(whole_disk_str, {'Selected': True, '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['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 hw_disk.Disk() objects
|
|
||||||
cli.print_standard(' ')
|
|
||||||
cli.print_info('Getting disk/partition details...')
|
|
||||||
object_list = [hw_disk.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 = None
|
|
||||||
|
|
||||||
# Make selection
|
|
||||||
selection = menu.simple_select()
|
|
||||||
if 'Current directory' in selection:
|
|
||||||
path = os.getcwd()
|
|
||||||
elif 'Enter manually' in selection:
|
|
||||||
path = cli.input_text('Please enter path: ')
|
|
||||||
elif 'Quit' in selection:
|
|
||||||
raise std.GenericAbort()
|
|
||||||
|
|
||||||
# Check
|
|
||||||
try:
|
|
||||||
path = pathlib.Path(path).resolve()
|
|
||||||
except TypeError:
|
|
||||||
invalid = True
|
|
||||||
if invalid or not path.is_dir():
|
|
||||||
cli.print_error(f'Invalid path: {path}')
|
|
||||||
raise std.GenericAbort()
|
|
||||||
|
|
||||||
# Done
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def set_mode(docopt_args) -> str:
|
def set_mode(docopt_args) -> str:
|
||||||
"""Set mode from docopt_args or user selection, returns str."""
|
"""Set mode from docopt_args or user selection, returns str."""
|
||||||
mode = '?'
|
mode = '?'
|
||||||
|
|
|
||||||
265
scripts/wk/clone/menus.py
Normal file
265
scripts/wk/clone/menus.py
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
"""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.")
|
||||||
|
|
@ -50,7 +50,7 @@ class Disk:
|
||||||
parent: str = field(init=False)
|
parent: str = field(init=False)
|
||||||
phy_sec: int = field(init=False)
|
phy_sec: int = field(init=False)
|
||||||
raw_details: dict[str, Any] = field(init=False)
|
raw_details: dict[str, Any] = field(init=False)
|
||||||
raw_smartctl: dict[str, Any] = field(init=False)
|
raw_smartctl: dict[str, Any] = field(init=False, default_factory=dict)
|
||||||
serial: str = field(init=False)
|
serial: str = field(init=False)
|
||||||
size: int = field(init=False)
|
size: int = field(init=False)
|
||||||
ssd: bool = field(init=False)
|
ssd: bool = field(init=False)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue