import os import shutil import tkinter as tk import subprocess import json import threading import webbrowser from typing import Optional, List, Tuple, Dict, Union import requests from shared_libs.common_tools import IconManager, Tooltip, LxTools, message_box_animation from shared_libs.gitea import GiteaUpdater, GiteaApiUrlError, GiteaVersionParseError, GiteaApiResponseError from .cfd_app_config import CfdConfigManager, LocaleStrings from .cfd_ui_setup import StyleManager, WidgetManager from shared_libs.animated_icon import AnimatedIcon from .cfd_settings_dialog import SettingsDialog from .cfd_file_operations import FileOperationsManager from .cfd_search_manager import SearchManager from .cfd_navigation_manager import NavigationManager from .cfd_view_manager import ViewManager from .cfd_sftp_manager import SFTPManager, PARAMIKO_AVAILABLE from shared_libs.message import CredentialsDialog, MessageDialog, InputDialog class CustomFileDialog(tk.Toplevel): """ A custom file dialog window that provides functionalities for file selection, directory navigation, search, and file operations. """ def __init__(self, parent: tk.Widget, initial_dir: Optional[str] = None, filetypes: Optional[List[Tuple[str, str]]] = None, mode: str = "open", title: str = LocaleStrings.CFD["title"]): """ Initializes the CustomFileDialog. """ super().__init__(parent) self.current_fs_type = "local" # "local" or "sftp" self.sftp_manager = SFTPManager() self.config_manager = CfdConfigManager() self.my_tool_tip: Optional[Tooltip] = None self.dialog_mode: str = mode self.gitea_api_url = CfdConfigManager.UPDATE_URL self.lib_version = CfdConfigManager.VERSION self.update_status: str = "" self.load_settings() self.geometry(self.settings["window_size_preset"]) min_width, min_height = self.get_min_size_from_preset( self.settings["window_size_preset"]) self.minsize(min_width, min_height) self.title(title) self.image: IconManager = IconManager() width, height = map( int, self.settings["window_size_preset"].split('x')) LxTools.center_window_cross_platform(self, width, height) self.parent: tk.Widget = parent self.transient(parent) self.grab_set() self.result: Optional[Union[str, List[str]]] = None self.current_dir: str = os.path.abspath( initial_dir) if initial_dir else os.path.expanduser("~") self.filetypes: List[Tuple[str, str]] = filetypes if filetypes else [ (LocaleStrings.CFD["all_files"], "*.* ")] self.current_filter_pattern: str = self.filetypes[0][1] self.history: List[str] = [] self.history_pos: int = -1 self.view_mode: tk.StringVar = tk.StringVar( value=self.settings.get("default_view_mode", "icons")) self.show_hidden_files: tk.BooleanVar = tk.BooleanVar(value=False) self.resize_job: Optional[str] = None self.last_width: int = 0 self.selected_item_frames: List[ttk.Frame] = [] self.search_results: List[str] = [] self.search_mode: bool = False self.original_path_text: str = "" self.items_to_load_per_batch: int = 250 self.item_path_map: Dict[int, str] = {} self.responsive_buttons_hidden: Optional[bool] = None self.search_job: Optional[str] = None self.search_thread: Optional[threading.Thread] = None self.search_process: Optional[subprocess.Popen] = None self.icon_manager: IconManager = IconManager() self._initialize_managers() self.widget_manager.filename_entry.bind( "", self.search_manager.execute_search) self.update_animation_settings() self.view_manager._update_view_mode_buttons() def initial_load() -> None: """Performs the initial loading and UI setup.""" self.update_idletasks() self.last_width = self.widget_manager.file_list_frame.winfo_width() self._handle_responsive_buttons(self.winfo_width()) self.navigation_manager.navigate_to(self.current_dir) self.after(10, initial_load) self.widget_manager.path_entry.bind( "", self.navigation_manager.handle_path_entry_return) self.widget_manager.home_button.config(command=self.go_to_local_home) self.bind("", self.search_manager.show_search_bar) if self.dialog_mode == "save": self.bind("", self.file_op_manager.delete_selected_item) if self.gitea_api_url and self.lib_version: self.update_thread = threading.Thread( target=self.check_for_updates, daemon=True) self.update_thread.start() def _initialize_managers(self) -> None: """Initializes or re-initializes all the manager classes.""" self.style_manager: StyleManager = StyleManager(self) self.file_op_manager: FileOperationsManager = FileOperationsManager( self) self.search_manager: SearchManager = SearchManager(self) self.navigation_manager: NavigationManager = NavigationManager(self) self.view_manager: ViewManager = ViewManager(self) self.widget_manager: WidgetManager = WidgetManager(self, self.settings) def load_settings(self) -> None: """Loads settings from the configuration file.""" self.settings = CfdConfigManager.load() size_preset = self.settings.get("window_size_preset", "1050x850") self.settings["window_size_preset"] = size_preset if hasattr(self, 'view_mode'): self.view_mode.set(self.settings.get("default_view_mode", "icons")) def get_min_size_from_preset(self, preset: str) -> Tuple[int, int]: """ Calculates the minimum window size based on a preset string. """ w, h = map(int, preset.split('x')) return max(650, w - 400), max(450, h - 400) def reload_config_and_rebuild_ui(self) -> None: """Reloads the configuration and rebuilds the entire UI.""" is_sftp_connected = (self.current_fs_type == "sftp") self.load_settings() self.geometry(self.settings["window_size_preset"]) min_width, min_height = self.get_min_size_from_preset( self.settings["window_size_preset"]) self.minsize(min_width, min_height) width, height = map( int, self.settings["window_size_preset"].split('x')) LxTools.center_window_cross_platform(self, width, height) for widget in self.winfo_children(): widget.destroy() self._initialize_managers() if is_sftp_connected: self.widget_manager.sftp_button.config( command=self.disconnect_sftp, style="Header.TButton.Active.Round") self.widget_manager.filename_entry.bind( "", self.search_manager.execute_search) self.view_manager._update_view_mode_buttons() self.responsive_buttons_hidden = None self.update_idletasks() self._handle_responsive_buttons(self.winfo_width()) self.update_animation_settings() if self.search_mode: self.search_manager.show_search_results_treeview() else: self.navigation_manager.navigate_to(self.current_dir) def open_settings_dialog(self) -> None: """Opens the settings dialog.""" SettingsDialog(self, dialog_mode=self.dialog_mode) def open_sftp_dialog(self): if not PARAMIKO_AVAILABLE: MessageDialog(message_type="error", text="Paramiko library is not installed.").show() return dialog = CredentialsDialog(self) credentials = dialog.show() if credentials: self.connect_sftp(credentials, is_new_connection=True) def connect_sftp(self, credentials, is_new_connection: bool = False): self.config(cursor="watch") self.update_idletasks() if is_new_connection and credentials.get("save_bookmark"): bookmark_name = credentials["bookmark_name"] bookmark_data = { "host": credentials["host"], "port": credentials["port"], "username": credentials["username"], "initial_path": credentials["initial_path"], "key_file": credentials["key_file"], } try: import keyring service_name = f"customfiledialog-sftp" if credentials["password"]: keyring.set_password(service_name, f"{bookmark_name}_password", credentials["password"]) bookmark_data["password_in_keyring"] = True if credentials["passphrase"]: keyring.set_password(service_name, f"{bookmark_name}_passphrase", credentials["passphrase"]) bookmark_data["passphrase_in_keyring"] = True self.config_manager.add_bookmark(bookmark_name, bookmark_data) self.after(100, self.reload_config_and_rebuild_ui) except Exception as e: MessageDialog(message_type="error", text=f"Could not save bookmark: {e}").show() success, message = self.sftp_manager.connect( host=credentials.get('host'), port=credentials.get('port'), username=credentials.get('username'), password=credentials.get('password'), key_file=credentials.get('key_file'), passphrase=credentials.get('passphrase') ) self.config(cursor="") if success: self.current_fs_type = "sftp" self.widget_manager.sftp_button.config( command=self.disconnect_sftp, style="Header.TButton.Active.Round") initial_path = credentials.get("initial_path", "/") self.navigation_manager.navigate_to(initial_path) else: MessageDialog(message_type="error", text=f"Connection failed: {message}").show() def connect_sftp_bookmark(self, data): credentials = data.copy() try: import keyring service_name = f"customfiledialog-sftp" bookmark_name = next(name for name, b_data in self.config_manager.load_bookmarks().items() if b_data == data) if credentials.get("password_in_keyring"): credentials["password"] = keyring.get_password(service_name, f"{bookmark_name}_password") if credentials.get("passphrase_in_keyring"): credentials["passphrase"] = keyring.get_password(service_name, f"{bookmark_name}_passphrase") except (ImportError, StopIteration, Exception) as e: MessageDialog(message_type="error", text=f"Could not retrieve credentials: {e}").show() return self.connect_sftp(credentials, is_new_connection=False) def edit_sftp_bookmark(self, name: str, data: dict): """Opens the credentials dialog to edit an existing SFTP bookmark.""" data['bookmark_name'] = name dialog = CredentialsDialog(self, title=f"Edit Bookmark: {name}", initial_data=data, is_edit_mode=True) new_data = dialog.show() if new_data: self.remove_sftp_bookmark(name, confirm=False) self.connect_sftp(new_data, is_new_connection=True) def remove_sftp_bookmark(self, name: str, confirm: bool = True): """Removes an SFTP bookmark and its credentials from the keyring.""" do_remove = False if confirm: confirm_dialog = MessageDialog( message_type="ask", text=f"Remove bookmark '{name}'?", buttons=["Yes", "No"]) if confirm_dialog.show(): do_remove = True else: do_remove = True if do_remove: try: import keyring service_name = f"customfiledialog-sftp" try: keyring.delete_password(service_name, f"{name}_password") except keyring.errors.PasswordDeleteError: pass try: keyring.delete_password(service_name, f"{name}_passphrase") except keyring.errors.PasswordDeleteError: pass except ImportError: pass except Exception as e: print(f"Could not remove credentials from keyring for {name}: {e}") self.config_manager.remove_bookmark(name) self.reload_config_and_rebuild_ui() def disconnect_sftp(self, path_to_navigate_to: Optional[str] = None): self.sftp_manager.disconnect() self.current_fs_type = "local" self.widget_manager.sftp_button.config( command=self.open_sftp_dialog, style="Header.TButton.Borderless.Round") target_path = path_to_navigate_to if path_to_navigate_to else os.path.expanduser("~") self.navigation_manager.navigate_to(target_path) def go_to_local_home(self): if self.current_fs_type == "sftp": self.disconnect_sftp() else: self.navigation_manager.navigate_to(os.path.expanduser("~")) def handle_sidebar_bookmark_click(self, local_path: str): if self.current_fs_type == "sftp": self.disconnect_sftp(path_to_navigate_to=local_path) else: self.navigation_manager.navigate_to(local_path) def update_animation_settings(self) -> None: """Updates the search animation icon based on current settings.""" use_pillow = self.settings.get('use_pillow_animation', False) anim_type = self.settings.get('animation_type', 'double') is_running = self.widget_manager.search_animation.running if is_running: self.widget_manager.search_animation.stop() self.widget_manager.search_animation.destroy() self.widget_manager.search_animation = AnimatedIcon( self.widget_manager.status_container, width=23, height=23, use_pillow=use_pillow, animation_type=anim_type, color="#2a6fde", highlight_color="#5195ff", bg=self.style_manager.bottom_color ) self.widget_manager.search_animation.grid( row=0, column=0, sticky='w', padx=(0, 5), pady=(4, 0)) self.widget_manager.search_animation.bind( "", lambda e: self.search_manager.activate_search()) self.my_tool_tip = Tooltip( self.widget_manager.search_animation, text=lambda: LocaleStrings.UI["cancel_search"] if self.widget_manager.search_animation.running else LocaleStrings.UI["start_search"] ) if is_running: self.widget_manager.search_animation.start() def check_for_updates(self) -> None: """Checks for library updates via the Gitea API in a background thread.""" try: new_version = GiteaUpdater.check_for_update( self.gitea_api_url, self.lib_version, ) self.after(0, self.update_ui_for_update, new_version) except (requests.exceptions.RequestException, GiteaApiUrlError, GiteaVersionParseError, GiteaApiResponseError): self.after(0, self.update_ui_for_update, "ERROR") except Exception: self.after(0, self.update_ui_for_update, "ERROR") def _run_installer(self, event: Optional[tk.Event] = None) -> None: """Runs the LxTools installer if it exists.""" installer_path = '/usr/local/bin/lxtools_installer' if os.path.exists(installer_path): try: subprocess.Popen([installer_path]) self.widget_manager.search_status_label.config( text="Installer started...") except OSError as e: self.widget_manager.search_status_label.config( text=f"Error starting installer: {e}") else: self.widget_manager.search_status_label.config( text=f"Installer not found at {installer_path}") def update_ui_for_update(self, new_version: Optional[str]) -> None: """ Updates the UI based on the result of the library update check. """ self.update_status = new_version icon = self.widget_manager.update_animation_icon icon.grid_remove() icon.hide() if new_version is None or new_version == "ERROR": return icon.grid(row=0, column=2, sticky='e', padx=(10, 5)) tooltip_msg = LocaleStrings.UI["install_new_version"].format( version=new_version) icon.start() icon.bind("", self._run_installer) Tooltip(icon, tooltip_msg) def get_file_icon(self, filename: str, size: str = 'large') -> tk.PhotoImage: """ Gets the appropriate icon for a given filename. """ ext = os.path.splitext(filename)[1].lower() if ext == '.py': return self.icon_manager.get_icon(f'python_{size}') if ext == '.pdf': return self.icon_manager.get_icon(f'pdf_{size}') if ext in ['.tar', '.zip', '.rar', '.7z', '.gz']: return self.icon_manager.get_icon(f'archive_{size}') if ext in ['.mp3', '.wav', '.ogg', '.flac']: return self.icon_manager.get_icon(f'audio_{size}') if ext in ['.mp4', '.mkv', '.avi', '.mov']: return self.icon_manager.get_icon(f'video_{size}') if size == 'large' else self.icon_manager.get_icon( 'video_small_file') if ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg']: return self.icon_manager.get_icon(f'picture_{size}') if ext == '.iso': return self.icon_manager.get_icon(f'iso_{size}') return self.icon_manager.get_icon(f'file_{size}') def on_window_resize(self, event: tk.Event) -> None: """ Handles the window resize event. """ if event.widget is self: if self.view_mode.get() == "icons" and not self.search_mode: new_width = self.widget_manager.file_list_frame.winfo_width() if abs(new_width - self.last_width) > 50: if self.resize_job: self.after_cancel(self.resize_job) def repopulate_icons() -> None: """Repopulates the file list icons.""" self.update_idletasks() self.view_manager.populate_files() self.resize_job = self.after(150, repopulate_icons) self.last_width = new_width self._handle_responsive_buttons(event.width) def _handle_responsive_buttons(self, window_width: int) -> None: """ Shows or hides buttons based on the window width. """ threshold = 850 container = self.widget_manager.responsive_buttons_container more_button = self.widget_manager.more_button should_be_hidden = window_width < threshold if should_be_hidden != self.responsive_buttons_hidden: if should_be_hidden: container.pack_forget() more_button.pack(side="left", padx=5) else: more_button.pack_forget() container.pack(side="left") self.responsive_buttons_hidden = should_be_hidden def show_more_menu(self) -> None: """Displays a 'more options' menu.""" more_menu = tk.Menu(self, tearoff=0, background=self.style_manager.header, foreground=self.style_manager.color_foreground, activebackground=self.style_manager.selection_color, activeforeground=self.style_manager.color_foreground, relief='flat', borderwidth=0) is_writable = os.access(self.current_dir, os.W_OK) creation_state = tk.NORMAL if is_writable and self.dialog_mode != "open" else tk.DISABLED more_menu.add_command(label=LocaleStrings.UI["new_folder"], command=self.file_op_manager.create_new_folder, image=self.icon_manager.get_icon('new_folder_small'), compound='left', state=creation_state) more_menu.add_command(label=LocaleStrings.UI["new_document"], command=self.file_op_manager.create_new_file, image=self.icon_manager.get_icon('new_document_small'), compound='left', state=creation_state) more_menu.add_separator() more_menu.add_command(label=LocaleStrings.VIEW["icon_view"], command=self.view_manager.set_icon_view, image=self.icon_manager.get_icon('icon_view'), compound='left') more_menu.add_command(label=LocaleStrings.VIEW["list_view"], command=self.view_manager.set_list_view, image=self.icon_manager.get_icon('list_view'), compound='left') more_menu.add_separator() hidden_files_label = LocaleStrings.UI["hide_hidden_files"] if self.show_hidden_files.get( ) else LocaleStrings.UI["show_hidden_files"] hidden_files_icon = self.icon_manager.get_icon( 'unhide') if self.show_hidden_files.get() else self.icon_manager.get_icon('hide') more_menu.add_command(label=hidden_files_label, command=self.view_manager.toggle_hidden_files, image=hidden_files_icon, compound='left') more_button = self.widget_manager.more_button x = more_button.winfo_rootx() y = more_button.winfo_rooty() + more_button.winfo_height() more_menu.tk_popup(x, y) def on_sidebar_resize(self, event: tk.Event) -> None: """ Handles the sidebar resize event, adjusting button text visibility. """ current_width = event.width threshold_width = 100 if current_width < threshold_width: for btn, original_text in self.widget_manager.sidebar_buttons: btn.config(text="", compound="top") for btn, original_text in self.widget_manager.device_buttons: btn.config(text="", compound="top") else: for btn, original_text in self.widget_manager.sidebar_buttons: btn.config(text=original_text, compound="left") for btn, original_text in self.widget_manager.device_buttons: btn.config(text=original_text, compound="left") def _on_devices_enter(self, event: tk.Event) -> None: """ Shows the scrollbar when the mouse enters the devices area. """ self.widget_manager.devices_scrollbar.grid( row=1, column=1, sticky="ns") def _on_devices_leave(self, event: tk.Event) -> None: """ Hides the scrollbar when the mouse leaves the devices area. """ x, y = event.x_root, event.y_root widget_x = self.widget_manager.devices_canvas.winfo_rootx() widget_y = self.widget_manager.devices_canvas.winfo_rooty() widget_width = self.widget_manager.devices_canvas.winfo_width() widget_height = self.widget_manager.devices_canvas.winfo_height() buffer = 5 if not (widget_x - buffer <= x <= widget_x + widget_width + buffer and widget_y - buffer <= y <= widget_y + widget_height + buffer): self.widget_manager.devices_scrollbar.grid_remove() def toggle_recursive_search(self) -> None: """ Toggles the recursive search option on or off.""" self.widget_manager.recursive_search.set( not self.widget_manager.recursive_search.get()) if self.widget_manager.recursive_search.get(): self.widget_manager.recursive_button.configure( style="Header.TButton.Active.Round") else: self.widget_manager.recursive_button.configure( style="Header.TButton.Borderless.Round") def update_selection_info(self, status_info: Optional[str] = None) -> None: """ Updates status bar, filename entry, and result based on current selection. """ self._update_disk_usage() status_text = "" is_sftp = self.current_fs_type == 'sftp' # Helper to get basename safely def get_basename(path): if not path: return "" if is_sftp: return path.split('/')[-1] return os.path.basename(path) if self.dialog_mode == 'multi': selected_paths = self.result if isinstance( self.result, list) else [] self.widget_manager.filename_entry.delete(0, tk.END) if selected_paths: filenames = [ f'"{get_basename(p)}"' for p in selected_paths] self.widget_manager.filename_entry.insert( 0, " ".join(filenames)) count = len(selected_paths) status_text = f"{count} {LocaleStrings.CFD['items_selected']}" else: status_text = "" else: path_exists = False if status_info: if is_sftp: path_exists = self.sftp_manager.exists(status_info) else: path_exists = os.path.exists(status_info) if status_info and path_exists: self.result = status_info basename = get_basename(status_info) self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.filename_entry.insert(0, basename) if self.view_manager._is_dir(status_info): content_count = self.view_manager._get_folder_content_count(status_info) if content_count is not None: status_text = f"'{basename}' ({content_count} {LocaleStrings.CFD['entries']})" else: status_text = f"'{basename}'" else: status_text = f"'{basename}'" elif status_info: status_text = status_info self.widget_manager.search_status_label.config(text=status_text) self.update_action_buttons_state() def _update_disk_usage(self) -> None: """Updates only the disk usage part of the status bar.""" if self.current_fs_type == "sftp": self.widget_manager.storage_label.config(text="SFTP Storage: N/A") self.widget_manager.storage_bar['value'] = 0 return try: # This can fail on certain file types like symlinks to other filesystems. total, used, free = shutil.disk_usage(self.current_dir) free_str = self._format_size(free) self.widget_manager.storage_label.config( text=f"{LocaleStrings.CFD['free_space']}: {free_str}") self.widget_manager.storage_bar['value'] = (used / total) * 100 except (FileNotFoundError, PermissionError): # If disk usage cannot be determined, just show N/A instead of an error. self.widget_manager.storage_label.config( text=f"{LocaleStrings.CFD['free_space']}: N/A") self.widget_manager.storage_bar['value'] = 0 def on_open(self) -> None: """Handles the 'Open' or 'OK' action based on the dialog mode.""" if self.dialog_mode == 'multi': if self.result and isinstance(self.result, list) and self.result: self.destroy() return selected_path = self.result if not selected_path or not isinstance(selected_path, str): return if self.dialog_mode == 'dir': if self.view_manager._is_dir(selected_path): self.destroy() elif self.dialog_mode == 'open': if not self.view_manager._is_dir(selected_path): self.destroy() def on_save(self) -> None: """Handles the 'Save' action, setting the selected file and closing the dialog.""" file_name = self.widget_manager.filename_entry.get() if file_name: self.result = os.path.join(self.current_dir, file_name) self.destroy() def on_cancel(self) -> None: """Handles the 'Cancel' action, clearing the selection and closing the dialog.""" self.result = None self.destroy() def get_result(self) -> Optional[Union[str, List[str]]]: """Returns the result of the dialog.""" return self.result def update_action_buttons_state(self) -> None: """Updates the state of action buttons based on current context.""" new_folder_state = tk.DISABLED new_file_state = tk.DISABLED trash_state = tk.DISABLED is_writable = False if self.dialog_mode != "open": if self.current_fs_type == 'sftp': is_writable = True else: is_writable = os.access(self.current_dir, os.W_OK) if is_writable: new_folder_state = tk.NORMAL new_file_state = tk.NORMAL if self.dialog_mode == "save": trash_state = tk.NORMAL if hasattr(self.widget_manager, 'new_folder_button'): self.widget_manager.new_folder_button.config( state=new_folder_state) if hasattr(self.widget_manager, 'new_file_button'): self.widget_manager.new_file_button.config(state=new_file_state) if hasattr(self.widget_manager, 'trash_button'): self.widget_manager.trash_button.config(state=trash_state) def _matches_filetype(self, filename: str) -> bool: """ Checks if a filename matches the current filetype filter. """ if self.current_filter_pattern == "*.*": return True patterns = self.current_filter_pattern.lower().split() fn_lower = filename.lower() for p in patterns: if p.startswith('*.'): if fn_lower.endswith(p[1:]): return True elif p.startswith('.'): if fn_lower.endswith(p): return True else: if fn_lower == p: return True return False def _format_size(self, size_bytes: Optional[int]) -> str: """ Formats a size in bytes into a human-readable string (KB, MB, GB). """ if size_bytes is None: return "" if size_bytes < 1024: return f"{size_bytes} B" if size_bytes < 1024**2: return f"{size_bytes/1024:.1f} KB" if size_bytes < 1024**3: return f"{size_bytes/1024**2:.1f} MB" return f"{size_bytes/1024**3:.1f} GB" def shorten_text(self, text: str, max_len: int) -> str: """ Shortens a string to a maximum length, adding '...' if truncated. """ return text if len(text) <= max_len else text[:max_len-3] + "..." def _get_mounted_devices(self) -> List[Tuple[str, str, bool]]: """ Retrieves a list of mounted devices on the system. """ devices: List[Tuple[str, str, bool]] = [] root_disk_name: Optional[str] = None try: result = subprocess.run(['lsblk', '-J', '-o', 'NAME,MOUNTPOINT,FSTYPE,SIZE,RO,RM,TYPE,LABEL,PKNAME'], capture_output=True, text=True, check=True) data = json.loads(result.stdout) for block_device in data.get('blockdevices', []): if 'children' in block_device: for child_device in block_device['children']: if child_device.get('mountpoint') == '/': root_disk_name = block_device.get('name') break if root_disk_name: break for block_device in data.get('blockdevices', []): if ( block_device.get('mountpoint') and block_device.get('type') not in ['loop', 'rom'] and block_device.get('mountpoint') != '/'): if block_device.get('name').startswith(root_disk_name) and not block_device.get('rm', False): pass else: name = block_device.get('name') mountpoint = block_device.get('mountpoint') label = block_device.get('label') removable = block_device.get('rm', False) display_name = label if label else name devices.append((display_name, mountpoint, removable)) if 'children' in block_device: for child_device in block_device['children']: if ( child_device.get('mountpoint') and child_device.get('type') not in ['loop', 'rom'] and child_device.get('mountpoint') != '/'): if block_device.get('name') == root_disk_name and not child_device.get('rm', False): pass else: name = child_device.get('name') mountpoint = child_device.get('mountpoint') label = child_device.get('label') removable = child_device.get('rm', False) display_name = label if label else name devices.append( (display_name, mountpoint, removable)) except Exception as e: print(f"Error getting mounted devices: {e}") return devices