import os import shutil import tkinter as tk from tkinter import ttk from datetime import datetime import subprocess import json from shared_libs.message import MessageDialog from shared_libs.common_tools import IconManager, Tooltip, ConfigManager, LxTools from cfd_app_config import AppConfig, CfdConfigManager from cfd_ui_setup import StyleManager, WidgetManager, get_xdg_user_dir try: import send2trash SEND2TRASH_AVAILABLE = True except ImportError: SEND2TRASH_AVAILABLE = False class SettingsDialog(tk.Toplevel): def __init__(self, parent, dialog_mode="save"): super().__init__(parent) self.transient(parent) self.grab_set() self.title("Einstellungen") self.settings = CfdConfigManager.load() self.dialog_mode = dialog_mode # Variables self.search_icon_pos = tk.StringVar( value=self.settings.get("search_icon_pos", "right")) self.button_box_pos = tk.StringVar( value=self.settings.get("button_box_pos", "left")) self.window_size_preset = tk.StringVar( value=self.settings.get("window_size_preset", "1050x850")) self.default_view_mode = tk.StringVar( value=self.settings.get("default_view_mode", "icons")) self.search_hidden_files = tk.BooleanVar( value=self.settings.get("search_hidden_files", False)) self.recursive_search = tk.BooleanVar( value=self.settings.get("recursive_search", True)) self.use_trash = tk.BooleanVar( value=self.settings.get("use_trash", False)) self.confirm_delete = tk.BooleanVar( value=self.settings.get("confirm_delete", False)) # --- UI Elements --- main_frame = ttk.Frame(self, padding=10) main_frame.pack(fill="both", expand=True) # Search Icon Position search_frame = ttk.LabelFrame( main_frame, text="Position der Such-Lupe", padding=10) search_frame.pack(fill="x", pady=5) ttk.Radiobutton(search_frame, text="Links", variable=self.search_icon_pos, value="left").pack(side="left", padx=5) ttk.Radiobutton(search_frame, text="Rechts", variable=self.search_icon_pos, value="right").pack(side="left", padx=5) # Button Box Position button_box_frame = ttk.LabelFrame( main_frame, text="Position der Dialog-Buttons", padding=10) button_box_frame.pack(fill="x", pady=5) ttk.Radiobutton(button_box_frame, text="Links", variable=self.button_box_pos, value="left").pack(side="left", padx=5) ttk.Radiobutton(button_box_frame, text="Rechts", variable=self.button_box_pos, value="right").pack(side="left", padx=5) # Window Size size_frame = ttk.LabelFrame( main_frame, text="Fenstergröße", padding=10) size_frame.pack(fill="x", pady=5) sizes = ["1050x850", "850x650", "650x450"] size_combo = ttk.Combobox( size_frame, textvariable=self.window_size_preset, values=sizes, state="readonly") size_combo.pack(fill="x") # Default View Mode view_mode_frame = ttk.LabelFrame( main_frame, text="Standardansicht", padding=10) view_mode_frame.pack(fill="x", pady=5) ttk.Radiobutton(view_mode_frame, text="Kacheln", variable=self.default_view_mode, value="icons").pack(side="left", padx=5) ttk.Radiobutton(view_mode_frame, text="Liste", variable=self.default_view_mode, value="list").pack(side="left", padx=5) # Search Hidden Files search_hidden_frame = ttk.LabelFrame( main_frame, text="Sucheinstellungen", padding=10) search_hidden_frame.pack(fill="x", pady=5) ttk.Checkbutton(search_hidden_frame, text="Versteckte Dateien und Ordner durchsuchen", variable=self.search_hidden_files).pack(anchor="w") ttk.Checkbutton(search_hidden_frame, text="Rekursiv suchen", variable=self.recursive_search).pack(anchor="w") # Deletion Settings delete_frame = ttk.LabelFrame( main_frame, text="Löscheinstellungen", padding=10) delete_frame.pack(fill="x", pady=5) self.use_trash_checkbutton = ttk.Checkbutton(delete_frame, text="Dateien in den Papierkorb verschieben (empfohlen)", variable=self.use_trash) self.use_trash_checkbutton.pack(anchor="w") if not SEND2TRASH_AVAILABLE: self.use_trash_checkbutton.config(state=tk.DISABLED) ttk.Label(delete_frame, text="(send2trash-Bibliothek nicht gefunden)", font=("TkDefaultFont", 9, "italic")).pack(anchor="w", padx=(20, 0)) self.confirm_delete_checkbutton = ttk.Checkbutton(delete_frame, text="Löschen/Verschieben ohne Bestätigung", variable=self.confirm_delete) self.confirm_delete_checkbutton.pack(anchor="w") # Disable deletion options in "open" mode if not self.dialog_mode == "save": self.use_trash_checkbutton.config(state=tk.DISABLED) self.confirm_delete_checkbutton.config(state=tk.DISABLED) info_label = ttk.Label(delete_frame, text="(Löschoptionen sind nur im Speichern-Modus verfügbar)", font=("TkDefaultFont", 9, "italic")) info_label.pack(anchor="w", padx=(20, 0)) # --- Action Buttons --- button_frame = ttk.Frame(main_frame) button_frame.pack(fill="x", pady=(10, 0)) ttk.Button(button_frame, text="Auf Standard zurücksetzen", command=self.reset_to_defaults).pack(side="left", padx=5) ttk.Button(button_frame, text="Speichern", command=self.save_settings).pack(side="right", padx=5) ttk.Button(button_frame, text="Abbrechen", command=self.destroy).pack(side="right") def save_settings(self): new_settings = { "search_icon_pos": self.search_icon_pos.get(), "button_box_pos": self.button_box_pos.get(), "window_size_preset": self.window_size_preset.get(), "default_view_mode": self.default_view_mode.get(), "search_hidden_files": self.search_hidden_files.get(), "recursive_search": self.recursive_search.get(), "use_trash": self.use_trash.get(), "confirm_delete": self.confirm_delete.get() } CfdConfigManager.save(new_settings) self.master.reload_config_and_rebuild_ui() self.destroy() def reset_to_defaults(self): defaults = CfdConfigManager._default_settings self.search_icon_pos.set(defaults["search_icon_pos"]) self.button_box_pos.set(defaults["button_box_pos"]) self.window_size_preset.set(defaults["window_size_preset"]) self.default_view_mode.set(defaults["default_view_mode"]) self.search_hidden_files.set(defaults["search_hidden_files"]) self.recursive_search.set(defaults["recursive_search"]) self.use_trash.set(defaults["use_trash"]) self.confirm_delete.set(defaults["confirm_delete"]) class CustomFileDialog(tk.Toplevel): def __init__(self, parent, initial_dir=None, filetypes=None, dialog_mode="open", title="File Dialog"): super().__init__(parent) self.my_tool_tip = None self.dialog_mode = dialog_mode self.load_settings() # Set the window size 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() width, height = map( int, self.settings["window_size_preset"].split('x')) LxTools.center_window_cross_platform(self, width, height) self.parent = parent self.transient(parent) self.grab_set() self.selected_file = None self.current_dir = os.path.abspath( initial_dir) if initial_dir else os.path.expanduser("~") self.filetypes = filetypes if filetypes else [("Alle Dateien", "*.* ")] self.current_filter_pattern = self.filetypes[0][1] self.history = [] self.history_pos = -1 self.view_mode = tk.StringVar( value=self.settings.get("default_view_mode", "icons")) self.show_hidden_files = tk.BooleanVar(value=False) self.resize_job = None self.last_width = 0 self.search_results = [] # Store search results self.search_mode = False # Track if in search mode self.original_path_text = "" # Store original path text self.items_to_load_per_batch = 250 self.item_path_map = {} self.responsive_buttons_hidden = None # State for responsive buttons self.search_job = None self.icon_manager = IconManager() self.style_manager = StyleManager(self) self.widget_manager = WidgetManager(self, self.settings) self._update_view_mode_buttons() # Defer initial navigation until the window geometry is calculated # to ensure the icon view gets the correct initial width. def initial_load(): # Force layout update to get correct widths self.update_idletasks() self.last_width = self.widget_manager.file_list_frame.winfo_width() self._handle_responsive_buttons(self.winfo_width()) self.navigate_to(self.current_dir) # Using after(10) gives the window manager a moment to process # the initial window drawing and sizing. self.after(10, initial_load) # Bind the intelligent return handler self.widget_manager.path_entry.bind( "", self.handle_path_entry_return) self.bind("", self.show_search_bar) # Bind the delete key only in "save" mode if self.dialog_mode == "save": self.bind("", self.delete_selected_item) def show_search_bar(self, event=None): # Ignore key presses if they are coming from an entry widget or have no character if isinstance(event.widget, (ttk.Entry, tk.Entry)) or not event.char.strip(): return self.search_mode = True self.widget_manager.filename_entry.focus_set() # Clear the field before inserting the new character to start a fresh search self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.filename_entry.insert(0, event.char) self.widget_manager.filename_entry.bind("", self.execute_search) self.widget_manager.filename_entry.bind("", self.hide_search_bar) def hide_search_bar(self, event=None): self.search_mode = False self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.search_status_label.config(text="") # Unbind search-specific events to restore normal behavior self.widget_manager.filename_entry.unbind("") self.widget_manager.filename_entry.unbind("") # Re-bind the default save action for the save dialog if self.dialog_mode == "save": self.widget_manager.filename_entry.bind("", lambda e: self.on_save()) self.populate_files() def toggle_search_mode(self, event=None): # This method might not be needed anymore if search is always active in the entry pass def handle_path_entry_return(self, event): """Handles the Enter key in the path entry to navigate. Search is handled by on_path_entry_key_release. """ path_text = self.widget_manager.path_entry.get().strip() potential_path = os.path.realpath(os.path.expanduser(path_text)) if os.path.isdir(potential_path): self.navigate_to(potential_path) # If not a directory, do nothing on Enter. Search is triggered on key release. def load_settings(self): 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): w, h = map(int, preset.split('x')) return max(650, w - 400), max(450, h - 400) def reload_config_and_rebuild_ui(self): self.load_settings() # Update geometry and minsize 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) # Re-create widgets for widget in self.winfo_children(): widget.destroy() self.style_manager = StyleManager(self) self.widget_manager = WidgetManager(self, self.settings) self._update_view_mode_buttons() # Reset responsive button state and re-evaluate self.responsive_buttons_hidden = None self.update_idletasks() self._handle_responsive_buttons(self.winfo_width()) # If search was active, reset it to avoid inconsistent state if self.search_mode: self.toggle_search_mode() # This will correctly reset the UI self.navigate_to(self.current_dir) def open_settings_dialog(self): SettingsDialog(self, dialog_mode=self.dialog_mode) def get_file_icon(self, filename, size='large'): 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 toggle_hidden_files(self): self.show_hidden_files.set(not self.show_hidden_files.get()) if self.show_hidden_files.get(): self.widget_manager.hidden_files_button.config( image=self.icon_manager.get_icon('unhide')) Tooltip(self.widget_manager.hidden_files_button, "Versteckte Dateien ausblenden") else: self.widget_manager.hidden_files_button.config( image=self.icon_manager.get_icon('hide')) Tooltip(self.widget_manager.hidden_files_button, "Versteckte Dateien anzeigen") self.populate_files() def on_window_resize(self, event): # This check is to prevent the resize event from firing for child widgets if event.widget is self: # Handle icon view redraw on width change, but not in search mode 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(): # Ensure all pending geometry changes are processed before redrawing self.update_idletasks() self.populate_files() self.resize_job = self.after(150, repopulate_icons) self.last_width = new_width # Handle responsive buttons in the top bar self._handle_responsive_buttons(event.width) def _handle_responsive_buttons(self, window_width): # This threshold might need adjustment based on your layout and button sizes threshold = 850 container = self.widget_manager.responsive_buttons_container more_button = self.widget_manager.more_button should_be_hidden = window_width < threshold # Only change the layout if the state is different from the current one if should_be_hidden != self.responsive_buttons_hidden: if should_be_hidden: # Hide individual buttons and show the 'more' button container.pack_forget() more_button.pack(side="left", padx=5) else: # Show individual buttons and hide the 'more' button more_button.pack_forget() container.pack(side="left") self.responsive_buttons_hidden = should_be_hidden def show_more_menu(self): # Create and display the dropdown menu for hidden buttons 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) # Determine the state for folder/file creation options 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="Neuer Ordner", command=self.create_new_folder, image=self.icon_manager.get_icon('new_folder_small'), compound='left', state=creation_state) more_menu.add_command(label="Neues Dokument", command=self.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="Kachelansicht", command=self.set_icon_view, image=self.icon_manager.get_icon('icon_view'), compound='left') more_menu.add_command(label="Listenansicht", command=self.set_list_view, image=self.icon_manager.get_icon('list_view'), compound='left') more_menu.add_separator() # Toggle hidden files option hidden_files_label = "Versteckte Dateien ausblenden" if self.show_hidden_files.get( ) else "Versteckte Dateien anzeigen" 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.toggle_hidden_files, image=hidden_files_icon, compound='left') # Position and show the menu 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): current_width = event.width # Define a threshold for when to hide/show text threshold_width = 100 # Adjust this value as needed if current_width < threshold_width: # Hide text, show only icons 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: # Show text 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): """Show scrollbar when mouse enters devices area""" self.widget_manager.devices_scrollbar.grid( row=1, column=1, sticky="ns") def _on_devices_leave(self, event): """Hide scrollbar when mouse leaves devices area""" # Check if mouse is really leaving 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() # Add small buffer to prevent flickering 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): """Toggle recursive search on/off and update button style""" 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_view_mode_buttons(self): """Set the visual state of the view mode buttons.""" if self.view_mode.get() == "icons": self.widget_manager.icon_view_button.configure( style="Header.TButton.Active.Round") self.widget_manager.list_view_button.configure( style="Header.TButton.Borderless.Round") else: self.widget_manager.list_view_button.configure( style="Header.TButton.Active.Round") self.widget_manager.icon_view_button.configure( style="Header.TButton.Borderless.Round") def set_icon_view(self): """Set icon view and update button styles""" self.view_mode.set("icons") self._update_view_mode_buttons() self.populate_files() def set_list_view(self): """Set list view and update button styles""" self.view_mode.set("list") self._update_view_mode_buttons() self.populate_files() def execute_search(self, event=None): search_term = self.widget_manager.filename_entry.get().strip() if not search_term: self.hide_search_bar() return self.widget_manager.search_status_label.config(text=f"Suche nach '{search_term}'...") self.update_idletasks() # Clear previous search results self.search_results.clear() # Determine search directories search_dirs = [self.current_dir] # If searching from home directory, also include XDG directories home_dir = os.path.expanduser("~") if os.path.abspath(self.current_dir) == os.path.abspath(home_dir): xdg_dirs = [ get_xdg_user_dir("XDG_DOWNLOAD_DIR", "Downloads"), get_xdg_user_dir("XDG_DOCUMENTS_DIR", "Documents"), get_xdg_user_dir("XDG_PICTURES_DIR", "Pictures"), get_xdg_user_dir("XDG_MUSIC_DIR", "Music"), get_xdg_user_dir("XDG_VIDEO_DIR", "Videos") ] # Add XDG directories that exist and are not already in home for xdg_dir in xdg_dirs: if (os.path.exists(xdg_dir) and os.path.abspath(xdg_dir) != os.path.abspath(home_dir) and xdg_dir not in search_dirs): search_dirs.append(xdg_dir) try: all_files = [] # Search in each directory for search_dir in search_dirs: if not os.path.exists(search_dir): continue # Change to directory and use relative paths to avoid path issues original_cwd = os.getcwd() try: os.chdir(search_dir) # Build find command based on recursive setting (use . for current directory) if self.settings.get("recursive_search", True): # Find both files and directories, following symlinks find_cmd = ['find', '-L', '.', '-iname', f'*{search_term}*'] else: # Find both files and directories, but only in the current level, following symlinks find_cmd = ['find', '-L', '.', '-maxdepth', '1', '-iname', f'*{search_term}*'] result = subprocess.run( find_cmd, capture_output=True, text=True, timeout=30) if result.returncode == 0: files = result.stdout.strip().split('\n') # Convert relative paths back to absolute paths directory_files = [] for f in files: if f and f.startswith('./'): abs_path = os.path.join( search_dir, f[2:]) # Remove './' prefix # Check if the path exists, as it might be a broken symlink or deleted if os.path.exists(abs_path): directory_files.append(abs_path) all_files.extend(directory_files) finally: os.chdir(original_cwd) # Remove duplicates while preserving order seen = set() unique_files = [] for file_path in all_files: if file_path not in seen: seen.add(file_path) unique_files.append(file_path) # Filter based on currently selected filter pattern and hidden file setting self.search_results = [] search_hidden = self.settings.get("search_hidden_files", False) for file_path in unique_files: # Check if path contains a hidden component (e.g., /.config/ or /some/path/to/.hidden_file) if not search_hidden: if any(part.startswith('.') for part in file_path.split(os.sep)): continue # Skip hidden files/files in hidden directories # Check if the path exists (it might have been deleted during the search) if os.path.exists(file_path): filename = os.path.basename(file_path) if self._matches_filetype(filename) or os.path.isdir(file_path): self.search_results.append(file_path) # Show search results in TreeView if self.search_results: self.show_search_results_treeview() folder_count = sum(1 for p in self.search_results if os.path.isdir(p)) file_count = len(self.search_results) - folder_count self.widget_manager.search_status_label.config(text=f"{folder_count} Ordner und {file_count} Dateien gefunden.") else: self.widget_manager.search_status_label.config(text=f"Keine Ergebnisse für '{search_term}'.") MessageDialog( message_type="info", text=f"Keine Dateien mit '{search_term}' gefunden.", title="Suche", master=self ).show() except subprocess.TimeoutExpired: MessageDialog( message_type="error", text="Suche dauert zu lange und wurde abgebrochen.", title="Suche", master=self ).show() except Exception as e: MessageDialog( message_type="error", text=f"Fehler bei der Suche: {e}", title="Suchfehler", master=self ).show() def show_search_results_treeview(self): """Show search results in TreeView format""" # Clear current file list and replace with search results for widget in self.widget_manager.file_list_frame.winfo_children(): widget.destroy() # Create TreeView for search results tree_frame = ttk.Frame(self.widget_manager.file_list_frame) tree_frame.pack(fill='both', expand=True) tree_frame.grid_rowconfigure(0, weight=1) tree_frame.grid_columnconfigure(0, weight=1) columns = ("path", "size", "modified") search_tree = ttk.Treeview( tree_frame, columns=columns, show="tree headings") # Configure columns search_tree.heading("#0", text="Dateiname", anchor="w") search_tree.column("#0", anchor="w", width=200, stretch=True) search_tree.heading("path", text="Pfad", anchor="w") search_tree.column("path", anchor="w", width=300, stretch=True) search_tree.heading("size", text="Größe", anchor="e") search_tree.column("size", anchor="e", width=100, stretch=False) search_tree.heading("modified", text="Geändert am", anchor="w") search_tree.column("modified", anchor="w", width=160, stretch=False) # Add scrollbars v_scrollbar = ttk.Scrollbar( tree_frame, orient="vertical", command=search_tree.yview) h_scrollbar = ttk.Scrollbar( tree_frame, orient="horizontal", command=search_tree.xview) search_tree.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set) search_tree.grid(row=0, column=0, sticky='nsew') v_scrollbar.grid(row=0, column=1, sticky='ns') h_scrollbar.grid(row=1, column=0, sticky='ew') # Populate with search results for file_path in self.search_results: try: filename = os.path.basename(file_path) directory = os.path.dirname(file_path) stat = os.stat(file_path) size = self._format_size(stat.st_size) modified_time = datetime.fromtimestamp( stat.st_mtime).strftime('%d.%m.%Y %H:%M') if os.path.isdir(file_path): icon = self.icon_manager.get_icon('folder_small') else: icon = self.get_file_icon(filename, 'small') search_tree.insert("", "end", text=f" {filename}", image=icon, values=(directory, size, modified_time)) except (FileNotFoundError, PermissionError): continue # Bind selection event def on_search_select(event): selection = search_tree.selection() if selection: item = search_tree.item(selection[0]) filename = item['text'].strip() directory = item['values'][0] full_path = os.path.join(directory, filename) # Update status bar with details try: stat = os.stat(full_path) size_str = self._format_size(stat.st_size) self.widget_manager.search_status_label.config( text=f"'{filename}' Größe: {size_str}") except (FileNotFoundError, PermissionError): self.widget_manager.search_status_label.config( text=f"'{filename}' nicht zugänglich") # Update filename entry self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.filename_entry.insert(0, filename) search_tree.bind("<>", on_search_select) # Bind double-click to select file def on_search_double_click(event): selection = search_tree.selection() if selection: item = search_tree.item(selection[0]) filename = item['text'].strip() directory = item['values'][0] # Exit search mode and navigate to the file's location self.hide_search_bar() self.navigate_to(directory, file_to_select=filename) search_tree.bind("", on_search_double_click) # Context menu def show_context_menu(event): iid = search_tree.identify_row(event.y) if not iid: return "break" search_tree.selection_set(iid) item = search_tree.item(iid) filename = item['text'].strip() directory = item['values'][0] full_path = os.path.join(directory, filename) self._show_context_menu(event, full_path) return "break" search_tree.bind("", show_context_menu) def _open_file_location(self, search_tree): selection = search_tree.selection() if not selection: return item = search_tree.item(selection[0]) filename = item['text'].strip() directory = item['values'][0] # Exit search mode and navigate to the directory self.hide_search_bar() self.navigate_to(directory) # Select the file in the list self.after(100, lambda: self._select_file_in_view(filename)) def _select_file_in_view(self, filename): if self.view_mode.get() == "list": for item_id in self.tree.get_children(): if self.tree.item(item_id, "text").strip() == filename: self.tree.selection_set(item_id) self.tree.focus(item_id) self.tree.see(item_id) break else: # icon view # This is more complex as items are in a grid. A simple selection is not straightforward. # For now, we just navigate to the folder. pass def _unbind_mouse_wheel_events(self): # Unbind all mouse wheel events from the root window self.unbind_all("") self.unbind_all("") self.unbind_all("") def populate_files(self, item_to_rename=None, item_to_select=None): # Unbind previous global mouse wheel events self._unbind_mouse_wheel_events() for widget in self.widget_manager.file_list_frame.winfo_children(): widget.destroy() self.widget_manager.path_entry.delete(0, tk.END) self.widget_manager.path_entry.insert(0, self.current_dir) self.selected_file = None self.update_status_bar() if self.view_mode.get() == "list": self.populate_list_view(item_to_rename, item_to_select) else: self.populate_icon_view(item_to_rename, item_to_select) def _get_sorted_items(self): try: items = os.listdir(self.current_dir) num_items = len(items) warning_message = None if num_items > AppConfig.MAX_ITEMS_TO_DISPLAY: warning_message = f"Zeige {AppConfig.MAX_ITEMS_TO_DISPLAY} von {num_items} Einträgen." items = items[:AppConfig.MAX_ITEMS_TO_DISPLAY] dirs = sorted([d for d in items if os.path.isdir( os.path.join(self.current_dir, d))], key=str.lower) files = sorted([f for f in items if not os.path.isdir( os.path.join(self.current_dir, f))], key=str.lower) return (dirs + files, None, warning_message) except PermissionError: return ([], "Zugriff verweigert.", None) except FileNotFoundError: return ([], "Verzeichnis nicht gefunden.", None) def _get_folder_content_count(self, folder_path): try: if not os.path.isdir(folder_path) or not os.access(folder_path, os.R_OK): return None items = os.listdir(folder_path) # Filter based on hidden files setting if not self.show_hidden_files.get(): items = [item for item in items if not item.startswith('.')] return len(items) except (PermissionError, FileNotFoundError): return None def _get_item_path_from_widget(self, widget): # Traverse up the widget hierarchy to find the item_frame while widget and not hasattr(widget, 'item_path'): widget = widget.master return getattr(widget, 'item_path', None) def _handle_icon_click(self, event): item_path = self._get_item_path_from_widget(event.widget) if item_path: item_frame = event.widget while not hasattr(item_frame, 'item_path'): item_frame = item_frame.master self.on_item_select(item_path, item_frame) def _handle_icon_double_click(self, event): item_path = self._get_item_path_from_widget(event.widget) if item_path: self.on_item_double_click(item_path) def _handle_icon_context_menu(self, event): item_path = self._get_item_path_from_widget(event.widget) if item_path: self._show_context_menu(event, item_path) def _handle_icon_rename_request(self, event): item_path = self._get_item_path_from_widget(event.widget) if item_path: item_frame = event.widget while not hasattr(item_frame, 'item_path'): item_frame = item_frame.master self.on_rename_request(event, item_path, item_frame) def populate_icon_view(self, item_to_rename=None, item_to_select=None): self.all_items, error, warning = self._get_sorted_items() self.currently_loaded_count = 0 self.icon_canvas = tk.Canvas(self.widget_manager.file_list_frame, highlightthickness=0, bg=self.style_manager.icon_bg_color) v_scrollbar = ttk.Scrollbar( self.widget_manager.file_list_frame, orient="vertical", command=self.icon_canvas.yview) self.icon_canvas.pack(side="left", fill="both", expand=True) self.icon_canvas.focus_set() v_scrollbar.pack(side="right", fill="y") container_frame = ttk.Frame(self.icon_canvas, style="Content.TFrame") self.icon_canvas.create_window( (0, 0), window=container_frame, anchor="nw") container_frame.bind("", lambda e: self.icon_canvas.configure( scrollregion=self.icon_canvas.bbox("all"))) def _on_mouse_wheel(event): if event.num == 4: delta = -1 elif event.num == 5: delta = 1 else: delta = -1 * int(event.delta / 120) self.icon_canvas.yview_scroll(delta, "units") # Check if scrolled to the bottom and if there are more items to load if self.currently_loaded_count < len(self.all_items) and self.icon_canvas.yview()[1] > 0.9: self._load_more_items_icon_view(container_frame) for widget in [self.icon_canvas, container_frame]: widget.bind("", _on_mouse_wheel) widget.bind("", _on_mouse_wheel) widget.bind("", _on_mouse_wheel) if warning: self.widget_manager.search_status_label.config(text=warning) if error: ttk.Label(container_frame, text=error).pack(pady=20) return widget_to_focus = self._load_more_items_icon_view( container_frame, item_to_rename, item_to_select) if widget_to_focus: def scroll_to_widget(): self.update_idletasks() if not widget_to_focus.winfo_exists(): return y = widget_to_focus.winfo_y() canvas_height = self.icon_canvas.winfo_height() scroll_region = self.icon_canvas.bbox("all") if not scroll_region: return scroll_height = scroll_region[3] if scroll_height > canvas_height: fraction = y / scroll_height self.icon_canvas.yview_moveto(fraction) self.after(100, scroll_to_widget) def _load_more_items_icon_view(self, container, item_to_rename=None, item_to_select=None): start_index = self.currently_loaded_count end_index = min(len(self.all_items), start_index + self.items_to_load_per_batch) if start_index >= end_index: return None # All items loaded item_width, item_height = 125, 100 frame_width = self.widget_manager.file_list_frame.winfo_width() col_count = max(1, frame_width // item_width - 1) row = start_index // col_count if col_count > 0 else 0 col = start_index % col_count if col_count > 0 else 0 widget_to_focus = None for i in range(start_index, end_index): name = self.all_items[i] if not self.show_hidden_files.get() and name.startswith('.'): continue path = os.path.join(self.current_dir, name) is_dir = os.path.isdir(path) if not is_dir and not self._matches_filetype(name): continue item_frame = ttk.Frame( container, width=item_width, height=item_height, style="Item.TFrame") item_frame.grid(row=row, column=col, padx=5, ipadx=25, pady=5) item_frame.grid_propagate(False) if name == item_to_rename: self.start_rename(item_frame, path) widget_to_focus = item_frame else: icon = self.icon_manager.get_icon( 'folder_large') if is_dir else self.get_file_icon(name, 'large') icon_label = ttk.Label( item_frame, image=icon, style="Icon.TLabel") icon_label.pack(pady=(10, 5)) name_label = ttk.Label(item_frame, text=self.shorten_text( name, 14), anchor="center", style="Item.TLabel") name_label.pack(fill="x", expand=True) Tooltip(item_frame, name) for widget in [item_frame, icon_label, name_label]: widget.bind("", lambda e, p=path: self.on_item_double_click(p)) widget.bind("", lambda e, p=path, f=item_frame: self.on_item_select(p, f)) widget.bind("", lambda e, p=path: self._show_context_menu(e, p)) widget.bind("", lambda e, p=path, f=item_frame: self.on_rename_request(e, p, f)) if name == item_to_select: self.on_item_select(path, item_frame) widget_to_focus = item_frame if col_count > 0: col = (col + 1) % col_count if col == 0: row += 1 else: row += 1 self.currently_loaded_count = end_index return widget_to_focus def populate_list_view(self, item_to_rename=None, item_to_select=None): self.all_items, error, warning = self._get_sorted_items() self.currently_loaded_count = 0 tree_frame = ttk.Frame(self.widget_manager.file_list_frame) tree_frame.pack(fill='both', expand=True) tree_frame.grid_rowconfigure(0, weight=1) tree_frame.grid_columnconfigure(0, weight=1) columns = ("size", "type", "modified") self.tree = ttk.Treeview( tree_frame, columns=columns, show="tree headings") self.tree.heading("#0", text="Name", anchor="w") self.tree.column("#0", anchor="w", width=250, stretch=True) self.tree.heading("size", text="Größe", anchor="e") self.tree.column("size", anchor="e", width=120, stretch=False) self.tree.heading("type", text="Typ", anchor="w") self.tree.column("type", anchor="w", width=120, stretch=False) self.tree.heading("modified", text="Geändert am", anchor="w") self.tree.column("modified", anchor="w", width=160, stretch=False) v_scrollbar = ttk.Scrollbar( tree_frame, orient="vertical", command=self.tree.yview) h_scrollbar = ttk.Scrollbar( tree_frame, orient="horizontal", command=self.tree.xview) self.tree.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set) self.tree.grid(row=0, column=0, sticky='nsew') self.tree.focus_set() v_scrollbar.grid(row=0, column=1, sticky='ns') h_scrollbar.grid(row=1, column=0, sticky='ew') def _on_scroll(*args): # Check if scrolled to the bottom and if there are more items to load if self.currently_loaded_count < len(self.all_items) and self.tree.yview()[1] > 0.9: # On-scroll loading should not trigger rename or select. self._load_more_items_list_view() v_scrollbar.set(*args) self.tree.configure(yscrollcommand=_on_scroll) self.tree.bind("", self.on_list_double_click) self.tree.bind("<>", self.on_list_select) self.tree.bind("", self.on_rename_request) self.tree.bind("", self.on_list_context_menu) if warning: self.widget_manager.search_status_label.config(text=warning) if error: self.tree.insert("", "end", text=error, values=()) return self._load_more_items_list_view(item_to_rename, item_to_select) def _load_more_items_list_view(self, item_to_rename=None, item_to_select=None): start_index = self.currently_loaded_count end_index = min(len(self.all_items), start_index + self.items_to_load_per_batch) if start_index >= end_index: return # All items loaded for i in range(start_index, end_index): name = self.all_items[i] if not self.show_hidden_files.get() and name.startswith('.'): continue path = os.path.join(self.current_dir, name) is_dir = os.path.isdir(path) if not is_dir and not self._matches_filetype(name): continue try: stat = os.stat(path) modified_time = datetime.fromtimestamp( stat.st_mtime).strftime('%d.%m.%Y %H:%M') if is_dir: icon, file_type, size = self.icon_manager.get_icon( 'folder_small'), "Ordner", "" else: icon, file_type, size = self.get_file_icon( name, 'small'), "Datei", self._format_size(stat.st_size) item_id = self.tree.insert("", "end", text=f" {name}", image=icon, values=( size, file_type, modified_time)) if name == item_to_rename: self.tree.selection_set(item_id) self.tree.focus(item_id) self.tree.see(item_id) self.start_rename(item_id, path) elif name == item_to_select: self.tree.selection_set(item_id) self.tree.focus(item_id) self.tree.see(item_id) except (FileNotFoundError, PermissionError): continue self.currently_loaded_count = end_index def on_item_select(self, path, item_frame): if hasattr(self, 'selected_item_frame') and self.selected_item_frame.winfo_exists(): self.selected_item_frame.state(['!selected']) for child in self.selected_item_frame.winfo_children(): if isinstance(child, ttk.Label): child.state(['!selected']) item_frame.state(['selected']) for child in item_frame.winfo_children(): if isinstance(child, ttk.Label): child.state(['selected']) self.selected_item_frame = item_frame self.selected_file = path self.update_status_bar(path) # Pass selected path if not os.path.isdir(path): self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.filename_entry.insert(0, os.path.basename(path)) def on_list_select(self, event): if not self.tree.selection(): return item_id = self.tree.selection()[0] item_text = self.tree.item(item_id, 'text').strip() path = os.path.join(self.current_dir, item_text) self.selected_file = path self.update_status_bar(path) # Pass selected path if not os.path.isdir(self.selected_file): self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.filename_entry.insert(0, item_text) def on_list_context_menu(self, event): iid = self.tree.identify_row(event.y) if not iid: return "break" self.tree.selection_set(iid) item_text = self.tree.item(iid, "text").strip() item_path = os.path.join(self.current_dir, item_text) self._show_context_menu(event, item_path) return "break" def on_rename_request(self, event, item_path=None, item_frame=None): if self.view_mode.get() == "list": if not self.tree.selection(): return item_id = self.tree.selection()[0] item_path = os.path.join( self.current_dir, self.tree.item(item_id, "text").strip()) self.start_rename(item_id, item_path) else: # icon view if item_path and item_frame: self.start_rename(item_frame, item_path) def on_item_double_click(self, path): if os.path.isdir(path): self.navigate_to(path) elif self.dialog_mode == "open": self.selected_file = path self.destroy() elif self.dialog_mode == "save": self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.filename_entry.insert( 0, os.path.basename(path)) self.on_save() def on_list_double_click(self, event): if not self.tree.selection(): return item_id = self.tree.selection()[0] item_text = self.tree.item(item_id, 'text').strip() path = os.path.join(self.current_dir, item_text) if os.path.isdir(path): self.navigate_to(path) elif self.dialog_mode == "open": self.selected_file = path self.destroy() elif self.dialog_mode == "save": self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.filename_entry.insert(0, item_text) self.on_save() def on_filter_change(self, event): selected_desc = self.widget_manager.filter_combobox.get() for desc, pattern in self.filetypes: if desc == selected_desc: self.current_filter_pattern = pattern break self.populate_files() def navigate_to(self, path, file_to_select=None): try: real_path = os.path.realpath( os.path.abspath(os.path.expanduser(path))) if not os.path.isdir(real_path): self.widget_manager.search_status_label.config( text=f"Fehler: Verzeichnis '{os.path.basename(path)}' nicht gefunden.") return if not os.access(real_path, os.R_OK): self.widget_manager.search_status_label.config( text=f"Zugriff auf '{os.path.basename(path)}' verweigert.") return self.current_dir = real_path if self.history_pos < len(self.history) - 1: self.history = self.history[:self.history_pos + 1] if not self.history or self.history[-1] != self.current_dir: self.history.append(self.current_dir) self.history_pos = len(self.history) - 1 self.populate_files(item_to_select=file_to_select) self.update_nav_buttons() self.update_status_bar() self.update_action_buttons_state() except Exception as e: self.widget_manager.search_status_label.config(text=f"Fehler: {e}") def go_back(self): if self.history_pos > 0: self.history_pos -= 1 self.current_dir = self.history[self.history_pos] self.populate_files() self.update_nav_buttons() self.update_status_bar() self.update_action_buttons_state() def go_forward(self): if self.history_pos < len(self.history) - 1: self.history_pos += 1 self.current_dir = self.history[self.history_pos] self.populate_files() self.update_nav_buttons() self.update_status_bar() self.update_action_buttons_state() def go_up_level(self): """Navigates one directory level up.""" new_path = os.path.dirname(self.current_dir) if new_path != self.current_dir: # Avoid getting stuck at the root self.navigate_to(new_path) def update_nav_buttons(self): self.widget_manager.back_button.config( state=tk.NORMAL if self.history_pos > 0 else tk.DISABLED) self.widget_manager.forward_button.config(state=tk.NORMAL if self.history_pos < len( self.history) - 1 else tk.DISABLED) def update_status_bar(self, selected_path=None): try: total, used, free = shutil.disk_usage(self.current_dir) free_str = self._format_size(free) self.widget_manager.storage_label.config( text=f"Freier Speicher: {free_str}") self.widget_manager.storage_bar['value'] = (used / total) * 100 status_text = "" if selected_path and os.path.exists(selected_path): if os.path.isdir(selected_path): # Display item count for directories content_count = self._get_folder_content_count( selected_path) if content_count is not None: status_text = f"'{os.path.basename(selected_path)}' ({content_count} Einträge)" else: status_text = f"'{os.path.basename(selected_path)}'" else: # Display size for files size = os.path.getsize(selected_path) size_str = self._format_size(size) status_text = f"'{os.path.basename(selected_path)}' Größe: {size_str}" self.widget_manager.search_status_label.config(text=status_text) except FileNotFoundError: self.widget_manager.search_status_label.config( text="Verzeichnis nicht gefunden") self.widget_manager.storage_label.config( text="Freier Speicher: Unbekannt") self.widget_manager.storage_bar['value'] = 0 def on_open(self): if self.selected_file and os.path.isfile(self.selected_file): self.destroy() def on_save(self): file_name = self.widget_manager.filename_entry.get() if file_name: self.selected_file = os.path.join(self.current_dir, file_name) self.destroy() def on_cancel(self): self.selected_file = None self.destroy() def get_selected_file(self): return self.selected_file def delete_selected_item(self, event=None): """Deletes or moves the selected item to trash based on settings.""" if not self.selected_file or not os.path.exists(self.selected_file): return use_trash = self.settings.get( "use_trash", False) and SEND2TRASH_AVAILABLE confirm = self.settings.get("confirm_delete", False) action_text = "in den Papierkorb verschieben" if use_trash else "endgültig löschen" item_name = os.path.basename(self.selected_file) if not confirm: dialog = MessageDialog( master=self, title="Bestätigung erforderlich", text=f"Möchten Sie '{item_name}' wirklich {action_text}?", message_type="question" ) if not dialog.show(): return try: if use_trash: send2trash.send2trash(self.selected_file) else: if os.path.isdir(self.selected_file): shutil.rmtree(self.selected_file) else: os.remove(self.selected_file) self.populate_files() self.widget_manager.search_status_label.config( text=f"'{item_name}' wurde erfolgreich entfernt.") except Exception as e: MessageDialog( master=self, title="Fehler", text=f"Fehler beim Entfernen von '{item_name}':\n{e}", message_type="error" ).show() def create_new_folder(self): self._create_new_item(is_folder=True) def create_new_file(self): self._create_new_item(is_folder=False) def _create_new_item(self, is_folder): base_name = "Neuer Ordner" if is_folder else "Neues Dokument.txt" new_name = self._get_unique_name(base_name) new_path = os.path.join(self.current_dir, new_name) try: if is_folder: os.mkdir(new_path) else: open(new_path, 'a').close() self.populate_files(item_to_rename=new_name) except Exception as e: self.widget_manager.search_status_label.config( text=f"Fehler beim Erstellen: {e}") def _get_unique_name(self, base_name): name, ext = os.path.splitext(base_name) counter = 1 new_name = base_name while os.path.exists(os.path.join(self.current_dir, new_name)): counter += 1 new_name = f"{name} {counter}{ext}" return new_name def _copy_to_clipboard(self, data): self.clipboard_clear() self.clipboard_append(data) self.widget_manager.search_status_label.config( text=f"'{self.shorten_text(data, 50)}' in Zwischenablage kopiert.") def _show_context_menu(self, event, item_path): if not item_path: return "break" # Destroy any existing context menu if hasattr(self, 'context_menu') and self.context_menu.winfo_exists(): self.context_menu.destroy() self.context_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) self.context_menu.add_command(label="Dateiname in Zwischenablage", command=lambda: self._copy_to_clipboard(os.path.basename(item_path))) self.context_menu.add_command( label="Pfad in Zwischenablage", command=lambda: self._copy_to_clipboard(item_path)) if self.search_mode: self.context_menu.add_separator() self.context_menu.add_command( label="Speicherort öffnen", command=lambda: self._open_file_location_from_context(item_path)) self.context_menu.tk_popup(event.x_root, event.y_root) return "break" def _open_file_location_from_context(self, file_path): directory = os.path.dirname(file_path) filename = os.path.basename(file_path) if self.search_mode: self.toggle_search_mode() self.navigate_to(directory) self.after(100, lambda: self._select_file_in_view(filename)) def start_rename(self, item_widget, item_path): if self.view_mode.get() == "icons": self._start_rename_icon_view(item_widget, item_path) else: # list view self._start_rename_list_view(item_widget) # item_widget is item_id def _start_rename_icon_view(self, item_frame, item_path): for child in item_frame.winfo_children(): child.destroy() entry = ttk.Entry(item_frame) entry.insert(0, os.path.basename(item_path)) entry.select_range(0, tk.END) entry.pack(fill="x", expand=True, padx=5, pady=5) entry.focus_set() def finish_rename(event): new_name = entry.get() new_path = os.path.join(self.current_dir, new_name) if new_name and new_path != item_path: if os.path.exists(new_path): self.widget_manager.search_status_label.config( text=f"'{new_name}' existiert bereits.") self.populate_files( item_to_select=os.path.basename(item_path)) return try: os.rename(item_path, new_path) self.populate_files(item_to_select=new_name) except Exception as e: self.widget_manager.search_status_label.config( text=f"Fehler beim Umbenennen: {e}") self.populate_files() else: self.populate_files(item_to_select=os.path.basename(item_path)) def cancel_rename(event): self.populate_files() entry.bind("", finish_rename) entry.bind("", finish_rename) entry.bind("", cancel_rename) def _start_rename_list_view(self, item_id): # First, ensure the item is visible by scrolling to it. self.tree.see(item_id) # Force the UI to process the scrolling and other pending events. self.tree.update_idletasks() # Now, get the bounding box. It should be available since the item is visible. bbox = self.tree.bbox(item_id, column="#0") # If bbox is still empty (e.g., view is not focused), abort to prevent crash. if not bbox: return x, y, width, height = bbox entry = ttk.Entry(self.tree) # Set a fixed width for the entry widget to prevent it from expanding too much entry_width = self.tree.column("#0", "width") entry.place(x=x, y=y, width=entry_width, height=height) item_text = self.tree.item(item_id, "text").strip() entry.insert(0, item_text) entry.select_range(0, tk.END) entry.focus_set() def finish_rename(event): new_name = entry.get() old_path = os.path.join(self.current_dir, item_text) new_path = os.path.join(self.current_dir, new_name) if new_name and new_path != old_path: if os.path.exists(new_path): self.widget_manager.search_status_label.config( text=f"'{new_name}' existiert bereits.") self.populate_files(item_to_select=item_text) else: try: os.rename(old_path, new_path) self.populate_files(item_to_select=new_name) except Exception as e: self.widget_manager.search_status_label.config( text=f"Fehler beim Umbenennen: {e}") self.populate_files() else: self.populate_files(item_to_select=item_text) entry.destroy() def cancel_rename(event): entry.destroy() entry.bind("", finish_rename) entry.bind("", finish_rename) entry.bind("", cancel_rename) def update_action_buttons_state(self): is_writable = os.access(self.current_dir, os.W_OK) state = tk.NORMAL if is_writable and self.dialog_mode != "open" else tk.DISABLED self.widget_manager.new_folder_button.config(state=state) self.widget_manager.new_file_button.config(state=state) def _matches_filetype(self, filename): if self.current_filter_pattern == "*.*": return True patterns = self.current_filter_pattern.replace( "*.", "").lower().split() fn_lower = filename.lower() for p in patterns: if fn_lower.endswith(p): return True return False def _format_size(self, size_bytes): 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, max_len): return text if len(text) <= max_len else text[:max_len-3] + "..." def _get_mounted_devices(self): devices = [] root_disk_name = 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) # First pass: Find the root disk name 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 # Second pass: Collect devices based on new criteria for block_device in data.get('blockdevices', []): # Process main device if it has a mountpoint if block_device.get('mountpoint') and \ block_device.get('type') not in ['loop', 'rom'] and \ block_device.get('mountpoint') != '/': # Exclude root mountpoint itself # Exclude non-removable partitions of the root disk if block_device.get('name').startswith(root_disk_name) and not block_device.get('rm', False): pass # Skip this device 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)) # Process children (partitions) 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') != '/': # Exclude root mountpoint itself # Exclude non-removable partitions of the root disk if block_device.get('name') == root_disk_name and not child_device.get('rm', False): pass # Skip this partition 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