Fix bugs and more work on the command line

This commit is contained in:
2025-10-24 08:19:22 -07:00
parent b834409684
commit 946ccaa449
6 changed files with 500 additions and 136 deletions

View File

@ -40,10 +40,11 @@
:initarg :message
:documentation "A message describing the error."))
(:report (lambda (condition stream)
(with-slots (source pos message) condition
(format
stream "Failed to parse date ~S~@[ at position ~A~]: ~A"
source pos message))))
(format
stream "Failed to parse date ~S~@[ at position ~A~]: ~A"
(date-parse-error-source condition)
(date-parse-error-position condition)
(date-parse-error-message condition))))
(:documentation "A condition representing a failure in parsing a date range."))
(defparameter *month-conversion-table*
@ -234,6 +235,23 @@ date ranges.."))
(defun clingon-filtering-options ()
"Return some options that can be used by many commands for filtering."
(list
(clingon:make-option
:list/filepath
:key :extra-trashes
:description "include additional trashes"
:short-name #\c
:long-name "include-trash")
(clingon:make-option
:list/filepath
:key :ignored-trashes
:description "ignore the given trash directory"
:long-name "ignore-trash")
(clingon:make-option
:flag
:key :only-explicit-dirs
:description "only use trash directories supplied with -c"
:short-name #\E
:long-name "explicit-trashes-only")
(clingon:make-option
:flag
:key :print-format-info
@ -329,9 +347,11 @@ string."
(defun list-nonexcluded-trash-dirs (cmd)
"Return a list of all trash directories, except those excluded by CMD."
(append (set-difference (cl-xdg-trash:list-trash-directories)
(clingon:getopt cmd :ignored-trashes)
:test #'uiop:pathname-equal)
(append (unless (clingon:getopt cmd :only-explicit-dirs)
(set-difference (cl-xdg-trash:list-trash-directories)
(mapcar #'uiop:ensure-directory-pathname
(clingon:getopt cmd :ignored-trashes))
:test #'uiop:pathname-equal))
(mapcar #'ensure-nonwild-pathname
(clingon:getopt cmd :extra-trashes))))
@ -371,6 +391,20 @@ string."
;; Formatting
(defun format-size (count &optional base-two (places 2))
"Pretty print COUNT, which is a number of bytes. This will append metric
suffixes as necessary. If BASE-TWO is non-nil, use MiB, GiB, etc. suffixes
instead."
(if (not count)
"N/A" ;; if find the size of something failed
(let* ((base (if base-two 1024 1000))
(power (min 10 (floor (if (zerop count) 0 (log count base))))))
(if (zerop power)
(format nil "~DB" count)
(format nil "~,VF~[~:[k~;K~]~:*~;M~;G~;T~;P~;E~;Z~;Y~;R~;Q~]~@[i~]B"
places (/ count (expt base power))
(1- power) base-two)))))
(defparameter *trashinfo-formatters*
`((#\# :index
"the index of the current file (used when prompting for files)")
@ -399,11 +433,24 @@ string."
stream (trashinfo-deletion-date info)
:format local-time:+asctime-format+))
"the (t)ime the file was trashed (pretty-printed local time)")
(#\t ,(lambda (stream info)
(#\s ,(lambda (stream info)
(format stream "~A" (trashed-file-size
(trashinfo-trash-directory info)
(trashinfo-name info))))
"the file's (s)size")
"the file's (s)size in bytes")
(#\h ,(lambda (stream info)
(format stream "~A"
(format-size (trashed-file-size
(trashinfo-trash-directory info)
(trashinfo-name info)))))
"the file's size with a (h)uman readable suffix (powers of 10)")
(#\H ,(lambda (stream info)
(format stream "~A"
(format-size (trashed-file-size
(trashinfo-trash-directory info)
(trashinfo-name info))
t)))
"the file's size with a (H)uman readable suffix (power of 2)")
(#\% ,(lambda (stream info)
(declare (ignore info))
(format stream "%"))
@ -539,14 +586,7 @@ The recognizes printf-style sequences are (parenthesis denote the mnemonic):~%")
"Return options for the \"list\" subcommand."
(append
(clingon-filtering-options)
(clingon-sort-options)
(list
(clingon:make-option
:list/filepath
:key :extra-trashes
:description "include additional trashes"
:short-name #\c
:long-name "include-trash"))))
(clingon-sort-options)))
(defun list/command ()
"Return the Clingon command for the \"list\" subcommand."
@ -600,10 +640,83 @@ The recognizes printf-style sequences are (parenthesis denote the mnemonic):~%")
;; Restore command
(declaim (inline single-item-list-p))
(defun single-item-list-p (list)
"Return non-nil if LIST has only one thing."
(and list (null (cdr list))))
(defun prompt-for-index (stream action max &optional allow-many)
"Prompt the user for an index between 1 and MAX to restore. With ALLOW-MANY,
return a list of many indices instead."
(when (zerop max)
(error "Nothing found..."))
(format stream "~&Select ~:[indices~;index~] to ~A: "
(or (eql 1 max) (not allow-many)) action)
(let ((resp-string (read-line stream nil))
(seperators '(#\Space #\Tab #\,)))
(unless resp-string
(error "No number provided"))
(let ((parts (uiop:split-string resp-string
:separator seperators))
(out (make-hash-table :test #'eql)))
(unless parts
(error "No number provided"))
(unless (or allow-many
(single-item-list-p parts))
(error "Only one item can be selected"))
(dolist (part parts)
(unless (every (lambda (c) (member c seperators)) part)
(let ((n (parse-integer part)))
(when (or (< n 1)
(> n max))
(error "Number ~A out of range [1,~A]" n max))
(setf (gethash (1- n) out) t))))
(loop for key being the hash-keys of out collect key))))
(defun restore/handler (cmd)
"Handler for the \"restore\" subcommand."
(le))
(if (clingon:getopt cmd :print-format-info)
(print-format-info t)
(let ((format (process-format-string (or (clingon:getopt cmd :format)
"%#: %t %o\\n")))
(infos (list-trashinfos-for-cmd cmd))
(target (clingon:getopt cmd :target))
(all (clingon:getopt cmd :all))
(cli-indices (clingon:getopt cmd :indices)))
(when (and all target)
(error "Only one of -a and -t can be supplied"))
(cond
((and (clingon:getopt cmd :dont-prompt-only-one)
(single-item-list-p infos))
(cl-xdg-trash:restore-file (car infos)))
((clingon:getopt cmd :all)
(mapc #'cl-xdg-trash:restore-file infos))
(cli-indices
(unless infos
(error "Nothing found..."))
(loop with sorted-arr = (sort-trashinfos-for-cmd
(coerce infos 'vector) cmd)
for i in cli-indices
when (or (> i (length sorted-arr))
(< i 1))
do (error "Index ~S out of bounds [1,~S]"
i (length sorted-arr))
do (cl-xdg-trash:restore-file (aref sorted-arr (1- i)))))
(t
(let ((sorted-arr (coerce infos 'vector)))
(sort-trashinfos-for-cmd sorted-arr cmd)
(loop for info across sorted-arr
for i upfrom 1
do (format-trashinfo t format info :index i))
(let ((idxs (prompt-for-index t "restore"
(length sorted-arr)
(not target))))
(if target
(cl-xdg-trash:restore-file (aref sorted-arr (car idxs))
:target target)
(loop for i in idxs
do (cl-xdg-trash:restore-file
(aref sorted-arr i)))))))))))
(defun restore/options ()
"Return options for the \"restore\" subcommand."
@ -611,10 +724,16 @@ The recognizes printf-style sequences are (parenthesis denote the mnemonic):~%")
(clingon-filtering-options)
(clingon-sort-options)
(list
(clingon:make-option
:filepath
:key :target
:description "where path to restore the file (exclusive with -a)"
:short-name #\t
:long-name "target")
(clingon:make-option
:flag
:key :all
:description "restore all files that match the pattern"
:description "restore all files that match the pattern (exclusive with -t)"
:short-name #\a
:long-name "all")
(clingon:make-option
@ -627,7 +746,7 @@ The recognizes printf-style sequences are (parenthesis denote the mnemonic):~%")
(clingon:make-option
:flag
:key :dont-prompt-only-one
:descrition "don't prompt if the pattern matches only one file"
:description "don't prompt if the pattern matches only one file"
:short-name #\O
:long-name "dont-prompt-only-one"))))
@ -635,23 +754,192 @@ The recognizes printf-style sequences are (parenthesis denote the mnemonic):~%")
"Rethrn the Clingon command for the \"restore\" subcommand."
(clingon:make-command
:name "restore"
:descrition "move files out of the trash"
:description "move files out of the trash"
:usage "[options] [pattern]"
:options (restore/options)
:handler #'restore/handler))
;; Toplevel command
(defun toplevel/options ()
"Return the toplevel options list."
;; Empty command
(defun prompt-yes-or-no (stream control &rest args)
"Prompt the user for a yes or no response."
(when control
(apply #'format stream control args)
(format stream "? (y/n) "))
(let ((resp-string (read-line stream nil)))
(when resp-string
(member resp-string '("yes" "y" "1") :test #'equalp))))
(defun prompt-to-empty (count quiet)
"Prompt for emptying the trash for the \"empty\" command. Specifically, used
with -n and -a."
(prompt-yes-or-no
t "Really erase ~@[~*the above ~]~@[~*~A~:* ~]file~P"
(not quiet) (or quiet (/= 1 count)) count))
(defun empty/handler (cmd)
"Handler for the \"empty\" subcommand."
(cond
((clingon:getopt cmd :print-format-info)
(print-format-info t))
(t
(let ((format (process-format-string
(or (clingon:getopt cmd :format)
"%#: %t %o\\n")))
(dry-run (clingon:getopt cmd :dry-run))
(infos (coerce (list-trashinfos-for-cmd cmd) 'vector))
(quiet (clingon:getopt cmd :quiet))
(yes (clingon:getopt cmd :yes))
(all (clingon:getopt cmd :all))
(indices (clingon:getopt cmd :indices)))
(when (and yes (not (or all indices)))
(error "Found -y with neither -a nor -n, doing nothing"))
(when (and quiet (not (or all indices)))
(error "Found -q with neither -a nor -n, doing nothing"))
(when (and (zerop (length infos)) (not quiet))
(error "Nothing found..."))
(unless all
(sort-trashinfos-for-cmd infos cmd))
(unless (or yes quiet)
(loop for info across infos
for i upfrom 1
when (or (not indices) (member i indices))
do (format-trashinfo t format info :index i)))
(cond
(all
(when (or yes (prompt-to-empty (length infos) quiet))
(loop for info across infos
do (cl-xdg-trash:empty-file info :dry-run dry-run))))
(indices
(when (or yes (prompt-to-empty (length indices) quiet))
(loop for i in indices
when (or (< i 1)
(> i (length infos)))
do (error "Index ~A out of bounds [1,~A]"
i (length infos))
else
do (cl-xdg-trash:empty-file (aref infos (1- i))
:dry-run dry-run))))
(t
(let ((index (prompt-for-index t "erase" (length infos) t)))
(dolist (i index)
(cl-xdg-trash:empty-file (aref infos i) :dry-run dry-run)))))))))
(defun empty/options ()
"Return options for the \"empty\" subcommand."
(append
(clingon-filtering-options)
(clingon-sort-options)
(list
(clingon:make-option
:flag
:key :dry-run
:description "print what would happen without actually deleting anything"
:short-name #\D
:long-name "dry-run")
(clingon:make-option
:flag
:key :all
:description "erase all matching files"
:short-name #\a
:long-name "all")
(clingon:make-option
:list/integer
:key :indices
:description
"erase the Nth file that matched the pattern (after sorting)"
:short-name #\n
:long-name "nth")
(clingon:make-option
:flag
:key :yes
:description "erase files without prompting (implies -q)"
:short-name #\y
:long-name "yes")
(clingon:make-option
:flag
:key :quiet
:description "be less verbose"
:short-name #\q
:long-name "quiet"))))
(defun empty/command ()
"Return the Clingon command for the \"empty\" subcommand."
(clingon:make-command
:name "empty"
:aliases '("erase" "rm")
:description "delete files from the trash permanently"
:options (empty/options)
:handler #'empty/handler))
;; List trash directories command
(defun list-trashes/handler (cmd)
"Handler for the \"list-trashes\" subcommand."
(let* ((only-home (clingon:getopt cmd :home))
(only-toplevel (clingon:getopt cmd :toplevel))
(null (clingon:getopt cmd :null))
(dirs (cond
((or (and only-home only-toplevel)
(not (or only-home only-toplevel)))
(cl-xdg-trash:list-trash-directories))
(only-home
(list (cl-xdg-trash:user-home-trash-directory)))
(only-toplevel
(cl-xdg-trash:list-toplevel-trash-directories)))))
(dolist (dir dirs)
(format t "~A" (uiop:native-namestring dir))
(if null
(write-char #\Nul)
(terpri)))))
(defun list-trashes/options ()
"Return options for the \"list-trashes\" subcommand."
(list
(clingon:make-option
:list
:key :ignored-trashes
:description "ignore the given trash directory"
:long-name "ignore-trash"
:persistent t)))
:flag
:key :home
:description "only list the user's home trash directory"
:short-name #\o
:long-name "home")
(clingon:make-option
:flag
:key :toplevel
:description "only list toplevel trash directories"
:short-name #\t
:long-name "toplevel")
(clingon:make-option
:flag
:key :null
:description "use \\0 (null byte) instead of newline"
:short-name #\0
:long-name "null")))
(defun list-trashes/command ()
"Return the Clingon command for the \"list-trashes\" subcommand."
(clingon:make-command
:name "list-trashes"
:description "list known trash directories"
:options (list-trashes/options)
:handler #'list-trashes/handler))
;; Size command
(defun size/handler (cmd)
"Handler for the \"size\" subcommand."
)
(defun size/options ()
"Return options for the \"size\" subcommand."
(list
()))
(defun size/command ()
"Return the Clingon command for the \"size\" subcommand.")
;; Toplevel command
(defun toplevel/command ()
"Return the toplevel command."
(clingon:make-command
@ -660,11 +948,16 @@ The recognizes printf-style sequences are (parenthesis denote the mnemonic):~%")
:version "0.1.0"
:license "GPL3"
:authors '("Alexander Rosenberg <zanderpkg@pm.me>")
:options (toplevel/options)
:sub-commands (list (list/command)
(put/command))
(put/command)
(restore/command)
(list-trashes/command)
(empty/command))
:handler #'(lambda (cmd)
(clingon:print-usage-and-exit cmd t))))
(let ((args (clingon:command-arguments cmd)))
(when args
(error "Unknown subcommand: ~S" (car args)))
(clingon:print-usage-and-exit cmd t)))))
(defparameter *toplevel/help-option*
(clingon:make-option