summaryrefslogtreecommitdiff
path: root/gptel-tools/move_to_trash.el
blob: 6ea97995e892849d48aaceea1baa0c9310d35041 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
;;; move_to_trash.el --- Move files/directories to trash for gptel  -*- lexical-binding: t; -*-

;; Copyright (C) 2025

;; Author: gptel-tool-writer
;; Keywords: convenience, tools, files

;; This file is not part of GNU Emacs.

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;;; Commentary:

;; This file provides a gptel tool for moving files and directories to the trash.
;; Files are moved to ~/.local/share/Trash/files with automatic timestamping for
;; name conflicts. The tool operates only within the home directory and /tmp
;; for security reasons.

;;; Code:

(require 'gptel)
(require 'subr-x)

(defun gptel--move-to-trash-generate-unique-name (original-name trash-dir)
  "Generate a unique name for ORIGINAL-NAME in TRASH-DIR.
If a file with the same name exists, append a timestamp in the format
YYYY-MM-DD-HH-MM-SS."
  (let* ((base-name (file-name-nondirectory original-name))
		 (target-path (expand-file-name base-name trash-dir)))
	(if (not (file-exists-p target-path))
		target-path
	  ;; Name conflict: add timestamp
	  (let* ((extension (file-name-extension base-name t))
			 (name-sans-ext (file-name-sans-extension base-name))
			 (timestamp (format-time-string "%Y-%m-%d-%H-%M-%S"))
			 (new-name (if extension
						  (concat name-sans-ext "-" timestamp extension)
						(concat base-name "-" timestamp))))
		(expand-file-name new-name trash-dir)))))

(defun gptel--move-to-trash-validate-path (path)
  "Validate that PATH is safe to trash.
Returns the expanded path if valid, signals an error otherwise.
Ensures path is within home directory or /tmp, and prevents
trashing of critical system directories."
  (let ((expanded-path (expand-file-name path))
		(home-dir (expand-file-name "~"))
		(critical-dirs (list (expand-file-name "~")
							(expand-file-name "~/.emacs.d")
							(expand-file-name "~/.config")
							"/tmp")))
	;; Security check: must be within allowed directories
	(unless (or (string-prefix-p home-dir expanded-path)
				(string-prefix-p "/tmp" expanded-path))
	  (error "Path must be within home directory or /tmp: %s" path))

	;; Prevent trashing critical directories
	(when (member expanded-path critical-dirs)
	  (error "Cannot trash critical directory: %s" path))

	;; Existence check
	(unless (file-exists-p expanded-path)
	  (error "File or directory does not exist: %s" path))

	expanded-path))

(defun gptel--move-to-trash-perform (expanded-path trash-dir)
  "Move EXPANDED-PATH to TRASH-DIR with unique naming.
Returns a formatted message describing the operation."
  (let* ((is-directory (file-directory-p expanded-path))
		 (is-symlink (file-symlink-p expanded-path))
		 (trash-path (gptel--move-to-trash-generate-unique-name
					 expanded-path trash-dir))
		 (item-type (cond
					 (is-symlink "Symlink")
					 (is-directory "Directory")
					 (t "File"))))

	;; Perform the move
	(condition-case move-err
		(progn
		  (rename-file expanded-path trash-path)

		  ;; Verify success
		  (cond
		   ((file-exists-p expanded-path)
			(error "Failed to move %s to trash - file still exists at original location"
				   expanded-path))
		   ((not (file-exists-p trash-path))
			(error "Move operation failed - file not found in trash"))
		   (t
			(format "%s moved to trash: %s → %s"
					item-type
					(abbreviate-file-name expanded-path)
					(file-name-nondirectory trash-path)))))
	  (permission-denied
	   (error "Permission denied: cannot move %s to trash" expanded-path))
	  (error
	   (error "Failed to move %s to trash: %s"
			  expanded-path (error-message-string move-err))))))

;; Main tool definition
(with-eval-after-load 'gptel
  (gptel-make-tool
   :name "move_to_trash"
   :function (lambda (path)
			   "Move PATH to the trash directory.
Creates the trash directory if needed, handles naming conflicts,
and provides detailed error messages."
			   (condition-case err
				   (let* ((trash-dir (expand-file-name "~/.local/share/Trash/files"))
						  (expanded-path (gptel--move-to-trash-validate-path path)))

					 ;; Ensure trash directory exists
					 (unless (file-exists-p trash-dir)
					   (make-directory trash-dir t))

					 ;; Move and return status message
					 (gptel--move-to-trash-perform expanded-path trash-dir))
				 (error
				  (error "Tool error: %s" (error-message-string err)))))
   :description "Move a file or directory to the trash (~/.local/share/Trash/files). Works recursively for directories. Handles name conflicts with timestamps. Operates only within home directory and /tmp. Does not follow symlinks. Synonyms: delete, remove, trash file/directory."
   :args (list '(:name "path"
					   :type string
					   :description "Path to the file or directory to move to trash. Must be within home directory or /tmp."))
   :category "filesystem"
   :confirm nil  ; No confirmation needed
   :include t))

;; Automatically add to gptel-tools on load
(add-to-list 'gptel-tools (gptel-get-tool '("filesystem" "move_to_trash")))

(provide 'move_to_trash)
;;; move_to_trash.el ends here