From 30681638746e34928047a06dc80cc3affe4df05d Mon Sep 17 00:00:00 2001 From: abstract-official Date: Fri, 17 Apr 2026 22:52:18 +0800 Subject: [PATCH 01/12] Refactor: separate guard module this is for later reuse in rm --- tests/test_empty/components/test_guard.py | 2 +- tests/test_empty/components/test_user.py | 2 +- trashcli/empty/empty_action.py | 4 ++-- trashcli/empty/empty_cmd.py | 2 +- trashcli/{empty => guard}/guard.py | 2 +- trashcli/{empty => guard}/is_input_interactive.py | 0 trashcli/{empty => guard}/user.py | 0 7 files changed, 6 insertions(+), 6 deletions(-) rename trashcli/{empty => guard}/guard.py (97%) rename trashcli/{empty => guard}/is_input_interactive.py (100%) rename trashcli/{empty => guard}/user.py (100%) diff --git a/tests/test_empty/components/test_guard.py b/tests/test_empty/components/test_guard.py index 824f27d1..10c8e3c1 100644 --- a/tests/test_empty/components/test_guard.py +++ b/tests/test_empty/components/test_guard.py @@ -1,7 +1,7 @@ import unittest from tests.support.py2mock import Mock, call -from trashcli.empty.guard import Guard, UserIntention +from trashcli.guard.guard import Guard, UserIntention class TestGuard(unittest.TestCase): diff --git a/tests/test_empty/components/test_user.py b/tests/test_empty/components/test_user.py index 4558ff36..c3d862a8 100644 --- a/tests/test_empty/components/test_user.py +++ b/tests/test_empty/components/test_user.py @@ -2,7 +2,7 @@ from tests.support.py2mock import Mock, call -from trashcli.empty.user import User +from trashcli.guard.user import User from trashcli.lib.my_input import HardCodedInput diff --git a/trashcli/empty/empty_action.py b/trashcli/empty/empty_action.py index c852f70b..e6511037 100644 --- a/trashcli/empty/empty_action.py +++ b/trashcli/empty/empty_action.py @@ -7,10 +7,10 @@ ) from trashcli.empty.emptier import Emptier from trashcli.empty.existing_file_remover import ExistingFileRemover -from trashcli.empty.guard import Guard +from trashcli.guard.guard import Guard from trashcli.empty.parse_reply import parse_reply from trashcli.empty.prepare_output_message import prepare_output_message -from trashcli.empty.user import User +from trashcli.guard.user import User from trashcli.fs import ContentsOf from trashcli.fstab.volume_listing import VolumesListing from trashcli.fstab.volume_of import VolumeOf diff --git a/trashcli/empty/empty_cmd.py b/trashcli/empty/empty_cmd.py index 483a3a25..d9803155 100644 --- a/trashcli/empty/empty_cmd.py +++ b/trashcli/empty/empty_cmd.py @@ -7,7 +7,7 @@ from trashcli.empty.empty_action import EmptyAction, EmptyActionArgs from trashcli.empty.errors import Errors from trashcli.empty.existing_file_remover import ExistingFileRemover -from trashcli.empty.is_input_interactive import is_input_interactive +from trashcli.guard.is_input_interactive import is_input_interactive from trashcli.empty.parser import Parser from trashcli.empty.print_time_action import PrintTimeAction, PrintTimeArgs from trashcli.fs import ContentsOf diff --git a/trashcli/empty/guard.py b/trashcli/guard/guard.py similarity index 97% rename from trashcli/empty/guard.py rename to trashcli/guard/guard.py index baa0534f..d0ddbfa3 100644 --- a/trashcli/empty/guard.py +++ b/trashcli/guard/guard.py @@ -1,6 +1,6 @@ from typing import Iterable, NamedTuple -from trashcli.empty.user import User +from trashcli.guard.user import User from trashcli.trash_dirs_scanner import TrashDir UserIntention = NamedTuple('UserIntention', diff --git a/trashcli/empty/is_input_interactive.py b/trashcli/guard/is_input_interactive.py similarity index 100% rename from trashcli/empty/is_input_interactive.py rename to trashcli/guard/is_input_interactive.py diff --git a/trashcli/empty/user.py b/trashcli/guard/user.py similarity index 100% rename from trashcli/empty/user.py rename to trashcli/guard/user.py From 1a89c8bb1df9f91ec0e314ee8bf7055f933c0edb Mon Sep 17 00:00:00 2001 From: abstract-official Date: Fri, 17 Apr 2026 22:59:41 +0800 Subject: [PATCH 02/12] Refactor: use generic names --- tests/test_empty/components/test_user.py | 2 +- trashcli/guard/guard.py | 2 +- trashcli/guard/user.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_empty/components/test_user.py b/tests/test_empty/components/test_user.py index c3d862a8..4df81e58 100644 --- a/tests/test_empty/components/test_user.py +++ b/tests/test_empty/components/test_user.py @@ -18,7 +18,7 @@ def test(self): self.parse_reply.return_value = 'result' self.input.set_reply('reply') - result = self.user.do_you_wanna_empty_trash_dirs(['trash_dirs']) + result = self.user.confirm(['trash_dirs']) assert [ result, diff --git a/trashcli/guard/guard.py b/trashcli/guard/guard.py index d0ddbfa3..162a8cc8 100644 --- a/trashcli/guard/guard.py +++ b/trashcli/guard/guard.py @@ -26,7 +26,7 @@ def _interactive(self, trash_dirs, # type: Iterable[TrashDir] ): # type: (...) -> UserIntention trash_dirs_list = list(trash_dirs) # type: Iterable[TrashDir] ok_to_empty = \ - self.user.do_you_wanna_empty_trash_dirs(trash_dirs_list) + self.user.confirm(trash_dirs_list) list_result = trash_dirs_list if ok_to_empty else [] return UserIntention(ok_to_empty=ok_to_empty, trash_dirs=list_result) diff --git a/trashcli/guard/user.py b/trashcli/guard/user.py index c76c3ccb..bf8c8ec9 100644 --- a/trashcli/guard/user.py +++ b/trashcli/guard/user.py @@ -10,6 +10,6 @@ def __init__(self, self.input = input self.parse_reply = parse_reply - def do_you_wanna_empty_trash_dirs(self, trash_dirs): - reply = self.input.read_input(self.prepare_output_message(trash_dirs)) + def confirm(self, items_to_confirm): + reply = self.input.read_input(self.prepare_output_message(items_to_confirm)) return self.parse_reply(reply) From 5a01149accc1917c539e60a559c9d1d05a40758d Mon Sep 17 00:00:00 2001 From: abstract-official Date: Fri, 17 Apr 2026 23:01:08 +0800 Subject: [PATCH 03/12] Fix: update test attrib --- tests/test_empty/components/test_guard.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_empty/components/test_guard.py b/tests/test_empty/components/test_guard.py index 10c8e3c1..a5d44ab2 100644 --- a/tests/test_empty/components/test_guard.py +++ b/tests/test_empty/components/test_guard.py @@ -6,11 +6,11 @@ class TestGuard(unittest.TestCase): def setUp(self): - self.user = Mock(spec=['do_you_wanna_empty_trash_dirs']) + self.user = Mock(spec=['confirm']) self.guard = Guard(self.user) def test_user_says_yes(self): - self.user.do_you_wanna_empty_trash_dirs.return_value = True + self.user.confirm.return_value = True result = self.guard.ask_the_user(True, ['trash_dirs']) @@ -18,7 +18,7 @@ def test_user_says_yes(self): trash_dirs=['trash_dirs']) == result def test_user_says_no(self): - self.user.do_you_wanna_empty_trash_dirs.return_value = False + self.user.confirm.return_value = False result = self.guard.ask_the_user(True, ['trash_dirs']) From b57da9e27b0522b07db316de896860ade06e44e6 Mon Sep 17 00:00:00 2001 From: abstract-official Date: Fri, 17 Apr 2026 23:11:20 +0800 Subject: [PATCH 04/12] Refactor: move parse_reply to guard --- tests/test_empty/components/test_parse_reply.py | 2 +- trashcli/empty/empty_action.py | 2 +- trashcli/{empty => guard}/parse_reply.py | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename trashcli/{empty => guard}/parse_reply.py (100%) diff --git a/tests/test_empty/components/test_parse_reply.py b/tests/test_empty/components/test_parse_reply.py index 6c0a8653..8b3d970d 100644 --- a/tests/test_empty/components/test_parse_reply.py +++ b/tests/test_empty/components/test_parse_reply.py @@ -1,6 +1,6 @@ import unittest -from trashcli.empty.parse_reply import parse_reply +from trashcli.guard.parse_reply import parse_reply class TestParseReply(unittest.TestCase): diff --git a/trashcli/empty/empty_action.py b/trashcli/empty/empty_action.py index e6511037..678cc581 100644 --- a/trashcli/empty/empty_action.py +++ b/trashcli/empty/empty_action.py @@ -8,7 +8,7 @@ from trashcli.empty.emptier import Emptier from trashcli.empty.existing_file_remover import ExistingFileRemover from trashcli.guard.guard import Guard -from trashcli.empty.parse_reply import parse_reply +from trashcli.guard.parse_reply import parse_reply from trashcli.empty.prepare_output_message import prepare_output_message from trashcli.guard.user import User from trashcli.fs import ContentsOf diff --git a/trashcli/empty/parse_reply.py b/trashcli/guard/parse_reply.py similarity index 100% rename from trashcli/empty/parse_reply.py rename to trashcli/guard/parse_reply.py From d722f85e595fb5480bddc1c0523298d8e9b7b26b Mon Sep 17 00:00:00 2001 From: abstract-official Date: Fri, 17 Apr 2026 23:29:23 +0800 Subject: [PATCH 05/12] Feat: comfirm before `trash-rm` --- trashcli/rm/prepare_output_message.py | 6 ++++++ trashcli/rm/rm_cmd.py | 15 +++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 trashcli/rm/prepare_output_message.py diff --git a/trashcli/rm/prepare_output_message.py b/trashcli/rm/prepare_output_message.py new file mode 100644 index 00000000..ae53f436 --- /dev/null +++ b/trashcli/rm/prepare_output_message.py @@ -0,0 +1,6 @@ +def prepare_output_message(info_files): + if not info_files: + return 'No files to be removed.' + else: + return 'The following files will be removed:\n' + '\n'.join( + ' - ' + info_file.original_location for info_file in info_files) + '\nProceed? (y/N) ' diff --git a/trashcli/rm/rm_cmd.py b/trashcli/rm/rm_cmd.py index 8c4f9ca2..aab6dddc 100644 --- a/trashcli/rm/rm_cmd.py +++ b/trashcli/rm/rm_cmd.py @@ -2,13 +2,19 @@ from trashcli.compat import Protocol from trashcli.fs import ContentsOf +from trashcli.guard.guard import Guard +from trashcli.guard.is_input_interactive import is_input_interactive +from trashcli.guard.parse_reply import parse_reply +from trashcli.guard.user import User from trashcli.lib.dir_checker import DirChecker from trashcli.lib.dir_reader import DirReader +from trashcli.lib.my_input import RealInput from trashcli.lib.user_info import SingleUserInfoProvider from trashcli.rm.cleanable_trashcan import CleanableTrashcan from trashcli.rm.file_remover import FileRemover from trashcli.rm.filter import Filter from trashcli.rm.list_trashinfo import ListTrashinfos +from trashcli.rm.prepare_output_message import prepare_output_message from trashcli.trash_dirs_scanner import TrashDirsScanner, TopTrashDirRules, \ trash_dir_found @@ -60,6 +66,7 @@ def run(self, argv, uid): for event, args in scanner.scan_trash_dirs(self.environ, uid): if event == trash_dir_found: + info_files = [] path, volume = args for type, arg in listing.list_from_volume_trashdir(path, volume): @@ -68,8 +75,12 @@ def run(self, argv, uid): elif type == 'trashed_file': original_location, info_file = arg if cmd.matches(original_location): - trashcan.delete_trash_info_and_backup_copy( - info_file) + info_files.append(info_file) + user = User(prepare_output_message, RealInput(), parse_reply) + guard = Guard(user) + if guard.ask_the_user(is_input_interactive(), info_files): + for info_file in info_files: + trashcan.delete_trash_info_and_backup_copy(info_file) def unable_to_parse_path(self, trashinfo): self.report_error('{}: unable to parse \'Path\''.format(trashinfo)) From 0b9596c84593aaedc63b333e2632f53fd7bd838d Mon Sep 17 00:00:00 2001 From: abstract-official Date: Fri, 17 Apr 2026 23:40:05 +0800 Subject: [PATCH 06/12] Fix: avoids being stuck when nothing to remove / empty --- trashcli/empty/empty_action.py | 14 +++++++++----- trashcli/rm/rm_cmd.py | 13 ++++++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/trashcli/empty/empty_action.py b/trashcli/empty/empty_action.py index 678cc581..5d533e84 100644 --- a/trashcli/empty/empty_action.py +++ b/trashcli/empty/empty_action.py @@ -65,8 +65,12 @@ def run_action(self, args.user_specified_trash_dirs, args.environ, args.uid) - delete_pass = self.guard.ask_the_user(args.interactive, - trash_dirs) - if delete_pass.ok_to_empty: - self.emptier.do_empty(delete_pass.trash_dirs, args.environ, - args.days, args.dry_run, args.verbose) + trash_dirs = list(trash_dirs) + if trash_dirs: # skip asking the user if there is nothing to delete; avoids being stuck + delete_pass = self.guard.ask_the_user(args.interactive, + trash_dirs) + if delete_pass.ok_to_empty: + self.emptier.do_empty(delete_pass.trash_dirs, args.environ, + args.days, args.dry_run, args.verbose) + else: + print('No trash directories to empty.') diff --git a/trashcli/rm/rm_cmd.py b/trashcli/rm/rm_cmd.py index aab6dddc..8d3e2acc 100644 --- a/trashcli/rm/rm_cmd.py +++ b/trashcli/rm/rm_cmd.py @@ -76,11 +76,14 @@ def run(self, argv, uid): original_location, info_file = arg if cmd.matches(original_location): info_files.append(info_file) - user = User(prepare_output_message, RealInput(), parse_reply) - guard = Guard(user) - if guard.ask_the_user(is_input_interactive(), info_files): - for info_file in info_files: - trashcan.delete_trash_info_and_backup_copy(info_file) + if info_files: # skip asking the user if there is nothing to delete; avoids being stuck + user = User(prepare_output_message, RealInput(), parse_reply) + guard = Guard(user) + if guard.ask_the_user(is_input_interactive(), info_files): + for info_file in info_files: + trashcan.delete_trash_info_and_backup_copy(info_file) + else: + print('No files to be removed in {}'.format(path)) def unable_to_parse_path(self, trashinfo): self.report_error('{}: unable to parse \'Path\''.format(trashinfo)) From 9ea4cca19e13f9c531999e6cd2ad27438f4a9b0c Mon Sep 17 00:00:00 2001 From: abstract-official Date: Fri, 17 Apr 2026 23:43:20 +0800 Subject: [PATCH 07/12] Fix: fix fake attrib access generated by AI This is what happens if you trust AI. --- trashcli/rm/prepare_output_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trashcli/rm/prepare_output_message.py b/trashcli/rm/prepare_output_message.py index ae53f436..9ae3a169 100644 --- a/trashcli/rm/prepare_output_message.py +++ b/trashcli/rm/prepare_output_message.py @@ -3,4 +3,4 @@ def prepare_output_message(info_files): return 'No files to be removed.' else: return 'The following files will be removed:\n' + '\n'.join( - ' - ' + info_file.original_location for info_file in info_files) + '\nProceed? (y/N) ' + ' - ' + info_file for info_file in info_files) + '\nProceed? (y/N) ' From 3f834aecab4f461106655ac827fed24d7dba1974 Mon Sep 17 00:00:00 2001 From: abstract-official Date: Fri, 17 Apr 2026 23:54:34 +0800 Subject: [PATCH 08/12] Fix: don't remove if user doesn't consent check `ok_to_empty` attrib rather than the `UserIntention` --- trashcli/rm/rm_cmd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trashcli/rm/rm_cmd.py b/trashcli/rm/rm_cmd.py index 8d3e2acc..da194925 100644 --- a/trashcli/rm/rm_cmd.py +++ b/trashcli/rm/rm_cmd.py @@ -79,7 +79,7 @@ def run(self, argv, uid): if info_files: # skip asking the user if there is nothing to delete; avoids being stuck user = User(prepare_output_message, RealInput(), parse_reply) guard = Guard(user) - if guard.ask_the_user(is_input_interactive(), info_files): + if guard.ask_the_user(is_input_interactive(), info_files).ok_to_empty: for info_file in info_files: trashcan.delete_trash_info_and_backup_copy(info_file) else: From 43340eaadd13fec095b002ebadd8760b0777b3f1 Mon Sep 17 00:00:00 2001 From: abstract-official Date: Sat, 18 Apr 2026 00:00:08 +0800 Subject: [PATCH 09/12] Fix: show original path rather than .trashinfo path --- trashcli/rm/prepare_output_message.py | 8 ++++---- trashcli/rm/rm_cmd.py | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/trashcli/rm/prepare_output_message.py b/trashcli/rm/prepare_output_message.py index 9ae3a169..dba9bbef 100644 --- a/trashcli/rm/prepare_output_message.py +++ b/trashcli/rm/prepare_output_message.py @@ -1,6 +1,6 @@ -def prepare_output_message(info_files): - if not info_files: +def prepare_output_message(files_to_remove): + if not files_to_remove: return 'No files to be removed.' else: - return 'The following files will be removed:\n' + '\n'.join( - ' - ' + info_file for info_file in info_files) + '\nProceed? (y/N) ' + return 'The following files / directories will be removed:\n' + '\n'.join( + ' - ' + file[0] for file in files_to_remove) + '\nProceed? (y/N) ' diff --git a/trashcli/rm/rm_cmd.py b/trashcli/rm/rm_cmd.py index da194925..0b4810dd 100644 --- a/trashcli/rm/rm_cmd.py +++ b/trashcli/rm/rm_cmd.py @@ -66,7 +66,7 @@ def run(self, argv, uid): for event, args in scanner.scan_trash_dirs(self.environ, uid): if event == trash_dir_found: - info_files = [] + files_to_remove = [] path, volume = args for type, arg in listing.list_from_volume_trashdir(path, volume): @@ -75,13 +75,13 @@ def run(self, argv, uid): elif type == 'trashed_file': original_location, info_file = arg if cmd.matches(original_location): - info_files.append(info_file) - if info_files: # skip asking the user if there is nothing to delete; avoids being stuck + files_to_remove.append(arg) + if files_to_remove: # skip asking the user if there is nothing to delete; avoids being stuck user = User(prepare_output_message, RealInput(), parse_reply) guard = Guard(user) - if guard.ask_the_user(is_input_interactive(), info_files).ok_to_empty: - for info_file in info_files: - trashcan.delete_trash_info_and_backup_copy(info_file) + if guard.ask_the_user(is_input_interactive(), files_to_remove).ok_to_empty: + for file in files_to_remove: + trashcan.delete_trash_info_and_backup_copy(file[1]) else: print('No files to be removed in {}'.format(path)) From 01382239fe8599c627f37b2fa58f7d2045a7be90 Mon Sep 17 00:00:00 2001 From: abstract-official Date: Sat, 18 Apr 2026 00:02:58 +0800 Subject: [PATCH 10/12] Fix: also include dir in "nothing removed" msg --- trashcli/rm/rm_cmd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trashcli/rm/rm_cmd.py b/trashcli/rm/rm_cmd.py index 0b4810dd..a55997a6 100644 --- a/trashcli/rm/rm_cmd.py +++ b/trashcli/rm/rm_cmd.py @@ -83,7 +83,7 @@ def run(self, argv, uid): for file in files_to_remove: trashcan.delete_trash_info_and_backup_copy(file[1]) else: - print('No files to be removed in {}'.format(path)) + print('No files / directories to be removed in {}'.format(path)) def unable_to_parse_path(self, trashinfo): self.report_error('{}: unable to parse \'Path\''.format(trashinfo)) From 37a3c38717e9252d97e684e18272a89f4615e616 Mon Sep 17 00:00:00 2001 From: abstract-official Date: Sat, 18 Apr 2026 00:37:39 +0800 Subject: [PATCH 11/12] Create __init__.py --- trashcli/guard/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 trashcli/guard/__init__.py diff --git a/trashcli/guard/__init__.py b/trashcli/guard/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/trashcli/guard/__init__.py @@ -0,0 +1 @@ + From 7ce59a6bd3c4982671c8d103ca651ff7c2cd1d3c Mon Sep 17 00:00:00 2001 From: abstract-official Date: Sun, 19 Apr 2026 13:29:28 +0800 Subject: [PATCH 12/12] Fix: add guard module to setup.cfg --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index f1efa5f9..92ed1539 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,6 +13,7 @@ long_description_content_type = text/x-rst packages = trashcli trashcli.empty + trashcli.guard trashcli.lib trashcli.list trashcli.list.minor_actions