sftp works with keyring, bookmark and edit bookmark
This commit is contained in:
@@ -23,16 +23,14 @@ class NavigationManager:
|
||||
def handle_path_entry_return(self, event: tk.Event) -> None:
|
||||
"""
|
||||
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()
|
||||
potential_path = os.path.realpath(os.path.expanduser(path_text))
|
||||
is_sftp = self.dialog.current_fs_type == "sftp"
|
||||
|
||||
if is_sftp:
|
||||
self.navigate_to(path_text)
|
||||
else:
|
||||
potential_path = os.path.realpath(os.path.expanduser(path_text))
|
||||
if os.path.isdir(potential_path):
|
||||
self.navigate_to(potential_path)
|
||||
elif os.path.isfile(potential_path):
|
||||
@@ -45,20 +43,34 @@ class NavigationManager:
|
||||
|
||||
def navigate_to(self, path: str, file_to_select: Optional[str] = None) -> None:
|
||||
"""
|
||||
Navigates to a specified directory path.
|
||||
|
||||
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.
|
||||
Navigates to a specified directory path, supporting both local and SFTP filesystems.
|
||||
"""
|
||||
try:
|
||||
real_path = os.path.realpath(
|
||||
os.path.abspath(os.path.expanduser(path)))
|
||||
is_sftp = self.dialog.current_fs_type == "sftp"
|
||||
|
||||
if is_sftp:
|
||||
# Resolve tilde to the remote home directory for SFTP
|
||||
if path == '~' or path.startswith('~/'):
|
||||
home_dir = self.dialog.sftp_manager.home_dir
|
||||
if home_dir:
|
||||
# Manual path joining with forward slashes
|
||||
if path.startswith('~/'):
|
||||
# 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']}")
|
||||
@@ -67,6 +79,7 @@ class NavigationManager:
|
||||
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
|
||||
if self.dialog.history_pos < len(self.dialog.history) - 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.widget_manager.search_animation.stop()
|
||||
|
||||
# Clear previous selection state before populating new view
|
||||
self.dialog.selected_item_frames.clear()
|
||||
self.dialog.result = None
|
||||
|
||||
self.dialog.view_manager.populate_files(
|
||||
item_to_select=file_to_select)
|
||||
self.dialog.view_manager.populate_files(item_to_select=file_to_select)
|
||||
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()
|
||||
|
||||
except Exception as e:
|
||||
self.dialog.widget_manager.search_status_label.config(
|
||||
text=f"{LocaleStrings.CFD['error_title']}: {e}")
|
||||
error_message = f"Error navigating to '{path}': {e}"
|
||||
self.dialog.widget_manager.search_status_label.config(text=error_message)
|
||||
|
||||
def go_back(self) -> None:
|
||||
"""Navigates to the previous directory in the history."""
|
||||
|
@@ -157,6 +157,21 @@ class SettingsDialog(tk.Toplevel):
|
||||
ttk.Label(sftp_frame, text=LocaleStrings.SET["paramiko_found"],
|
||||
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"],
|
||||
variable=self.keep_bookmarks_on_reset)
|
||||
self.keep_bookmarks_checkbutton.pack(anchor="w", pady=(5,0))
|
||||
|
@@ -7,10 +7,18 @@ except ImportError:
|
||||
paramiko = None
|
||||
PARAMIKO_AVAILABLE = False
|
||||
|
||||
try:
|
||||
import keyring
|
||||
KEYRING_AVAILABLE = True
|
||||
except ImportError:
|
||||
keyring = None
|
||||
KEYRING_AVAILABLE = False
|
||||
|
||||
class SFTPManager:
|
||||
def __init__(self):
|
||||
self.client = None
|
||||
self.sftp = None
|
||||
self.home_dir = None
|
||||
|
||||
def connect(self, host, port, username, password=None, key_file=None, passphrase=None):
|
||||
if not PARAMIKO_AVAILABLE:
|
||||
@@ -27,15 +35,28 @@ class SFTPManager:
|
||||
password=password,
|
||||
key_filename=key_file,
|
||||
passphrase=passphrase,
|
||||
timeout=10
|
||||
timeout=10,
|
||||
allow_agent=False,
|
||||
look_for_keys=False
|
||||
)
|
||||
self.sftp = self.client.open_sftp()
|
||||
self.home_dir = self.get_home_directory()
|
||||
return True, "Connection successful."
|
||||
except Exception as e:
|
||||
self.client = None
|
||||
self.sftp = None
|
||||
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):
|
||||
if self.sftp:
|
||||
self.sftp.close()
|
||||
@@ -43,6 +64,7 @@ class SFTPManager:
|
||||
if self.client:
|
||||
self.client.close()
|
||||
self.client = None
|
||||
self.home_dir = None
|
||||
|
||||
def list_directory(self, path):
|
||||
if not self.sftp:
|
||||
|
@@ -283,8 +283,14 @@ class WidgetManager:
|
||||
]
|
||||
self.sidebar_buttons = []
|
||||
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",
|
||||
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)
|
||||
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")
|
||||
btn.grid(row=row_counter, column=0, sticky="ew")
|
||||
row_counter += 1
|
||||
btn.bind("<Button-3>", lambda event,
|
||||
n=name: self._show_sftp_bookmark_context_menu(event, n))
|
||||
btn.bind("<Button-3>", lambda event, n=name, d=data: self._show_sftp_bookmark_context_menu(event, n, d))
|
||||
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)
|
||||
|
||||
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')
|
||||
context_menu.add_command(
|
||||
label=LocaleStrings.UI["remove_bookmark"],
|
||||
@@ -384,7 +397,7 @@ class WidgetManager:
|
||||
button_text = f" {device_name[:15]}\n{device_name[15:]}"
|
||||
|
||||
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)
|
||||
self.device_buttons.append((btn, button_text))
|
||||
|
||||
|
@@ -104,17 +104,25 @@ class ViewManager:
|
||||
|
||||
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:
|
||||
if self.dialog.current_fs_type == "sftp":
|
||||
if not self.dialog.sftp_manager.path_is_dir(folder_path):
|
||||
return None
|
||||
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():
|
||||
# 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)
|
||||
@@ -618,9 +626,12 @@ class ViewManager:
|
||||
"""
|
||||
Programmatically selects a file in the current view.
|
||||
"""
|
||||
is_sftp = self.dialog.current_fs_type == "sftp"
|
||||
|
||||
if self.dialog.view_mode.get() == "list":
|
||||
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.focus(item_id)
|
||||
self.dialog.tree.see(item_id)
|
||||
@@ -630,6 +641,11 @@ class ViewManager:
|
||||
return
|
||||
|
||||
container_frame = self.dialog.icon_canvas.winfo_children()[0]
|
||||
|
||||
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():
|
||||
|
@@ -148,6 +148,8 @@ class CustomFileDialog(tk.Toplevel):
|
||||
|
||||
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"])
|
||||
@@ -163,6 +165,10 @@ class CustomFileDialog(tk.Toplevel):
|
||||
|
||||
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(
|
||||
"<Return>", self.search_manager.execute_search)
|
||||
self.view_manager._update_view_mode_buttons()
|
||||
@@ -198,13 +204,38 @@ class CustomFileDialog(tk.Toplevel):
|
||||
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['host'],
|
||||
port=credentials['port'],
|
||||
username=credentials['username'],
|
||||
password=credentials['password'],
|
||||
key_file=credentials['key_file'],
|
||||
passphrase=credentials['passphrase']
|
||||
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="")
|
||||
@@ -216,48 +247,78 @@ class CustomFileDialog(tk.Toplevel):
|
||||
|
||||
initial_path = credentials.get("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:
|
||||
MessageDialog(message_type="error",
|
||||
text=f"Connection failed: {message}").show()
|
||||
|
||||
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):
|
||||
confirm = MessageDialog(
|
||||
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"]).show()
|
||||
if confirm:
|
||||
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):
|
||||
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")
|
||||
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):
|
||||
if self.current_fs_type == "sftp":
|
||||
@@ -265,6 +326,12 @@ class CustomFileDialog(tk.Toplevel):
|
||||
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)
|
||||
@@ -497,6 +564,15 @@ class CustomFileDialog(tk.Toplevel):
|
||||
"""
|
||||
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(
|
||||
@@ -504,7 +580,7 @@ class CustomFileDialog(tk.Toplevel):
|
||||
self.widget_manager.filename_entry.delete(0, tk.END)
|
||||
if selected_paths:
|
||||
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(
|
||||
0, " ".join(filenames))
|
||||
count = len(selected_paths)
|
||||
@@ -512,20 +588,27 @@ class CustomFileDialog(tk.Toplevel):
|
||||
else:
|
||||
status_text = ""
|
||||
else:
|
||||
if status_info and (self.current_fs_type == 'sftp' or os.path.exists(status_info)):
|
||||
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, os.path.basename(status_info))
|
||||
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)
|
||||
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']})"
|
||||
status_text = f"'{basename}' ({content_count} {LocaleStrings.CFD['entries']})"
|
||||
else:
|
||||
status_text = f"'{os.path.basename(status_info)}'"
|
||||
status_text = f"'{basename}'"
|
||||
else:
|
||||
status_text = f"'{os.path.basename(status_info)}'"
|
||||
status_text = f"'{basename}'"
|
||||
elif status_info:
|
||||
status_text = status_info
|
||||
|
||||
@@ -539,17 +622,17 @@ class CustomFileDialog(tk.Toplevel):
|
||||
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:
|
||||
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']}: {LocaleStrings.CFD['unknown']}")
|
||||
text=f"{LocaleStrings.CFD['free_space']}: N/A")
|
||||
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:
|
||||
"""Handles the 'Open' or 'OK' action based on the dialog mode."""
|
||||
|
90
message.py
90
message.py
@@ -143,16 +143,25 @@ class MessageDialog:
|
||||
|
||||
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.result = None
|
||||
self.is_edit_mode = is_edit_mode
|
||||
self.initial_data = initial_data or {}
|
||||
|
||||
self.window = tk.Toplevel(master)
|
||||
self.window.title(title)
|
||||
self.window.resizable(False, False)
|
||||
|
||||
try:
|
||||
import keyring
|
||||
self.keyring_available = True
|
||||
except ImportError:
|
||||
self.keyring_available = False
|
||||
|
||||
style = ttk.Style(self.window)
|
||||
style.configure("Creds.TEntry", padding=(5, 2))
|
||||
|
||||
@@ -168,7 +177,6 @@ class CredentialsDialog:
|
||||
# Port
|
||||
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.insert(0, "22")
|
||||
self.port_entry.grid(row=1, column=1, sticky="w", pady=2)
|
||||
|
||||
# Username
|
||||
@@ -177,9 +185,8 @@ class CredentialsDialog:
|
||||
self.username_entry.grid(row=2, column=1, sticky="ew", pady=2)
|
||||
|
||||
# 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.insert(0, "~")
|
||||
self.path_entry.grid(row=3, column=1, sticky="ew", pady=2)
|
||||
|
||||
# Auth Method
|
||||
@@ -213,15 +220,37 @@ class CredentialsDialog:
|
||||
self.passphrase_entry = ttk.Entry(frame, show="*", width=40, style="Creds.TEntry")
|
||||
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
|
||||
button_frame = ttk.Frame(frame)
|
||||
button_frame.grid(row=8, column=1, sticky="e", pady=(15, 0))
|
||||
connect_button = ttk.Button(button_frame, text="Connect", command=self._on_connect)
|
||||
button_frame.grid(row=9, column=1, sticky="e", pady=(15, 0))
|
||||
|
||||
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)
|
||||
|
||||
cancel_button = ttk.Button(button_frame, text="Cancel", command=self._on_cancel)
|
||||
cancel_button.pack(side="left")
|
||||
|
||||
self._populate_initial_data()
|
||||
self._toggle_auth_fields()
|
||||
|
||||
self.window.bind("<Return>", lambda event: self._on_connect())
|
||||
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())
|
||||
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]:
|
||||
ssh_path = os.path.expanduser("~/.ssh")
|
||||
keys = []
|
||||
@@ -283,7 +336,26 @@ class CredentialsDialog:
|
||||
self.window.update_idletasks()
|
||||
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):
|
||||
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 = {
|
||||
"host": self.host_entry.get(),
|
||||
"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,
|
||||
"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,
|
||||
"save_bookmark": save_bookmark,
|
||||
"bookmark_name": bookmark_name
|
||||
}
|
||||
self.window.destroy()
|
||||
|
||||
@@ -304,6 +378,8 @@ class CredentialsDialog:
|
||||
return self.result
|
||||
|
||||
|
||||
|
||||
|
||||
class InputDialog:
|
||||
"""
|
||||
A simple dialog for getting a single line of text input from the user.
|
||||
|
Reference in New Issue
Block a user