diff options
| author | Craig Jennings <c@cjennings.net> | 2025-10-12 11:47:26 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-10-12 11:47:26 -0500 |
| commit | 092304d9e0ccc37cc0ddaa9b136457e56a1cac20 (patch) | |
| tree | ea81999b8442246c978b364dd90e8c752af50db5 /modules/dwim-shell-config.el | |
changing repositories
Diffstat (limited to 'modules/dwim-shell-config.el')
| -rw-r--r-- | modules/dwim-shell-config.el | 732 |
1 files changed, 732 insertions, 0 deletions
diff --git a/modules/dwim-shell-config.el b/modules/dwim-shell-config.el new file mode 100644 index 00000000..a1ace2be --- /dev/null +++ b/modules/dwim-shell-config.el @@ -0,0 +1,732 @@ +;; dwim-shell-config.el --- Dired Shell Commands -*- coding: utf-8; lexical-binding: t; -*- +;; +;;; Commentary: +;; +;; This module provides a collection of DWIM (Do What I Mean) shell commands +;; for common file operations in Dired and other buffers. It leverages the +;; `dwim-shell-command' package to execute shell commands on marked files +;; with smart templating and progress tracking. +;; +;; Features: +;; - Audio/Video conversion (mp3, opus, webp, HEVC) +;; - Image manipulation (resize, flip, format conversion) +;; - PDF operations (merge, split, password protection, OCR) +;; - Archive management (zip/unzip) +;; - Document conversion (epub to org, docx to pdf, pdf to txt) +1;; - Git operations (clone from clipboard) +;; - External file opening with context awareness +;; +;; Workflow: +;; 1. *Mark files in Dired/Dirvish* +;; - Use =m= to mark individual files +;; - Use =* .= to mark by extension +;; - Use =% m= to mark by regexp +;; - Or operate on the file under cursor if nothing is marked +;; +;; 2. *Execute a DWIM command* +;; - Call the command via =M-x dwim-shell-commands-[command-name]= +;; - Or bind frequently used commands to keys +;; +;; 3. *Command execution* +;; - The command runs asynchronously in the background +;; - A =*Async Shell Command*= buffer shows progress +;; - Files are processed with smart templating (replacing =<<f>>=, =<<fne>>=, etc.) +;; +;; 4. *Results* +;; - New files appear in the Dired/Dirvish buffer +;; - Buffer auto-refreshes when command completes +;; - Errors appear in the async buffer if something fails +;; +;; Requirements: +;; The commands rely on various external utilities that need to be installed: +;; - ffmpeg: Audio/video conversion +;; - imagemagick (convert): Image manipulation +;; - qpdf: PDF operations +;; - tesseract: OCR functionality +;; - pandoc: Document conversion +;; - atool: Archive extraction +;; - rsvg-convert: SVG to PNG conversion +;; - pdftotext: PDF text extraction +;; - git: Version control operations +;; - gpgconf: GPG agent management +;; +;; On Arch Linux, install the requirements with: +;; #+begin_src bash +;; sudo pacman -S --needed ffmpeg imagemagick qpdf tesseract tesseract-data-eng pandoc atool librsvg poppler git gnupg zip unzip mkvtoolnix-cli mpv ruby +;; #+end_src +;; +;; On MacOS, install the requirements with: +;; #+begin_src bash +;; brew install ffmpeg imagemagick qpdf tesseract pandoc atool librsvg poppler gnupg mkvtoolnix mpv +;; #+end_src +;; +;; Usage: +;; Commands operate on marked files in Dired or the current file in other modes. +;; The package automatically replaces standard shell commands with DWIM versions +;; for a more intuitive experience. +;; +;; Template Variables: +;; - <<f>>: Full path to file +;; - <<fne>>: File name without extension +;; - <<e>>: File extension +;; - <<td>>: Temporary directory +;; - <<cb>>: Clipboard contents +;; - <<*>>: All marked files +;; + +;;; Code: + +(require 'system-utils) + +;; -------------------------- Dwim Shell Commands Menu ------------------------- + +(defun dwim-shell-commands-menu () + "Select and execute a dwim-shell-command function with prettified names." + (interactive) + (let* ((commands (cl-loop for symbol being the symbols + when (and (fboundp symbol) + (string-prefix-p "cj/dwim-shell-commands-" (symbol-name symbol)) + (not (eq symbol 'dwim-shell-commands-menu))) + collect symbol)) + ;; Create alist of (pretty-name . command-symbol) + (command-alist (mapcar (lambda (cmd) + (cons (replace-regexp-in-string + "-" " " + (replace-regexp-in-string + "^cj/dwim-shell-commands-" + "" + (symbol-name cmd))) + cmd)) + commands)) + (selected (completing-read "Command: " + command-alist + nil + t + nil + 'dwim-shell-command-history)) + (command (alist-get selected command-alist nil nil #'string=))) + (when command + (call-interactively command)))) + +(with-eval-after-load 'dired + (define-key dired-mode-map (kbd "M-D") #'dwim-shell-commands-menu)) + +;; ----------------------------- Dwim Shell Command ---------------------------- + +(use-package dwim-shell-command + :defer 0.5 + :bind (([remap shell-command] . dwim-shell-command) + :map dired-mode-map + ([remap dired-do-async-shell-command] . dwim-shell-command) + ([remap dired-do-shell-command] . dwim-shell-command) + ([remap dired-smart-shell-command] . dwim-shell-command)) + :init + (defun cj/dwim-shell-commands-convert-audio-to-mp3 () + "Convert all marked audio to mp3(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to mp3" + "ffmpeg -stats -n -i '<<f>>' -acodec libmp3lame '<<fne>>.mp3'" + :utils "ffmpeg")) + + (defun cj/dwim-shell-commands-convert-audio-to-opus () + "Convert all marked audio to opus(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to opus" + "ffmpeg -stats -n -i '<<f>>' -c:a libopus -vbr on -compression_level 10 -b:a 256k '<<fne>>.opus'" + :utils "ffmpeg")) + + (defun cj/dwim-shell-commands-view-image-exif-metadata () + "View EXIF metadata in image(s)." + (interactive) + (dwim-shell-command-on-marked-files + "View EXIF" + "exiftool '<<f>>'" + :utils "exiftool")) + + (defun cj/dwim-shell-commands-flip-image-horizontally () + "Horizontally flip image(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Image horizontal flip" + "convert -verbose -flop '<<f>>' '<<fne>>_h_flipped.<<e>>'" + :utils "convert")) + + (defun cj/dwim-shell-commands-flip-image-vertically () + "Horizontally flip image(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Image vertical flip" + "convert -verbose -flip '<<f>>' '<<fne>>_v_flipped.<<e>>'" + :utils "convert")) + + (defun cj/dwim-shell-commands-convert-image-to () + "Convert all marked images to a specified format." + (interactive) + (let ((format (completing-read "Convert to format: " + '("jpg" "png" "webp" "gif" "bmp" "tiff") + nil t))) + (dwim-shell-command-on-marked-files + (format "Convert to %s" format) + (format "convert -verbose '<<f>>' '<<fne>>.%s'" format) + :utils "convert"))) + + (defun cj/dwim-shell-commands-convert-svg-to-png () + "Convert all marked svg(s) to png(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to png" + "rsvg-convert -b white '<<f>>' -f png -o '<<fne>>.png'" + :utils "rsvg-convert")) + + (defun cj/dwim-shell-commands-join-images-into-pdf () + "Join all marked images as a single pdf." + (interactive) + (dwim-shell-command-on-marked-files + "Join as pdf" + (format "convert -verbose '<<*>>' '<<%s(u)>>'" + (dwim-shell-command-read-file-name + "Join as pdf named (default \"joined.pdf\"): " + :extension "pdf" + :default "joined.pdf")) + :utils "convert")) + + (defun cj/dwim-shell-commands-extract-pdf-page-number () + "Keep a page from pdf." + (interactive) + (let ((page-num (read-number "Keep page number: " 1))) + (dwim-shell-command-on-marked-files + "Keep pdf page" + (format "qpdf '<<f>>' --pages . %d -- '<<fne>>_%d.<<e>>'" page-num page-num) + :utils "qpdf"))) + + (defun cj/dwim-shell-commands-ocr-text-from-image-using-tesseract () + "Extract text from image via tesseract." + (interactive) + (dwim-shell-command-on-marked-files + "Extract text from image via tesseract." + "tesseract '<<f>>' -" + :utils "tesseract")) + + (defun cj/dwim-shell-commands-convert-video-to-webp () + "Convert all marked videos to webp(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to webp" + "ffmpeg -i '<<f>>' -vcodec libwebp -filter:v fps=fps=10 -compression_level 3 -loop 0 -preset default -an -vsync 0 '<<fne>>'.webp" + :utils "ffmpeg")) + + (defun cj/dwim-shell-commands-convert-video-to-high-compatibility-mp4 () + "Convert all marked video(s) to MP4 format with H.264/AAC." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to MP4" + "ffmpeg -i '<<f>>' -c:v libx264 -preset slow -crf 23 -profile:v baseline -level 3.0 -c:a aac -b:a 128k '<<fne>>.mp4'" + :utils "ffmpeg")) + + (defun cj/dwim-shell-commands-convert-video-to-hevc-mkv () + "Convert all marked videos to HEVC (H.265) in MKV container." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to HEVC/H.265" + "ffmpeg -i '<<f>>' -c:v libx265 -preset slower -crf 22 -x265-params profile=main10:level=4.0 -c:a copy -c:s copy '<<fne>>.mkv'" + :utils "ffmpeg")) + + (defun cj/dwim-shell-commands-extract-archive-smartly () + "Unzip all marked archives (of any kind) using =atool'. + +If there's only one file, unzip it to current directory. +Otherwise, unzip it to an appropriately named subdirectory " + (interactive) + (dwim-shell-command-on-marked-files + "Unzip" "atool --extract --subdir --explain '<<f>>'" + :utils "atool")) + + (defun cj/dwim-shell-commands-zip-file-or-directory () + "Zip all marked files into archive.zip." + (interactive) + (dwim-shell-command-on-marked-files + "Zip" (if (eq 1 (seq-length (dwim-shell-command--files))) + "zip -r '<<fne>>.<<e>>' '<<f>>'" + "zip -r '<<archive.zip(u)>>' '<<*>>'") + :utils "zip")) + + (defun cj/dwim-shell-commands-tar-gzip-file-or-directory () + "Tar gzip all marked files into archive.tar.gz." + (interactive) + (dwim-shell-command-on-marked-files + "Tar gzip" (if (eq 1 (seq-length (dwim-shell-command--files))) + "tar czf '<<fne>>.tar.gz' '<<f>>'" + "tar czf '<<archive.tar.gz(u)>>' '<<*>>'") + :utils "tar")) + + (defun cj/dwim-shell-commands-epub-to-org () + "Convert epub(s) to org." + (interactive) + (dwim-shell-command-on-marked-files + "epub to org" + "pandoc --from=epub --to=org '<<f>>' > '<<fne>>.org'" + :extensions "epub" + :utils "pandoc")) + + (defun cj/dwim-shell-commands-document-to-pdf () + "Convert document(s) to pdf (via latex). + +Supports docx, odt, and other pandoc-compatible formats." + (interactive) + (dwim-shell-command-on-marked-files + "Document to pdf (via latex)" + "pandoc -t latex '<<f>>' -o '<<fne>>.pdf'" + :extensions '("docx" "odt" "odp" "ods" "rtf" "doc") + :utils "pdflatex")) + + (defun cj/dwim-shell-commands-pdf-to-txt () + "Convert pdf to txt." + (interactive) + (dwim-shell-command-on-marked-files + "pdf to txt" + "pdftotext -layout '<<f>>' '<<fne>>.txt'" + :utils "pdftotext")) + + (defun cj/dwim-shell-commands-resize-image-by-factor () + "Resize marked image(s) by factor." + (interactive) + (dwim-shell-command-on-marked-files + "Resize image" + (let ((factor (read-number "Resize scaling factor: " 0.5))) + (format "convert -resize %%%d '<<f>>' '<<fne>>_x%.2f.<<e>>'" + (* 100 factor) factor)) + :utils "convert")) + + (defun cj/dwim-shell-commands-resize-image-in-pixels () + "Resize marked image(s) in pixels." + (interactive) + (dwim-shell-command-on-marked-files + "Resize image" + (let ((width (read-number "Resize width (pixels): " 500))) + (format "convert -resize %dx '<<f>>' '<<fne>>_x%d.<<e>>'" width width)) + :utils "convert")) + + (defun cj/dwim-shell-commands-pdf-password-protect () + "Add a password to pdf(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Password protect pdf" + (format "qpdf --verbose --encrypt '%s' '%s' 256 -- '<<f>>' '<<fne>>_protected.<<e>>'" + (read-passwd "user-password: ") + (read-passwd "owner-password: ")) + :utils "qpdf" + :extensions "pdf")) + + (defun cj/dwim-shell-commands-pdf-password-unprotect () + "Remove a password from pdf(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Remove protection from pdf" + (format "qpdf --verbose --decrypt --password='%s' -- '<<f>>' '<<fne>>_unprotected.<<e>>'" + (read-passwd "password: ")) + :utils "qpdf" + :extensions "pdf")) + + (defun cj/dwim-shell-commands-video-trim () + "Trim video with options for beginning, end, or both." + (interactive) + (let* ((trim-type (completing-read "Trim from: " + '("Beginning" "End" "Both") + nil t)) + (command (pcase trim-type + ("Beginning" + (let ((seconds (read-number "Seconds to trim from beginning: " 5))) + (format "ffmpeg -i '<<f>>' -y -ss %d -c:v copy -c:a copy '<<fne>>_trimmed.<<e>>'" + seconds))) + ("End" + (let ((seconds (read-number "Seconds to trim from end: " 5))) + (format "ffmpeg -sseof -%d -i '<<f>>' -y -c:v copy -c:a copy '<<fne>>_trimmed.<<e>>'" + seconds))) + ("Both" + (let ((start (read-number "Seconds to trim from beginning: " 5)) + (end (read-number "Seconds to trim from end: " 5))) + (format "ffmpeg -i '<<f>>' -y -ss %d -sseof -%d -c:v copy -c:a copy '<<fne>>_trimmed.<<e>>'" + start end)))))) + (dwim-shell-command-on-marked-files + (format "Trim video (%s)" trim-type) + command + :silent-success t + :utils "ffmpeg"))) + + (defun cj/dwim-shell-commands-drop-audio-from-video () + "Drop audio from all marked videos." + (interactive) + (dwim-shell-command-on-marked-files + "Drop audio" + "ffmpeg -i '<<f>>' -c copy -an '<<fne>>_no_audio.<<e>>'" + :utils "ffmpeg")) + + (defun cj/dwim-shell-commands-open-externally () + "Open file(s) externally." + (interactive) + (dwim-shell-command-on-marked-files + "Open externally" + (cond ((eq system-type 'darwin) + (if (derived-mode-p 'prog-mode) + (format "open -a Xcode --args --line %d '<<f>>'" + (line-number-at-pos (point))) + "open '<<f>>'")) + ((eq system-type 'windows-nt) + "start '<<f>>'") + (t ;; Linux/Unix + "xdg-open '<<f>>' 2>/dev/null || (echo 'Failed to open with xdg-open' && exit 1)")) + :silent-success t + :utils (cond ((eq system-type 'darwin) "open") + ((eq system-type 'windows-nt) "start") + (t "xdg-open")))) + + + (defun cj/dwim-shell-commands-git-clone-clipboard-url () + "Clone git URL in clipboard to `default-directory'." + (interactive) + (dwim-shell-command-on-marked-files + (format "Clone %s" (file-name-base (current-kill 0))) + "git clone <<cb>>" + :utils "git")) + + (defun cj/dwim-shell-commands-open-file-manager () + "Open the default file manager in the current directory." + (interactive) + (dwim-shell-command-on-marked-files + "Open file manager" + (cond ((eq system-type 'darwin) + "open .") + ((eq system-type 'windows-nt) + "explorer .") + (t ;; Linux/Unix - try multiple options + (cond ((executable-find "thunar") "thunar .") + ((executable-find "nautilus") "nautilus .") + ((executable-find "dolphin") "dolphin .") + ((executable-find "pcmanfm") "pcmanfm .") + (t "xdg-open .")))) + :silent-success t + :no-progress t)) + + (defun cj/dwim-shell-commands-count-words-lines () + "Count words, lines, and characters in text file(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Word count" + "wc -lwc '<<f>>'" + :utils "wc")) + + (defun cj/dwim-shell-commands-checksum () + "Generate checksums for file(s) and save to .checksum file." + (interactive) + (let ((algorithm (completing-read "Algorithm: " + '("md5" "sha1" "sha256" "sha512") + nil t))) + (dwim-shell-command-on-marked-files + (format "Generate %s checksum" algorithm) + (format "%ssum '<<f>>' | tee '<<f>>.%s'" algorithm algorithm) + :utils (format "%ssum" algorithm)))) + + (defun cj/dwim-shell-commands-backup-with-timestamp () + "Create dated backup of file(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Backup with date" + "cp -p '<<f>>' '<<f>>.$(date +%Y%m%d_%H%M%S).bak'" + :utils '("cp" "date"))) + + (defun cj/dwim-shell-commands-optimize-image-for-web () + "Optimize image(s) for web (reduce file size)." + (interactive) + (dwim-shell-command-on-marked-files + "Optimize image" + "convert '<<f>>' -strip -interlace Plane -gaussian-blur 0.05 -quality 85% '<<fne>>_optimized.<<e>>'" + :utils "convert")) + + (defun cj/dwim-shell-commands-csv-to-json () + "Convert CSV to JSON." + (interactive) + (dwim-shell-command-on-marked-files + "CSV to JSON" + "python -c \"import csv,json,sys; print(json.dumps(list(csv.DictReader(open('<<f>>')))))\" > '<<fne>>.json'" + :extensions "csv" + :utils "python")) + + (defun cj/dwim-shell-commands-json-to-yaml () + "Convert JSON to YAML." + (interactive) + (dwim-shell-command-on-marked-files + "JSON to YAML" + "python -c \"import json,yaml,sys; yaml.dump(json.load(open('<<f>>')), open('<<fne>>.yaml', 'w'))\" && echo 'Created <<fne>>.yaml'" + :extensions "json" + :utils "python")) + + (defun cj/dwim-shell-commands-extract-urls-from-file () + "Extract all URLs from file(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Extract URLs" + "grep -Eo 'https?://[^[:space:]]+' '<<f>>'" + :utils "grep")) + + (defun cj/dwim-shell-commands-extract-emails-from-file () + "Extract all email addresses from file(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Extract emails" + "grep -Eo '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}' '<<f>>'" + :utils "grep")) + + (defun cj/dwim-shell-commands-create-gif-from-video () + "Create animated GIF from video." + (interactive) + (let ((fps (read-number "FPS for GIF: " 10)) + (scale (read-number "Scale (pixels width): " 480))) + (dwim-shell-command-on-marked-files + "Create GIF" + (format "ffmpeg -i '<<f>>' -vf 'fps=%d,scale=%d:-1:flags=lanczos' '<<fne>>.gif'" fps scale) + :utils "ffmpeg"))) + + (defun cj/dwim-shell-commands-concatenate-videos () + "Concatenate multiple videos into one." + (interactive) + (dwim-shell-command-on-marked-files + "Concatenate videos" + "echo '<<*>>' | tr ' ' '\n' | sed 's/^/file /' > '<<td>>/filelist.txt' && ffmpeg -f concat -safe 0 -i '<<td>>/filelist.txt' -c copy '<<concatenated.mp4(u)>>'" + :utils "ffmpeg")) + + (defun cj/dwim-shell-commands-create-video-thumbnail () + "Extract thumbnail from video at specific time." + (interactive) + (let ((time (read-string "Time (HH:MM:SS or seconds): " "00:00:05"))) + (dwim-shell-command-on-marked-files + "Extract video thumbnail" + (format "ffmpeg -i '<<f>>' -ss %s -vframes 1 '<<fne>>_thumb.jpg'" time) + :utils "ffmpeg"))) + + (defun cj/dwim-shell-commands-merge-pdfs () + "Merge multiple PDFs into one." + (interactive) + (dwim-shell-command-on-marked-files + "Merge PDFs" + "qpdf --empty --pages '<<*>>' -- '<<merged.pdf(u)>>'" + :extensions "pdf" + :utils "qpdf")) + + (defun cj/dwim-shell-commands-split-pdf-by-pages () + "Split PDF into individual pages." + (interactive) + (dwim-shell-command-on-marked-files + "Split PDF pages" + "qpdf --split-pages '<<f>>' '<<fne>>-page-%d.pdf'" + :extensions "pdf" + :utils "qpdf")) + + (defun cj/dwim-shell-commands-compress-pdf () + "Compress PDF file size." + (interactive) + (let ((quality (completing-read "Quality: " + '("screen" "ebook" "printer" "prepress") + nil t "ebook"))) + (dwim-shell-command-on-marked-files + "Compress PDF" + (format "gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/%s -dNOPAUSE -dBATCH -sOutputFile='<<fne>>_compressed.pdf' '<<f>>'" quality) + :extensions "pdf" + :utils "gs"))) + + (defun cj/dwim-shell-commands-ascii-art () + "Convert image to ASCII art." + (interactive) + (dwim-shell-command-on-marked-files + "Create ASCII art" + "jp2a --width=80 '<<f>>'" + :utils "jp2a")) + + (defun cj/dwim-shell-commands-text-to-speech () + "Convert text file to speech (audio file)." + (interactive) + (let ((voice (if (eq system-type 'darwin) + (completing-read "Voice: " '("Alex" "Samantha" "Victoria" "Karen") nil t "Alex") + "en"))) + (dwim-shell-command-on-marked-files + "Text to speech" + (if (eq system-type 'darwin) + (format "say -v %s -o '<<fne>>.aiff' -f '<<f>>'" voice) + "espeak -f '<<f>>' -w '<<fne>>.wav'") + :utils (if (eq system-type 'darwin) "say" "espeak")))) + + (defun cj/dwim-shell-commands-remove-empty-directories () + "Remove all empty directories recursively." + (interactive) + (when (yes-or-no-p "Remove all empty directories? ") + (dwim-shell-command-on-marked-files + "Remove empty dirs" + "find . -type d -empty -delete" + :utils "find"))) + + (defun cj/dwim-shell-commands-create-thumbnail-from-image () + "Create thumbnail(s) from image(s)." + (interactive) + (let ((size (read-number "Thumbnail size (pixels): " 200))) + (dwim-shell-command-on-marked-files + "Create thumbnail" + (format "convert '<<f>>' -thumbnail %dx%d '<<fne>>_thumb.<<e>>'" size size) + :utils "convert"))) + + (defun cj/dwim-shell-commands-extract-audio-from-video () + "Extract audio track from video file(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Extract audio" + "ffmpeg -i '<<f>>' -vn -acodec copy '<<fne>>.m4a'" + :utils "ffmpeg")) + + (defun cj/dwim-shell-commands-normalize-audio-volume () + "Normalize audio volume in file(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Normalize audio" + "ffmpeg -i '<<f>>' -af 'loudnorm=I=-16:LRA=11:TP=-1.5' '<<fne>>_normalized.<<e>>'" + :utils "ffmpeg")) + + (defun cj/dwim-shell-commands-remove-zip-encryption () + "Remove password protection from zip file(s)." + (interactive) + (let ((password (read-passwd "Current password: "))) + (dwim-shell-command-on-marked-files + "Remove zip encryption" + (format "TMPDIR=$(mktemp -d) && unzip -P '%s' '<<f>>' -d \"$TMPDIR\" && cd \"$TMPDIR\" && zip -r archive.zip * && mv archive.zip '<<fne>>_decrypted.zip' && rm -rf \"$TMPDIR\"" + password) + :utils '("unzip" "zip")))) + + (defun cj/dwim-shell-commands-create-encrypted-zip () + "Create password-protected zip of file(s)." + (interactive) + (let ((password (read-passwd "Password: "))) + (dwim-shell-command-on-marked-files + "Create encrypted zip" + (format "zip -r -e -P '%s' '<<archive.zip(u)>>' '<<*>>'" password) + :utils "zip"))) + + + (defun cj/dwim-shell-commands-list-archive-contents () + "List contents of archive without extracting." + (interactive) + (dwim-shell-command-on-marked-files + "List archive contents" + "atool --list '<<f>>'" + :utils "atool")) + + (defun cj/dwim-shell-commands-count-words-lines-in-text-file () + "Count words, lines, and characters in text file(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Word count" + "wc -lwc '<<f>>'" + :utils "wc")) + + (defun cj/dwim-shell-commands-make-executable () + "Make file(s) executable." + (interactive) + (dwim-shell-command-on-marked-files + "Make executable" + "chmod +x '<<f>>'" + :silent-success t + :utils "chmod")) + + (defun cj/dwim-shell-commands-secure-delete () + "Securely delete file(s) by overwriting with random data." + (interactive) + (when (yes-or-no-p "This will permanently destroy files. Continue? ") + (dwim-shell-command-on-marked-files + "Secure delete" + "shred -vfz -n 3 '<<f>>'" + :utils "shred"))) + + (defun cj/dwim-shell-commands-sanitize-filename () + "Sanitize filename(s) - remove spaces and special characters." + (interactive) + (dwim-shell-command-on-marked-files + "Sanitize filename" + "NEW_NAME=$(echo '<<b>>' | tr ' ' '_' | tr -cd '[:alnum:]._-'); mv '<<f>>' \"$(dirname '<<f>>')/${NEW_NAME}\"" + :utils '("tr" "mv"))) + + (defun cj/dwim-shell-commands-number-files-sequentially () + "Rename files with sequential numbers." + (interactive) + (let ((prefix (read-string "Prefix (optional): ")) + (start (read-number "Start number: " 1))) + (dwim-shell-command-on-marked-files + "Number files" + (format "mv '<<f>>' '<<d>>/%s<<n>>.<<e>>'" prefix) + :utils "mv"))) + + (defun cj/dwim-shell-commands-git-history () + "Show git history for file(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Git history" + "git log --oneline --follow '<<f>>'" + :utils "git")) + + (defun cj/dwim-shell-commands-encrypt-with-gpg () + "Encrypt file(s) with GPG." + (interactive) + (let ((recipient (read-string "Recipient email (or leave empty for symmetric): "))) + (dwim-shell-command-on-marked-files + "GPG encrypt" + (if (string-empty-p recipient) + "gpg --symmetric --cipher-algo AES256 '<<f>>'" + (format "gpg --encrypt --recipient '%s' '<<f>>'" recipient)) + :utils "gpg"))) + + (defun cj/dwim-shell-commands-decrypt-with-gpg () + "Decrypt GPG file(s)." + (interactive) + (dwim-shell-command-on-marked-files + "GPG decrypt" + "gpg --decrypt '<<f>>' > '<<fne>>'" + :extensions '("gpg" "asc" "pgp") + :utils "gpg")) + + +(defun cj/dwim-shell-commands-markdown-to-html5-and-open () + "Convert markdown file to HTML in specified directory and open it." + (interactive) + (let ((files (dwim-shell-command--files))) + ;; verify it's a markdown file + (unless (and files + (= 1 (length files)) + (string-match-p "\\.\\(md\\|markdown\\|mkd\\|mdown\\)\\'" (car files))) + (user-error "Please place cursor on a single markdown file")) + (let* ((dest-dir (expand-file-name (read-directory-name "Destination directory: " default-directory))) + (base-name (file-name-sans-extension (file-name-nondirectory (car files)))) + (output-file (expand-file-name (concat base-name ".html") dest-dir))) + (dwim-shell-command-on-marked-files + "Convert markdown to HTML" + (format "pandoc --standalone --from=markdown --to=html5 --metadata title='<<fne>>' '<<f>>' -o '%s'" + output-file) + :utils "pandoc" + :on-completion (lambda (&rest args) + (when (file-exists-p output-file) + (cj/xdg-open output-file) + (message "Opened %s" output-file))))))) + + + + (defun cj/dwim-shell-commands-kill-gpg-agent () + "Kill (thus restart) gpg agent. + +Useful for when you get this error: +gpg: public key decryption failed: No pinentry +gpg: decryption failed: No pinentry" + (interactive) + (dwim-shell-command-on-marked-files + "Kill gpg agent" + "gpgconf --kill gpg-agent" + :utils "gpgconf" + :silent-success t))) + +(provide 'dwim-shell-config) +;;; dwim-shell-config.el ends here. |
