sftp works with keyring, bookmark and edit bookmark
This commit is contained in:
@@ -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)
|
@@ -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))
|
||||||
|
@@ -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
|
@@ -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))
|
||||||
|
|
||||||
|
@@ -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:
|
||||||
|
@@ -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."""
|
||||||
|
90
message.py
90
message.py
@@ -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.
|
||||||
|
Reference in New Issue
Block a user