sftp works with keyring, bookmark and edit bookmark

This commit is contained in:
2025-08-16 01:06:34 +02:00
parent cc48f874ac
commit 48034626f1
7 changed files with 355 additions and 119 deletions

View File

@@ -23,50 +23,63 @@ class NavigationManager:
def handle_path_entry_return(self, event: tk.Event) -> None: def handle_path_entry_return(self, event: tk.Event) -> None:
""" """
Handles the Return key press in the path entry field. Handles the Return key press in the path entry field.
It attempts to navigate to the entered path. If the path is a file,
it navigates to the containing directory and selects the file.
Args:
event: The tkinter event that triggered this handler.
""" """
path_text = self.dialog.widget_manager.path_entry.get().strip() path_text = self.dialog.widget_manager.path_entry.get().strip()
potential_path = os.path.realpath(os.path.expanduser(path_text)) is_sftp = self.dialog.current_fs_type == "sftp"
if os.path.isdir(potential_path): if is_sftp:
self.navigate_to(potential_path) self.navigate_to(path_text)
elif os.path.isfile(potential_path):
directory = os.path.dirname(potential_path)
filename = os.path.basename(potential_path)
self.navigate_to(directory, file_to_select=filename)
else: else:
self.dialog.widget_manager.search_status_label.config( potential_path = os.path.realpath(os.path.expanduser(path_text))
text=f"{LocaleStrings.CFD['path_not_found']}: {self.dialog.shorten_text(path_text, 50)}") if os.path.isdir(potential_path):
self.navigate_to(potential_path)
elif os.path.isfile(potential_path):
directory = os.path.dirname(potential_path)
filename = os.path.basename(potential_path)
self.navigate_to(directory, file_to_select=filename)
else:
self.dialog.widget_manager.search_status_label.config(
text=f"{LocaleStrings.CFD['path_not_found']}: {self.dialog.shorten_text(path_text, 50)}")
def navigate_to(self, path: str, file_to_select: Optional[str] = None) -> None: def navigate_to(self, path: str, file_to_select: Optional[str] = None) -> None:
""" """
Navigates to a specified directory path. Navigates to a specified directory path, supporting both local and SFTP filesystems.
This is the core navigation method. It validates the path, checks for
read permissions, updates the dialog's current directory, manages the
navigation history, and refreshes the file view.
Args:
path (str): The absolute path to navigate to.
file_to_select (str, optional): If provided, this filename will be
selected after navigation. Defaults to None.
""" """
try: try:
real_path = os.path.realpath( is_sftp = self.dialog.current_fs_type == "sftp"
os.path.abspath(os.path.expanduser(path)))
if not os.path.isdir(real_path): if is_sftp:
self.dialog.widget_manager.search_status_label.config( # Resolve tilde to the remote home directory for SFTP
text=f"{LocaleStrings.CFD['error_title']}: {LocaleStrings.CFD['directory']} '{os.path.basename(path)}' {LocaleStrings.CFD['not_found']}") if path == '~' or path.startswith('~/'):
return home_dir = self.dialog.sftp_manager.home_dir
if not os.access(real_path, os.R_OK): if home_dir:
self.dialog.widget_manager.search_status_label.config( # Manual path joining with forward slashes
text=f"{LocaleStrings.CFD['access_to']} '{os.path.basename(path)}' {LocaleStrings.CFD['denied']}") if path.startswith('~/'):
return # home_dir might be '/', so avoid '//'
path = home_dir.rstrip('/') + '/' + path[2:]
else:
path = home_dir
else: # Fallback if home_dir is not set
path = '/'
# The SFTP manager will handle path validation.
if not self.dialog.sftp_manager.path_is_dir(path):
self.dialog.widget_manager.search_status_label.config(
text=f"Error: Directory '{os.path.basename(path)}' not found on SFTP server.")
return
real_path = path
else:
# Local filesystem logic
real_path = os.path.realpath(os.path.abspath(os.path.expanduser(path)))
if not os.path.isdir(real_path):
self.dialog.widget_manager.search_status_label.config(
text=f"{LocaleStrings.CFD['error_title']}: {LocaleStrings.CFD['directory']} '{os.path.basename(path)}' {LocaleStrings.CFD['not_found']}")
return
if not os.access(real_path, os.R_OK):
self.dialog.widget_manager.search_status_label.config(
text=f"{LocaleStrings.CFD['access_to']} '{os.path.basename(path)}' {LocaleStrings.CFD['denied']}")
return
self.dialog.current_dir = real_path self.dialog.current_dir = real_path
if self.dialog.history_pos < len(self.dialog.history) - 1: if self.dialog.history_pos < len(self.dialog.history) - 1:
self.dialog.history = self.dialog.history[:self.dialog.history_pos + 1] self.dialog.history = self.dialog.history[:self.dialog.history_pos + 1]
@@ -75,19 +88,17 @@ class NavigationManager:
self.dialog.history_pos = len(self.dialog.history) - 1 self.dialog.history_pos = len(self.dialog.history) - 1
self.dialog.widget_manager.search_animation.stop() self.dialog.widget_manager.search_animation.stop()
# Clear previous selection state before populating new view
self.dialog.selected_item_frames.clear() self.dialog.selected_item_frames.clear()
self.dialog.result = None self.dialog.result = None
self.dialog.view_manager.populate_files( self.dialog.view_manager.populate_files(item_to_select=file_to_select)
item_to_select=file_to_select)
self.update_nav_buttons() self.update_nav_buttons()
self.dialog.update_selection_info() # Use the new central update method self.dialog.update_selection_info()
self.dialog.update_action_buttons_state() self.dialog.update_action_buttons_state()
except Exception as e: except Exception as e:
self.dialog.widget_manager.search_status_label.config( error_message = f"Error navigating to '{path}': {e}"
text=f"{LocaleStrings.CFD['error_title']}: {e}") self.dialog.widget_manager.search_status_label.config(text=error_message)
def go_back(self) -> None: def go_back(self) -> None:
"""Navigates to the previous directory in the history.""" """Navigates to the previous directory in the history."""
@@ -128,4 +139,4 @@ class NavigationManager:
self.dialog.widget_manager.back_button.config( self.dialog.widget_manager.back_button.config(
state=tk.NORMAL if self.dialog.history_pos > 0 else tk.DISABLED) state=tk.NORMAL if self.dialog.history_pos > 0 else tk.DISABLED)
self.dialog.widget_manager.forward_button.config(state=tk.NORMAL if self.dialog.history_pos < len( self.dialog.widget_manager.forward_button.config(state=tk.NORMAL if self.dialog.history_pos < len(
self.dialog.history) - 1 else tk.DISABLED) self.dialog.history) - 1 else tk.DISABLED)

View File

@@ -157,6 +157,21 @@ class SettingsDialog(tk.Toplevel):
ttk.Label(sftp_frame, text=LocaleStrings.SET["paramiko_found"], ttk.Label(sftp_frame, text=LocaleStrings.SET["paramiko_found"],
font=("TkDefaultFont", 9)).pack(anchor="w") font=("TkDefaultFont", 9)).pack(anchor="w")
# Keyring status
try:
import keyring
keyring_available = True
except ImportError:
keyring_available = False
if keyring_available:
ttk.Label(sftp_frame, text="Keyring library found. Passwords will be stored securely.",
font=("TkDefaultFont", 9)).pack(anchor="w", pady=(5,0))
else:
ttk.Label(sftp_frame, text="Keyring library not found. Passwords cannot be saved.",
font=("TkDefaultFont", 9, "italic")).pack(anchor="w", pady=(5,0))
self.keep_bookmarks_checkbutton = ttk.Checkbutton(sftp_frame, text=LocaleStrings.SET["keep_sftp_bookmarks"], self.keep_bookmarks_checkbutton = ttk.Checkbutton(sftp_frame, text=LocaleStrings.SET["keep_sftp_bookmarks"],
variable=self.keep_bookmarks_on_reset) variable=self.keep_bookmarks_on_reset)
self.keep_bookmarks_checkbutton.pack(anchor="w", pady=(5,0)) self.keep_bookmarks_checkbutton.pack(anchor="w", pady=(5,0))

View File

@@ -7,10 +7,18 @@ except ImportError:
paramiko = None paramiko = None
PARAMIKO_AVAILABLE = False PARAMIKO_AVAILABLE = False
try:
import keyring
KEYRING_AVAILABLE = True
except ImportError:
keyring = None
KEYRING_AVAILABLE = False
class SFTPManager: class SFTPManager:
def __init__(self): def __init__(self):
self.client = None self.client = None
self.sftp = None self.sftp = None
self.home_dir = None
def connect(self, host, port, username, password=None, key_file=None, passphrase=None): def connect(self, host, port, username, password=None, key_file=None, passphrase=None):
if not PARAMIKO_AVAILABLE: if not PARAMIKO_AVAILABLE:
@@ -27,15 +35,28 @@ class SFTPManager:
password=password, password=password,
key_filename=key_file, key_filename=key_file,
passphrase=passphrase, passphrase=passphrase,
timeout=10 timeout=10,
allow_agent=False,
look_for_keys=False
) )
self.sftp = self.client.open_sftp() self.sftp = self.client.open_sftp()
self.home_dir = self.get_home_directory()
return True, "Connection successful." return True, "Connection successful."
except Exception as e: except Exception as e:
self.client = None self.client = None
self.sftp = None self.sftp = None
return False, str(e) return False, str(e)
def get_home_directory(self):
if not self.sftp:
return None
try:
# normalize('.') is a common way to get the default directory, usually home.
return self.sftp.normalize('.')
except Exception:
# Fallback to root if normalize fails
return "/"
def disconnect(self): def disconnect(self):
if self.sftp: if self.sftp:
self.sftp.close() self.sftp.close()
@@ -43,6 +64,7 @@ class SFTPManager:
if self.client: if self.client:
self.client.close() self.client.close()
self.client = None self.client = None
self.home_dir = None
def list_directory(self, path): def list_directory(self, path):
if not self.sftp: if not self.sftp:
@@ -148,4 +170,4 @@ class SFTPManager:
@property @property
def is_connected(self): def is_connected(self):
return self.sftp is not None return self.sftp is not None

View File

@@ -283,8 +283,14 @@ class WidgetManager:
] ]
self.sidebar_buttons = [] self.sidebar_buttons = []
for config in sidebar_buttons_config: for config in sidebar_buttons_config:
# Special case for "Computer" button to not disconnect SFTP
if config['path'] == '/':
command = lambda p=config['path']: self.dialog.navigation_manager.navigate_to(p)
else:
command = lambda p=config['path']: self.dialog.handle_sidebar_bookmark_click(p)
btn = ttk.Button(sidebar_buttons_frame, text=f" {config['name']}", image=config['icon'], compound="left", btn = ttk.Button(sidebar_buttons_frame, text=f" {config['name']}", image=config['icon'], compound="left",
command=lambda p=config['path']: self.dialog.navigation_manager.navigate_to(p), style="Dark.TButton.Borderless") command=command, style="Dark.TButton.Borderless")
btn.pack(fill="x", pady=1) btn.pack(fill="x", pady=1)
self.sidebar_buttons.append((btn, f" {config['name']}")) self.sidebar_buttons.append((btn, f" {config['name']}"))
@@ -314,12 +320,19 @@ class WidgetManager:
command=lambda d=data: self.dialog.connect_sftp_bookmark(d), style="Dark.TButton.Borderless") command=lambda d=data: self.dialog.connect_sftp_bookmark(d), style="Dark.TButton.Borderless")
btn.grid(row=row_counter, column=0, sticky="ew") btn.grid(row=row_counter, column=0, sticky="ew")
row_counter += 1 row_counter += 1
btn.bind("<Button-3>", lambda event, btn.bind("<Button-3>", lambda event, n=name, d=data: self._show_sftp_bookmark_context_menu(event, n, d))
n=name: self._show_sftp_bookmark_context_menu(event, n))
self.sftp_bookmark_buttons.append(btn) self.sftp_bookmark_buttons.append(btn)
def _show_sftp_bookmark_context_menu(self, event, name): def _show_sftp_bookmark_context_menu(self, event, name, data):
context_menu = tk.Menu(self.dialog, tearoff=0) context_menu = tk.Menu(self.dialog, tearoff=0)
edit_icon = self.dialog.icon_manager.get_icon('key_small')
context_menu.add_command(
label="Edit Bookmark", # Replace with LocaleString later
image=edit_icon,
compound=tk.LEFT,
command=lambda: self.dialog.edit_sftp_bookmark(name, data))
trash_icon = self.dialog.icon_manager.get_icon('trash_small2') trash_icon = self.dialog.icon_manager.get_icon('trash_small2')
context_menu.add_command( context_menu.add_command(
label=LocaleStrings.UI["remove_bookmark"], label=LocaleStrings.UI["remove_bookmark"],
@@ -384,7 +397,7 @@ class WidgetManager:
button_text = f" {device_name[:15]}\n{device_name[15:]}" button_text = f" {device_name[:15]}\n{device_name[15:]}"
btn = ttk.Button(self.devices_scrollable_frame, text=button_text, image=icon, compound="left", btn = ttk.Button(self.devices_scrollable_frame, text=button_text, image=icon, compound="left",
command=lambda p=mount_point: self.dialog.navigation_manager.navigate_to(p), style="Dark.TButton.Borderless") command=lambda p=mount_point: self.dialog.handle_sidebar_bookmark_click(p), style="Dark.TButton.Borderless")
btn.pack(fill="x", pady=1) btn.pack(fill="x", pady=1)
self.device_buttons.append((btn, button_text)) self.device_buttons.append((btn, button_text))

View File

@@ -104,18 +104,26 @@ class ViewManager:
def _get_folder_content_count(self, folder_path: str) -> Optional[int]: def _get_folder_content_count(self, folder_path: str) -> Optional[int]:
""" """
Counts the number of items in a given folder. Counts the number of items in a given folder, supporting both local and SFTP.
""" """
if self.dialog.current_fs_type == "sftp":
return None
try: try:
if not os.path.isdir(folder_path) or not os.access(folder_path, os.R_OK): if self.dialog.current_fs_type == "sftp":
return None if not self.dialog.sftp_manager.path_is_dir(folder_path):
return None
items = os.listdir(folder_path) items, error = self.dialog.sftp_manager.list_directory(folder_path)
if error:
return None
else:
if not os.path.isdir(folder_path) or not os.access(folder_path, os.R_OK):
return None
items = os.listdir(folder_path)
if not self.dialog.show_hidden_files.get(): if not self.dialog.show_hidden_files.get():
items = [item for item in items if not item.startswith('.')] # For SFTP, items are attrs, for local they are strings
if self.dialog.current_fs_type == "sftp":
items = [item for item in items if not item.filename.startswith('.')]
else:
items = [item for item in items if not item.startswith('.')]
return len(items) return len(items)
except (PermissionError, FileNotFoundError): except (PermissionError, FileNotFoundError):
@@ -618,9 +626,12 @@ class ViewManager:
""" """
Programmatically selects a file in the current view. Programmatically selects a file in the current view.
""" """
is_sftp = self.dialog.current_fs_type == "sftp"
if self.dialog.view_mode.get() == "list": if self.dialog.view_mode.get() == "list":
for item_id, path in self.dialog.item_path_map.items(): for item_id, path in self.dialog.item_path_map.items():
if os.path.basename(path) == filename: basename = path.split('/')[-1] if is_sftp else os.path.basename(path)
if basename == filename:
self.dialog.tree.selection_set(item_id) self.dialog.tree.selection_set(item_id)
self.dialog.tree.focus(item_id) self.dialog.tree.focus(item_id)
self.dialog.tree.see(item_id) self.dialog.tree.see(item_id)
@@ -630,7 +641,12 @@ class ViewManager:
return return
container_frame = self.dialog.icon_canvas.winfo_children()[0] container_frame = self.dialog.icon_canvas.winfo_children()[0]
target_path = os.path.join(self.dialog.current_dir, filename)
if is_sftp:
# Ensure forward slashes for SFTP paths
target_path = f"{self.dialog.current_dir}/{filename}".replace("//", "/")
else:
target_path = os.path.join(self.dialog.current_dir, filename)
for widget in container_frame.winfo_children(): for widget in container_frame.winfo_children():
if hasattr(widget, 'item_path') and widget.item_path == target_path: if hasattr(widget, 'item_path') and widget.item_path == target_path:

View File

@@ -148,6 +148,8 @@ class CustomFileDialog(tk.Toplevel):
def reload_config_and_rebuild_ui(self) -> None: def reload_config_and_rebuild_ui(self) -> None:
"""Reloads the configuration and rebuilds the entire UI.""" """Reloads the configuration and rebuilds the entire UI."""
is_sftp_connected = (self.current_fs_type == "sftp")
self.load_settings() self.load_settings()
self.geometry(self.settings["window_size_preset"]) self.geometry(self.settings["window_size_preset"])
@@ -163,6 +165,10 @@ class CustomFileDialog(tk.Toplevel):
self._initialize_managers() 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.widget_manager.filename_entry.bind(
"<Return>", self.search_manager.execute_search) "<Return>", self.search_manager.execute_search)
self.view_manager._update_view_mode_buttons() self.view_manager._update_view_mode_buttons()
@@ -198,13 +204,38 @@ class CustomFileDialog(tk.Toplevel):
self.config(cursor="watch") self.config(cursor="watch")
self.update_idletasks() 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( success, message = self.sftp_manager.connect(
host=credentials['host'], host=credentials.get('host'),
port=credentials['port'], port=credentials.get('port'),
username=credentials['username'], username=credentials.get('username'),
password=credentials['password'], password=credentials.get('password'),
key_file=credentials['key_file'], key_file=credentials.get('key_file'),
passphrase=credentials['passphrase'] passphrase=credentials.get('passphrase')
) )
self.config(cursor="") self.config(cursor="")
@@ -216,48 +247,78 @@ class CustomFileDialog(tk.Toplevel):
initial_path = credentials.get("initial_path", "/") initial_path = credentials.get("initial_path", "/")
self.navigation_manager.navigate_to(initial_path) self.navigation_manager.navigate_to(initial_path)
if is_new_connection:
save_bookmark = MessageDialog(
message_type="ask",
text="Connection successful. Save as bookmark?",
buttons=["Yes", "No"]
).show()
if save_bookmark:
bookmark_name = InputDialog(
self,
title="Save Bookmark",
prompt="Enter a name for the bookmark:",
initial_value=credentials['host']
).show()
if bookmark_name:
self.config_manager.add_bookmark(
bookmark_name, credentials)
self.reload_config_and_rebuild_ui()
else: else:
MessageDialog(message_type="error", MessageDialog(message_type="error",
text=f"Connection failed: {message}").show() text=f"Connection failed: {message}").show()
def connect_sftp_bookmark(self, data): def connect_sftp_bookmark(self, data):
self.connect_sftp(data, is_new_connection=False) 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)
def remove_sftp_bookmark(self, name): if credentials.get("password_in_keyring"):
confirm = MessageDialog( credentials["password"] = keyring.get_password(service_name, f"{bookmark_name}_password")
message_type="ask", if credentials.get("passphrase_in_keyring"):
text=f"Remove bookmark '{name}'?", credentials["passphrase"] = keyring.get_password(service_name, f"{bookmark_name}_passphrase")
buttons=["Yes", "No"]).show()
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: 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.config_manager.remove_bookmark(name)
self.reload_config_and_rebuild_ui() self.reload_config_and_rebuild_ui()
def disconnect_sftp(self): def disconnect_sftp(self, path_to_navigate_to: Optional[str] = None):
self.sftp_manager.disconnect() self.sftp_manager.disconnect()
self.current_fs_type = "local" self.current_fs_type = "local"
self.widget_manager.sftp_button.config( self.widget_manager.sftp_button.config(
command=self.open_sftp_dialog, style="Header.TButton.Borderless.Round") command=self.open_sftp_dialog, style="Header.TButton.Borderless.Round")
self.navigation_manager.navigate_to(os.path.expanduser("~")) 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): def go_to_local_home(self):
if self.current_fs_type == "sftp": if self.current_fs_type == "sftp":
@@ -265,6 +326,12 @@ class CustomFileDialog(tk.Toplevel):
else: else:
self.navigation_manager.navigate_to(os.path.expanduser("~")) 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: def update_animation_settings(self) -> None:
"""Updates the search animation icon based on current settings.""" """Updates the search animation icon based on current settings."""
use_pillow = self.settings.get('use_pillow_animation', False) use_pillow = self.settings.get('use_pillow_animation', False)
@@ -497,6 +564,15 @@ class CustomFileDialog(tk.Toplevel):
""" """
self._update_disk_usage() self._update_disk_usage()
status_text = "" 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': if self.dialog_mode == 'multi':
selected_paths = self.result if isinstance( selected_paths = self.result if isinstance(
@@ -504,7 +580,7 @@ class CustomFileDialog(tk.Toplevel):
self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.filename_entry.delete(0, tk.END)
if selected_paths: if selected_paths:
filenames = [ filenames = [
f'"{os.path.basename(p)}"' for p in selected_paths] f'"{get_basename(p)}"' for p in selected_paths]
self.widget_manager.filename_entry.insert( self.widget_manager.filename_entry.insert(
0, " ".join(filenames)) 0, " ".join(filenames))
count = len(selected_paths) count = len(selected_paths)
@@ -512,20 +588,27 @@ class CustomFileDialog(tk.Toplevel):
else: else:
status_text = "" status_text = ""
else: else:
if status_info and (self.current_fs_type == 'sftp' or os.path.exists(status_info)): path_exists = False
self.result = status_info if status_info:
self.widget_manager.filename_entry.delete(0, tk.END) if is_sftp:
self.widget_manager.filename_entry.insert( path_exists = self.sftp_manager.exists(status_info)
0, os.path.basename(status_info))
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"'{os.path.basename(status_info)}' ({content_count} {LocaleStrings.CFD['entries']})"
else:
status_text = f"'{os.path.basename(status_info)}'"
else: else:
status_text = f"'{os.path.basename(status_info)}'" 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: elif status_info:
status_text = status_info status_text = status_info
@@ -539,17 +622,17 @@ class CustomFileDialog(tk.Toplevel):
self.widget_manager.storage_bar['value'] = 0 self.widget_manager.storage_bar['value'] = 0
return return
try: try:
# This can fail on certain file types like symlinks to other filesystems.
total, used, free = shutil.disk_usage(self.current_dir) total, used, free = shutil.disk_usage(self.current_dir)
free_str = self._format_size(free) free_str = self._format_size(free)
self.widget_manager.storage_label.config( self.widget_manager.storage_label.config(
text=f"{LocaleStrings.CFD['free_space']}: {free_str}") text=f"{LocaleStrings.CFD['free_space']}: {free_str}")
self.widget_manager.storage_bar['value'] = (used / total) * 100 self.widget_manager.storage_bar['value'] = (used / total) * 100
except FileNotFoundError: except (FileNotFoundError, PermissionError):
# If disk usage cannot be determined, just show N/A instead of an error.
self.widget_manager.storage_label.config( self.widget_manager.storage_label.config(
text=f"{LocaleStrings.CFD['free_space']}: {LocaleStrings.CFD['unknown']}") text=f"{LocaleStrings.CFD['free_space']}: N/A")
self.widget_manager.storage_bar['value'] = 0 self.widget_manager.storage_bar['value'] = 0
self.widget_manager.search_status_label.config(
text=LocaleStrings.CFD["directory_not_found"])
def on_open(self) -> None: def on_open(self) -> None:
"""Handles the 'Open' or 'OK' action based on the dialog mode.""" """Handles the 'Open' or 'OK' action based on the dialog mode."""

View File

@@ -143,16 +143,25 @@ class MessageDialog:
class CredentialsDialog: class CredentialsDialog:
""" """
A dialog for securely entering SSH/SFTP credentials. A dialog for securely entering and editing SSH/SFTP credentials.
""" """
def __init__(self, master: Optional[tk.Tk] = None, title: str = "SFTP Connection"): def __init__(self, master: Optional[tk.Tk] = None, title: str = "SFTP Connection",
initial_data: Optional[dict] = None, is_edit_mode: bool = False):
self.master = master self.master = master
self.result = None self.result = None
self.is_edit_mode = is_edit_mode
self.initial_data = initial_data or {}
self.window = tk.Toplevel(master) self.window = tk.Toplevel(master)
self.window.title(title) self.window.title(title)
self.window.resizable(False, False) self.window.resizable(False, False)
try:
import keyring
self.keyring_available = True
except ImportError:
self.keyring_available = False
style = ttk.Style(self.window) style = ttk.Style(self.window)
style.configure("Creds.TEntry", padding=(5, 2)) style.configure("Creds.TEntry", padding=(5, 2))
@@ -168,7 +177,6 @@ class CredentialsDialog:
# Port # Port
ttk.Label(frame, text="Port:").grid(row=1, column=0, sticky="w", pady=2) ttk.Label(frame, text="Port:").grid(row=1, column=0, sticky="w", pady=2)
self.port_entry = ttk.Entry(frame, width=10, style="Creds.TEntry") self.port_entry = ttk.Entry(frame, width=10, style="Creds.TEntry")
self.port_entry.insert(0, "22")
self.port_entry.grid(row=1, column=1, sticky="w", pady=2) self.port_entry.grid(row=1, column=1, sticky="w", pady=2)
# Username # Username
@@ -177,9 +185,8 @@ class CredentialsDialog:
self.username_entry.grid(row=2, column=1, sticky="ew", pady=2) self.username_entry.grid(row=2, column=1, sticky="ew", pady=2)
# Initial Path # Initial Path
ttk.Label(frame, text="Initial Path:").grid(row=3, column=0, sticky="w", pady=2) ttk.Label(frame, text="Initial Remote Directory:").grid(row=3, column=0, sticky="w", pady=2)
self.path_entry = ttk.Entry(frame, width=40, style="Creds.TEntry") self.path_entry = ttk.Entry(frame, width=40, style="Creds.TEntry")
self.path_entry.insert(0, "~")
self.path_entry.grid(row=3, column=1, sticky="ew", pady=2) self.path_entry.grid(row=3, column=1, sticky="ew", pady=2)
# Auth Method # Auth Method
@@ -213,15 +220,37 @@ class CredentialsDialog:
self.passphrase_entry = ttk.Entry(frame, show="*", width=40, style="Creds.TEntry") self.passphrase_entry = ttk.Entry(frame, show="*", width=40, style="Creds.TEntry")
self.passphrase_entry.grid(row=7, column=1, sticky="ew", pady=2) self.passphrase_entry.grid(row=7, column=1, sticky="ew", pady=2)
# Bookmark
self.bookmark_frame = ttk.LabelFrame(frame, text="Bookmark", padding=10)
self.bookmark_frame.grid(row=8, column=0, columnspan=2, sticky="ew", pady=5)
self.save_bookmark_var = tk.BooleanVar()
self.save_bookmark_check = ttk.Checkbutton(self.bookmark_frame, text="Save as bookmark", variable=self.save_bookmark_var, command=self._toggle_bookmark_name)
self.save_bookmark_check.pack(anchor="w")
if not self.keyring_available:
keyring_info_label = ttk.Label(self.bookmark_frame,
text="Python 'keyring' library not found.\nPasswords will not be saved.",
font=("TkDefaultFont", 9, "italic"))
keyring_info_label.pack(anchor="w", pady=(5,0))
self.save_bookmark_check.config(state=tk.DISABLED)
self.bookmark_name_label = ttk.Label(self.bookmark_frame, text="Bookmark Name:")
self.bookmark_name_entry = ttk.Entry(self.bookmark_frame, style="Creds.TEntry")
# Buttons # Buttons
button_frame = ttk.Frame(frame) button_frame = ttk.Frame(frame)
button_frame.grid(row=8, column=1, sticky="e", pady=(15, 0)) button_frame.grid(row=9, column=1, sticky="e", pady=(15, 0))
connect_button = ttk.Button(button_frame, text="Connect", command=self._on_connect)
connect_text = "Save Changes" if self.is_edit_mode else "Connect"
connect_button = ttk.Button(button_frame, text=connect_text, command=self._on_connect)
connect_button.pack(side="left", padx=5) connect_button.pack(side="left", padx=5)
cancel_button = ttk.Button(button_frame, text="Cancel", command=self._on_cancel) cancel_button = ttk.Button(button_frame, text="Cancel", command=self._on_cancel)
cancel_button.pack(side="left") cancel_button.pack(side="left")
self._populate_initial_data()
self._toggle_auth_fields() self._toggle_auth_fields()
self.window.bind("<Return>", lambda event: self._on_connect()) self.window.bind("<Return>", lambda event: self._on_connect())
self.window.protocol("WM_DELETE_WINDOW", self._on_cancel) self.window.protocol("WM_DELETE_WINDOW", self._on_cancel)
@@ -233,6 +262,30 @@ class CredentialsDialog:
LxTools.center_window_cross_platform(self.window, self.window.winfo_width(), self.window.winfo_height()) LxTools.center_window_cross_platform(self.window, self.window.winfo_width(), self.window.winfo_height())
self.host_entry.focus_set() self.host_entry.focus_set()
def _populate_initial_data(self):
if not self.initial_data:
self.port_entry.insert(0, "22")
self.path_entry.insert(0, "~")
return
self.host_entry.insert(0, self.initial_data.get("host", ""))
self.port_entry.insert(0, self.initial_data.get("port", "22"))
self.username_entry.insert(0, self.initial_data.get("username", ""))
self.path_entry.insert(0, self.initial_data.get("initial_path", "~"))
if self.initial_data.get("key_file"):
self.auth_method.set("keyfile")
self.keyfile_entry.insert(0, self.initial_data.get("key_file", ""))
else:
self.auth_method.set("password")
if self.is_edit_mode:
# In edit mode, we don't show the "save as bookmark" option,
# as we are already editing one. The name is fixed.
self.bookmark_frame.grid_remove()
# We still need to know the bookmark name for saving.
self.bookmark_name_entry.insert(0, self.initial_data.get("bookmark_name", ""))
def _get_ssh_keys(self) -> List[str]: def _get_ssh_keys(self) -> List[str]:
ssh_path = os.path.expanduser("~/.ssh") ssh_path = os.path.expanduser("~/.ssh")
keys = [] keys = []
@@ -283,7 +336,26 @@ class CredentialsDialog:
self.window.update_idletasks() self.window.update_idletasks()
self.window.geometry("") self.window.geometry("")
def _toggle_bookmark_name(self):
if self.save_bookmark_var.get():
self.bookmark_name_label.pack(anchor="w", pady=(5,0))
self.bookmark_name_entry.pack(fill="x")
else:
self.bookmark_name_label.pack_forget()
self.bookmark_name_entry.pack_forget()
self.window.update_idletasks()
self.window.geometry("")
def _on_connect(self): def _on_connect(self):
save_bookmark = self.save_bookmark_var.get() or self.is_edit_mode
bookmark_name = self.bookmark_name_entry.get()
if save_bookmark and not bookmark_name:
# In edit mode, the bookmark name comes from initial_data, so this check is for new bookmarks
if not self.is_edit_mode:
MessageDialog(message_type="error", text="Bookmark name cannot be empty.", master=self.window).show()
return
self.result = { self.result = {
"host": self.host_entry.get(), "host": self.host_entry.get(),
"port": int(self.port_entry.get() or 22), "port": int(self.port_entry.get() or 22),
@@ -292,6 +364,8 @@ class CredentialsDialog:
"password": self.password_entry.get() if self.auth_method.get() == "password" else None, "password": self.password_entry.get() if self.auth_method.get() == "password" else None,
"key_file": self.keyfile_entry.get() if self.auth_method.get() == "keyfile" else None, "key_file": self.keyfile_entry.get() if self.auth_method.get() == "keyfile" else None,
"passphrase": self.passphrase_entry.get() if self.auth_method.get() == "keyfile" else None, "passphrase": self.passphrase_entry.get() if self.auth_method.get() == "keyfile" else None,
"save_bookmark": save_bookmark,
"bookmark_name": bookmark_name
} }
self.window.destroy() self.window.destroy()
@@ -304,6 +378,8 @@ class CredentialsDialog:
return self.result return self.result
class InputDialog: class InputDialog:
""" """
A simple dialog for getting a single line of text input from the user. A simple dialog for getting a single line of text input from the user.