After a brief interaction with AI, I now have a very handy Python (PyQt6) script which lists current user-added paths for “Copy To” and “Move To” commands from ~/.config/dolphinrc, enabling the user to add/remove any as well as verifying all at the end, and asking the user to remove or keep them.
I’ve added this script to top-level Dolphin context menu as a service under the name “Manage Quick Paths…”
EDIT: The script has been updated to include automatic sorting of paths alphabetically.
#!/usr/bin/env python3
import sys
import os
import shutil
import re
from pathlib import Path
from PyQt6.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QListWidget, QListWidgetItem, QPushButton, QLineEdit,
QCheckBox, QFileDialog, QGroupBox, QMessageBox, QDialog
)
from PyQt6.QtCore import Qt
CONFIG_PATH = Path.home() / ".config" / "dolphinrc"
class DolphinPathManager(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Dolphin Quick-Paths Manager")
self.resize(600, 500)
self.copy_paths = []
self.move_paths = []
self.init_ui()
self.load_config()
def init_ui(self):
main_layout = QVBoxLayout(self)
# --- EXISTING PATHS SECTION ---
list_group = QGroupBox("Existing Quick-Paths (Check items to REMOVE)")
list_layout = QVBoxLayout(list_group)
self.path_list = QListWidget()
list_layout.addWidget(self.path_list)
btn_remove = QPushButton("Remove Selected Items")
btn_remove.clicked.connect(self.remove_checked_items)
list_layout.addWidget(btn_remove)
main_layout.addWidget(list_group)
# --- ADD NEW PATH SECTION ---
add_group = QGroupBox("Add New Path")
add_layout = QVBoxLayout(add_group)
path_input_layout = QHBoxLayout()
self.path_input = QLineEdit()
self.path_input.setPlaceholderText("Select or enter a folder path...")
btn_browse = QPushButton("Browse...")
btn_browse.clicked.connect(self.browse_folder)
path_input_layout.addWidget(self.path_input)
path_input_layout.addWidget(btn_browse)
add_layout.addLayout(path_input_layout)
options_layout = QHBoxLayout()
self.chk_copy = QCheckBox("Add to 'Copy To'")
self.chk_copy.setChecked(True)
self.chk_move = QCheckBox("Add to 'Move To'")
self.chk_move.setChecked(True)
options_layout.addWidget(self.chk_copy)
options_layout.addWidget(self.chk_move)
options_layout.addStretch()
btn_add = QPushButton("Add Path")
btn_add.clicked.connect(self.add_new_path)
options_layout.addWidget(btn_add)
add_layout.addLayout(options_layout)
main_layout.addWidget(add_group)
# --- SAVE / CLOSE BUTTONS ---
action_layout = QHBoxLayout()
action_layout.addStretch()
btn_save = QPushButton("Save && Exit")
btn_save.setStyleSheet("font-weight: bold;")
btn_save.clicked.connect(self.save_and_exit)
action_layout.addWidget(btn_save)
btn_cancel = QPushButton("Cancel")
btn_cancel.clicked.connect(self.close)
action_layout.addWidget(btn_cancel)
main_layout.addLayout(action_layout)
def sort_paths(self):
"""Sorts copy and move path lists alphabetically (case-insensitive)."""
self.copy_paths.sort(key=str.lower)
self.move_paths.sort(key=str.lower)
def load_config(self):
"""Reads paths from dolphinrc."""
if not CONFIG_PATH.exists():
QMessageBox.warning(self, "Warning", f"Configuration file not found:\n{CONFIG_PATH}")
return
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
lines = f.readlines()
current_section = None
for line in lines:
line_str = line.strip()
if line_str.startswith('[') and line_str.endswith(']'):
current_section = line_str
continue
if current_section in ('[kuick-copy]', '[kuick-move]') and line_str.startswith('Paths'):
if '=' in line_str:
raw_paths = line_str.split('=', 1)[1].split(',')
parsed_paths = [
os.path.abspath(os.path.expanduser(p.strip()))
for p in raw_paths if p.strip()
]
if current_section == '[kuick-copy]':
self.copy_paths = parsed_paths
else:
self.move_paths = parsed_paths
# Store initial state for change detection
self.initial_copy_paths = list(self.copy_paths)
self.initial_move_paths = list(self.move_paths)
# Re-order paths alphabetically
self.sort_paths()
self.refresh_list_widget()
def refresh_list_widget(self):
"""Populates the list widget with existing unique paths."""
self.path_list.clear()
all_unique = sorted(list(set(self.copy_paths + self.move_paths)))
for path in all_unique:
in_copy = path in self.copy_paths
in_move = path in self.move_paths
tags = []
if in_copy: tags.append("Copy To")
if in_move: tags.append("Move To")
label = f"{path} [{', '.join(tags)}]"
item = QListWidgetItem(label)
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
item.setCheckState(Qt.CheckState.Unchecked)
item.setData(Qt.ItemDataRole.UserRole, path)
self.path_list.addItem(item)
def browse_folder(self):
folder = QFileDialog.getExistingDirectory(self, "Select Folder", str(Path.home()))
if folder:
self.path_input.setText(folder)
def remove_checked_items(self):
"""Removes checked items from memory."""
for i in range(self.path_list.count() - 1, -1, -1):
item = self.path_list.item(i)
if item.checkState() == Qt.CheckState.Checked:
path = item.data(Qt.ItemDataRole.UserRole)
if path in self.copy_paths: self.copy_paths.remove(path)
if path in self.move_paths: self.move_paths.remove(path)
self.refresh_list_widget()
def add_new_path(self):
path = self.path_input.text().strip()
if not path:
return
path = os.path.abspath(os.path.expanduser(path))
if not path.endswith('/'):
path += '/'
if not self.chk_copy.isChecked() and not self.chk_move.isChecked():
QMessageBox.warning(self, "Selection Required", "Please select at least one menu item ('Copy To' or 'Move To').")
return
if self.chk_copy.isChecked() and path not in self.copy_paths:
self.copy_paths.append(path)
if self.chk_move.isChecked() and path not in self.move_paths:
self.move_paths.append(path)
# Re-order paths alphabetically after adding
self.sort_paths()
self.path_input.clear()
self.refresh_list_widget()
def get_invalid_paths(self):
"""Returns a sorted list of unique paths that do not exist on disk."""
all_paths = set(self.copy_paths + self.move_paths)
return sorted([p for p in all_paths if not os.path.isdir(p)])
def save_and_exit(self):
"""Verifies paths, prompts for removal of invalid ones, and saves to dolphinrc."""
# --- CHECK IF ANY CHANGES WERE MADE ---
if self.copy_paths == getattr(self, 'initial_copy_paths', []) and \
self.move_paths == getattr(self, 'initial_move_paths', []):
self.close()
return
# --- VERIFY PATHS ---
invalid_paths = self.get_invalid_paths()
if invalid_paths:
# Create a custom dialog to present invalid paths with checkboxes
dialog = QDialog(self)
dialog.setWindowTitle("Invalid Paths Detected")
dialog.resize(500, 300)
d_layout = QVBoxLayout(dialog)
d_layout.addWidget(QLabel("<b>The following paths do not exist or are unreachable:</b>"))
d_layout.addWidget(QLabel("Select any paths you wish to remove before saving:"))
invalid_list = QListWidget()
for path in invalid_paths:
item = QListWidgetItem(path)
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
item.setCheckState(Qt.CheckState.Checked) # Checked by default for easy cleanup
invalid_list.addItem(item)
d_layout.addWidget(invalid_list)
btn_box = QHBoxLayout()
btn_remove_selected = QPushButton("Remove Selected & Save")
btn_keep_all = QPushButton("Keep Invalid Paths & Save")
btn_box.addWidget(btn_remove_selected)
btn_box.addWidget(btn_keep_all)
d_layout.addLayout(btn_box)
# Dialog action handling (kept inside 'if invalid_paths:')
def on_remove_selected():
for i in range(invalid_list.count()):
item = invalid_list.item(i)
if item.checkState() == Qt.CheckState.Checked:
bad_path = item.text()
if bad_path in self.copy_paths: self.copy_paths.remove(bad_path)
if bad_path in self.move_paths: self.move_paths.remove(bad_path)
dialog.accept()
btn_remove_selected.clicked.connect(on_remove_selected)
btn_keep_all.clicked.connect(dialog.reject)
# Show dialog
dialog.exec()
# --- SAVE TO FILE & BACKUP ---
if CONFIG_PATH.exists():
backup_path = CONFIG_PATH.with_suffix('.bak')
shutil.copy2(CONFIG_PATH, backup_path)
copy_str = ",".join([p if p.endswith('/') else p + '/' for p in self.copy_paths])
move_str = ",".join([p if p.endswith('/') else p + '/' for p in self.move_paths])
content = []
if CONFIG_PATH.exists():
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
content = f.readlines()
def update_or_append_section(section_name, path_value):
nonlocal content
sec_idx = -1
for idx, line in enumerate(content):
if line.strip() == section_name:
sec_idx = idx
break
if sec_idx != -1:
paths_idx = -1
for j in range(sec_idx + 1, len(content)):
if content[j].strip().startswith('['):
break
if content[j].strip().startswith('Paths'):
paths_idx = j
break
if paths_idx != -1:
content[paths_idx] = f"Paths[$e]={path_value}\n"
else:
content.insert(sec_idx + 1, f"Paths[$e]={path_value}\n")
else:
content.append(f"\n{section_name}\nPaths[$e]={path_value}\n")
update_or_append_section("[kuick-copy]", copy_str)
update_or_append_section("[kuick-move]", move_str)
with open(CONFIG_PATH, 'w', encoding='utf-8') as f:
f.writelines(content)
QMessageBox.information(
self,
"Saved",
"Configuration updated successfully.<br>"
"Backup saved to <i>dolphinrc.bak</i>.<br>"
"<b>Dolphin should be restarted to reflect any changes made.</b>"
)
self.close()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = DolphinPathManager()
window.show()
sys.exit(app.exec())