From: Andreas Gerler Date: Wed, 24 Dec 2025 13:53:24 +0000 (+0100) Subject: update dwim-shell-command X-Git-Url: http://git.bundesbrandschatzamt.de/?a=commitdiff_plain;h=48bee4a04b4496ea78645929c5fe1270a4820173;p=emacsconfig.git update dwim-shell-command --- diff --git a/elpa/dwim-shell-command-20250218.1720/dwim-shell-command-autoloads.el b/elpa/dwim-shell-command-20250218.1720/dwim-shell-command-autoloads.el deleted file mode 100644 index ba4bfd37..00000000 --- a/elpa/dwim-shell-command-20250218.1720/dwim-shell-command-autoloads.el +++ /dev/null @@ -1,337 +0,0 @@ -;;; dwim-shell-command-autoloads.el --- automatically extracted autoloads (do not edit) -*- lexical-binding: t -*- -;; Generated by the `loaddefs-generate' function. - -;; This file is part of GNU Emacs. - -;;; Code: - -(add-to-list 'load-path (or (and load-file-name (directory-file-name (file-name-directory load-file-name))) (car load-path))) - - - -;;; Generated autoloads from dwim-shell-command.el - -(autoload 'dwim-shell-command "dwim-shell-command" "\ -Execute DWIM shell command asynchronously using noweb templates. - -Which files - - `dwim-shell-command' attempts to guess which file(s) you may want - the command to operate on. - - 1. If visiting a `dired' buffer, draw the marked file(s). - 2. If visiting any other buffer with an associated file, use that. - -Templates - - Operate on drawn files using either the following: - - <> (file path) - <> (file path without extension) - <> (extension) - <> (generate a temporary directory) - <<*>> (all files joined) - <> (clipboard) - <>, <<1n>>, or <> (for current iteration) - - For example: - - With drawn files '(\"path/to/image1.png\" \"path/to/image2.png\") - - \"convert '<>' '<>.jpg'\" expands to - - \"convert 'path/to/image1.png' 'path/to/image1.jpg'\" - \"convert 'path/to/image2.png' 'path/to/image2.jpg'\" - - while \"ls -lh <<*>>\" expands to - - \"ls -lh path/to/image1.png path/to/image2.png\" - -Focus - - `dwim-shell-command' creates a process buffer to capture command - output, but doesn't display or focus on it by default. Instead, - it tries to guess what's more convenient to focus on. - - While the process is busy, show a spinner in the minibuffer. No - focus changes. - - After process is finished: - - 1. If there were any files created in the `default-directory', - jump to a `dired' buffer and move point to the new file (via - `dired-jump'). - - 2. If no new files were created, automatically switch focus to the - process buffer and display its output. - - Note: You can prevent this automatic focus by prepending your - command with whitespace. - - | - V - \" convert '<>' '<>.jpg'\" - - 3. If the shell command caused any errors, offer to focus the - process buffer and display its output. - -Quick exit - - Process buffers are read-only and can be quickly closed by - pressing `q'. - -Prefix - - With PREFIX, execute command that number of times. - -(fn PREFIX)" t) -(register-definition-prefixes "dwim-shell-command" '("dwim-shell-command-")) - - -;;; Generated autoloads from dwim-shell-commands.el - -(autoload 'dwim-shell-commands-audio-to-mp3 "dwim-shell-commands" "\ -Convert all marked audio to mp3(s)." t) -(autoload 'dwim-shell-extract-har-content "dwim-shell-commands" "\ -Extract all har content fields to files. - -(fn PREFIX)" t) -(autoload 'dwim-shell-extract-har-urls "dwim-shell-commands" "\ -Get all request URLs." t) -(autoload 'dwim-shell-view-open-ports-per-app "dwim-shell-commands" "\ -View open ports per app" t) -(autoload 'dwim-shell-commands-open-clipboard-url "dwim-shell-commands" "\ -Open clipboard URL. Offer to stream if possible." t) -(autoload 'dwim-shell-commands-stream-clipboard-url "dwim-shell-commands" "\ -Stream clipboard URL using mpv." t) -(autoload 'dwim-shell-commands-download-clipboard-stream-url "dwim-shell-commands" "\ -Download clipboard URL." t) -(autoload 'dwim-shell-commands-image-clear-exif-metadata "dwim-shell-commands" "\ -Clear EXIF metadata in image(s)." t) -(autoload 'dwim-shell-commands-image-scan-code "dwim-shell-commands" "\ -Scan any code (ie. qr, bar, etc) from image(s)." t) -(autoload 'dwim-shell-commands-image-exif-metadata "dwim-shell-commands" "\ -View EXIF metadata in image(s)." t) -(autoload 'dwim-shell-commands-tesseract-ocr-text-from-image "dwim-shell-commands" "\ -Extract text from image via tesseract." t) -(autoload 'dwim-shell-commands-image-view-location-in-openstreetmap "dwim-shell-commands" "\ -Open image(s) location in map/browser." t) -(autoload 'dwim-shell-commands-image-reverse-geocode-location "dwim-shell-commands" "\ -Reverse geocode image(s) location." t) -(autoload 'dwim-shell-commands-image-horizontal-flip "dwim-shell-commands" "\ -Horizontally flip image(s)." t) -(autoload 'dwim-shell-commands-image-vertical-flip "dwim-shell-commands" "\ -Horizontally flip image(s)." t) -(autoload 'dwim-shell-commands-image-to-jpg "dwim-shell-commands" "\ -Convert all marked images to jpg(s)." t) -(autoload 'dwim-shell-commands-image-to-png "dwim-shell-commands" "\ -Convert all marked images to png(s)." t) -(autoload 'dwim-shell-commands-svg-to-png "dwim-shell-commands" "\ -Convert all marked svg(s) to png(s)." t) -(autoload 'dwim-shell-commands-svg-to-favicons "dwim-shell-commands" "\ -Convert svg to common favicons." t) -(autoload 'dwim-shell-commands-make-transparent-png "dwim-shell-commands" "\ -Create a transparent png." t) -(autoload 'dwim-shell-commands-join-as-pdf "dwim-shell-commands" "\ -Join all marked images as a single pdf." t) -(autoload 'dwim-shell-commands-keep-pdf-page "dwim-shell-commands" "\ -Keep a page from pdf." t) -(autoload 'dwim-shell-commands-join-images-horizontally "dwim-shell-commands" "\ -Join all marked images horizontally as a single image." t) -(autoload 'dwim-shell-commands-join-images-vertically "dwim-shell-commands" "\ -Join all marked images vertically as a single image." t) -(autoload 'dwim-shell-commands-image-to-grayscale "dwim-shell-commands" "\ -Convert all marked images to grayscale." t) -(autoload 'dwim-shell-commands-reorient-image "dwim-shell-commands" "\ -Reorient images." t) -(autoload 'dwim-shell-commands-gif-to-video "dwim-shell-commands" "\ -Convert all marked gif(s) to video(s)." t) -(autoload 'dwim-shell-commands-macos-empty-trash "dwim-shell-commands" "\ -Empty macOS trash." t) -(autoload 'dwim-shell-commands-macos-ocr-text-from-desktop-region "dwim-shell-commands" "\ -Select a macOS desktop area to OCR and copy recognized text to kill ring." t) -(autoload 'dwim-shell-commands-macos-ocr-text-from-image "dwim-shell-commands" "\ -OCR file and copy recognized text to kill ring." t) -(autoload 'dwim-shell-commands-macos-convert-to-mp4 "dwim-shell-commands" "\ -Convert to mov to mp4" t) -(autoload 'dwim-shell-commands-video-to-gif "dwim-shell-commands" "\ -Convert all marked videos to gif(s)." t) -(autoload 'dwim-shell-commands-video-to-mov "dwim-shell-commands" "\ -Convert all marked videos to mov(s)." t) -(autoload 'dwim-shell-commands-video-to-webp "dwim-shell-commands" "\ -Convert all marked videos to webp(s)." t) -(autoload 'dwim-shell-commands-webp-to-video "dwim-shell-commands" "\ -Convert all marked webp(s) to video(s)." t) -(autoload 'dwim-shell-commands-webp-to-gif "dwim-shell-commands" "\ -Convert all marked webp(s) to gif(s)." t) -(autoload 'dwim-shell-commands-video-to-hevc-mkv "dwim-shell-commands" "\ -Convert all marked videos to hevc mkv." t) -(autoload 'dwim-shell-commands-video-to-optimized-gif "dwim-shell-commands" "\ -Convert all marked videos to optimized gif(s)." t) -(autoload 'dwim-shell-commands-unzip "dwim-shell-commands" "\ -Unzip all marked archives (of any kind) using `atool'." t) -(autoload 'dwim-shell-commands-zip "dwim-shell-commands" "\ -Zip all marked files into archive.zip." t) -(autoload 'dwim-shell-commands-zip-password-protect "dwim-shell-commands" "\ -Protect/encrypt zip file(s) with password." t) -(autoload 'dwim-shell-commands-optimize-gif "dwim-shell-commands" "\ -Convert all marked videos to optimized gif(s)." t) -(autoload 'dwim-shell-commands-speed-up-gif "dwim-shell-commands" "\ -Speeds up gif(s)." t) -(autoload 'dwim-shell-commands-image-apply-ios-round-corners "dwim-shell-commands" "\ -Apply iOS round corners to image(s)." t) -(autoload 'dwim-shell-commands-clip-round-rect-gif "dwim-shell-commands" "\ -Clip gif(s) with round rectangle." t) -(autoload 'dwim-shell-commands-resize-gif "dwim-shell-commands" "\ -Resize marked gif(s)." t) -(autoload 'dwim-shell-commands-epub-to-org "dwim-shell-commands" "\ -Convert epub(s) to org." t) -(autoload 'dwim-shell-commands-docx-to-pdf "dwim-shell-commands" "\ -Convert docx(s) to pdf (via latex)." t) -(autoload 'dwim-shell-commands-kill-process "dwim-shell-commands" "\ -Select and kill process." t) -(autoload 'dwim-shell-commands-macos-add-to-photos "dwim-shell-commands" "\ -Add to Photos.app." t) -(autoload 'dwim-shell-commands-macos-toggle-bluetooth-device-connection "dwim-shell-commands" "\ -Toggle Bluetooth device connection." t) -(autoload 'dwim-shell-commands-macos-bin-plist-to-xml "dwim-shell-commands" "\ -Convert binary plist to xml." t) -(autoload 'dwim-shell-commands-macos-toggle-dark-mode "dwim-shell-commands" "\ -Toggle macOS dark mode." t) -(autoload 'dwim-shell-commands-macos-toggle-menu-bar-autohide "dwim-shell-commands" "\ -Toggle macOS dark mode." t) -(autoload 'dwim-shell-commands-pdf-to-txt "dwim-shell-commands" "\ -Convert pdf to txt." t) -(autoload 'dwim-shell-commands-resize-image-by-factor "dwim-shell-commands" "\ -Resize marked image(s) by factor." t) -(autoload 'dwim-shell-commands-resize-image-in-pixels "dwim-shell-commands" "\ -Resize marked image(s) in pixels." t) -(autoload 'dwim-shell-commands-pdf-password-protect "dwim-shell-commands" "\ -Add a password to pdf(s)." t) -(autoload 'dwim-shell-commands-pdf-password-unprotect "dwim-shell-commands" "\ -Remove a password from pdf(s)." t) -(autoload 'dwim-shell-commands-video-to-mp3 "dwim-shell-commands" "\ -Convert video(s) to mp3." t) -(autoload 'dwim-shell-commands-video-to-mp3-with-artwork "dwim-shell-commands" "\ -Convert video(s) to mp3 (keep frame as artwork)." t) -(autoload 'dwim-shell-commands-ndjson-to-org "dwim-shell-commands" "\ -Convert ndjson file to org." t) -(autoload 'dwim-shell-commands-set-media-artwork-image-metadata "dwim-shell-commands" "\ -Set image artwork metadata for media file(s)." t) -(autoload 'dwim-shell-commands-video-trim-beginning "dwim-shell-commands" "\ -Drop audio from all marked videos." t) -(autoload 'dwim-shell-commands-video-trim-end "dwim-shell-commands" "\ -Drop audio from all marked videos." t) -(autoload 'dwim-shell-commands-video-to-thumbnail "dwim-shell-commands" "\ -Generate a thumbnail for marked video(s)." t) -(autoload 'dwim-shell-commands-drop-video-audio "dwim-shell-commands" "\ -Drop audio from all marked videos." t) -(autoload 'dwim-shell-commands-ping-google "dwim-shell-commands" "\ -Ping google.com." t) -(autoload 'dwim-shell-commands-speed-up-video "dwim-shell-commands" "\ -Speed up video(s)." t) -(autoload 'dwim-shell-commands-speed-up-video-fragment "dwim-shell-commands" "\ -Speed up fragment in video(s)." t) -(autoload 'dwim-shell-commands-resize-video "dwim-shell-commands" "\ -Resize marked images." t) -(autoload 'dwim-shell-commands-clipboard-to-qr "dwim-shell-commands" "\ -Generate a QR code from clipboard." t) -(autoload 'dwim-shell-commands-sha-256-hash-file-at-clipboard-url "dwim-shell-commands" "\ -Download file at clipboard URL and generate SHA-256 hash." t) -(autoload 'dwim-shell-commands-view-sqlite-schema-diagram "dwim-shell-commands" "\ -View sqlite schema diagram." t) -(autoload 'dwim-shell-commands-open-externally "dwim-shell-commands" "\ -Open file(s) externally." t) -(autoload 'dwim-shell-commands-macos-caffeinate "dwim-shell-commands" "\ -Invoke caffeinate to prevent mac from sleeping." t) -(autoload 'dwim-shell-commands-macos-make-finder-alias "dwim-shell-commands" "\ -Make macOS Finder alias." t) -(autoload 'dwim-shell-commands-macos-version-and-hardware-overview-info "dwim-shell-commands" "\ -View macOS version and hardware overview info." t) -(autoload 'dwim-shell-commands-macos-reveal-in-finder "dwim-shell-commands" "\ -Reveal selected files in macOS Finder." t) -(autoload 'dwim-shell-commands-macos-share "dwim-shell-commands" "\ -Share selected files from macOS." t) -(autoload 'dwim-shell-commands-macos-toggle-display-rotation "dwim-shell-commands" "\ -Rotate display." t) -(autoload 'dwim-shell-commands-make-swift-package-library "dwim-shell-commands" "\ -Create a swift package library" t) -(autoload 'dwim-shell-commands-make-swift-package-executable "dwim-shell-commands" "\ -Create a swift package executable" t) -(autoload 'dwim-shell-commands-macos-set-default-app "dwim-shell-commands" "\ -Set default app for file(s)." t) -(autoload 'dwim-shell-commands-macos-open-with "dwim-shell-commands" "\ -Open file(s) with specific external app." t) -(autoload 'dwim-shell-commands-macos-open-with-firefox "dwim-shell-commands" "\ -Open file(s) in Firefox." t) -(autoload 'dwim-shell-commands-macos-open-with-safari "dwim-shell-commands" "\ -Open file(s) in Safari." t) -(autoload 'dwim-shell-commands-macos-start-recording-window "dwim-shell-commands" "\ -Select and start recording a macOS window." t) -(autoload 'dwim-shell-commands-macos-end-recording-window "dwim-shell-commands" "\ -Stop recording a macOS window." t) -(autoload 'dwim-shell-commands-macos-abort-recording-window "dwim-shell-commands" "\ -Stop recording a macOS window." t) -(autoload 'dwim-shell-commands-macos-screenshot-window "dwim-shell-commands" "\ -Select and screenshot macOS window." t) -(autoload 'dwim-shell-commands-files-combined-size "dwim-shell-commands" "\ -Get files combined file size." t) -(autoload 'dwim-shell-commands-image-to-icns "dwim-shell-commands" "\ -Convert png to icns icon." t) -(autoload 'dwim-shell-commands-image-add-drop-shadow "dwim-shell-commands" "\ -Add a drop shadow." t) -(autoload 'dwim-shell-commands-image-trim-borders "dwim-shell-commands" "\ -Trim image(s) border (useful for video screenshots)." t) -(autoload 'dwim-shell-commands-git-clone-clipboard-url-to-downloads "dwim-shell-commands" "\ -Clone git URL in clipboard to \"~/Downloads/\"." t) -(autoload 'dwim-shell-commands-http-serve-dir "dwim-shell-commands" "\ -HTTP serve current directory." t) -(autoload 'dwim-shell-commands-git-clone-clipboard-url "dwim-shell-commands" "\ -Clone git URL in clipboard to `default-directory'." t) -(autoload 'dwim-shell-commands-pass-git-pull "dwim-shell-commands" "\ -Pass git pull." t) -(autoload 'dwim-shell-commands-git-list-untracked-files "dwim-shell-commands" "\ -List untracked git files in `default-directory'." t) -(autoload 'dwim-shell-commands-git-delete-untracked-files "dwim-shell-commands" "\ -Delete untracked git files in `default-directory'." t) -(autoload 'dwim-shell-commands-external-ip "dwim-shell-commands" "\ -Copy external IP to kill ring." t) -(autoload 'dwim-shell-commands-macos-install-iphone-device-ipa "dwim-shell-commands" "\ -Install iPhone device .ipa. -Needs ideviceinstaller and libmobiledevice installed." t) -(autoload 'dwim-shell-commands-copy-to-downloads "dwim-shell-commands" "\ -Copy file to ~/Downloads." t) -(autoload 'dwim-shell-commands-duplicate "dwim-shell-commands" "\ -Duplicate file." t) -(autoload 'dwim-shell-commands-rename-all "dwim-shell-commands" "\ -Rename all marked file(s)." t) -(autoload 'dwim-shell-commands-move-to-downloads "dwim-shell-commands" "\ -Move file to ~/Downloads." t) -(autoload 'dwim-shell-commands-copy-to-desktop "dwim-shell-commands" "\ -Copy file to ~/Desktop." t) -(autoload 'dwim-shell-commands-move-to-desktop "dwim-shell-commands" "\ -Move file to ~/Desktop." t) -(autoload 'dwim-shell-commands-kill-gpg-agent "dwim-shell-commands" "\ -Kill (thus restart) gpg agent. - -Useful for when you get this error: - -gpg: public key decryption failed: No pinentry -gpg: decryption failed: No pinentry" t) -(autoload 'dwim-shell-commands-upload-to-0x0 "dwim-shell-commands" "\ -Upload the marked files to 0x0.st" t) -(register-definition-prefixes "dwim-shell-commands" '("dwim-shell-commands-")) - -;;; End of scraped data - -(provide 'dwim-shell-command-autoloads) - -;; Local Variables: -;; version-control: never -;; no-byte-compile: t -;; no-update-autoloads: t -;; no-native-compile: t -;; coding: utf-8-emacs-unix -;; End: - -;;; dwim-shell-command-autoloads.el ends here diff --git a/elpa/dwim-shell-command-20250218.1720/dwim-shell-command-pkg.el b/elpa/dwim-shell-command-20250218.1720/dwim-shell-command-pkg.el deleted file mode 100644 index 4b0f089c..00000000 --- a/elpa/dwim-shell-command-20250218.1720/dwim-shell-command-pkg.el +++ /dev/null @@ -1,7 +0,0 @@ -;; -*- no-byte-compile: t; lexical-binding: nil -*- -(define-package "dwim-shell-command" "20250218.1720" - "Shell commands with DWIM behaviour." - '((emacs "28.1")) - :url "https://github.com/xenodium/dwim-shell-command" - :commit "4b077432a94873e5f505c8f569743cfd984eebb1" - :revdesc "4b077432a948") diff --git a/elpa/dwim-shell-command-20250218.1720/dwim-shell-command.el b/elpa/dwim-shell-command-20250218.1720/dwim-shell-command.el deleted file mode 100644 index 425cacc1..00000000 --- a/elpa/dwim-shell-command-20250218.1720/dwim-shell-command.el +++ /dev/null @@ -1,1072 +0,0 @@ -;;; dwim-shell-command.el --- Shell commands with DWIM behaviour -*- lexical-binding: t; -*- - -;; Copyright (C) 2022 Alvaro Ramirez https://xenodium.com - -;; Author: Alvaro Ramirez -;; Package-Requires: ((emacs "28.1")) -;; URL: https://github.com/xenodium/dwim-shell-command -;; Package-Version: 20250218.1720 -;; Package-Revision: 4b077432a948 - -;; This package 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, or (at your option) -;; any later version. - -;; This package 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. - -;; You should have received a copy of the GNU General Public License -;; along with GNU Emacs. If not, see . - -;;; Commentary: - -;; Provides `dwim-shell-command' as an opinionated DWIM alternative to -;; `shell-command'. -;; -;; Use `dwim-shell-command-on-marked-files' to create your own command -;; line utilities, invoked via M-x. -;; -;; See examples at https://github.com/xenodium/dwim-shell-command/blob/main/dwim-shell-commands.el - -;;; Code: - -(require 'cl-lib) -(require 'comint) -(require 'dired) -(require 'dired-aux) -(require 'map) -(require 'seq) -(require 'simple) -(require 'subr-x) -(require 'view) - -(defcustom dwim-shell-command-prompt - "DWIM shell command (<> <> <>): " - "`dwim-shell-command' prompt. Modify if shorter is preferred." - :type 'string - :group 'dwim-shell-command) - -(defcustom dwim-shell-command-default-command - " '<>'" - "Set to nil if no default shell command wanted." - :type 'string - :group 'dwim-shell-command) - -(defcustom dwim-shell-command-buffer-name - "DWIM shell command" - "`dwim-shell-command' buffer name. Modify if shorter is preferred." - :type 'string - :group 'dwim-shell-command) - -(defcustom dwim-shell-command-prompt-on-error nil - "If t, prompt user to focus buffer on process error. -Otherwise, automatically focus buffer on process error." - :type 'boolean - :group 'dwim-shell-command) - -(defcustom dwim-shell-command-use-absolute-paths nil - "If t, generate absolute paths in templates. Relative otherwise." - :type 'boolean - :group 'dwim-shell-command) - -(defcustom dwim-shell-command-shell-util nil - "Shell util, for example: \"zsh\" or \"bash\". -Set to nil to use `shell-file-name'." - :type 'string - :group 'dwim-shell-command) - -(defcustom dwim-shell-command-shell-args nil - "Shell util, for example: '(\"-x\" \"-c\"). -Set to nil to use `shell-command-switch'." - :type '(repeat string) - :group 'dwim-shell-command) - -(defcustom dwim-shell-command-shell-trace nil - "Attempt to add --xtrace to shell command to debug." - :type 'boolean - :group 'dwim-shell-command) - -(defcustom dwim-shell-command-done-buffer-name - (lambda (name) - (format "%s %s" name (propertize "done" 'face 'success))) - "Function to format buffer name on success. -Use `identify' to remove formatting." - :type 'function - :group 'dwim-shell-command) - -(defcustom dwim-shell-command-error-buffer-name - (lambda (name) - (format "%s %s" name (propertize "error" 'face 'error))) - "Function to format buffer name on error. -Use `identify' to remove formatting." - :type 'function - :group 'dwim-shell-command) - -(defvar dwim-shell-command--commands nil "All commands in progress.") - -(cl-defstruct - dwim-shell-command--command - "Describes a command in progress." - script - process - name - calling-buffer - reporter - on-completion - files-before - silent-success - error-autofocus - monitor-directory) - -;;;###autoload -(defun dwim-shell-command (prefix) - "Execute DWIM shell command asynchronously using noweb templates. - -Which files - - `dwim-shell-command' attempts to guess which file(s) you may want - the command to operate on. - - 1. If visiting a `dired' buffer, draw the marked file(s). - 2. If visiting any other buffer with an associated file, use that. - -Templates - - Operate on drawn files using either the following: - - <> (file path) - <> (file path without extension) - <> (extension) - <> (generate a temporary directory) - <<*>> (all files joined) - <> (clipboard) - <>, <<1n>>, or <> (for current iteration) - - For example: - - With drawn files '(\"path/to/image1.png\" \"path/to/image2.png\") - - \"convert '<>' '<>.jpg'\" expands to - - \"convert 'path/to/image1.png' 'path/to/image1.jpg'\" - \"convert 'path/to/image2.png' 'path/to/image2.jpg'\" - - while \"ls -lh <<*>>\" expands to - - \"ls -lh path/to/image1.png path/to/image2.png\" - -Focus - - `dwim-shell-command' creates a process buffer to capture command - output, but doesn't display or focus on it by default. Instead, - it tries to guess what's more convenient to focus on. - - While the process is busy, show a spinner in the minibuffer. No - focus changes. - - After process is finished: - - 1. If there were any files created in the `default-directory', - jump to a `dired' buffer and move point to the new file (via - `dired-jump'). - - 2. If no new files were created, automatically switch focus to the - process buffer and display its output. - - Note: You can prevent this automatic focus by prepending your - command with whitespace. - - | - V - \" convert '<>' '<>.jpg'\" - - 3. If the shell command caused any errors, offer to focus the - process buffer and display its output. - -Quick exit - - Process buffers are read-only and can be quickly closed by - pressing `q'. - -Prefix - - With PREFIX, execute command that number of times." - (interactive "p") - (let ((script (dwim-shell-command--read-shell-command))) - (dwim-shell-command-on-marked-files - dwim-shell-command-buffer-name script - :repeat prefix - :shell-util dwim-shell-command-shell-util - :shell-args dwim-shell-command-shell-args - :silent-success (string-prefix-p " " script) - :error-autofocus (not dwim-shell-command-prompt-on-error)))) - -(defun dwim-shell-command--read-shell-command () - "Read a shell command from the minibuffer, using `shell-command-history'." - (minibuffer-with-setup-hook - (lambda () - (beginning-of-line) - (setq-local minibuffer-default-add-function - #'minibuffer-default-add-shell-commands)) - (read-from-minibuffer dwim-shell-command-prompt dwim-shell-command-default-command nil nil 'shell-command-history))) - -(cl-defun dwim-shell-command-on-marked-files (buffer-name script &key utils extensions shell-util shell-args shell-trace shell-pipe post-process-template on-completion repeat silent-success no-progress error-autofocus monitor-directory focus-now join-separator temp-dir) - "Create DWIM utilities executing templated SCRIPT on given files. - -Here's a simple utility invoking SCRIPT to convert image files to jpg. - - (defun dwim-shell-command-convert-image-to-jpg () - \"Convert all marked images to jpg(s).\" - (interactive) - (dwim-shell-command-on-marked-files - \"Convert to jpg\" - \"convert -verbose '<>' '<>.jpg'\" - :utils \"convert\")) - -Check `dwim-shell-command-commands.el' for more examples. - -All command process output is written to a buffer with BUFFER-NAME. - -All params explained in `dwim-shell-command-execute-script'. - -Which files - - `dwim-shell-command-on-marked-files' attempts to guess which file(s) - you may want the command to operate on. - - 1. If visiting a `dired' buffer, draw the marked file(s). - 2. If visiting any other buffer with an associated file, use that. - -Templates - - Operate on drawn files using either the following: - - <> (file path) - <> (file path without extension) - <> (extension) - <> (generate a temporary directory) - <<*>> (all files joined) - <> (clipboard) - <>, <<1n>>, or <> (for current iteration) - - For example: - - With drawn files '(\"path/to/image1.png\" \"path/to/image2.png\") - - \"convert '<>' '<>.jpg'\" expands to - - \"convert 'path/to/image1.png' 'path/to/image1.jpg'\" - \"convert 'path/to/image2.png' 'path/to/image2.jpg'\" - - while \"ls -lh <<*>>\" expands to - - \"ls -lh path/to/image1.png path/to/image2.png\" - -Focus - - `dwim-shell-command-on-marked-files' creates a process buffer to - capture command output, but doesn't display or focus on it by - default. Instead, it tries to guess what's more convenient to focus - on. - - While the process is busy, show a spinner in the minibuffer. No - focus changes. - - After process is finished: - - 1. If there were any files created in the `default-directory', - jump to a `dired' buffer and move point to the new file (via - `dired-jump'). - - 2. If no new files were created, automatically switch focus to the - process buffer and display its output. - - Note: You can prevent this automatic focus by prepending your - command with whitespace. - - | - V - \" convert '<>' '<>.jpg'\" - - 3. If the shell command caused any errors, offer to focus the - process buffer and display its output. - -Quick exit - - Process buffers are read-only and can be quickly closed by - pressing `q'." - (dwim-shell-command-execute-script buffer-name script - :files (dwim-shell-command--files) - :utils utils - :extensions extensions - :shell-util shell-util - :shell-args shell-args - :shell-trace shell-trace - :shell-pipe shell-pipe - :post-process-template post-process-template - :on-completion on-completion - :silent-success silent-success - :no-progress no-progress - :repeat repeat - :error-autofocus error-autofocus - :monitor-directory monitor-directory - :focus-now focus-now - :join-separator join-separator - :temp-dir temp-dir)) - -(cl-defun dwim-shell-command-execute-script (buffer-name script &key files extensions shell-util shell-args shell-trace shell-pipe utils post-process-template on-completion silent-success temp-dir repeat no-progress error-autofocus monitor-directory focus-now join-separator) - "Execute a script asynchronously, DWIM style with SCRIPT and BUFFER-NAME. - -:FILES are used to instantiate SCRIPT as a noweb template. - - The following are supported: - - <> (file path) - <> (file path without extension) - <> (extension) - <> (generate a temporary directory) - <<*>> (all files joined) - <> (clipboard) - <>, <<1n>>, or <> (for current iteration) - - For example: - - Given :FILES '(\"path/to/image1.png\" \"path/to/image2.png\") - - \"convert '<>' '<>.jpg'\" expands to - - \"convert 'path/to/image1.png' 'path/to/image1.jpg'\" - \"convert 'path/to/image2.png' 'path/to/image2.jpg'\" - - and \"ls -lh <<*>>\" expands to - - \"ls -lh path/to/image1.png path/to/image2.png\" - -:EXTENSIONS ensures that all files in :FILES have the given -extensions. Can be either single string \"png\" or a list '(\"png\" \"jpg\"). - -:SHELL-UTIL and :SHELL-ARGS can be used to specify SCRIPT interpreter. - - For python, use: - - (dwim-shell-command-execute-script - \"Print Pi\" - \"import math - print math.pi\" - :shell-util \"python\" - :shell-args \"-c\") - -:SHELL-PIPE can be used to pipe SCRIPT to it - - For swift, use: - - (dwim-shell-command-on-marked-files - \"Print Pi\" - \"print(Double.pi)\" - :shell-pipe \"swift -\") - -:UTILS ensures that all needed command line utilities are installed. -Can be either a single string \"ffmpeg\" or a list '(\"ffmpet\" \"convert\"). - -:POST-PROCESS-TEMPLATE enables processing template further after noweb -instantiation. - -:ON-COMPLETION is invoked after SCRIPT executes (disabling DWIM -internal behavior). - -:SILENT-SUCCESS to avoid jumping to process buffer if neither error - nor file generated. - -:TEMP-DIR to generate a temporary directory for this command. -This is implied when <> appears in the script. - -:REPEAT Use to repeat script N number of times. - -:NO-PROGRESS Suppress progress reporting. - -:ERROR-AUTOFOCUS Automatically focus process buffer on error. - -:MONITOR-DIRECTORY Monitor this directory for new files. - -:FOCUS-NOW Immediately focus process buffer once started." - (cl-assert buffer-name nil "Script must have a buffer name") - (cl-assert (not (string-empty-p script)) nil "Script must not be empty") - (when (stringp extensions) - (setq extensions (list extensions))) - (when (and shell-util (stringp shell-util)) - (setq shell-util (list shell-util))) - (setq shell-util (or shell-util - (when shell-file-name - (list shell-file-name)) - '("zsh"))) - (setq shell-args (or shell-args - (when shell-command-switch - (list shell-command-switch)) - '("-x" "-c"))) - (when (and shell-args (stringp shell-args)) - (setq shell-args (list shell-args))) - ;; See if -x can be prepended. - (when (and (not (seq-contains-p shell-args "-x")) - (or shell-trace dwim-shell-command-shell-trace) - (apply #'dwim-shell-command--program-test - (seq-concatenate - 'list shell-util '("-x") shell-args (list "echo")))) - (setq shell-args (seq-concatenate 'list '("-x") shell-args))) - (when (stringp utils) - (setq utils (list utils))) - (when (and (string-match-p "\<\\>" script 0) (not temp-dir)) - (setq temp-dir (make-temp-file "dwim-shell-command-" t))) - (when (and repeat (> repeat 1)) - (cl-assert (<= (length files) 1) nil - "Must not repeat when multiple files are selected.") - (setq files (make-list repeat (or (seq-first files) "_no_file_selected_")))) - (when (seq-empty-p files) - (cl-assert (not (or (dwim-shell-command--contains-multi-file-ref script) - (dwim-shell-command--contains-single-file-ref script))) - nil "No files found to expand %s" - (or (dwim-shell-command--contains-multi-file-ref script) - (dwim-shell-command--contains-single-file-ref script)))) - (when extensions - (seq-do (lambda (file) - (cl-assert (seq-contains-p extensions (downcase (file-name-extension file))) - nil "Not a .%s file" (string-join extensions " ."))) - files)) - (seq-do (lambda (util) - (cl-assert (executable-find util) nil (format "%s not installed" util))) - utils) - (let* ((replacements (dwim-shell-command--extract-queries script)) - (proc-buffer (generate-new-buffer (format "*%s*" buffer-name))) - (template script) - (script "") - (files-before) - (proc) - (progress-reporter) - (padding (dwim-shell-command--digits (length files))) - (n (or (dwim-shell-command--n-start-value template padding) "1"))) - (if (seq-empty-p files) - (setq script (dwim-shell-command--expand-file-template template nil post-process-template temp-dir n replacements)) - (if (dwim-shell-command--contains-multi-file-ref template) - (setq script (dwim-shell-command--expand-files-template template files post-process-template temp-dir replacements join-separator)) - (seq-do (lambda (file) - (setq script - (concat script "\n" - (dwim-shell-command--expand-file-template template file post-process-template temp-dir n replacements))) - (setq n (dwim-shell-command--increment-string n padding))) - files))) - (setq script (string-trim script)) - (with-current-buffer proc-buffer - (let ((inhibit-message t)) - ;; Silence noise of entering shell-mode. - (comint-mode)) - (setq default-directory default-directory) - (shell-command-save-pos-or-erase) - (view-mode +1) - (setq view-exit-action 'kill-buffer)) - (setq files-before (dwim-shell-command--default-directory-files monitor-directory)) - (setq proc (apply #'start-process (seq-concatenate 'list - (list (buffer-name proc-buffer) proc-buffer) - shell-util - shell-args - (if shell-pipe - (list (format "echo '%s' | %s" script shell-pipe)) - (list script))))) - (set-process-query-on-exit-flag proc nil) - (if no-progress - (dwim-shell-command--message "%s started" (process-name proc)) - (setq progress-reporter (make-progress-reporter - ;; Append space so "done" is spaced when - ;; progress reporter is finished: - ;; - ;; *DWIM shell command* done - (concat (process-name proc) " "))) - (progress-reporter-update progress-reporter)) - ;; Momentarily set buffer to same window, so it's next in recent stack. - ;; Makes finding the shell command buffer a lot easier. - (let ((current (current-buffer))) - (pop-to-buffer-same-window proc-buffer) - (pop-to-buffer-same-window current)) - (when focus-now - (switch-to-buffer proc-buffer)) - (if (equal (process-status proc) 'exit) - (dwim-shell-command--finalize (current-buffer) - files-before - proc - progress-reporter - on-completion - silent-success - error-autofocus - monitor-directory) - (setq dwim-shell-command--commands - (push (cons (process-name proc) - (make-dwim-shell-command--command :script script - :process proc - :name (process-name proc) - :calling-buffer (current-buffer) - :files-before files-before - :reporter progress-reporter - :on-completion on-completion - :silent-success silent-success - :error-autofocus error-autofocus - :monitor-directory monitor-directory)) - dwim-shell-command--commands)) - (set-process-sentinel proc #'dwim-shell-command--sentinel) - (set-process-filter proc #'dwim-shell-command--filter)))) - -(cl-defun dwim-shell-command-read-file-name (prompt &key extension default) - "Invoke `read-string' with PROMPT. -Validates :EXTENSION and returns :DEFAULT if empty input." - (let ((file-name (read-string prompt))) - (cond ((string-empty-p (string-trim file-name)) - default) - ((and extension - (string-equal (file-name-extension file-name) extension)) - file-name) - ((and extension - (not (string-equal (file-name-extension file-name) extension))) - (user-error "Name must end in .%s" extension)) - (t - file-name)))) - -(defun dwim-shell-command--message (message &rest args) - "Like `dwim-shell-command--message' but non-blocking. -MESSAGE and ARGS same as `dwim-shell-command--message'." - (let ((message-id (random))) - (message (propertize (apply #'format message args) 'message-id message-id)) - (run-with-timer 3 nil - (lambda () - (when (and (current-message) - (eq (get-text-property 0 'message-id (current-message)) - message-id)) - (message nil)))))) - -(defun dwim-shell-command--extract-queries (template) - "Extract queries from TEMPLATE. - -For all queries, request a value from the user. - -For example: - - \"Hello <> world <>\" => - - ((\"<>\" . \"100\") - (\"<>\" . \"200\"))" - (let ((matches) - (pos 0)) - (while (and (< pos (length template)) - (string-match "<<\\([[:alpha:]]\\|[[:blank:]]\\)+:\\([[:alnum:]]\\|[.]\\)*>>" template pos)) - (setq pos (1+ (match-beginning 0))) - (let ((match 0)) - (push (match-string match template) matches) - (setq match (1+ match)))) - (seq-map (lambda (match) - (let* ((query (split-string (string-remove-suffix ">>" (string-remove-prefix "<<" match)) ":")) - (prompt (nth 0 query)) - (default-value (if (string-empty-p (nth 1 query)) - nil - (nth 1 query))) - (value (if (string-match-p "^\\([[:digit:]]\\|[.]\\)+$" default-value) - (number-to-string (read-number (format "%s: " prompt) - (string-to-number default-value))) - (string-trim (read-string (concat prompt - (if default-value - (format " (default %s): " default-value) - ": ")))))) - (result (cons match (if (string-empty-p value) - default-value - value)))) - (cl-assert (cdr result) nil "Must have a value")result)) - (seq-uniq (nreverse matches))))) - -(defun dwim-shell-command--digits (n) - "Return the number of digits in N." - (let ((count 0)) - (while (> n 0) - (setq n (/ n 10)) - (setq count (1+ count))) - count)) - -(defun dwim-shell-command--number-to-string (n padding) - "Convert N to string using PADDING for number of digits." - (format (format "%%0%dd" padding) n)) - -(defun dwim-shell-command--expand-files-template (template files &optional post-process-template temp-dir replacements join-separator) - "Expand TEMPLATE using FILES. - -Expand using <<*>> for FILES. - -Note: This expander cannot be used to expand <>, <>, or <>. - - For example: - - Given FILES '(\"path/to/image1.png\" \"path/to/image2.png\") - - \"du -csh '<<*>>'\" expands to - - \"du -csh 'path/to/image1.png' 'path/to/image2.png'\" - -Use POST-PROCESS-TEMPLATE to further expand template given own logic. - -Set TEMP-DIR to a unique temp directory to this template. - -REPLACEMENTS is a cons list of literals to replace with values. - -JOIN-SEPARATOR is used to join files from <<*>>." - (cl-assert (not (and (dwim-shell-command--contains-multi-file-ref template) - (dwim-shell-command--contains-single-file-ref template))) - nil "Must not have %s and %s in the same template" - (dwim-shell-command--contains-multi-file-ref template) - (dwim-shell-command--contains-single-file-ref template)) - (setq files (seq-map (lambda (file) - (if dwim-shell-command-use-absolute-paths - (expand-file-name file) - (file-relative-name (expand-file-name file) default-directory))) - files)) - - (mapc (lambda (replacement) - (setq template - (string-replace (car replacement) (cdr replacement) template))) - replacements) - - ;; Try to use quotes surrounding <<*>> in each path. - ;; "'<<*>>'" with '("path/to/image1.png" "path/to/image2.png") -> "'path/to/image1.png' 'path/to/image2.png'" - (when-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\<\\*\>\>" template)) - (unescaped-quote (nth 0 quoting)) - (escaped-quote (nth 1 quoting))) - (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\<\\*\>\>\\)\\([^ ]\\)" - (string-join (seq-map (lambda (file) - (concat unescaped-quote - (string-replace unescaped-quote - escaped-quote file) unescaped-quote)) - files) - (or join-separator " ")) - template nil nil 0))) - - ;; "<>" -> some.txt (if unique) - ;; -> some(1).txt (if it exist) - (when-let* ((found (string-match "\<\<\\([^ ]?+\\)(u)\>\>" template)) - (name (match-string 1 template))) - (setq template (replace-regexp-in-string "\<\<\\([^ ]?+\\)(u)\>\>" - (dwim-shell-command--unique-new-file-path name) - template nil nil 0))) - - ;; "<<~>>" -> "/home/user" (or equivalent). - (when (string-match "\<\<~\>\>" template) - (setq template (replace-regexp-in-string "\<\<~\>\>" - (expand-file-name "~") - template nil nil 0))) - - ;; "<<*>>" with '("path/to/image1.png" "path/to/image2.png") -> "path/to/image1.png path/to/image2.png" - (setq template (replace-regexp-in-string "\\(\<\<\\*\>\>\\)" (string-join files (or join-separator " ")) template nil nil 1)) - - ;; "<>" with TEMP-DIR -> "/var/folders/m7/ky091cp56d5g68nyhl4y7frc0000gn/T/dwim-shell-command-JNK4V5" - (setq template (replace-regexp-in-string "\\(\<\\>\\)" temp-dir template nil nil 1)) - - ;; "<>" with (current-kill 0) -> "whatever was in kill ring" - (when (string-match "\<\\>" template) - (setq template (replace-regexp-in-string "\\(\<\\>\\)" (current-kill 0) - template nil nil 1))) - - (when post-process-template - (setq template (funcall post-process-template template files))) - template) - -(defun dwim-shell-command--escaped-quote-around (needle haystack &optional unbalanced) - "Find NEEDLE in HAYSTACK that's surrounded by either ' or \". - -Set UNBALANCED to t if NEEDLE isn't surrounded by quotes on both sides. - -For example: - - \"\<\\>\" \"before \"<>\" after\" => (\"\"\" \"\\\\\"\") - - \"\<\\>\" \"before '<>' after\" => (\"'\" \"'\"'\"'\")" - (when-let ((found (string-match (format "\\([^ ]\\)\\(%s\\)\\([^ ]\\)" needle) haystack)) - (unescaped-quote (if unbalanced - (or (match-string 1 haystack) - (match-string 3 haystack)) - (cl-assert (string-equal (match-string 1 haystack) - (match-string 3 haystack)) nil - "%s must match %s" - (match-string 1 haystack) - (match-string 3 haystack)) - (match-string 1 haystack))) - (escaped-quote "'")) - ;; Known quoted quotes. - (cond - ((string-equal unescaped-quote "\"") - (setq escaped-quote "\\\\\"")) - ((string-equal unescaped-quote "'") - (setq escaped-quote "'\"'\"'")) - ;; Ignore slashes as user may be joining paths. - ((string-equal unescaped-quote "/") - (setq escaped-quote "/")) - (t - (error "Couldn't figure out how to quote for \"%s\" using %s and %s" - haystack - (match-string 1 haystack) - (match-string 3 haystack)))) - (list unescaped-quote escaped-quote))) - -(defun dwim-shell-command--expand-file-template (template file &optional post-process-template temp-dir current replacements) - "Expand TEMPLATE using FILE. - -Expand using <> for FILE, <> for FILE without extension, and -<> for FILE extension. <>, <<1n>>, or <> is replaced with -CURRENT. <> expands to unique \"some(1).txt\". - -Note: This expander cannot be used to expand <<*>>. - - For example: - - Given FILE \"path/to/image.png\" - - \"convert '<>' '<>.jpg'\" expands to - - \"convert 'path/to/image.png' 'path/to/image.jpg'\" - -Use POST-PROCESS-TEMPLATE to further expand template given own logic. - -Set TEMP-DIR to a unique temp directory to this template. - -REPLACEMENTS is a cons list of literals to replace with values." - (cl-assert (not (and (dwim-shell-command--contains-multi-file-ref template) - (dwim-shell-command--contains-single-file-ref template))) - nil "Must not have %s and %s in the same template" - (dwim-shell-command--contains-multi-file-ref template) - (dwim-shell-command--contains-single-file-ref template)) - - (mapc (lambda (replacement) - (setq template - (string-replace (car replacement) (cdr replacement) template))) - replacements) - - (when file - (setq file (if dwim-shell-command-use-absolute-paths - (expand-file-name file) - (file-relative-name (expand-file-name file) default-directory))) - ;; "<>" with "/path/tmp.txt" -> "/path/tmp" - (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\\>" template t)) - (unescaped-quote (nth 0 quoting)) - (escaped-quote (nth 1 quoting))) - (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\\>\\)" - (string-replace unescaped-quote escaped-quote (file-name-sans-extension file)) - template nil nil 2)) - (setq template (replace-regexp-in-string "\\(\<\\>\\)" (file-name-sans-extension file) template nil nil 1))) - - ;; "<>" with "/path/tmp.txt" -> "tmp.txt" - (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\\>" template t)) - (unescaped-quote (nth 0 quoting)) - (escaped-quote (nth 1 quoting))) - (setq template (replace-regexp-in-string "\\(\<\\>\\)\\([^ ]\\)" - (string-replace unescaped-quote escaped-quote (file-name-nondirectory file)) - template nil nil 1)) - (setq template (replace-regexp-in-string "\\(\<\\>\\)" (file-name-nondirectory file) template nil nil 1))) - - ;; "<>" with "/path/tmp.txt" -> "tmp" - (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\\>" template t)) - (unescaped-quote (nth 0 quoting)) - (escaped-quote (nth 1 quoting))) - (setq template (replace-regexp-in-string "\\(\<\\>\\)\\([^ ]\\)" - (string-replace unescaped-quote escaped-quote - (file-name-sans-extension (file-name-nondirectory file))) - template nil nil 1)) - (setq template (replace-regexp-in-string "\\(\<\\>\\)" (file-name-sans-extension - (file-name-nondirectory file)) template nil nil 1))) - - ;; "<>" with "/path/tmp.txt" -> "txt" - (if (file-name-extension file) - (setq template (replace-regexp-in-string "\\(\<\\>\\)" (file-name-extension file) template nil nil 1)) - ;; File had no extension. Attempt to remove .<>. - (setq template (replace-regexp-in-string "\\(\.\<\\>\\)" "" template nil nil 1))) - - ;; "<>" with "/path/file.jpg" -> "/path/file.jpg" - (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\\>" template)) - (unescaped-quote (nth 0 quoting)) - (escaped-quote (nth 1 quoting))) - (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\\>\\)\\([^ ]\\)" - (string-replace unescaped-quote escaped-quote file) - template nil nil 2)) - (setq template (replace-regexp-in-string "\\(\<\\>\\)" file template nil nil 1))) - - ;; "<>" with "/path/file.jpg" -> "/path/file(1).jpg" - (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\\>" template)) - (unescaped-quote (nth 0 quoting)) - (escaped-quote (nth 1 quoting))) - (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\\>\\)\\([^ ]\\)" - (string-replace unescaped-quote escaped-quote - (dwim-shell-command--unique-new-file-path file)) - template nil nil 2)) - (setq template (replace-regexp-in-string "\\(\<\\>\\)" - (dwim-shell-command--unique-new-file-path file) template nil nil 1)))) - - ;; "<>" -> some.txt (if unique) - ;; -> some(1).txt (if it exist) - (when-let* ((found (string-match "\<\<\\([^ ]?+\\)(u)\>\>" template)) - (name (match-string 1 template))) - (setq template (replace-regexp-in-string "\<\<\\([^ ]?+\\)(u)\>\>" - (dwim-shell-command--unique-new-file-path name) - template nil nil 0))) - - ;; "<<~>>" -> "/home/user" (or equivalent). - (when (string-match "\<\<~\>\>" template) - (setq template (replace-regexp-in-string "\<\<~\>\>" - (expand-file-name "~") - template nil nil 0))) - - ;; "<>" with TEMP-DIR -> "/var/folders/m7/ky091cp56d5g68nyhl4y7frc0000gn/T/dwim-shell-command-JNK4V5" - (setq template (replace-regexp-in-string "\\(\<\\>\\)" temp-dir template nil nil 1)) - - ;; "<>" with (current-kill 0) -> "whatever was in kill ring" - (when (string-match "\<\\>" template) - (setq template (replace-regexp-in-string "\\(\<\\>\\)" (current-kill 0) - template nil nil 1))) - - ;; "<>" or "<" or "<<1n>" with current. - (setq template (replace-regexp-in-string "\\(\<\<[[:alnum:]]?+n\>\>\\)" current - template nil nil 1)) - - (when post-process-template - (setq template (funcall post-process-template template file))) - template) - -(defun dwim-shell-command--contains-single-file-ref (template) - "Check for <>, <>, or <> in TEMPLATE." - (cond ((string-match "\<\\>" template) - "<>") - ((string-match "\<\\>" template) - "<>") - ((string-match "\<\\>" template) - "<>") - ((string-match "\<\\>" template) - "<>") - ((string-match "\<\\>" template) - "<>"))) - -(defun dwim-shell-command--contains-multi-file-ref (template) - "Check for <<*>> in TEMPLATE." - (when (string-match "\<\<\\*\>\>" template) - "<<*>>")) - -(defun dwim-shell-command--default-directory-files (override) - "List of files in current buffer's `default-directory'. -Use OVERRIDE to override `default-directory'." - (when-let ((default-directory (or override default-directory))) - (seq-map (lambda (filename) - (file-name-concat default-directory filename)) - (cl-remove-if - (lambda (e) (member e '("." ".."))) - (directory-files default-directory))))) - -(defun dwim-shell-command--last-modified-between (before after) - "Compare files in BEFORE and AFTER and return oldest file in diff." - (car (last (seq-sort #'file-newer-than-file-p - (seq-difference after before))))) - -(defun dwim-shell-command--finalize (calling-buffer files-before process progress-reporter on-completion silent-success error-autofocus monitor-directory) - "Finalize script execution. - -CALLING-BUFFER, FILES-BEFORE, PROCESS, PROGRESS-REPORTER, -ERROR-AUTOFOCUS, ON-COMPLETION, SILENT-SUCCESS, and MONITOR-DIRECTORY are -all needed to finalize processing." - (let ((oldest-new-file) - (files-after)) - (when progress-reporter - (progress-reporter-done progress-reporter)) - (if (= (process-exit-status process) 0) - (progn - (dwim-shell-command--message (funcall dwim-shell-command-done-buffer-name (process-name process))) - (with-current-buffer (process-buffer process) - (rename-buffer (generate-new-buffer-name (funcall dwim-shell-command-done-buffer-name (process-name process))))) - (if on-completion - (funcall on-completion (process-buffer process) process) - (with-current-buffer calling-buffer - (if (equal major-mode 'dired-mode) - (progn (when revert-buffer-function - (funcall revert-buffer-function nil t)) - ;; Region is not accurate if new files added. Wipe it. - (when (use-region-p) - (deactivate-mark))) - (when (and (or buffer-auto-save-file-name - buffer-file-name) - (not (verify-visited-file-modtime))) - ;; Already visiting a file. Revert if modified by command. - (revert-buffer :ignore-auto :noconfirm))) - (with-current-buffer (process-buffer process) - (setq files-after (dwim-shell-command--default-directory-files monitor-directory))) - (setq oldest-new-file - (dwim-shell-command--last-modified-between - files-before - files-after)) - ;; There's at least one new file. Show that. - (if oldest-new-file - (dired-jump nil oldest-new-file) - ;; Files may have been deleted but harder to track. - ;; Open dired and refresh to show files are gone. - (unless (equal (length files-after) - (length files-before)) - (dired monitor-directory) - (when revert-buffer-function - (funcall revert-buffer-function nil t))))) - (unless (equal (process-buffer process) - (window-buffer (selected-window))) - (if (or oldest-new-file silent-success) - (kill-buffer (process-buffer process)) - (unless silent-success - (switch-to-buffer (process-buffer process))))))) - (if on-completion - (funcall on-completion (process-buffer process) process) - (if (and (buffer-name (process-buffer process)) - (or error-autofocus - ;; Buffer already selected. Don't ask. - (equal (process-buffer process) - (window-buffer (selected-window))) - (ignore-error quit - (y-or-n-p (format "%s error, see output? " - (buffer-name (process-buffer process))))))) - (progn - (with-current-buffer (process-buffer process) - (rename-buffer (generate-new-buffer-name (funcall dwim-shell-command-error-buffer-name (process-name process))))) - (when (or error-autofocus - (equal (process-buffer process) - (window-buffer (selected-window)))) - (dwim-shell-command--message (funcall dwim-shell-command-error-buffer-name (process-name process)))) - (switch-to-buffer (process-buffer process))) - (kill-buffer (process-buffer process))))) - (setq dwim-shell-command--commands - (map-delete dwim-shell-command--commands (process-name process))))) - -(defun dwim-shell-command--unique-new-file-path (file-path) - "Return a unique FILE-PATH. - -If FILE-PATH already contains a number in the format (n), set counter to n. - -\"/tmp/blah.txt\" -> \"/tmp/blah(1).txt\" -\"/tmp/blah(2).txt\" -> \"/tmp/blah(3).txt\"" - (let* ((name (file-name-sans-extension file-path)) - (extension (file-name-extension file-path)) - (counter (if (string-match "(\\([0-9]+\\))$" name) - (string-to-number (match-string 1 name)) - 0))) - (when (string-match "(\\([0-9]+\\))$" name) - (setq name(replace-match "" t t name))) - (while (file-exists-p file-path) - (setq counter (1+ counter)) - (setq file-path (if extension - (format "%s(%d).%s" name counter extension) - (format "%s(%d)" name counter)))) - file-path)) - -(defun dwim-shell-command--sentinel (process _) - "Handles PROCESS sentinel and STATE." - (let ((exec (map-elt dwim-shell-command--commands (process-name process)))) - (dwim-shell-command--finalize (dwim-shell-command--command-calling-buffer exec) - (dwim-shell-command--command-files-before exec) - process - (dwim-shell-command--command-reporter exec) - (dwim-shell-command--command-on-completion exec) - (dwim-shell-command--command-silent-success exec) - (dwim-shell-command--command-error-autofocus exec) - (dwim-shell-command--command-monitor-directory exec)))) - -(defun dwim-shell-command--filter (process output) - "Handles PROCESS filtering and STATE and OUTPUT." - (when-let* ((exec (map-elt dwim-shell-command--commands (process-name process))) - (reporter (dwim-shell-command--command-reporter exec))) - (progress-reporter-update reporter)) - (comint-output-filter process output)) - -(defun dwim-shell-command--file-extensions () - "Return buffer file extension or marked/region extensions for a `dired' buffer." - (seq-uniq - (seq-map - #'file-name-extension - (seq-filter - #'file-name-extension - (dwim-shell-command--files))))) - -(defun dwim-shell-command--files () - "Return buffer file (if available) or marked/region files for a `dired' buffer." - (cl-assert (not (and (use-region-p) (let ((files (dired-get-marked-files nil nil nil t))) - ;; Based on `dired-number-of-marked-files'. - (cond ((null (cdr files)) - nil) - ((and (= (length files) 2) - (eq (car files) t)) - t) - (t - (not (seq-empty-p files))))))) - nil "Region and marked files both active. Choose one only.") - (if (buffer-file-name) - (list (buffer-file-name)) - (or - (dwim-shell-command--dired-paths-in-region) - (dired-get-marked-files)))) - -(defun dwim-shell-command--dired-paths-in-region () - "If `dired' buffer, return region files. nil otherwise." - (when (and (equal major-mode 'dired-mode) - (use-region-p)) - (let ((start (region-beginning)) - (end (region-end)) - (paths)) - (save-excursion - (save-restriction - (goto-char start) - (while (< (point) end) - ;; Skip non-file lines. - (while (and (< (point) end) (dired-between-files)) - (forward-line 1)) - (when (dired-get-filename nil t) - (setq paths (append paths (list (dired-get-filename nil t))))) - (forward-line 1)))) - paths))) - -(defun dwim-shell-command--n-start-value (template padding) - "Extract n start value from TEMPLATE using PADDING. -Falls back to \"1\"." - (when (string-match "\<\<\\([[:alnum:]]?+\\)n\>\>" template) - (if (string-empty-p (match-string 1 template)) - (dwim-shell-command--increment-string (number-to-string 0) padding) - (if-let* ((start-string (match-string 1 template)) - (start-n (string-to-number start-string))) - (dwim-shell-command--increment-string (number-to-string (1- start-n)) padding) - (match-string 1 template))))) - -(defun dwim-shell-command--increment-string (text padding) - "Increment TEXT using PADDING. -\"a\" -> \"b\" -\"1\" -> \"2\"" - (cond ((string-match "^[[:alpha:]]$" text) ;; char - (char-to-string (1+ (string-to-char (match-string 0 text))))) - ((string-match "^[[:digit:]]+$" text) ;; char - (dwim-shell-command--number-to-string (1+ (string-to-number (match-string 0 text))) padding)))) - -(defun dwim-shell-command--program-test (program &rest args) - "Test that running PROGRAM with ARGS is successful." - (eq 0 (apply #'call-process program nil nil nil args))) - -(cl-defun dwim-shell-command-foreach (fun &key monitor-directory) - "Execute FUN for each file. -Monitor :MONITOR-DIRECTORY for new file and `dired-jump' to it." - (let ((files (dwim-shell-command--files)) - (files-before (dwim-shell-command--default-directory-files monitor-directory)) - (oldest-new-file) - (created-file) - (jump-to)) - (mapc (lambda (file-path) - (setq created-file (or (funcall fun file-path) - created-file))) - files) - (setq oldest-new-file (dwim-shell-command--last-modified-between - files-before - (dwim-shell-command--default-directory-files monitor-directory))) - (setq jump-to (or oldest-new-file created-file)) - (when jump-to - (dired-jump nil jump-to)))) - -(provide 'dwim-shell-command) - -;;; dwim-shell-command.el ends here diff --git a/elpa/dwim-shell-command-20250218.1720/dwim-shell-commands.el b/elpa/dwim-shell-command-20250218.1720/dwim-shell-commands.el deleted file mode 100644 index 7984af97..00000000 --- a/elpa/dwim-shell-command-20250218.1720/dwim-shell-commands.el +++ /dev/null @@ -1,1751 +0,0 @@ -;;; dwim-shell-commands.el --- Useful commands -*- lexical-binding: t; -*- - -;; Copyright (C) 2022 Alvaro Ramirez - -;; Author: Alvaro Ramirez -;; URL: https://github.com/xenodium/dwim-shell-command - -;; This package 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, or (at your option) -;; any later version. - -;; This package 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. - -;; You should have received a copy of the GNU General Public License -;; along with GNU Emacs. If not, see . - -;;; Commentary: - -;; A collection of useful commands created via -;; `dwim-shell-command-on-marked-files'. - -;;; Code: - -(require 'browse-url) -(require 'cl-lib) -(require 'dwim-shell-command) -(require 'files) -(require 'proced) -(require 'seq) -(require 'subr-x) - -;;;###autoload -(defun dwim-shell-commands-audio-to-mp3 () - "Convert all marked audio to mp3(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Convert to mp3" - "ffmpeg -stats -n -i '<>' -acodec libmp3lame '<>.mp3'" - :utils "ffmpeg")) - -;;;###autoload -(defun dwim-shell-extract-har-content (prefix) - "Extract all har content fields to files." - (interactive "P") - (when prefix - (setq prefix (string-trim (read-string "Transcription locale: " "ja-JP"))) - (when (string-empty-p prefix) - (error "No locale given"))) - (dwim-shell-command-on-marked-files - "Extract har response content" - (format "declare -A mime_map=( \ - [\"audio/mpeg\"]=\"mp3\" \ - [\"image/jpeg\"]=\"jpg\" \ - [\"text/plain\"]=\"txt\" \ - [\"application/json\"]=\"json\" \ - # TODO: Add more mappings if needed. -) -outdir='<>' -mkdir -p \"${outdir}\" - : > \"${outdir}/<>.org\" -jq -r '.log.entries[] | @base64' '<>' | while read -r entry; do - url=$(echo \"$entry\" | base64 --decode | jq -r '.request.url') - basename=$(echo \"$entry\" | base64 --decode | jq -r '.request.url | capture(\"(?<=//)[^/]+/(?.*)\") | .path | gsub(\"[^a-zA-Z0-9]\"; \"_\")') - mime=$(echo \"$entry\" | base64 --decode | jq -r '.response.content.mimeType') - extension=${mime_map[$mime]:-\"bin\"} - name=\"${outdir}/${basename:0:255}.${extension}\" - echo \"${name}\" - content=$(echo \"$entry\" | base64 --decode | jq -r '.response.content.text') - echo \"$content\" | base64 --decode > \"${name}\" - if [ -f \"${name}\" ] && %s; then - transcription=$(macosrec --speech-to-text --locale %s --input \"${name}\") - if [ $? -eq 0 ]; then - transcribed_name=\"${outdir}/${transcription}.${extension}\" - mv \"${name}\" \"${transcribed_name}\" - basename=$(basename \"${transcribed_name}\") - echo \"[[file:${basename}][${transcription}]] [[${url}][remote]]\" >> \"${outdir}/<>.org\" - fi - fi -done" - (if prefix - "true" - "false") - prefix - (if prefix - "true" - "false")) - :utils "jq" - :extensions "har")) - -;;;###autoload -(defun dwim-shell-extract-har-urls () - "Get all request URLs." - (interactive) - (dwim-shell-command-on-marked-files - "Extract har request URLs" - "jq -r '.log.entries[].request.url' '<>'" - :utils "jq")) - -;;;###autoload -(defun dwim-shell-view-open-ports-per-app () - "View open ports per app" - (interactive) - (dwim-shell-command-on-marked-files - "Ports per app" - ;; https://x.com/nurmiwilliam/status/1823228630664634695 - "sudo lsof -iTCP -sTCP:LISTEN -n -P | awk 'NR>1 {print $9, $1, $2}' | sed 's/.*://' | sort -u | while read port process pid; do echo \"Port $port: $(ps -p $pid -o command= | sed 's/^-//') (PID: $pid)\"; done | sort -n" - :utils "jq")) - -;;;###autoload -(defun dwim-shell-commands-open-clipboard-url () - "Open clipboard URL. Offer to stream if possible." - (interactive) - (let ((url (or (current-kill 0) - (user-error "Nothing in clipboard")))) - (dwim-shell-commands-url-browse url))) - -(defun dwim-shell-commands-url-browse (url &rest args) - "If URL is playable media, offer to open in mpv. Else browser. -Optional argument ARGS as per `browse-url-default-browser'" - (if (and (or (string-match-p "^http[s]?://.*youtube.com" url) - (string-match-p "^http[s]?://.*m.youtube.com" url) - (string-match-p "^http[s]?://.*youtu.be" url) - (string-match-p "^http[s]?://.*soundcloud.com" url) - (string-match-p "^http[s]?://.*redditmedia.com" url) - (string-match-p "^http[s]?://.*reddit.com" url) - (string-match-p "^http[s]?://.*bandcamp.com" url)) - (y-or-n-p "Stream from mpv? ")) - (dwim-shell-command-on-marked-files - "Streaming" - (format "mpv --geometry=30%%x30%%+100%%+0%% '%s'" url) - :utils "mpv" - :no-progress t - :error-autofocus t - :silent-success t) - (funcall #'browse-url-default-browser url args))) - -;;;###autoload -(defun dwim-shell-commands-stream-clipboard-url () - "Stream clipboard URL using mpv." - (interactive) - (cl-assert (string-match-p "^http[s]?://" (current-kill 0)) nil "Not a URL") - (dwim-shell-command-on-marked-files - "Streaming" - "mpv --geometry=30%x30%+100%+0% \"<>\"" - :utils "mpv" - :no-progress t - :error-autofocus t - :silent-success t)) - -;;;###autoload -(defun dwim-shell-commands-download-clipboard-stream-url () - "Download clipboard URL." - (interactive) - (cl-assert (string-match-p "^http[s]?://" (current-kill 0)) nil "Not a URL") - (dwim-shell-command-on-marked-files - "Downloading" - "youtube-dl --newline -o \"~/Downloads/%(title)s.%(ext)s\" \"<>\"" - :utils "youtube-dl" - :no-progress t - :error-autofocus t - :monitor-directory "~/Downloads" - :silent-success t)) - -;;;###autoload -(defun dwim-shell-commands-image-clear-exif-metadata () - "Clear EXIF metadata in image(s)." - (interactive) - (dwim-shell-command-on-marked-files - "View EXIF" - "cp '<>' '<>_cleared.<>' - exiftool -all:all= -overwrite_original '<>_cleared.<>'" - :utils "exiftool")) - -;;;###autoload -(defun dwim-shell-commands-image-scan-code () - "Scan any code (ie. qr, bar, etc) from image(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Scan code" - "zbarimg --quiet '<>'" - :utils "zbarimg")) - -;;;###autoload -(defun dwim-shell-commands-image-exif-metadata () - "View EXIF metadata in image(s)." - (interactive) - (dwim-shell-command-on-marked-files - "View EXIF" - "exiftool '<>'" - :utils "exiftool")) - -;;;###autoload -(defun dwim-shell-commands-tesseract-ocr-text-from-image () - "Extract text from image via tesseract." - (interactive) - (dwim-shell-command-on-marked-files - "Extract text from image via tesseract." - "tesseract '<>' -" - :utils "tesseract")) - -;;;###autoload -(defun dwim-shell-commands-image-view-location-in-openstreetmap () - "Open image(s) location in map/browser." - (interactive) - (dwim-shell-command-on-marked-files - "Browse location" - "lat=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<>' | tail -n 1 | cut -s -d',' -f2-2)\" - if [ -z \"$lat\" ]; then - echo \"no latitude\" - exit 1 - fi - lon=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<>' | tail -n 1 | cut -s -d',' -f3-3)\" - if [ -z \"$lon\" ]; then - echo \"no longitude\" - exit 1 - fi - if [[ $OSTYPE == darwin* ]]; then - open \"http://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}&layers=C\" - else - xdg-open \"http://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}&layers=C\" - fi" - :utils "exiftool" - :error-autofocus t - :silent-success t)) - -;;;###autoload -(defun dwim-shell-commands-image-reverse-geocode-location () - "Reverse geocode image(s) location." - (interactive) - (dwim-shell-command-on-marked-files - "Reverse geocode" - "lat=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<>' | tail -n 1 | cut -s -d',' -f2-2)\" - if [ -z \"$lat\" ]; then - echo \"no latitude\" - exit 1 - fi - lon=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<>' | tail -n 1 | cut -s -d',' -f3-3)\" - if [ -z \"$lon\" ]; then - echo \"no longitude\" - exit 1 - fi - json=$(curl \"https://nominatim.openstreetmap.org/reverse?format=json&accept-language=en&lat=${lat}&lon=${lon}&zoom=18&addressdetails=1\") - echo \"json_start $json json_end\"" - :utils '("exiftool" "curl") - :silent-success t - :error-autofocus t - :on-completion (lambda (buffer _process) - (with-current-buffer buffer - (goto-char (point-min)) - (let ((matches '())) - (while (re-search-forward "^json_start\\(.*?\\)json_end" nil t) - (push (match-string 1) matches)) - (message "%s" (string-join (seq-map (lambda (json) - (map-elt (json-parse-string json :object-type 'alist) 'display_name)) - matches) - "\n"))) - (kill-buffer buffer))))) - -;;;###autoload -(defun dwim-shell-commands-image-horizontal-flip () - "Horizontally flip image(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Image horizontal flip" - "convert -verbose -flop '<>' '<>_h_flipped.<>'" - :utils "convert")) - -;;;###autoload -(defun dwim-shell-commands-image-vertical-flip () - "Horizontally flip image(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Image vertical flip" - "convert -verbose -flip '<>' '<>_v_flipped.<>'" - :utils "convert")) - -;;;###autoload -(defun dwim-shell-commands-image-to-jpg () - "Convert all marked images to jpg(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Convert to jpg" - "convert -verbose '<>' '<>.jpg'" - :utils "convert")) - -;;;###autoload -(defun dwim-shell-commands-image-to-png () - "Convert all marked images to png(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Convert to png" - "convert -verbose '<>' '<>.png'" - :utils "convert")) - -;;;###autoload -(defun dwim-shell-commands-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 png -o '<>.png'" - :utils "rsvg-convert")) - -;;;###autoload -(defun dwim-shell-commands-svg-to-favicons () - "Convert svg to common favicons." - (interactive) - (dwim-shell-command-on-marked-files - "Create favicons" - "echo '>\">' - rsvg-convert -o '<>-16.png' -w 16 -h 16 '<>' - echo '>-16.png\" sizes=\"16x16\">' - rsvg-convert -o '<>-32.png' -w 32 -h 32 '<>' - echo '>-32.png\" sizes=\"32x32\">' - rsvg-convert -o '<>-48.png' -w 48 -h 48 '<>' - echo '>-48.png\" sizes=\"48x48\">' - convert '<>-16.png' '<>-32.png' '<>-48.png' '<>.ico' - echo '>.ico'\">' - rsvg-convert -o '<>-64.png' -w 64 -h 64 '<>' - echo '>-64.png\" sizes=\"64x64\">' - rsvg-convert -o '<>-180.png' -w 180 -h 180 '<>' - echo '>-180.png\" sizes=\"180x180\">' - read -n 1 -s -r" - :extensions "svg" - :utils '("convert" "rsvg-convert") - :focus-now t)) - -;;;###autoload -(defun dwim-shell-commands-make-transparent-png () - "Create a transparent png." - (interactive) - (dwim-shell-command-on-marked-files - "Create transparent png" - "convert -verbose -size <>x<> xc:none '<>x<>.png(u)>>'" - :utils "convert")) - -;;;###autoload -(defun dwim-shell-commands-join-as-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")) - -;;;###autoload -(defun dwim-shell-commands-keep-pdf-page () - "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 '<>' --pages . %d -- '<>_%d.<>'" page-num page-num) - :utils "qpdf"))) - -;;;###autoload -(defun dwim-shell-commands-join-images-horizontally () - "Join all marked images horizontally as a single image." - (interactive) - (let ((filename (format "joined.%s" - (or (seq-first (dwim-shell-command--file-extensions)) "png")))) - (dwim-shell-command-on-marked-files - "Join images horizontally" - (format "convert -verbose '<<*>>' +append '<<%s(u)>>'" - (dwim-shell-command-read-file-name - (format "Join as image named (default \"%s\"): " filename) - :default filename)) - :utils "convert"))) - -;;;###autoload -(defun dwim-shell-commands-join-images-vertically () - "Join all marked images vertically as a single image." - (interactive) - (let ((filename (format "joined.%s" - (or (seq-first (dwim-shell-command--file-extensions)) "png")))) - (dwim-shell-command-on-marked-files - "Join images vertically" - (format "convert -verbose '<<*>>' -gravity center -append '<<%s(u)>>'" - (dwim-shell-command-read-file-name - (format "Join as image named (default \"%s\"): " filename) - :default filename)) - :utils "convert"))) - -;;;###autoload -(defun dwim-shell-commands-image-to-grayscale () - "Convert all marked images to grayscale." - (interactive) - (dwim-shell-command-on-marked-files - "Convert image to grayscale" - "convert -verbose -type Grayscale '<>' '<>_grayscale.<>'" - :utils "convert")) - -;;;###autoload -(defun dwim-shell-commands-reorient-image () - "Reorient images." - (interactive) - (dwim-shell-command-on-marked-files - "Reorient image" - "convert -verbose -auto-orient '<>' '<>_reoriented.<>'" - :utils "convert")) - -;;;###autoload -(defun dwim-shell-commands-gif-to-video () - "Convert all marked gif(s) to video(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Convert to gif" - "ffmpeg -i '<>' -movflags faststart -pix_fmt yuv420p -vf 'scale=trunc(iw/2)*2:trunc(ih/2)*2' '<>.mp4'" - :utils "ffmpeg")) - -;;;###autoload -(defun dwim-shell-commands-macos-empty-trash () - "Empty macOS trash." - (interactive) - (when (y-or-n-p "Empty macOS trash? ") - (dwim-shell-command-on-marked-files - "Empty macOS trash" - "trash -e -y" - :silent-success t - :utils "trash"))) - -;;;###autoload -(defun dwim-shell-commands-macos-ocr-text-from-desktop-region () - "Select a macOS desktop area to OCR and copy recognized text to kill ring." - (interactive) - (dwim-shell-command-on-marked-files - "OCR area" - "macosrec --ocr" - ;; brew install xenodium/macosrec/macosrec - :utils "macosrec" - :on-completion - (lambda (buffer process) - (when-let ((success (= (process-exit-status process) 0)) - (text (with-current-buffer buffer - (string-trim (buffer-string))))) - (progn - (kill-new text) - (switch-to-buffer buffer) - (goto-char (point-min)) - (message "OCR copied to clipboard")))))) - -;;;###autoload -(defun dwim-shell-commands-macos-ocr-text-from-image () - "OCR file and copy recognized text to kill ring." - (interactive) - (dwim-shell-command-on-marked-files - "OCR area" - "macosrec --ocr --clipboard --input '<>'" - ;; brew install xenodium/macosrec/macosrec - :utils "macosrec" - :on-completion - (lambda (buffer process) - (when-let ((success (= (process-exit-status process) 0)) - (text (with-current-buffer buffer - (string-trim (buffer-string))))) - (progn - (kill-new text) - (switch-to-buffer buffer) - (goto-char (point-min)) - (message "OCR copied to clipboard")))))) - -;;;###autoload -(defun dwim-shell-commands-macos-convert-to-mp4 () - "Convert to mov to mp4" - (interactive) - (dwim-shell-command-on-marked-files - "Convert to mov to mp4" - ;; "ffmpeg -loglevel quiet -stats -y -i <>.mov -vcodec h264 -acodec copy <>.mp4" - ;; Found the encoder via ffmpeg -encoders | grep videotoolbox, - ;; source https://www.reddit.com/r/ffmpeg/comments/14pqeex/getting_0_gpu_utilization_with_apple_silicons/ - "ffmpeg -i '<>' -map_metadata 0 \ - -c:v hevc_videotoolbox -q:v 35 -preset fast -c:a aac -b:a 128k -tag:v hvc1 '<>'.mp4" - :utils "ffmpeg")) - -;;;###autoload -(defun dwim-shell-commands-video-to-gif () - "Convert all marked videos to gif(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Convert to gif" - "ffmpeg -loglevel quiet -stats -y -i '<>' -pix_fmt rgb24 -r 15 '<>.gif'" - :utils "ffmpeg")) - -;;;###autoload -(defun dwim-shell-commands-video-to-mov () - "Convert all marked videos to mov(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Convert to mov" - "ffmpeg -i '<>' -c:v libx264 -c:a aac '<>.mov'" - :utils "ffmpeg")) - -;;;###autoload -(defun dwim-shell-commands-video-to-webp () - "Convert all marked videos to webp(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Convert to webp" - "ffmpeg -i '<>' -vcodec libwebp -filter:v fps=fps=10 -compression_level 3 -lossless 1 -loop 0 -preset default -an -vsync 0 '<>'.webp" - :utils "ffmpeg")) - -;;;###autoload -(defun dwim-shell-commands-webp-to-video () - "Convert all marked webp(s) to video(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Convert webp to video" - "convert '<>' '<>/<>.gif' - ffmpeg -i '<>/<>.gif' -movflags faststart -pix_fmt yuv420p -vf 'scale=trunc(iw/2)*2:trunc(ih/2)*2' '<>.mp4'" - :utils '("ffmpeg" "convert") - :extensions "webp")) - -;;;###autoload -(defun dwim-shell-commands-webp-to-gif () - "Convert all marked webp(s) to gif(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Convert webp to video" - "convert '<>' '<>.gif'" - :utils '("convert") - :extensions "webp")) - -;;;###autoload -(defun dwim-shell-commands-video-to-hevc-mkv () - "Convert all marked videos to hevc mkv." - (interactive) - (dwim-shell-command-on-marked-files - "Convert video to h265 " - "REPO_DIR=/tmp/other_video_transcoding - if ! [ -d \"$REPO_DIR\" ] - then - git clone https://github.com/donmelton/other_video_transcoding.git $REPO_DIR - fi - pushd $REPO_DIR - git pull origin master || echo \"skipping repo update...\" - popd - ruby $REPO_DIR/bin/other-transcode --hevc '<>'" - :utils '("git" "ffmpeg" "mkvtoolnix" "mpv"))) - -;;;###autoload -(defun dwim-shell-commands-video-to-optimized-gif () - "Convert all marked videos to optimized gif(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Convert to optimized gif" - "ffmpeg -loglevel quiet -stats -y -i '<>' -pix_fmt rgb24 -r 15 '<>.gif' - gifsicle -O3 '<>.gif' --lossy=80 -o '<>.gif'" - :utils '("ffmpeg" "gifsicle"))) - -;;;###autoload -(defun dwim-shell-commands-unzip () - "Unzip all marked archives (of any kind) using `atool'." - (interactive) - (dwim-shell-command-on-marked-files - "Unzip" "atool --extract --explain '<>'" - :utils "atool")) - -;;;###autoload -(defun dwim-shell-commands-zip () - "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 '<>.<>' '<>'" - "zip -r '<>' '<<*>>'") - :utils "zip")) - -;;;###autoload -(defun dwim-shell-commands-zip-password-protect () - "Protect/encrypt zip file(s) with password." - (interactive) - (dwim-shell-command-on-marked-files - "Add zip password" "zipcloak --output-file '<>_protected.<>' '<>'" - :extensions "zip" - :utils "zipcloak")) - -;;;###autoload -(defun dwim-shell-commands-optimize-gif () - "Convert all marked videos to optimized gif(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Convert to optimized gif" - "gifsicle -O3 '<>' --lossy=90 -o '<>_optimized.gif'" - :utils '("ffmpeg" "gifsicle"))) - -;;;###autoload -(defun dwim-shell-commands-speed-up-gif () - "Speeds up gif(s)." - (interactive) - (let ((factor (string-to-number - (completing-read "Speed up x times: " '("1" "1.5" "2" "2.5" "3" "4"))))) - (dwim-shell-command-on-marked-files - "Speed up gif" - (format "gifsicle -U '<>' <> -O2 -o '<>_x%s.<>'" factor) - :extensions "gif" :utils '("gifsicle" "identify") - :post-process-template (lambda (script file) - (string-replace "<>" (dwim-shell-commands--gifsicle-frames-every factor file) script))))) - -;;;###autoload -(defun dwim-shell-commands-image-apply-ios-round-corners () - "Apply iOS round corners to image(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Speed up gif" - "set -o xtrace - width=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=noprint_wrappers=1:nokey=1 '<>') - height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 '<>') - corner=$((${width}/4)) - echo ${corner} - convert -size ${width}x${height} xc:none -fill white -draw \"roundRectangle 0,0 ${width},${height} ${corner},${corner}\" '<>' -compose SrcIn -composite '<>_ios_round.<>'" - :utils '("ffprobe" "convert"))) - -;;;###autoload -(defun dwim-shell-commands-clip-round-rect-gif () - "Clip gif(s) with round rectangle." - (interactive) - (dwim-shell-command-on-marked-files - "Clip round rect gif(s)" - "width=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=noprint_wrappers=1:nokey=1 '<>') - height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 '<>') - convert -quiet -size \"${width}x${height}\" xc:none -fill black -draw \"roundRectangle 0,0,${width},${height} <>,<>\" '<>/mask.png' - convert '<>' -coalesce -background black -alpha remove -alpha off '<>/no_alpha.<>' - # https://stackoverflow.com/a/66990135 - convert '<>/no_alpha.<>' -quiet -coalesce -alpha extract null: \\( '<>/mask.png' -alpha extract \\) -compose multiply -layers composite '<>/alpha.gif' - convert '<>/no_alpha.<>' null: '<>/alpha.gif' -quiet -alpha off -compose copy_opacity -layers composite '<>_rounded.<>' - # Turn looping on. - mogrify -loop 0 '<>_rounded.<>' - gifsicle -O3 '<>_rounded.<>' --lossy=80 -o '<>_rounded.<>'" - :extensions "gif" - :utils '("ffprobe" "convert"))) - -;;;###autoload -(defun dwim-shell-commands-resize-gif () - "Resize marked gif(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Resize marked gif(s)" - "gifsicle --scale <> '<>' -o '<>_x<>.gif'" - :extensions "gif" - :utils "gifsicle")) - -;;;###autoload -(defun 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 '<>' > '<>.org'" - :extensions "epub" - :utils "pandoc")) - -;;;###autoload -(defun dwim-shell-commands-docx-to-pdf () - "Convert docx(s) to pdf (via latex)." - (interactive) - (dwim-shell-command-on-marked-files - "docx to pdf (via latex)" - "pandoc -t latex '<>' -o '<>.pdf'" - :extensions "docx" ;; brew install mactex - :utils "pdflatex")) - -;;;###autoload -(defun dwim-shell-commands-kill-process () - "Select and kill process." - (interactive) - (let* ((pid-width 5) - (comm-width 25) - (user-width 10) - (processes (proced-process-attributes)) - (candidates - (mapcar (lambda (attributes) - (let* ((process (cdr attributes)) - (pid (format (format "%%%ds" pid-width) (map-elt process 'pid))) - (user (format (format "%%-%ds" user-width) - (truncate-string-to-width - (map-elt process 'user) user-width nil nil t))) - (comm (format (format "%%-%ds" comm-width) - (truncate-string-to-width - (map-elt process 'comm) comm-width nil nil t))) - (args-width (- (window-width) (+ pid-width user-width comm-width 3))) - (args (map-elt process 'args))) - (cons (if args - (format "%s %s %s %s" pid user comm (truncate-string-to-width args args-width nil nil t)) - (format "%s %s %s" pid user comm)) - process))) - processes)) - (selection (map-elt candidates - (completing-read "kill process: " - (seq-sort - (lambda (p1 p2) - (string-lessp (nth 2 (split-string (string-trim (car p1)))) - (nth 2 (split-string (string-trim (car p2)))))) - candidates) nil t))) - (prompt-title (format "%s %s %s" - (map-elt selection 'pid) - (map-elt selection 'user) - (map-elt selection 'comm)))) - (when (y-or-n-p (format "Kill %s?" prompt-title)) - (dwim-shell-command-on-marked-files - (format "Kill %s" prompt-title) - (format "kill -9 %d" (map-elt selection 'pid)) - :utils "kill" - :error-autofocus t - :silent-success t)))) - -;;;###autoload -(defun dwim-shell-commands-macos-add-to-photos () - "Add to Photos.app." - (interactive) - (dwim-shell-command-on-marked-files - "Add to Photos.app" - "osascript <>\" - end tell -EOF" - :silent-success t - :utils "osascript" - :on-completion (lambda (buffer process) - (if-let ((success (= (process-exit-status process) 0))) - (progn - (kill-buffer buffer) - (start-process "Open Photos" nil "open" "-a" "Photos")) - (switch-to-buffer buffer))))) - -;;;###autoload -(defun dwim-shell-commands-macos-toggle-bluetooth-device-connection () - "Toggle Bluetooth device connection." - (interactive) - (let* ((devices (seq-filter - (lambda (line) - ;; Keep lines like: af-8c-3b-b1-99-af - Device name - (string-match-p "^[0-9a-f]\\{2\\}" line)) - (with-current-buffer (get-buffer-create "*BluetoothConnector*") - (erase-buffer) - ;; BluetoothConnector exits with 64 if no param is given. - ;; Invoke with no params to get a list of devices. - (unless (eq 64 (call-process "BluetoothConnector" nil (current-buffer))) - (kill-buffer (current-buffer)) - (error (buffer-string))) - (let ((lines (split-string (buffer-string) "\n"))) - (kill-buffer (current-buffer)) - lines)))) - (candidates (mapcar (lambda (device) - ;; key (device name) : value (address) - (cons (nth 1 (split-string device " - ")) - (nth 0 (split-string device " - ")))) - devices)) - (selected-name (completing-read "Toggle connection: " - (seq-sort #'string-lessp candidates) nil t)) - (address (map-elt candidates selected-name))) - (dwim-shell-command-on-marked-files - (format "Toggle %s" selected-name) - (format "BluetoothConnector %s --notify" address) - :utils "BluetoothConnector" - ;; :error-autofocus t - ;; :silent-success t - ))) - -;;;###autoload -(defun dwim-shell-commands-macos-bin-plist-to-xml () - "Convert binary plist to xml." - (interactive) - (dwim-shell-command-on-marked-files - "Convert binary plist to xml" - "plutil -convert xml1 -o '<>.xml' '<>'" - :utils "plutil")) - -;;;###autoload -(defun dwim-shell-commands-macos-toggle-dark-mode () - "Toggle macOS dark mode." - (interactive) - (dwim-shell-command-on-marked-files - "Toggle dark mode" - "dark-mode" - :utils "dark-mode" ;; brew install dark-mode - :silent-success t)) - -;;;###autoload -(defun dwim-shell-commands-macos-toggle-menu-bar-autohide () - "Toggle macOS dark mode." - (interactive) - (dwim-shell-command-on-marked-files - "Toggle menu bar auto-hide." - "current_status=$(osascript -e 'tell application \"System Events\" to get autohide menu bar of dock preferences') - -if [ \"$current_status\" = \"true\" ]; then - osascript -e 'tell application \"System Events\" to set autohide menu bar of dock preferences to false' - echo \"Auto-hide disabled.\" -else - osascript -e 'tell application \"System Events\" to set autohide menu bar of dock preferences to true' - echo \"Auto-hide enabled.\" -fi" - :utils "osascript" - :shell-util "zsh" - :silent-success t)) - -;;;###autoload -(defun dwim-shell-commands-pdf-to-txt () - "Convert pdf to txt." - (interactive) - (dwim-shell-command-on-marked-files - "pdf to txt" - "pdftotext -layout '<>' '<>.txt'" - :utils "pdftotext")) - -;;;###autoload -(defun 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 '<>' '<>_x%.2f.<>'" - (* 100 factor) factor)) - :utils "convert")) - -;;;###autoload -(defun 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 '<>' '<>_x%d.<>'" width width)) - :utils "convert")) - -;;;###autoload -(defun 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 -- '<>' '<>_protected.<>'" - (read-passwd "user-password: ") - (read-passwd "owner-password: ")) - :utils "qpdf" - :extensions "pdf")) - -;;;###autoload -(defun 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' -- '<>' '<>_unprotected.<>'" - (read-passwd "password: ")) - :utils "qpdf" - :extensions "pdf")) - -(defun dwim-shell-commands--gifsicle-frames-every (skipping-every file) - "Generate frames SKIPPING-EVERY count for video FILE." - (string-join - (seq-map (lambda (n) (format "'#%d'" n)) - (number-sequence 0 (string-to-number - ;; Get total frames count. - (seq-first (process-lines "identify" "-format" "%n\n" file))) - skipping-every)) " ")) - -;;;###autoload -(defun dwim-shell-commands-video-to-mp3 () - "Convert video(s) to mp3." - (interactive) - (dwim-shell-command-on-marked-files - "Convert to mp3" - "ffmpeg -i '<>' -vn -ab 128k -ar 44100 -y '<>.mp3'" - :utils "ffmpeg")) - -;;;###autoload -(defun dwim-shell-commands-video-to-mp3-with-artwork () - "Convert video(s) to mp3 (keep frame as artwork)." - (interactive) - (dwim-shell-command-on-marked-files - "Convert to mp3" - "ffmpeg -i '<>' -vf 'select=eq(n\\,0)' -q:v 3 cover.jpg -i '<>' -vn -ab 128k -ar 44100 -y -map_metadata 0 -id3v2_version 3 -write_id3v1 1 -metadata:s:v title='Album cover' -metadata:s:v comment='Cover (front)' '<>.mp3'" - :utils "ffmpeg")) - -;;;###autoload -(defun dwim-shell-commands-ndjson-to-org () - "Convert ndjson file to org." - (interactive) - (unless (eq (length (dwim-shell-command--files)) 1) - (error "Only 1 file supported")) - (let* ((emacs-bin (file-truename (expand-file-name invocation-name - invocation-directory))) - (source (nth 0 (dwim-shell-command--files))) - (destination (concat (file-name-sans-extension - source) ".org")) - (fields (with-temp-buffer - (insert-file-contents source) - (buffer-substring-no-properties (point-min) (line-end-position)) - (read-string "Fields: " (mapconcat 'identity (mapcar (lambda (item) - (symbol-name (car item))) - (json-read-from-string - (buffer-substring-no-properties - (point-min) (line-end-position)))) - " "))))) - (dwim-shell-command-on-marked-files - "Convert ndjson to org" - (format "%s --quick --batch --eval \"%s\"" emacs-bin - (replace-regexp-in-string - "\"" "\\\\\"" - (prin1-to-string - `(progn - (require 'org) - (require 'json) - (defun convert-to-org-table (ndjson) - (let ((rows (mapcar #'json-read-from-string - (split-string ndjson "\n" t)))) - (orgtbl-to-orgtbl - (append - (list (split-string ,fields)) - '(hline) - (mapcar (lambda (obj) - (mapcar (lambda (key) - (or (alist-get (intern key) obj) "")) - (split-string ,fields))) - rows)) nil))) - (with-temp-buffer - (insert-file-contents ,source) - (let ((org (convert-to-org-table (buffer-string)))) - (with-temp-file ,destination - (insert org)))))))) - :extensions "ndjson"))) - -;;;###autoload -(defun dwim-shell-commands-set-media-artwork-image-metadata () - "Set image artwork metadata for media file(s)." - (interactive) - (let ((artwork-file (file-name-unquote - (read-file-name "Select artwork image: " - nil nil t))) - (should-backup (y-or-n-p "Create backup files? "))) - (unless (file-regular-p artwork-file) - (user-error "Not a file")) - (unless should-backup - (unless (y-or-n-p "Override file(s)? ") - (user-error "Aborted"))) - (dwim-shell-command-on-marked-files - "Set album artwork" - (format (if should-backup - "ffmpeg -i '<>' -i '%s' -map_metadata 0 -map 0:a -map 1 -c copy -disposition:v:0 attached_pic '<>.tmp.<>' && mv -f '<>' '<>.bak' && mv '<>.tmp.<>' '<>'" - "ffmpeg -i '<>' -i '%s' -map_metadata 0 -map 0:a -map 1 -c copy -disposition:v:0 attached_pic '<>.tmp.<>' && mv -f '<>.tmp.<>' '<>'") - artwork-file) - :utils "AtomicParsley" - :silent-success t))) - -;;;###autoload -(defun dwim-shell-commands-video-trim-beginning () - "Drop audio from all marked videos." - (interactive) - (dwim-shell-command-on-marked-files - "Trim beginning" - "ffmpeg -i '<>' -y -ss <> -c:v copy -c:a copy '<>_trimmed.<>'" - :silent-success t - :utils "ffmpeg")) - -;;;###autoload -(defun dwim-shell-commands-video-trim-end () - "Drop audio from all marked videos." - (interactive) - (dwim-shell-command-on-marked-files - "Trim beginning" - "ffmpeg -sseof -<> -i '<>' -y -c:v copy -c:a copy '<>_trimmed.<>'" - :silent-success t - :utils "ffmpeg")) - -;;;###autoload -(defun dwim-shell-commands-video-to-thumbnail () - "Generate a thumbnail for marked video(s)." - (interactive) - (let ((temp-dir (make-temp-file "thumbnails-" t))) - (dwim-shell-command-on-marked-files - "Thumbnail with ffmpeg" - "ffmpeg -i '<>' -ss 00:00:01.000 -vframes 1 '<>.jpg'" - :utils "ffmpeg"))) - -;;;###autoload -(defun dwim-shell-commands-drop-video-audio () - "Drop audio from all marked videos." - (interactive) - (dwim-shell-command-on-marked-files - "Drop audio" - "ffmpeg -i '<>' -c copy -an '<>_no_audio.<>'" - :utils "ffmpeg")) - -;;;###autoload -(defun dwim-shell-commands-ping-google () - "Ping google.com." - (interactive) - (dwim-shell-command-on-marked-files - "Ping google.com" - "ping -c 3 google.com" - :utils "ping" - :focus-now t)) - -;;;###autoload -(defun dwim-shell-commands-speed-up-video () - "Speed up video(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Speed up video" - (let ((factor (read-number "Resize scaling factor: " 2))) - (format "ffmpeg -i '<>' -an -filter:v 'setpts=%s*PTS' '<>_x%s.<>'" - (/ 1 (float factor)) factor)) - :utils "ffmpeg")) - -;;;###autoload -(defun dwim-shell-commands-speed-up-video-fragment () - "Speed up fragment in video(s)." - (interactive) - (let ((start (read-number "Start (seconds): ")) - (end (read-number "End (seconds): ")) - (factor (read-number "Speed up factor: " 2))) - (dwim-shell-command-on-marked-files - "Speed up fragment in video" - (format "ffmpeg -i '<>' -filter_complex '[0:v]trim=start=0:end=%d,setpts=PTS-STARTPTS[v0];[0:v]trim=start=%d:end=%d,setpts=(PTS-1)/%d[v1];[0:v]trim=start=%d,setpts=PTS-STARTPTS[v2];[v0][v1][v2]concat=n=3:v=1:a=0' -preset fast '<>_%d:%dx%d.<>'" start start end factor end start end factor) - :utils "ffmpeg"))) - -(defun dwim-shell-commands-set-song-title () - "Set song(s) title." - (interactive) - (let ((title (replace-regexp-in-string "'" "\'" (read-string "New title: ")))) - (dwim-shell-command-on-marked-files - "Set song(s) title" - (format "ffmpeg -i '<>' -metadata title='%s' -codec copy '<>/<>_temp.<>' && mv -f '<>/<>_temp.<>' '<>'" - title) - :utils "ffmpeg"))) - -;;;###autoload -(defun dwim-shell-commands-resize-video () - "Resize marked images." - (interactive) - (dwim-shell-command-on-marked-files - "Resize video" - " -eval $(ffprobe -v quiet -show_format -of flat=s=_ -show_entries stream=width '<>'); -width=${streams_stream_0_width}; -zmodload zsh/mathfunc -width=$((rint($width * <>))); -# Make it even or face 'not divisible by 2' errors. -if [[ $((width%2)) -ne 0 ]] then - width=$(($width - 1)) -fi -ffmpeg -n -i '<>' -vf \"scale=$width:-2\" '<>_x<>.<>'" - :utils "ffmpeg")) - -;;;###autoload -(defun dwim-shell-commands-clipboard-to-qr () - "Generate a QR code from clipboard." - (interactive) - (let ((temp-file (concat (temporary-file-directory) "qr-code"))) - (dwim-shell-command-on-marked-files - "Generate a QR code from clipboard" - (format "qrencode -s10 -o %s %s" temp-file (shell-quote-argument (current-kill 0))) - :utils "qrencode" - :on-completion (lambda (buffer _process) - (kill-buffer buffer) - (switch-to-buffer (find-file-noselect temp-file t)))))) - -;;;###autoload -(defun dwim-shell-commands-sha-256-hash-file-at-clipboard-url () - "Download file at clipboard URL and generate SHA-256 hash." - (interactive) - (unless (string-match-p "^http[s]?://" (current-kill 0)) - (user-error "No URL in clipboard")) - (dwim-shell-command-on-marked-files - "Generate SHA-256 hash from clipboard URL." - "curl -s '<>' | sha256sum - | cut -d ' ' -f1" - :utils '("curl" "sha256sum") - :on-completion - (lambda (buffer process) - (if-let ((success (= (process-exit-status process) 0)) - (hash (with-current-buffer buffer - (string-trim (buffer-string))))) - (progn - (kill-buffer buffer) - (kill-new hash) - (message "Copied %s to clipboard" - (propertize hash 'face 'font-lock-string-face))) - (switch-to-buffer buffer))))) - -;;;###autoload -(defun dwim-shell-commands-view-sqlite-schema-diagram () - "View sqlite schema diagram." - (interactive) - (dwim-shell-command-on-marked-files - "View sqlite schema" - "set -e -temp_dir=\"${TMPDIR:-/tmp/}\" -file_name=\"sqlite-schema-diagram.sql\" -file_path=\"${temp_dir}/${file_name}\" -url=\"https://gitlab.com/Screwtapello/sqlite-schema-diagram/-/raw/main/sqlite-schema-diagram.sql\" - -if [[ ! -f \"$file_path\" ]]; then - curl -o \"$file_path\" \"$url\" -fi - -sqlite3 -list \"<>\" < $file_path > \"<>.dot\" -dot -Tsvg \"<>.dot\" > \"<>.svg\" -echo \"<>.svg\" -" - :utils '("dot" "sqlite3") - :on-completion - (lambda (buffer process) - (if (= (process-exit-status process) 0) - (with-current-buffer buffer - (let ((svg-file (string-trim (buffer-string)))) - (if (string-suffix-p "svg" svg-file) - (progn - (find-file svg-file) - (kill-buffer buffer)) - (switch-to-buffer buffer)))) - (switch-to-buffer buffer))))) - -;;;###autoload -(defun dwim-shell-commands-open-externally () - "Open file(s) externally." - (interactive) - (dwim-shell-command-on-marked-files - "Open externally" - (if (eq system-type 'darwin) - (if (derived-mode-p 'prog-mode) - (format "xed --line %d '<>'" - (line-number-at-pos (point))) - "open '<>'") - "xdg-open '<>'") - :shell-util "zsh" - :shell-args '("-x" "-c") - :silent-success t - :utils (if (eq system-type 'darwin) - "open" - "xdg-open"))) - -;;;###autoload -(defun dwim-shell-commands-macos-caffeinate () - "Invoke caffeinate to prevent mac from sleeping." - (interactive) - (dwim-shell-command-on-marked-files - "Caffeinate" - "caffeinate" - :utils "caffeinate" - :no-progress t - :focus-now t)) - -;;;###autoload -(defun dwim-shell-commands-macos-make-finder-alias () - "Make macOS Finder alias." - (interactive) - (let ((files (dwim-shell-command--files)) - (target-dir (read-directory-name "Select target dir: " "/Applications" nil t))) - (dwim-shell-command-on-marked-files - "Make macOS alias" - (format "osascript -e 'tell application \"Finder\" to make alias file to POSIX file \"<>\" at POSIX file \"%s\"'" - target-dir) - :utils "osascript" - :no-progress t - :silent-success t - :on-completion (lambda (buffer _process) - (kill-buffer buffer) - (dired-jump nil (file-name-concat target-dir (file-name-nondirectory (nth 0 files)))))))) - -;;;###autoload -(defun dwim-shell-commands-macos-version-and-hardware-overview-info () - "View macOS version and hardware overview info." - (interactive) - (dwim-shell-command-on-marked-files - "macOS hardware overview" - "sw_vers; system_profiler SPHardwareDataType" - :utils '("sw_vers" "system_profiler"))) - -;;;###autoload -(defun dwim-shell-commands-macos-reveal-in-finder () - "Reveal selected files in macOS Finder." - (interactive) - (dwim-shell-command-on-marked-files - "Reveal in Finder" - "import AppKit - NSWorkspace.shared.activateFileViewerSelecting([\"<<*>>\"].map{URL(fileURLWithPath:$0)})" - :silent-success t - :shell-pipe "swift -" - :join-separator ", " - :utils "swift")) - -(defun dwim-shell-commands--macos-sharing-services () - "Return a list of sharing services." - (let* ((source (format "import AppKit - NSSharingService.sharingServices(forItems: [ - %s - ]).forEach { - print(\"\\($0.title)\") - }" - (string-join (mapcar (lambda (file) - (format "URL(fileURLWithPath: \"%s\")" file)) - (dwim-shell-command--files)) - ", "))) - (services (split-string - (string-trim - ;; Remove anything that doesn't start with alpha characters - ;; There may be compilation warnings. - (replace-regexp-in-string "^[^[:alpha:]].*\n" "" - (shell-command-to-string (format "echo '%s' | swift -" source)))) - "\n"))) - (when (seq-empty-p services) - (error "No sharing services available")) - services)) - -;;;###autoload -(defun dwim-shell-commands-macos-share () - "Share selected files from macOS." - (interactive) - (let* ((services (dwim-shell-commands--macos-sharing-services)) - (service-name (completing-read "Share via: " services)) - (selection (seq-position services service-name #'string-equal))) - (dwim-shell-command-on-marked-files - "Share" - (format - "import AppKit - - _ = NSApplication.shared - - NSApp.setActivationPolicy(.regular) - - class MyWindow: NSWindow, NSSharingServiceDelegate { - func sharingService( - _ sharingService: NSSharingService, - didShareItems items: [Any] - ) { - NSApplication.shared.terminate(nil) - } - - func sharingService( - _ sharingService: NSSharingService, didFailToShareItems items: [Any], error: Error - ) { - let error = error as NSError - if error.domain == NSCocoaErrorDomain && error.code == NSUserCancelledError { - NSApplication.shared.terminate(nil) - } - exit(1) - } - } - - let window = MyWindow( - contentRect: NSRect(x: 0, y: 0, width: 0, height: 0), - styleMask: [], - backing: .buffered, - defer: false) - - let services = NSSharingService.sharingServices(forItems: [\"<<*>>\"].map{URL(fileURLWithPath:$0)}) - let service = services[%s] - service.delegate = window - service.perform(withItems: [\"<<*>>\"].map{URL(fileURLWithPath:$0)}) - - NSApp.run()" selection) - :silent-success t - :shell-pipe "swift -" - :join-separator ", " - :no-progress t - :utils "swift"))) - -;;;###autoload -(defun dwim-shell-commands-macos-toggle-display-rotation () - "Rotate display." - (interactive) - ;; # Display_ID Resolution ____Display_Bounds____ Rotation - ;; 2 0x2b347692 1440x2560 0 0 1440 2560 270 [main] - ;; From fb-rotate output, get the `current-rotation' from Column 7, row 1 zero-based. - (let ((current-rotation (nth 7 (split-string (nth 1 (process-lines "fb-rotate" "-i")))))) - (dwim-shell-command-on-marked-files - "macOS hardware overview" - (format "fb-rotate -d 1 -r %s" (if (equal current-rotation "270") "0" "270")) - :utils "fb-rotate"))) - -;;;###autoload -(defun dwim-shell-commands-make-swift-package-library () - "Create a swift package library" - (interactive) - (dwim-shell-command-on-marked-files - "Create a swift package library" - "swift package init --type library" - :utils "swift")) - -;;;###autoload -(defun dwim-shell-commands-make-swift-package-executable () - "Create a swift package executable" - (interactive) - (dwim-shell-command-on-marked-files - "Create a swift package executable" - "swift package init --type executable" - :utils "swift")) - -(defun dwim-shell-commands--macos-apps () - "Return alist of macOS apps (\"Emacs\" . \"/Applications/Emacs.app\")." - (mapcar (lambda (path) - (cons (file-name-base path) path)) - (seq-sort - #'string-lessp - (seq-mapcat (lambda (paths) - (directory-files-recursively - paths "\\.app$" t (lambda (path) - (not (string-suffix-p ".app" path))))) - '("/Applications" "~/Applications" "/System/Applications"))))) - -;;;###autoload -(defun dwim-shell-commands-macos-set-default-app () - "Set default app for file(s)." - (interactive) - (let* ((apps (dwim-shell-commands--macos-apps)) - (selection (progn - (cl-assert apps nil "No apps found") - (completing-read "Set default app: " apps nil t)))) - (dwim-shell-command-on-marked-files - "Set default app" - (format "duti -s \"%s\" '<>' all" - (string-trim - (shell-command-to-string (format "defaults read '%s/Contents/Info.plist' CFBundleIdentifier" - (map-elt apps selection))))) - :silent-success t - :no-progress t - :utils "duti"))) - -;;;###autoload -(defun dwim-shell-commands-macos-open-with () - "Open file(s) with specific external app." - (interactive) - (let* ((apps (dwim-shell-commands--macos-apps)) - (selection (progn - (cl-assert apps nil "No apps found") - (completing-read "Open with: " apps nil t)))) - (dwim-shell-command-on-marked-files - "Open with" - (format "open -a '%s' '<<*>>'" (map-elt apps selection)) - :silent-success t - :no-progress t - :utils "open"))) - -;;;###autoload -(defun dwim-shell-commands-macos-open-with-firefox () - "Open file(s) in Firefox." - (interactive) - (dwim-shell-command-on-marked-files - "Open in Firefox" - "open -a Firefox '<<*>>'" - :silent-success t - :no-progress t - :utils "open")) - -;;;###autoload -(defun dwim-shell-commands-macos-open-with-safari () - "Open file(s) in Safari." - (interactive) - (dwim-shell-command-on-marked-files - "Open in Safari" - "open -a Safari '<<*>>'" - :silent-success t - :no-progress t - :utils "open")) - -;;;###autoload -(defun dwim-shell-commands-macos-start-recording-window () - "Select and start recording a macOS window." - (interactive) - (let* ((window (dwim-shell-commands--macos-select-window)) - (path (dwim-shell-commands--generate-path "~/Screenshots" (car window) ".gif")) - (buffer-file-name path) ;; override so <> picks it up - (inhibit-message t)) - ;; Silence echo to avoid unrelated messages making into animation. - (cl-letf (((symbol-function 'dwim-shell-command--message) - (lambda (fmt &rest args) nil))) - (dwim-shell-command-on-marked-files - "Start recording a macOS window." - (format - "macosrec --record '%s' --gif --output '<>'" - (cdr window)) - :silent-success t - :monitor-directory "~/Screenshots" - :no-progress t - :utils '("ffmpeg" "macosrec") - :on-completion - (lambda (buffer process) - (if (= (process-exit-status process) 0) - (progn - "Saved recording" - (dired-jump nil path) - (kill-buffer buffer)) - (with-current-buffer buffer - (goto-char (point-min)) - (if (search-forward "Aborted" nil t) - (progn - (message "Aborted recording") - (kill-buffer buffer)) - (switch-to-buffer buffer))))))))) - -(defun dwim-shell-commands--generate-path (dir name ext) - "Generate a timestamped path with DIR, NAME, and EXT." - (concat (file-name-as-directory (expand-file-name dir)) - (format-time-string "%Y-%m-%d-%H:%M:%S-") - name ext)) - -(defun dwim-shell-commands--macos-select-window () - "Return a list of macOS windows." - (if-let* ((line (completing-read - "Select: " - (process-lines "macosrec" "--list") nil t)) - (window-info (split-string line " ")) - (window-number (string-to-number (nth 0 window-info))) - (window-app (nth 1 window-info)) - (valid (> window-number 0))) - (cons window-app window-number) - (user-error "No window found"))) - -;;;###autoload -(defun dwim-shell-commands-macos-end-recording-window () - "Stop recording a macOS window." - (interactive) - (let ((inhibit-message t)) - (cl-letf (((symbol-function 'dwim-shell-command--message) - (lambda (fmt &rest args) nil))) - (dwim-shell-command-on-marked-files - "End recording macOS window." - "macosrec --save" - :silent-success t - :no-progress t - :error-autofocus t - :utils "macosrec")))) - -;;;###autoload -(defun dwim-shell-commands-macos-abort-recording-window () - "Stop recording a macOS window." - (interactive) - (let ((inhibit-message t)) - (cl-letf (((symbol-function 'dwim-shell-command--message) - (lambda (fmt &rest args) nil))) - (dwim-shell-command-on-marked-files - "Abort recording macOS window." - "macosrec --abort" - :silent-success t - :no-progress t - :utils "macosrec")))) - -;;;###autoload -(defun dwim-shell-commands-macos-screenshot-window () - "Select and screenshot macOS window." - (interactive) - ;; Silence echo to avoid unrelated messages making into screenshot. - (let ((window (dwim-shell-commands--macos-select-window)) - (inhibit-message t)) - (dwim-shell-command-on-marked-files - "Start recording a macOS window." - (format "macosrec --screenshot %s" (cdr window)) - :silent-success t - :monitor-directory "~/Screenshots" - :no-progress t - :utils "macosrec"))) - -;;;###autoload -(defun dwim-shell-commands-files-combined-size () - "Get files combined file size." - (interactive) - (dwim-shell-command-on-marked-files - "Get files combined file size" - "du -csh '<<*>>'" - :utils "du" - :on-completion (lambda (buffer _process) - (with-current-buffer buffer - (message "Total size: %s" - (progn - (re-search-backward "\\(^[ 0-9.,]+[A-Za-z]+\\).*total$") - (match-string 1)))) - (kill-buffer buffer)))) - -;;;###autoload -(defun dwim-shell-commands-image-to-icns () - "Convert png to icns icon." - (interactive) - (dwim-shell-command-on-marked-files - "Convert png to icns icon." - " - # Based on http://stackoverflow.com/questions/12306223/how-to-manually-create-icns-files-using-iconutil - # Note: png must be 1024x1024 - mkdir '<>.iconset' - sips -z 16 16 '<>' --out '<>.iconset/icon_16x16.png' - sips -z 32 32 '<>' --out '<>.iconset/icon_16x16@2x.png' - sips -z 32 32 '<>' --out '<>.iconset/icon_32x32.png' - sips -z 64 64 '<>' --out '<>.iconset/icon_32x32@2x.png' - sips -z 128 128 '<>' --out '<>.iconset/icon_128x128.png' - sips -z 256 256 '<>' --out '<>.iconset/icon_128x128@2x.png' - sips -z 256 256 '<>' --out '<>.iconset/icon_256x256@2x.png' - sips -z 512 512 '<>' --out '<>.iconset/icon_512x512.png' - sips -z 512 512 '<>' --out '<>.iconset/icon_256x256@2x.png' - sips -z 1024 1024 '<>' --out '<>.iconset/icon_512x512@2x.png' - iconutil -c icns '<>.iconset'" - :utils '("sips" "iconutil") - :extensions "png")) - -;;;###autoload -(defun dwim-shell-commands-image-add-drop-shadow () - "Add a drop shadow." - (interactive) - (dwim-shell-command-on-marked-files - "Add a drop shadow." - "convert <> -bordercolor white -border 13 \\( +clone -background black -shadow 80x3+2+2 \\) +swap -background white -layers merge +repage <>-shadow.<>" - :utils "convert")) - -;;;###autoload -(defun dwim-shell-commands-image-trim-borders () - "Trim image(s) border (useful for video screenshots)." - (interactive) - (dwim-shell-command-on-marked-files - "Trim image border" - "magick convert -fuzz 3% -define trim:percent-background=0% -trim +repage '<>' '<>_trimmed.<>'" - :utils "magick")) - -;;;###autoload -(defun dwim-shell-commands-git-clone-clipboard-url-to-downloads () - "Clone git URL in clipboard to \"~/Downloads/\"." - (interactive) - (cl-assert (or (string-match-p "^\\(http\\|https\\|ssh\\)://" (current-kill 0)) - (string-match-p "^git@" (current-kill 0))) nil "No URL in clipboard") - (let* ((url (current-kill 0)) - (download-dir (expand-file-name "~/Downloads/")) - (project-dir (concat download-dir (file-name-base url))) - (default-directory download-dir)) - (when (or (not (file-exists-p project-dir)) - (when (y-or-n-p (format "%s exists. delete?" (file-name-base url))) - (delete-directory project-dir t) - t)) - (dwim-shell-command-on-marked-files - (format "Clone %s" (file-name-base url)) - (format "git clone %s" url) - :utils "git" - :on-completion (lambda (buffer _process) - (kill-buffer buffer) - (dired project-dir)))))) - -;;;###autoload -(defun dwim-shell-commands-http-serve-dir () - "HTTP serve current directory." - (interactive) - (cond ((executable-find "python3") - (dwim-shell-command-on-marked-files - "HTTP serve current dir" - "python3 -m http.server" - :utils "python3" - :focus-now t - :no-progress t)) - ((executable-find "python2") - (dwim-shell-command-on-marked-files - "HTTP serve current dir" - "python2 -m SimpleHTTPServer" - :utils "python2" - :focus-now t - :no-progress t)) - ((executable-find "python") - (dwim-shell-command-on-marked-files - "HTTP serve current dir" - "python -m SimpleHTTPServer" - :utils "python" - :focus-now t - :no-progress t)) - (t - (error "No python found")))) - -;;;###autoload -(defun 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 <>" - :utils "git")) - -;;;###autoload -(defun dwim-shell-commands-pass-git-pull () - "Pass git pull." - (interactive) - (dwim-shell-command-on-marked-files - "pass git pull" - "pass git pull" - :utils '("pass" "git") - :silent-success t)) - -;;;###autoload -(defun dwim-shell-commands-git-list-untracked-files () - "List untracked git files in `default-directory'." - (interactive) - (dwim-shell-command-on-marked-files - "List untracked" - "git ls-files --others ." - :utils "git" - :focus-now t)) - -;;;###autoload -(defun dwim-shell-commands-git-delete-untracked-files () - "Delete untracked git files in `default-directory'." - (interactive) - (when (y-or-n-p (format "Clean '%s'? \n\n%s\n...\n\n" - default-directory - (string-join - (seq-take (process-lines "git" "ls-files" "--others" ".") 3) - "\n"))) - (dwim-shell-command-on-marked-files - "Clean untracked" - "git clean -f ." - :utils "git" - :silent-success t))) - -;;;###autoload -(defun dwim-shell-commands-external-ip () - "Copy external IP to kill ring." - (interactive) - (let ((ip (car (last (process-lines "curl" "ifconfig.me"))))) - (kill-new ip) - (message "Copied %s" ip))) - -;;;###autoload -(defun dwim-shell-commands-macos-install-iphone-device-ipa () - "Install iPhone device .ipa. -Needs ideviceinstaller and libmobiledevice installed." - (interactive) - (dwim-shell-command-on-marked-files - "Install .ipa" - "ideviceinstaller -i '<>'" - :utils "ideviceinstaller")) - -;;;###autoload -(defun dwim-shell-commands-copy-to-downloads () - "Copy file to ~/Downloads." - (interactive) - (dwim-shell-command-foreach - (lambda (file) - (copy-file file "~/Downloads/" 1) - (file-name-concat "~/Downloads" (file-name-nondirectory file))) - :monitor-directory "~/Downloads")) - -;;;###autoload -(defun dwim-shell-commands-duplicate () - "Duplicate file." - (interactive) - (dwim-shell-command-on-marked-files - "Duplicate file(s)." - "cp -R '<>' '<>'" - :utils "cp")) - -;;;###autoload -(defun dwim-shell-commands-rename-all () - "Rename all marked file(s)." - (interactive) - (dwim-shell-command-on-marked-files - "Rename all" - "mv '<>' '<>(<>).<>'" - :utils "mv")) - -;;;###autoload -(defun dwim-shell-commands-move-to-downloads () - "Move file to ~/Downloads." - (interactive) - (dwim-shell-command-foreach - (lambda (file) - (rename-file file "~/Downloads/" 1) - (when (buffer-file-name) - (rename-buffer (file-name-nondirectory file)) - (set-visited-file-name - (file-name-concat "~/Downloads" (file-name-nondirectory file))) - (set-buffer-modified-p nil)) - (file-name-concat "~/Downloads" (file-name-nondirectory file))) - :monitor-directory "~/Downloads")) - -;;;###autoload -(defun dwim-shell-commands-copy-to-desktop () - "Copy file to ~/Desktop." - (interactive) - (dwim-shell-command-foreach - (lambda (file) - (copy-file file "~/Desktop/" 1) - (file-name-concat "~/Desktop" (file-name-nondirectory file))) - :monitor-directory "~/Desktop")) - -;;;###autoload -(defun dwim-shell-commands-move-to-desktop () - "Move file to ~/Desktop." - (interactive) - (dwim-shell-command-foreach - (lambda (file) - (rename-file file "~/Desktop/" 1) - (when (buffer-file-name) - (rename-buffer (file-name-nondirectory file)) - (set-visited-file-name - (file-name-concat "~/Desktop" (file-name-nondirectory file))) - (set-buffer-modified-p nil)) - (file-name-concat "~/Desktop" (file-name-nondirectory file))) - :monitor-directory "~/Desktop")) - -;;;###autoload -(defun 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)) - -;; Based on -;; https://apps.bram85.nl/git/bram/gists/src/commit/31ac3363da925daafa2420b7f96c67612ca28241/gists/dwim-0x0-upload.el -;;;###autoload -(defun dwim-shell-commands-upload-to-0x0 () - "Upload the marked files to 0x0.st" - (interactive) - (dwim-shell-command-on-marked-files - "0x0 upload" - "curl -Ffile=@<> -Fsecret= https://0x0.st" - :utils "curl" - :post-process-template - ;; Insert the single quotes at the appropriate place according to - ;; 0x0.st example online: - ;; curl -F'file=@yourfile.png' -Fsecret= https://0x0.st - ;; - ;; The placement of these single quotes confuse the escaping - ;; mechanisms of dwim-shell-command, as it considers @ as the - ;; opening 'quote' as it appears right in front of <>. - (lambda (template path) - (string-replace "-Ffile" "-F'file" - (string-replace path (concat path "'") template))) - :on-completion - (lambda (buffer process) - (if (= (process-exit-status process) 0) - (with-current-buffer buffer - (let ((url (car (last (split-string (string-trim (buffer-string)) "\n"))))) - (eww url) - (kill-new url) - (message "Copied: %s" (current-kill 0))) - (kill-buffer buffer)) - (switch-to-buffer buffer))))) - -(provide 'dwim-shell-commands) - -;;; dwim-shell-commands.el ends here diff --git a/elpa/dwim-shell-command-20251211.1325/dwim-shell-command-autoloads.el b/elpa/dwim-shell-command-20251211.1325/dwim-shell-command-autoloads.el new file mode 100644 index 00000000..cc9921d6 --- /dev/null +++ b/elpa/dwim-shell-command-20251211.1325/dwim-shell-command-autoloads.el @@ -0,0 +1,354 @@ +;;; dwim-shell-command-autoloads.el --- automatically extracted autoloads (do not edit) -*- lexical-binding: t -*- +;; Generated by the `loaddefs-generate' function. + +;; This file is part of GNU Emacs. + +;;; Code: + +(add-to-list 'load-path (or (and load-file-name (directory-file-name (file-name-directory load-file-name))) (car load-path))) + + + +;;; Generated autoloads from dwim-shell-command.el + +(autoload 'dwim-shell-command "dwim-shell-command" "\ +Execute DWIM shell command asynchronously using noweb templates. + +Which files + + `dwim-shell-command' attempts to guess which file(s) you may want + the command to operate on. + + 1. If visiting a `dired' buffer, draw the marked file(s). + 2. If visiting any other buffer with an associated file, use that. + +Templates + + Operate on drawn files using either the following: + + <> (file path) + <> (file path without extension) + <> (extension) + <> (generate a temporary directory) + <<*>> (all files joined) + <> (clipboard) + <>, <<1n>>, or <> (for current iteration) + + For example: + + With drawn files '(\"path/to/image1.png\" \"path/to/image2.png\") + + \"convert '<>' '<>.jpg'\" expands to + + \"convert 'path/to/image1.png' 'path/to/image1.jpg'\" + \"convert 'path/to/image2.png' 'path/to/image2.jpg'\" + + while \"ls -lh <<*>>\" expands to + + \"ls -lh path/to/image1.png path/to/image2.png\" + +Focus + + `dwim-shell-command' creates a process buffer to capture command + output, but doesn't display or focus on it by default. Instead, + it tries to guess what's more convenient to focus on. + + While the process is busy, show a spinner in the minibuffer. No + focus changes. + + After process is finished: + + 1. If there were any files created in the `default-directory', + jump to a `dired' buffer and move point to the new file (via + `dired-jump'). + + 2. If no new files were created, automatically switch focus to the + process buffer and display its output. + + Note: You can prevent this automatic focus by prepending your + command with whitespace. + + | + V + \" convert '<>' '<>.jpg'\" + + 3. If the shell command caused any errors, offer to focus the + process buffer and display its output. + +Quick exit + + Process buffers are read-only and can be quickly closed by + pressing `q'. + +Prefix + + With PREFIX, execute command that number of times. + +(fn PREFIX)" t) +(register-definition-prefixes "dwim-shell-command" '("dwim-shell-command-")) + + +;;; Generated autoloads from dwim-shell-commands.el + +(autoload 'dwim-shell-commands-audio-to-mp3 "dwim-shell-commands" "\ +Convert all marked audio to mp3(s)." t) +(autoload 'dwim-shell-extract-har-content "dwim-shell-commands" "\ +Extract all har content fields to files. + +(fn PREFIX)" t) +(autoload 'dwim-shell-extract-har-urls "dwim-shell-commands" "\ +Get all request URLs." t) +(autoload 'dwim-shell-view-open-ports-per-app "dwim-shell-commands" "\ +View open ports per app" t) +(autoload 'dwim-shell-commands-open-clipboard-url "dwim-shell-commands" "\ +Open clipboard URL. Offer to stream if possible." t) +(autoload 'dwim-shell-commands-stream-clipboard-url "dwim-shell-commands" "\ +Stream clipboard URL using mpv." t) +(autoload 'dwim-shell-commands-download-clipboard-stream-url "dwim-shell-commands" "\ +Download clipboard URL." t) +(autoload 'dwim-shell-commands-image-clear-exif-metadata "dwim-shell-commands" "\ +Clear EXIF metadata in image(s)." t) +(autoload 'dwim-shell-commands-image-scan-code "dwim-shell-commands" "\ +Scan any code (ie. qr, bar, etc) from image(s)." t) +(autoload 'dwim-shell-commands-image-exif-metadata "dwim-shell-commands" "\ +View EXIF metadata in image(s)." t) +(autoload 'dwim-shell-commands-tesseract-ocr-text-from-image "dwim-shell-commands" "\ +Extract text from image via tesseract." t) +(autoload 'dwim-shell-commands-image-view-location-in-openstreetmap "dwim-shell-commands" "\ +Open image(s) location in map/browser." t) +(autoload 'dwim-shell-commands-image-reverse-geocode-location "dwim-shell-commands" "\ +Reverse geocode image(s) location." t) +(autoload 'dwim-shell-commands-image-horizontal-flip "dwim-shell-commands" "\ +Horizontally flip image(s)." t) +(autoload 'dwim-shell-commands-image-vertical-flip "dwim-shell-commands" "\ +Horizontally flip image(s)." t) +(autoload 'dwim-shell-commands-image-to-jpg "dwim-shell-commands" "\ +Convert all marked images to jpg(s)." t) +(autoload 'dwim-shell-commands-image-to-png "dwim-shell-commands" "\ +Convert all marked images to png(s)." t) +(autoload 'dwim-shell-commands-image-to-webp "dwim-shell-commands" "\ +Convert all marked images to webp(s)." t) +(autoload 'dwim-shell-commands-svg-to-png "dwim-shell-commands" "\ +Convert all marked svg(s) to png(s)." t) +(autoload 'dwim-shell-commands-svg-to-favicons "dwim-shell-commands" "\ +Convert svg to common favicons." t) +(autoload 'dwim-shell-commands-make-transparent-png "dwim-shell-commands" "\ +Create a transparent png." t) +(autoload 'dwim-shell-commands-join-as-pdf "dwim-shell-commands" "\ +Join all marked images as a single pdf." t) +(autoload 'dwim-shell-commands-keep-pdf-page "dwim-shell-commands" "\ +Keep a page from pdf." t) +(autoload 'dwim-shell-commands-join-images-horizontally "dwim-shell-commands" "\ +Join all marked images horizontally as a single image." t) +(autoload 'dwim-shell-commands-join-images-vertically "dwim-shell-commands" "\ +Join all marked images vertically as a single image." t) +(autoload 'dwim-shell-commands-image-to-grayscale "dwim-shell-commands" "\ +Convert all marked images to grayscale." t) +(autoload 'dwim-shell-commands-reorient-image "dwim-shell-commands" "\ +Reorient images." t) +(autoload 'dwim-shell-commands-gif-to-video "dwim-shell-commands" "\ +Convert all marked gif(s) to video(s)." t) +(autoload 'dwim-shell-commands-extract-first-video-frame "dwim-shell-commands" "\ +Extract first frame from video(s)." t) +(autoload 'dwim-shell-commands-set-video-framerate "dwim-shell-commands" "\ +Set framerate for marked video(s)." t) +(autoload 'dwim-shell-commands-macos-empty-trash "dwim-shell-commands" "\ +Empty macOS trash." t) +(autoload 'dwim-shell-commands-macos-ocr-text-from-desktop-region "dwim-shell-commands" "\ +Select a macOS desktop area to OCR and copy recognized text to kill ring." t) +(autoload 'dwim-shell-commands-macos-ocr-text-from-image "dwim-shell-commands" "\ +OCR file and copy recognized text to kill ring." t) +(autoload 'dwim-shell-commands-macos-convert-to-mp4 "dwim-shell-commands" "\ +Convert to mov to mp4" t) +(autoload 'dwim-shell-commands-video-to-gif "dwim-shell-commands" "\ +Convert all marked videos to gif(s)." t) +(autoload 'dwim-shell-commands-video-to-mov "dwim-shell-commands" "\ +Convert all marked videos to mov(s)." t) +(autoload 'dwim-shell-commands-video-to-webp "dwim-shell-commands" "\ +Convert all marked videos to webp(s)." t) +(autoload 'dwim-shell-commands-webp-to-video "dwim-shell-commands" "\ +Convert all marked webp(s) to video(s)." t) +(autoload 'dwim-shell-commands-webp-to-gif "dwim-shell-commands" "\ +Convert all marked webp(s) to gif(s)." t) +(autoload 'dwim-shell-commands-video-to-hevc-mkv "dwim-shell-commands" "\ +Convert all marked videos to hevc mkv." t) +(autoload 'dwim-shell-commands-video-to-optimized-gif "dwim-shell-commands" "\ +Convert all marked videos to optimized gif(s)." t) +(autoload 'dwim-shell-commands-unzip "dwim-shell-commands" "\ +Unzip all marked archives (of any kind) using `atool'." t) +(autoload 'dwim-shell-commands-zip "dwim-shell-commands" "\ +Zip all marked files into archive.zip." t) +(autoload 'dwim-shell-commands-zip-password-protect "dwim-shell-commands" "\ +Protect/encrypt zip file(s) with password." t) +(autoload 'dwim-shell-commands-optimize-gif "dwim-shell-commands" "\ +Convert all marked videos to optimized gif(s)." t) +(autoload 'dwim-shell-commands-speed-up-gif "dwim-shell-commands" "\ +Speeds up gif(s)." t) +(autoload 'dwim-shell-commands-image-apply-ios-round-corners "dwim-shell-commands" "\ +Apply iOS round corners to image(s)." t) +(autoload 'dwim-shell-commands-clip-round-rect-gif "dwim-shell-commands" "\ +Clip gif(s) with round rectangle." t) +(autoload 'dwim-shell-commands-resize-gif "dwim-shell-commands" "\ +Resize marked gif(s)." t) +(autoload 'dwim-shell-commands-epub-to-org "dwim-shell-commands" "\ +Convert epub(s) to org." t) +(autoload 'dwim-shell-commands-docx-to-pdf "dwim-shell-commands" "\ +Convert docx(s) to pdf (via latex)." t) +(autoload 'dwim-shell-commands-kill-process "dwim-shell-commands" "\ +Select and kill process." t) +(autoload 'dwim-shell-commands-macos-add-to-photos "dwim-shell-commands" "\ +Add to Photos.app." t) +(autoload 'dwim-shell-commands-macos-toggle-bluetooth-device-connection "dwim-shell-commands" "\ +Toggle Bluetooth device connection." t) +(autoload 'dwim-shell-commands-macos-bin-plist-to-xml "dwim-shell-commands" "\ +Convert binary plist to xml." t) +(autoload 'dwim-shell-commands-macos-toggle-dark-mode "dwim-shell-commands" "\ +Toggle macOS dark mode." t) +(autoload 'dwim-shell-commands-macos-toggle-menu-bar-autohide "dwim-shell-commands" "\ +Toggle macOS dark mode." t) +(autoload 'dwim-shell-commands-pdf-to-txt "dwim-shell-commands" "\ +Convert pdf to txt." t) +(autoload 'dwim-shell-commands-resize-image-by-factor "dwim-shell-commands" "\ +Resize marked image(s) by factor." t) +(autoload 'dwim-shell-commands-resize-image-in-pixels "dwim-shell-commands" "\ +Resize marked image(s) in pixels." t) +(autoload 'dwim-shell-commands-pdf-password-protect "dwim-shell-commands" "\ +Add a password to pdf(s)." t) +(autoload 'dwim-shell-commands-pdf-password-unprotect "dwim-shell-commands" "\ +Remove a password from pdf(s)." t) +(autoload 'dwim-shell-commands-video-to-mp3 "dwim-shell-commands" "\ +Convert video(s) to mp3." t) +(autoload 'dwim-shell-commands-video-to-mp3-with-artwork "dwim-shell-commands" "\ +Convert video(s) to mp3 (keep frame as artwork)." t) +(autoload 'dwim-shell-commands-ndjson-to-org "dwim-shell-commands" "\ +Convert ndjson file to org." t) +(autoload 'dwim-shell-commands-set-media-artwork-image-metadata "dwim-shell-commands" "\ +Set image artwork metadata for media file(s)." t) +(autoload 'dwim-shell-commands-video-trim-beginning "dwim-shell-commands" "\ +Drop audio from all marked videos." t) +(autoload 'dwim-shell-commands-video-trim-end "dwim-shell-commands" "\ +Drop audio from all marked videos." t) +(autoload 'dwim-shell-commands-video-to-thumbnail "dwim-shell-commands" "\ +Generate a thumbnail for marked video(s)." t) +(autoload 'dwim-shell-commands-drop-video-audio "dwim-shell-commands" "\ +Drop audio from all marked videos." t) +(autoload 'dwim-shell-commands-ping-google "dwim-shell-commands" "\ +Ping google.com." t) +(autoload 'dwim-shell-commands-speed-up-video "dwim-shell-commands" "\ +Speed up video(s)." t) +(autoload 'dwim-shell-commands-speed-up-video-fragment "dwim-shell-commands" "\ +Speed up fragment in video(s)." t) +(autoload 'dwim-shell-commands-resize-video "dwim-shell-commands" "\ +Resize marked images." t) +(autoload 'dwim-shell-commands-clipboard-to-qr "dwim-shell-commands" "\ +Generate a QR code from clipboard." t) +(autoload 'dwim-shell-commands-sha-256-hash-file-at-clipboard-url "dwim-shell-commands" "\ +Download file at clipboard URL and generate SHA-256 hash." t) +(autoload 'dwim-shell-commands-view-sqlite-schema-diagram "dwim-shell-commands" "\ +View sqlite schema diagram." t) +(autoload 'dwim-shell-commands-open-externally "dwim-shell-commands" "\ +Open file(s) externally." t) +(autoload 'dwim-shell-commands-macos-caffeinate "dwim-shell-commands" "\ +Invoke caffeinate to prevent mac from sleeping." t) +(autoload 'dwim-shell-commands-macos-make-finder-alias "dwim-shell-commands" "\ +Make macOS Finder alias." t) +(autoload 'dwim-shell-commands-macos-version-and-hardware-overview-info "dwim-shell-commands" "\ +View macOS version and hardware overview info." t) +(autoload 'dwim-shell-commands-macos-reveal-in-finder "dwim-shell-commands" "\ +Reveal selected files in macOS Finder." t) +(autoload 'dwim-shell-commands-macos-share "dwim-shell-commands" "\ +Share selected files from macOS." t) +(autoload 'dwim-shell-commands-macos-toggle-display-rotation "dwim-shell-commands" "\ +Rotate display." t) +(autoload 'dwim-shell-commands-make-swift-package-library "dwim-shell-commands" "\ +Create a swift package library" t) +(autoload 'dwim-shell-commands-make-swift-package-executable "dwim-shell-commands" "\ +Create a swift package executable" t) +(autoload 'dwim-shell-commands-macos-set-default-app "dwim-shell-commands" "\ +Set default app for file(s)." t) +(autoload 'dwim-shell-commands-macos-open-with "dwim-shell-commands" "\ +Open file(s) with specific external app." t) +(autoload 'dwim-shell-commands-macos-open-with-firefox "dwim-shell-commands" "\ +Open file(s) in Firefox." t) +(autoload 'dwim-shell-commands-macos-open-with-safari "dwim-shell-commands" "\ +Open file(s) in Safari." t) +(autoload 'dwim-shell-commands-macos-start-recording-window "dwim-shell-commands" "\ +Select and start recording a macOS window." t) +(autoload 'dwim-shell-commands-macos-end-recording-window "dwim-shell-commands" "\ +Stop recording a macOS window." t) +(autoload 'dwim-shell-commands-macos-abort-recording-window "dwim-shell-commands" "\ +Stop recording a macOS window." t) +(autoload 'dwim-shell-commands-macos-screenshot-window "dwim-shell-commands" "\ +Select and screenshot macOS window." t) +(autoload 'dwim-shell-commands-files-combined-size "dwim-shell-commands" "\ +Get files combined file size." t) +(autoload 'dwim-shell-commands-image-to-icns "dwim-shell-commands" "\ +Convert png to icns icon." t) +(autoload 'dwim-shell-commands-image-add-drop-shadow "dwim-shell-commands" "\ +Add a drop shadow." t) +(autoload 'dwim-shell-commands-image-trim-borders "dwim-shell-commands" "\ +Trim image(s) border (useful for video screenshots)." t) +(autoload 'dwim-shell-commands-http-serve-dir "dwim-shell-commands" "\ +HTTP serve current directory." t) +(autoload 'dwim-shell-commands-git-clone-clipboard-url "dwim-shell-commands" "\ +Clone git URL in clipboard to a directory. +With C-u ARG, prompt for directory from +`dwim-shell-commands-git-clone-dirs'. + +With C-u C-u ARG, prompt for any directory. + +Without prefix, use the first directory in +`dwim-shell-commands-git-clone-dirs'. + +(fn &optional ARG)" t) +(autoload 'dwim-shell-commands-pass-git-pull "dwim-shell-commands" "\ +Pass git pull." t) +(autoload 'dwim-shell-commands-git-list-untracked-files "dwim-shell-commands" "\ +List untracked git files in `default-directory'." t) +(autoload 'dwim-shell-commands-git-delete-untracked-files "dwim-shell-commands" "\ +Delete untracked git files in `default-directory'." t) +(autoload 'dwim-shell-commands-external-ip "dwim-shell-commands" "\ +Copy external IP to kill ring." t) +(autoload 'dwim-shell-commands-macos-install-iphone-device-ipa "dwim-shell-commands" "\ +Install iPhone device .ipa. +Needs ideviceinstaller and libmobiledevice installed." t) +(autoload 'dwim-shell-commands-copy-to-downloads "dwim-shell-commands" "\ +Copy file to ~/Downloads." t) +(autoload 'dwim-shell-commands-duplicate "dwim-shell-commands" "\ +Duplicate file. + +With prefix, duplicate it n TIMES. + +(fn TIMES)" t) +(autoload 'dwim-shell-commands-rename-all "dwim-shell-commands" "\ +Rename all marked file(s)." t) +(autoload 'dwim-shell-commands-move-to-downloads "dwim-shell-commands" "\ +Move file to ~/Downloads." t) +(autoload 'dwim-shell-commands-copy-to-desktop "dwim-shell-commands" "\ +Copy file to ~/Desktop." t) +(autoload 'dwim-shell-commands-move-to-desktop "dwim-shell-commands" "\ +Move file to ~/Desktop." t) +(autoload 'dwim-shell-commands-kill-gpg-agent "dwim-shell-commands" "\ +Kill (thus restart) gpg agent. + +Useful for when you get this error: + +gpg: public key decryption failed: No pinentry +gpg: decryption failed: No pinentry" t) +(autoload 'dwim-shell-commands-upload-to-0x0 "dwim-shell-commands" "\ +Upload the marked files to 0x0.st" t) +(register-definition-prefixes "dwim-shell-commands" '("dwim-shell-commands-")) + +;;; End of scraped data + +(provide 'dwim-shell-command-autoloads) + +;; Local Variables: +;; version-control: never +;; no-byte-compile: t +;; no-update-autoloads: t +;; no-native-compile: t +;; coding: utf-8-emacs-unix +;; End: + +;;; dwim-shell-command-autoloads.el ends here diff --git a/elpa/dwim-shell-command-20251211.1325/dwim-shell-command-pkg.el b/elpa/dwim-shell-command-20251211.1325/dwim-shell-command-pkg.el new file mode 100644 index 00000000..6521d43e --- /dev/null +++ b/elpa/dwim-shell-command-20251211.1325/dwim-shell-command-pkg.el @@ -0,0 +1,7 @@ +;; -*- no-byte-compile: t; lexical-binding: nil -*- +(define-package "dwim-shell-command" "20251211.1325" + "Shell commands with DWIM behaviour." + '((emacs "28.1")) + :url "https://github.com/xenodium/dwim-shell-command" + :commit "35ca64d529987578dbbdf3f07d34310ee727b4a5" + :revdesc "35ca64d52998") diff --git a/elpa/dwim-shell-command-20251211.1325/dwim-shell-command.el b/elpa/dwim-shell-command-20251211.1325/dwim-shell-command.el new file mode 100644 index 00000000..b4ed1f34 --- /dev/null +++ b/elpa/dwim-shell-command-20251211.1325/dwim-shell-command.el @@ -0,0 +1,1072 @@ +;;; dwim-shell-command.el --- Shell commands with DWIM behaviour -*- lexical-binding: t; -*- + +;; Copyright (C) 2022 Alvaro Ramirez https://xenodium.com + +;; Author: Alvaro Ramirez +;; Package-Requires: ((emacs "28.1")) +;; URL: https://github.com/xenodium/dwim-shell-command +;; Package-Version: 20251211.1325 +;; Package-Revision: 35ca64d52998 + +;; This package 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, or (at your option) +;; any later version. + +;; This package 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. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: + +;; Provides `dwim-shell-command' as an opinionated DWIM alternative to +;; `shell-command'. +;; +;; Use `dwim-shell-command-on-marked-files' to create your own command +;; line utilities, invoked via M-x. +;; +;; See examples at https://github.com/xenodium/dwim-shell-command/blob/main/dwim-shell-commands.el + +;;; Code: + +(require 'cl-lib) +(require 'comint) +(require 'dired) +(require 'dired-aux) +(require 'map) +(require 'seq) +(require 'simple) +(require 'subr-x) +(require 'view) + +(defcustom dwim-shell-command-prompt + "DWIM shell command (<> <> <>): " + "`dwim-shell-command' prompt. Modify if shorter is preferred." + :type 'string + :group 'dwim-shell-command) + +(defcustom dwim-shell-command-default-command + " '<>'" + "Set to nil if no default shell command wanted." + :type 'string + :group 'dwim-shell-command) + +(defcustom dwim-shell-command-buffer-name + "DWIM shell command" + "`dwim-shell-command' buffer name. Modify if shorter is preferred." + :type 'string + :group 'dwim-shell-command) + +(defcustom dwim-shell-command-prompt-on-error nil + "If t, prompt user to focus buffer on process error. +Otherwise, automatically focus buffer on process error." + :type 'boolean + :group 'dwim-shell-command) + +(defcustom dwim-shell-command-use-absolute-paths nil + "If t, generate absolute paths in templates. Relative otherwise." + :type 'boolean + :group 'dwim-shell-command) + +(defcustom dwim-shell-command-shell-util nil + "Shell util, for example: \"zsh\" or \"bash\". +Set to nil to use `shell-file-name'." + :type 'string + :group 'dwim-shell-command) + +(defcustom dwim-shell-command-shell-args nil + "Shell util, for example: '(\"-x\" \"-c\"). +Set to nil to use `shell-command-switch'." + :type '(repeat string) + :group 'dwim-shell-command) + +(defcustom dwim-shell-command-shell-trace nil + "Attempt to add --xtrace to shell command to debug." + :type 'boolean + :group 'dwim-shell-command) + +(defcustom dwim-shell-command-done-buffer-name + (lambda (name) + (format "%s %s" name (propertize "done" 'face 'success))) + "Function to format buffer name on success. +Use `identify' to remove formatting." + :type 'function + :group 'dwim-shell-command) + +(defcustom dwim-shell-command-error-buffer-name + (lambda (name) + (format "%s %s" name (propertize "error" 'face 'error))) + "Function to format buffer name on error. +Use `identify' to remove formatting." + :type 'function + :group 'dwim-shell-command) + +(defvar dwim-shell-command--commands nil "All commands in progress.") + +(cl-defstruct + dwim-shell-command--command + "Describes a command in progress." + script + process + name + calling-buffer + reporter + on-completion + files-before + silent-success + error-autofocus + monitor-directory) + +;;;###autoload +(defun dwim-shell-command (prefix) + "Execute DWIM shell command asynchronously using noweb templates. + +Which files + + `dwim-shell-command' attempts to guess which file(s) you may want + the command to operate on. + + 1. If visiting a `dired' buffer, draw the marked file(s). + 2. If visiting any other buffer with an associated file, use that. + +Templates + + Operate on drawn files using either the following: + + <> (file path) + <> (file path without extension) + <> (extension) + <> (generate a temporary directory) + <<*>> (all files joined) + <> (clipboard) + <>, <<1n>>, or <> (for current iteration) + + For example: + + With drawn files '(\"path/to/image1.png\" \"path/to/image2.png\") + + \"convert '<>' '<>.jpg'\" expands to + + \"convert 'path/to/image1.png' 'path/to/image1.jpg'\" + \"convert 'path/to/image2.png' 'path/to/image2.jpg'\" + + while \"ls -lh <<*>>\" expands to + + \"ls -lh path/to/image1.png path/to/image2.png\" + +Focus + + `dwim-shell-command' creates a process buffer to capture command + output, but doesn't display or focus on it by default. Instead, + it tries to guess what's more convenient to focus on. + + While the process is busy, show a spinner in the minibuffer. No + focus changes. + + After process is finished: + + 1. If there were any files created in the `default-directory', + jump to a `dired' buffer and move point to the new file (via + `dired-jump'). + + 2. If no new files were created, automatically switch focus to the + process buffer and display its output. + + Note: You can prevent this automatic focus by prepending your + command with whitespace. + + | + V + \" convert '<>' '<>.jpg'\" + + 3. If the shell command caused any errors, offer to focus the + process buffer and display its output. + +Quick exit + + Process buffers are read-only and can be quickly closed by + pressing `q'. + +Prefix + + With PREFIX, execute command that number of times." + (interactive "p") + (let ((script (dwim-shell-command--read-shell-command))) + (dwim-shell-command-on-marked-files + dwim-shell-command-buffer-name script + :repeat prefix + :shell-util dwim-shell-command-shell-util + :shell-args dwim-shell-command-shell-args + :silent-success (string-prefix-p " " script) + :error-autofocus (not dwim-shell-command-prompt-on-error)))) + +(defun dwim-shell-command--read-shell-command () + "Read a shell command from the minibuffer, using `shell-command-history'." + (minibuffer-with-setup-hook + (lambda () + (beginning-of-line) + (setq-local minibuffer-default-add-function + #'minibuffer-default-add-shell-commands)) + (read-from-minibuffer dwim-shell-command-prompt dwim-shell-command-default-command nil nil 'shell-command-history))) + +(cl-defun dwim-shell-command-on-marked-files (buffer-name script &key utils extensions shell-util shell-args shell-trace shell-pipe post-process-template on-completion repeat silent-success no-progress error-autofocus monitor-directory focus-now join-separator temp-dir) + "Create DWIM utilities executing templated SCRIPT on given files. + +Here's a simple utility invoking SCRIPT to convert image files to jpg. + + (defun dwim-shell-command-convert-image-to-jpg () + \"Convert all marked images to jpg(s).\" + (interactive) + (dwim-shell-command-on-marked-files + \"Convert to jpg\" + \"convert -verbose '<>' '<>.jpg'\" + :utils \"convert\")) + +Check `dwim-shell-command-commands.el' for more examples. + +All command process output is written to a buffer with BUFFER-NAME. + +All params explained in `dwim-shell-command-execute-script'. + +Which files + + `dwim-shell-command-on-marked-files' attempts to guess which file(s) + you may want the command to operate on. + + 1. If visiting a `dired' buffer, draw the marked file(s). + 2. If visiting any other buffer with an associated file, use that. + +Templates + + Operate on drawn files using either the following: + + <> (file path) + <> (file path without extension) + <> (extension) + <> (generate a temporary directory) + <<*>> (all files joined) + <> (clipboard) + <>, <<1n>>, or <> (for current iteration) + + For example: + + With drawn files '(\"path/to/image1.png\" \"path/to/image2.png\") + + \"convert '<>' '<>.jpg'\" expands to + + \"convert 'path/to/image1.png' 'path/to/image1.jpg'\" + \"convert 'path/to/image2.png' 'path/to/image2.jpg'\" + + while \"ls -lh <<*>>\" expands to + + \"ls -lh path/to/image1.png path/to/image2.png\" + +Focus + + `dwim-shell-command-on-marked-files' creates a process buffer to + capture command output, but doesn't display or focus on it by + default. Instead, it tries to guess what's more convenient to focus + on. + + While the process is busy, show a spinner in the minibuffer. No + focus changes. + + After process is finished: + + 1. If there were any files created in the `default-directory', + jump to a `dired' buffer and move point to the new file (via + `dired-jump'). + + 2. If no new files were created, automatically switch focus to the + process buffer and display its output. + + Note: You can prevent this automatic focus by prepending your + command with whitespace. + + | + V + \" convert '<>' '<>.jpg'\" + + 3. If the shell command caused any errors, offer to focus the + process buffer and display its output. + +Quick exit + + Process buffers are read-only and can be quickly closed by + pressing `q'." + (dwim-shell-command-execute-script buffer-name script + :files (dwim-shell-command--files) + :utils utils + :extensions extensions + :shell-util shell-util + :shell-args shell-args + :shell-trace shell-trace + :shell-pipe shell-pipe + :post-process-template post-process-template + :on-completion on-completion + :silent-success silent-success + :no-progress no-progress + :repeat repeat + :error-autofocus error-autofocus + :monitor-directory monitor-directory + :focus-now focus-now + :join-separator join-separator + :temp-dir temp-dir)) + +(cl-defun dwim-shell-command-execute-script (buffer-name script &key files extensions shell-util shell-args shell-trace shell-pipe utils post-process-template on-completion silent-success temp-dir repeat no-progress error-autofocus monitor-directory focus-now join-separator) + "Execute a script asynchronously, DWIM style with SCRIPT and BUFFER-NAME. + +:FILES are used to instantiate SCRIPT as a noweb template. + + The following are supported: + + <> (file path) + <> (file path without extension) + <> (extension) + <> (generate a temporary directory) + <<*>> (all files joined) + <> (clipboard) + <>, <<1n>>, or <> (for current iteration) + + For example: + + Given :FILES '(\"path/to/image1.png\" \"path/to/image2.png\") + + \"convert '<>' '<>.jpg'\" expands to + + \"convert 'path/to/image1.png' 'path/to/image1.jpg'\" + \"convert 'path/to/image2.png' 'path/to/image2.jpg'\" + + and \"ls -lh <<*>>\" expands to + + \"ls -lh path/to/image1.png path/to/image2.png\" + +:EXTENSIONS ensures that all files in :FILES have the given +extensions. Can be either single string \"png\" or a list '(\"png\" \"jpg\"). + +:SHELL-UTIL and :SHELL-ARGS can be used to specify SCRIPT interpreter. + + For python, use: + + (dwim-shell-command-execute-script + \"Print Pi\" + \"import math + print math.pi\" + :shell-util \"python\" + :shell-args \"-c\") + +:SHELL-PIPE can be used to pipe SCRIPT to it + + For swift, use: + + (dwim-shell-command-on-marked-files + \"Print Pi\" + \"print(Double.pi)\" + :shell-pipe \"swift -\") + +:UTILS ensures that all needed command line utilities are installed. +Can be either a single string \"ffmpeg\" or a list '(\"ffmpet\" \"convert\"). + +:POST-PROCESS-TEMPLATE enables processing template further after noweb +instantiation. + +:ON-COMPLETION is invoked after SCRIPT executes (disabling DWIM +internal behavior). + +:SILENT-SUCCESS to avoid jumping to process buffer if neither error + nor file generated. + +:TEMP-DIR to generate a temporary directory for this command. +This is implied when <> appears in the script. + +:REPEAT Use to repeat script N number of times. + +:NO-PROGRESS Suppress progress reporting. + +:ERROR-AUTOFOCUS Automatically focus process buffer on error. + +:MONITOR-DIRECTORY Monitor this directory for new files. + +:FOCUS-NOW Immediately focus process buffer once started." + (cl-assert buffer-name nil "Script must have a buffer name") + (cl-assert (not (string-empty-p script)) nil "Script must not be empty") + (when (stringp extensions) + (setq extensions (list extensions))) + (when (and shell-util (stringp shell-util)) + (setq shell-util (list shell-util))) + (setq shell-util (or shell-util + (when shell-file-name + (list shell-file-name)) + '("zsh"))) + (setq shell-args (or shell-args + (when shell-command-switch + (list shell-command-switch)) + '("-x" "-c"))) + (when (and shell-args (stringp shell-args)) + (setq shell-args (list shell-args))) + ;; See if -x can be prepended. + (when (and (not (seq-contains-p shell-args "-x")) + (or shell-trace dwim-shell-command-shell-trace) + (apply #'dwim-shell-command--program-test + (seq-concatenate + 'list shell-util '("-x") shell-args (list "echo")))) + (setq shell-args (seq-concatenate 'list '("-x") shell-args))) + (when (stringp utils) + (setq utils (list utils))) + (when (and (string-match-p "\<\\>" script 0) (not temp-dir)) + (setq temp-dir (make-temp-file "dwim-shell-command-" t))) + (when (and repeat (> repeat 1)) + (cl-assert (<= (length files) 1) nil + "Must not repeat when multiple files are selected.") + (setq files (make-list repeat (or (seq-first files) "_no_file_selected_")))) + (when (seq-empty-p files) + (cl-assert (not (or (dwim-shell-command--contains-multi-file-ref script) + (dwim-shell-command--contains-single-file-ref script))) + nil "No files found to expand %s" + (or (dwim-shell-command--contains-multi-file-ref script) + (dwim-shell-command--contains-single-file-ref script)))) + (when extensions + (seq-do (lambda (file) + (cl-assert (seq-contains-p extensions (downcase (file-name-extension file))) + nil "Not a .%s file" (string-join extensions " ."))) + files)) + (seq-do (lambda (util) + (cl-assert (executable-find util) nil (format "%s not installed" util))) + utils) + (let* ((replacements (dwim-shell-command--extract-queries script)) + (proc-buffer (generate-new-buffer (format "*%s*" buffer-name))) + (template script) + (script "") + (files-before) + (proc) + (progress-reporter) + (padding (dwim-shell-command--digits (length files))) + (n (or (dwim-shell-command--n-start-value template padding) "1"))) + (if (seq-empty-p files) + (setq script (dwim-shell-command--expand-file-template template nil post-process-template temp-dir n replacements)) + (if (dwim-shell-command--contains-multi-file-ref template) + (setq script (dwim-shell-command--expand-files-template template files post-process-template temp-dir replacements join-separator)) + (seq-do (lambda (file) + (setq script + (concat script "\n" + (dwim-shell-command--expand-file-template template file post-process-template temp-dir n replacements))) + (setq n (dwim-shell-command--increment-string n padding))) + files))) + (setq script (string-trim script)) + (with-current-buffer proc-buffer + (let ((inhibit-message t)) + ;; Silence noise of entering shell-mode. + (comint-mode)) + (setq default-directory default-directory) + (shell-command-save-pos-or-erase) + (view-mode +1) + (setq view-exit-action 'kill-buffer)) + (setq files-before (dwim-shell-command--default-directory-files monitor-directory)) + (setq proc (apply #'start-process (seq-concatenate 'list + (list (buffer-name proc-buffer) proc-buffer) + shell-util + shell-args + (if shell-pipe + (list (format "echo '%s' | %s" script shell-pipe)) + (list script))))) + (set-process-query-on-exit-flag proc nil) + (if no-progress + (dwim-shell-command--message "%s started" (process-name proc)) + (setq progress-reporter (make-progress-reporter + ;; Append space so "done" is spaced when + ;; progress reporter is finished: + ;; + ;; *DWIM shell command* done + (concat (process-name proc) " "))) + (progress-reporter-update progress-reporter)) + ;; Momentarily set buffer to same window, so it's next in recent stack. + ;; Makes finding the shell command buffer a lot easier. + (let ((current (current-buffer))) + (pop-to-buffer-same-window proc-buffer) + (pop-to-buffer-same-window current)) + (when focus-now + (switch-to-buffer proc-buffer)) + (if (equal (process-status proc) 'exit) + (dwim-shell-command--finalize (current-buffer) + files-before + proc + progress-reporter + on-completion + silent-success + error-autofocus + monitor-directory) + (setq dwim-shell-command--commands + (push (cons (process-name proc) + (make-dwim-shell-command--command :script script + :process proc + :name (process-name proc) + :calling-buffer (current-buffer) + :files-before files-before + :reporter progress-reporter + :on-completion on-completion + :silent-success silent-success + :error-autofocus error-autofocus + :monitor-directory monitor-directory)) + dwim-shell-command--commands)) + (set-process-sentinel proc #'dwim-shell-command--sentinel) + (set-process-filter proc #'dwim-shell-command--filter)))) + +(cl-defun dwim-shell-command-read-file-name (prompt &key extension default) + "Invoke `read-string' with PROMPT. +Validates :EXTENSION and returns :DEFAULT if empty input." + (let ((file-name (read-string prompt))) + (cond ((string-empty-p (string-trim file-name)) + default) + ((and extension + (string-equal (file-name-extension file-name) extension)) + file-name) + ((and extension + (not (string-equal (file-name-extension file-name) extension))) + (user-error "Name must end in .%s" extension)) + (t + file-name)))) + +(defun dwim-shell-command--message (message &rest args) + "Like `dwim-shell-command--message' but non-blocking. +MESSAGE and ARGS same as `dwim-shell-command--message'." + (let ((message-id (random))) + (message (propertize (apply #'format message args) 'message-id message-id)) + (run-with-timer 3 nil + (lambda () + (when (and (current-message) + (eq (get-text-property 0 'message-id (current-message)) + message-id)) + (message nil)))))) + +(defun dwim-shell-command--extract-queries (template) + "Extract queries from TEMPLATE. + +For all queries, request a value from the user. + +For example: + + \"Hello <> world <>\" => + + ((\"<>\" . \"100\") + (\"<>\" . \"200\"))" + (let ((matches) + (pos 0)) + (while (and (< pos (length template)) + (string-match "<<\\([[:alpha:]]\\|[[:blank:]]\\)+:\\([[:alnum:]]\\|[.]\\)*>>" template pos)) + (setq pos (1+ (match-beginning 0))) + (let ((match 0)) + (push (match-string match template) matches) + (setq match (1+ match)))) + (seq-map (lambda (match) + (let* ((query (split-string (string-remove-suffix ">>" (string-remove-prefix "<<" match)) ":")) + (prompt (nth 0 query)) + (default-value (if (string-empty-p (nth 1 query)) + nil + (nth 1 query))) + (value (if (string-match-p "^\\([[:digit:]]\\|[.]\\)+$" default-value) + (number-to-string (read-number (format "%s: " prompt) + (string-to-number default-value))) + (string-trim (read-string (concat prompt + (if default-value + (format " (default %s): " default-value) + ": ")))))) + (result (cons match (if (string-empty-p value) + default-value + value)))) + (cl-assert (cdr result) nil "Must have a value")result)) + (seq-uniq (nreverse matches))))) + +(defun dwim-shell-command--digits (n) + "Return the number of digits in N." + (let ((count 0)) + (while (> n 0) + (setq n (/ n 10)) + (setq count (1+ count))) + count)) + +(defun dwim-shell-command--number-to-string (n padding) + "Convert N to string using PADDING for number of digits." + (format (format "%%0%dd" padding) n)) + +(defun dwim-shell-command--expand-files-template (template files &optional post-process-template temp-dir replacements join-separator) + "Expand TEMPLATE using FILES. + +Expand using <<*>> for FILES. + +Note: This expander cannot be used to expand <>, <>, or <>. + + For example: + + Given FILES '(\"path/to/image1.png\" \"path/to/image2.png\") + + \"du -csh '<<*>>'\" expands to + + \"du -csh 'path/to/image1.png' 'path/to/image2.png'\" + +Use POST-PROCESS-TEMPLATE to further expand template given own logic. + +Set TEMP-DIR to a unique temp directory to this template. + +REPLACEMENTS is a cons list of literals to replace with values. + +JOIN-SEPARATOR is used to join files from <<*>>." + (cl-assert (not (and (dwim-shell-command--contains-multi-file-ref template) + (dwim-shell-command--contains-single-file-ref template))) + nil "Must not have %s and %s in the same template" + (dwim-shell-command--contains-multi-file-ref template) + (dwim-shell-command--contains-single-file-ref template)) + (setq files (seq-map (lambda (file) + (if dwim-shell-command-use-absolute-paths + (expand-file-name file) + (file-relative-name (expand-file-name file) default-directory))) + files)) + + (mapc (lambda (replacement) + (setq template + (string-replace (car replacement) (cdr replacement) template))) + replacements) + + ;; Try to use quotes surrounding <<*>> in each path. + ;; "'<<*>>'" with '("path/to/image1.png" "path/to/image2.png") -> "'path/to/image1.png' 'path/to/image2.png'" + (when-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\<\\*\>\>" template)) + (unescaped-quote (nth 0 quoting)) + (escaped-quote (nth 1 quoting))) + (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\<\\*\>\>\\)\\([^ ]\\)" + (string-join (seq-map (lambda (file) + (concat unescaped-quote + (string-replace unescaped-quote + escaped-quote file) unescaped-quote)) + files) + (or join-separator " ")) + template nil nil 0))) + + ;; "<>" -> some.txt (if unique) + ;; -> some(1).txt (if it exist) + (when-let* ((found (string-match "\<\<\\([^ ]?+\\)(u)\>\>" template)) + (name (match-string 1 template))) + (setq template (replace-regexp-in-string "\<\<\\([^ ]?+\\)(u)\>\>" + (dwim-shell-command--unique-new-file-path name) + template nil nil 0))) + + ;; "<<~>>" -> "/home/user" (or equivalent). + (when (string-match "\<\<~\>\>" template) + (setq template (replace-regexp-in-string "\<\<~\>\>" + (expand-file-name "~") + template nil nil 0))) + + ;; "<<*>>" with '("path/to/image1.png" "path/to/image2.png") -> "path/to/image1.png path/to/image2.png" + (setq template (replace-regexp-in-string "\\(\<\<\\*\>\>\\)" (string-join files (or join-separator " ")) template nil nil 1)) + + ;; "<>" with TEMP-DIR -> "/var/folders/m7/ky091cp56d5g68nyhl4y7frc0000gn/T/dwim-shell-command-JNK4V5" + (setq template (replace-regexp-in-string "\\(\<\\>\\)" temp-dir template nil nil 1)) + + ;; "<>" with (current-kill 0) -> "whatever was in kill ring" + (when (string-match "\<\\>" template) + (setq template (replace-regexp-in-string "\\(\<\\>\\)" (current-kill 0) + template nil nil 1))) + + (when post-process-template + (setq template (funcall post-process-template template files))) + template) + +(defun dwim-shell-command--escaped-quote-around (needle haystack &optional unbalanced) + "Find NEEDLE in HAYSTACK that's surrounded by either ' or \". + +Set UNBALANCED to t if NEEDLE isn't surrounded by quotes on both sides. + +For example: + + \"\<\\>\" \"before \"<>\" after\" => (\"\"\" \"\\\\\"\") + + \"\<\\>\" \"before '<>' after\" => (\"'\" \"'\"'\"'\")" + (when-let ((found (string-match (format "\\([^ ]\\)\\(%s\\)\\([^ ]\\)" needle) haystack)) + (unescaped-quote (if unbalanced + (or (match-string 1 haystack) + (match-string 3 haystack)) + (cl-assert (string-equal (match-string 1 haystack) + (match-string 3 haystack)) nil + "%s must match %s" + (match-string 1 haystack) + (match-string 3 haystack)) + (match-string 1 haystack))) + (escaped-quote "'")) + ;; Known quoted quotes. + (cond + ((string-equal unescaped-quote "\"") + (setq escaped-quote "\\\\\"")) + ((string-equal unescaped-quote "'") + (setq escaped-quote "'\"'\"'")) + ;; Ignore slashes as user may be joining paths. + ((string-equal unescaped-quote "/") + (setq escaped-quote "/")) + (t + (error "Couldn't figure out how to quote for \"%s\" using %s and %s" + haystack + (match-string 1 haystack) + (match-string 3 haystack)))) + (list unescaped-quote escaped-quote))) + +(defun dwim-shell-command--expand-file-template (template file &optional post-process-template temp-dir current replacements) + "Expand TEMPLATE using FILE. + +Expand using <> for FILE, <> for FILE without extension, and +<> for FILE extension. <>, <<1n>>, or <> is replaced with +CURRENT. <> expands to unique \"some(1).txt\". + +Note: This expander cannot be used to expand <<*>>. + + For example: + + Given FILE \"path/to/image.png\" + + \"convert '<>' '<>.jpg'\" expands to + + \"convert 'path/to/image.png' 'path/to/image.jpg'\" + +Use POST-PROCESS-TEMPLATE to further expand template given own logic. + +Set TEMP-DIR to a unique temp directory to this template. + +REPLACEMENTS is a cons list of literals to replace with values." + (cl-assert (not (and (dwim-shell-command--contains-multi-file-ref template) + (dwim-shell-command--contains-single-file-ref template))) + nil "Must not have %s and %s in the same template" + (dwim-shell-command--contains-multi-file-ref template) + (dwim-shell-command--contains-single-file-ref template)) + + (mapc (lambda (replacement) + (setq template + (string-replace (car replacement) (cdr replacement) template))) + replacements) + + (when file + (setq file (if dwim-shell-command-use-absolute-paths + (expand-file-name file) + (file-relative-name (expand-file-name file) default-directory))) + ;; "<>" with "/path/tmp.txt" -> "/path/tmp" + (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\\>" template t)) + (unescaped-quote (nth 0 quoting)) + (escaped-quote (nth 1 quoting))) + (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\\>\\)" + (string-replace unescaped-quote escaped-quote (file-name-sans-extension file)) + template nil nil 2)) + (setq template (replace-regexp-in-string "\\(\<\\>\\)" (file-name-sans-extension file) template nil nil 1))) + + ;; "<>" with "/path/tmp.txt" -> "tmp.txt" + (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\\>" template t)) + (unescaped-quote (nth 0 quoting)) + (escaped-quote (nth 1 quoting))) + (setq template (replace-regexp-in-string "\\(\<\\>\\)\\([^ ]\\)" + (string-replace unescaped-quote escaped-quote (file-name-nondirectory file)) + template nil nil 1)) + (setq template (replace-regexp-in-string "\\(\<\\>\\)" (file-name-nondirectory file) template nil nil 1))) + + ;; "<>" with "/path/tmp.txt" -> "tmp" + (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\\>" template t)) + (unescaped-quote (nth 0 quoting)) + (escaped-quote (nth 1 quoting))) + (setq template (replace-regexp-in-string "\\(\<\\>\\)\\([^ ]\\)" + (string-replace unescaped-quote escaped-quote + (file-name-sans-extension (file-name-nondirectory file))) + template nil nil 1)) + (setq template (replace-regexp-in-string "\\(\<\\>\\)" (file-name-sans-extension + (file-name-nondirectory file)) template nil nil 1))) + + ;; "<>" with "/path/tmp.txt" -> "txt" + (if (file-name-extension file) + (setq template (replace-regexp-in-string "\\(\<\\>\\)" (file-name-extension file) template nil nil 1)) + ;; File had no extension. Attempt to remove .<>. + (setq template (replace-regexp-in-string "\\(\.\<\\>\\)" "" template nil nil 1))) + + ;; "<>" with "/path/file.jpg" -> "/path/file.jpg" + (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\\>" template)) + (unescaped-quote (nth 0 quoting)) + (escaped-quote (nth 1 quoting))) + (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\\>\\)\\([^ ]\\)" + (string-replace unescaped-quote escaped-quote file) + template nil nil 2)) + (setq template (replace-regexp-in-string "\\(\<\\>\\)" file template nil nil 1))) + + ;; "<>" with "/path/file.jpg" -> "/path/file(1).jpg" + (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\\>" template)) + (unescaped-quote (nth 0 quoting)) + (escaped-quote (nth 1 quoting))) + (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\\>\\)\\([^ ]\\)" + (string-replace unescaped-quote escaped-quote + (dwim-shell-command--unique-new-file-path file)) + template nil nil 2)) + (setq template (replace-regexp-in-string "\\(\<\\>\\)" + (dwim-shell-command--unique-new-file-path file) template nil nil 1)))) + + ;; "<>" -> some.txt (if unique) + ;; -> some(1).txt (if it exist) + (when-let* ((found (string-match "\<\<\\([^ ]?+\\)(u)\>\>" template)) + (name (match-string 1 template))) + (setq template (replace-regexp-in-string "\<\<\\([^ ]?+\\)(u)\>\>" + (dwim-shell-command--unique-new-file-path name) + template nil nil 0))) + + ;; "<<~>>" -> "/home/user" (or equivalent). + (when (string-match "\<\<~\>\>" template) + (setq template (replace-regexp-in-string "\<\<~\>\>" + (expand-file-name "~") + template nil nil 0))) + + ;; "<>" with TEMP-DIR -> "/var/folders/m7/ky091cp56d5g68nyhl4y7frc0000gn/T/dwim-shell-command-JNK4V5" + (setq template (replace-regexp-in-string "\\(\<\\>\\)" temp-dir template nil nil 1)) + + ;; "<>" with (current-kill 0) -> "whatever was in kill ring" + (when (string-match "\<\\>" template) + (setq template (replace-regexp-in-string "\\(\<\\>\\)" (current-kill 0) + template nil nil 1))) + + ;; "<>" or "<" or "<<1n>" with current. + (setq template (replace-regexp-in-string "\\(\<\<[[:alnum:]]?+n\>\>\\)" current + template nil nil 1)) + + (when post-process-template + (setq template (funcall post-process-template template file))) + template) + +(defun dwim-shell-command--contains-single-file-ref (template) + "Check for <>, <>, or <> in TEMPLATE." + (cond ((string-match "\<\\>" template) + "<>") + ((string-match "\<\\>" template) + "<>") + ((string-match "\<\\>" template) + "<>") + ((string-match "\<\\>" template) + "<>") + ((string-match "\<\\>" template) + "<>"))) + +(defun dwim-shell-command--contains-multi-file-ref (template) + "Check for <<*>> in TEMPLATE." + (when (string-match "\<\<\\*\>\>" template) + "<<*>>")) + +(defun dwim-shell-command--default-directory-files (override) + "List of files in current buffer's `default-directory'. +Use OVERRIDE to override `default-directory'." + (when-let ((default-directory (or override default-directory))) + (seq-map (lambda (filename) + (file-name-concat default-directory filename)) + (cl-remove-if + (lambda (e) (member e '("." ".."))) + (directory-files default-directory))))) + +(defun dwim-shell-command--last-modified-between (before after) + "Compare files in BEFORE and AFTER and return oldest file in diff." + (car (last (seq-sort #'file-newer-than-file-p + (seq-difference after before))))) + +(defun dwim-shell-command--finalize (calling-buffer files-before process progress-reporter on-completion silent-success error-autofocus monitor-directory) + "Finalize script execution. + +CALLING-BUFFER, FILES-BEFORE, PROCESS, PROGRESS-REPORTER, +ERROR-AUTOFOCUS, ON-COMPLETION, SILENT-SUCCESS, and MONITOR-DIRECTORY are +all needed to finalize processing." + (let ((oldest-new-file) + (files-after)) + (when progress-reporter + (progress-reporter-done progress-reporter)) + (if (= (process-exit-status process) 0) + (progn + (dwim-shell-command--message (funcall dwim-shell-command-done-buffer-name (process-name process))) + (with-current-buffer (process-buffer process) + (rename-buffer (generate-new-buffer-name (funcall dwim-shell-command-done-buffer-name (process-name process))))) + (if on-completion + (funcall on-completion (process-buffer process) process) + (with-current-buffer calling-buffer + (if (equal major-mode 'dired-mode) + (progn (when revert-buffer-function + (funcall revert-buffer-function nil t)) + ;; Region is not accurate if new files added. Wipe it. + (when (use-region-p) + (deactivate-mark))) + (when (and (or buffer-auto-save-file-name + buffer-file-name) + (not (verify-visited-file-modtime))) + ;; Already visiting a file. Revert if modified by command. + (revert-buffer :ignore-auto :noconfirm))) + (with-current-buffer (process-buffer process) + (setq files-after (dwim-shell-command--default-directory-files monitor-directory))) + (setq oldest-new-file + (dwim-shell-command--last-modified-between + files-before + files-after)) + ;; There's at least one new file. Show that. + (if oldest-new-file + (dired-jump nil oldest-new-file) + ;; Files may have been deleted but harder to track. + ;; Open dired and refresh to show files are gone. + (unless (equal (length files-after) + (length files-before)) + (dired monitor-directory) + (when revert-buffer-function + (funcall revert-buffer-function nil t))))) + (unless (equal (process-buffer process) + (window-buffer (selected-window))) + (if (or oldest-new-file silent-success) + (kill-buffer (process-buffer process)) + (unless silent-success + (switch-to-buffer (process-buffer process))))))) + (if on-completion + (funcall on-completion (process-buffer process) process) + (if (and (buffer-name (process-buffer process)) + (or error-autofocus + ;; Buffer already selected. Don't ask. + (equal (process-buffer process) + (window-buffer (selected-window))) + (ignore-error quit + (y-or-n-p (format "%s error, see output? " + (buffer-name (process-buffer process))))))) + (progn + (with-current-buffer (process-buffer process) + (rename-buffer (generate-new-buffer-name (funcall dwim-shell-command-error-buffer-name (process-name process))))) + (when (or error-autofocus + (equal (process-buffer process) + (window-buffer (selected-window)))) + (dwim-shell-command--message (funcall dwim-shell-command-error-buffer-name (process-name process)))) + (switch-to-buffer (process-buffer process))) + (kill-buffer (process-buffer process))))) + (setq dwim-shell-command--commands + (map-delete dwim-shell-command--commands (process-name process))))) + +(defun dwim-shell-command--unique-new-file-path (file-path) + "Return a unique FILE-PATH. + +If FILE-PATH already contains a number in the format (n), set counter to n. + +\"/tmp/blah.txt\" -> \"/tmp/blah(1).txt\" +\"/tmp/blah(2).txt\" -> \"/tmp/blah(3).txt\"" + (let* ((name (file-name-sans-extension file-path)) + (extension (file-name-extension file-path)) + (counter (if (string-match "(\\([0-9]+\\))$" name) + (string-to-number (match-string 1 name)) + 0))) + (when (string-match "(\\([0-9]+\\))$" name) + (setq name(replace-match "" t t name))) + (while (file-exists-p file-path) + (setq counter (1+ counter)) + (setq file-path (if extension + (format "%s(%d).%s" name counter extension) + (format "%s(%d)" name counter)))) + file-path)) + +(defun dwim-shell-command--sentinel (process _) + "Handles PROCESS sentinel and STATE." + (let ((exec (map-elt dwim-shell-command--commands (process-name process)))) + (dwim-shell-command--finalize (dwim-shell-command--command-calling-buffer exec) + (dwim-shell-command--command-files-before exec) + process + (dwim-shell-command--command-reporter exec) + (dwim-shell-command--command-on-completion exec) + (dwim-shell-command--command-silent-success exec) + (dwim-shell-command--command-error-autofocus exec) + (dwim-shell-command--command-monitor-directory exec)))) + +(defun dwim-shell-command--filter (process output) + "Handles PROCESS filtering and STATE and OUTPUT." + (when-let* ((exec (map-elt dwim-shell-command--commands (process-name process))) + (reporter (dwim-shell-command--command-reporter exec))) + (progress-reporter-update reporter)) + (comint-output-filter process output)) + +(defun dwim-shell-command--file-extensions () + "Return buffer file extension or marked/region extensions for a `dired' buffer." + (seq-uniq + (seq-map + #'file-name-extension + (seq-filter + #'file-name-extension + (dwim-shell-command--files))))) + +(defun dwim-shell-command--files () + "Return buffer file (if available) or marked/region files for a `dired' buffer." + (cl-assert (not (and (use-region-p) (let ((files (dired-get-marked-files nil nil nil t))) + ;; Based on `dired-number-of-marked-files'. + (cond ((null (cdr files)) + nil) + ((and (= (length files) 2) + (eq (car files) t)) + t) + (t + (not (seq-empty-p files))))))) + nil "Region and marked files both active. Choose one only.") + (if (buffer-file-name) + (list (buffer-file-name)) + (or + (dwim-shell-command--dired-paths-in-region) + (dired-get-marked-files)))) + +(defun dwim-shell-command--dired-paths-in-region () + "If `dired' buffer, return region files. nil otherwise." + (when (and (equal major-mode 'dired-mode) + (use-region-p)) + (let ((start (region-beginning)) + (end (region-end)) + (paths)) + (save-excursion + (save-restriction + (goto-char start) + (while (< (point) end) + ;; Skip non-file lines. + (while (and (< (point) end) (dired-between-files)) + (forward-line 1)) + (when (dired-get-filename nil t) + (setq paths (append paths (list (dired-get-filename nil t))))) + (forward-line 1)))) + paths))) + +(defun dwim-shell-command--n-start-value (template padding) + "Extract n start value from TEMPLATE using PADDING. +Falls back to \"1\"." + (when (string-match "\<\<\\([[:alnum:]]?+\\)n\>\>" template) + (if (string-empty-p (match-string 1 template)) + (dwim-shell-command--increment-string (number-to-string 0) padding) + (if-let* ((start-string (match-string 1 template)) + (start-n (string-to-number start-string))) + (dwim-shell-command--increment-string (number-to-string (1- start-n)) padding) + (match-string 1 template))))) + +(defun dwim-shell-command--increment-string (text padding) + "Increment TEXT using PADDING. +\"a\" -> \"b\" +\"1\" -> \"2\"" + (cond ((string-match "^[[:alpha:]]$" text) ;; char + (char-to-string (1+ (string-to-char (match-string 0 text))))) + ((string-match "^[[:digit:]]+$" text) ;; char + (dwim-shell-command--number-to-string (1+ (string-to-number (match-string 0 text))) padding)))) + +(defun dwim-shell-command--program-test (program &rest args) + "Test that running PROGRAM with ARGS is successful." + (eq 0 (apply #'call-process program nil nil nil args))) + +(cl-defun dwim-shell-command-foreach (fun &key monitor-directory) + "Execute FUN for each file. +Monitor :MONITOR-DIRECTORY for new file and `dired-jump' to it." + (let ((files (dwim-shell-command--files)) + (files-before (dwim-shell-command--default-directory-files monitor-directory)) + (oldest-new-file) + (created-file) + (jump-to)) + (mapc (lambda (file-path) + (setq created-file (or (funcall fun file-path) + created-file))) + files) + (setq oldest-new-file (dwim-shell-command--last-modified-between + files-before + (dwim-shell-command--default-directory-files monitor-directory))) + (setq jump-to (or oldest-new-file created-file)) + (when jump-to + (dired-jump nil jump-to)))) + +(provide 'dwim-shell-command) + +;;; dwim-shell-command.el ends here diff --git a/elpa/dwim-shell-command-20251211.1325/dwim-shell-commands.el b/elpa/dwim-shell-command-20251211.1325/dwim-shell-commands.el new file mode 100644 index 00000000..06f78261 --- /dev/null +++ b/elpa/dwim-shell-command-20251211.1325/dwim-shell-commands.el @@ -0,0 +1,1859 @@ +;;; dwim-shell-commands.el --- Useful commands -*- lexical-binding: t; -*- + +;; Copyright (C) 2022 Alvaro Ramirez + +;; Author: Alvaro Ramirez +;; URL: https://github.com/xenodium/dwim-shell-command + +;; This package 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, or (at your option) +;; any later version. + +;; This package 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. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: + +;; A collection of useful commands created via +;; `dwim-shell-command-on-marked-files'. + +;;; Code: + +(require 'browse-url) +(require 'cl-lib) +(require 'dwim-shell-command) +(require 'files) +(require 'proced) +(require 'seq) +(require 'subr-x) + +;;;###autoload +(defun dwim-shell-commands-audio-to-mp3 () + "Convert all marked audio to mp3(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to mp3" + "ffmpeg -stats -n -i '<>' -acodec libmp3lame '<>.mp3'" + :utils "ffmpeg")) + +;;;###autoload +(defun dwim-shell-extract-har-content (prefix) + "Extract all har content fields to files." + (interactive "P") + (when prefix + (setq prefix (string-trim (read-string "Transcription locale: " "ja-JP"))) + (when (string-empty-p prefix) + (error "No locale given"))) + (dwim-shell-command-on-marked-files + "Extract har response content" + (format "declare -A mime_map=( \ + [\"audio/mpeg\"]=\"mp3\" \ + [\"image/jpeg\"]=\"jpg\" \ + [\"text/plain\"]=\"txt\" \ + [\"application/json\"]=\"json\" \ + # TODO: Add more mappings if needed. +) +outdir='<>' +mkdir -p \"${outdir}\" + : > \"${outdir}/<>.org\" +jq -r '.log.entries[] | @base64' '<>' | while read -r entry; do + url=$(echo \"$entry\" | base64 --decode | jq -r '.request.url') + basename=$(echo \"$entry\" | base64 --decode | jq -r '.request.url | capture(\"(?<=//)[^/]+/(?.*)\") | .path | gsub(\"[^a-zA-Z0-9]\"; \"_\")') + mime=$(echo \"$entry\" | base64 --decode | jq -r '.response.content.mimeType') + extension=${mime_map[$mime]:-\"bin\"} + name=\"${outdir}/${basename:0:255}.${extension}\" + echo \"${name}\" + content=$(echo \"$entry\" | base64 --decode | jq -r '.response.content.text') + echo \"$content\" | base64 --decode > \"${name}\" + if [ -f \"${name}\" ] && %s; then + transcription=$(macosrec --speech-to-text --locale %s --input \"${name}\") + if [ $? -eq 0 ]; then + transcribed_name=\"${outdir}/${transcription}.${extension}\" + mv \"${name}\" \"${transcribed_name}\" + basename=$(basename \"${transcribed_name}\") + echo \"[[file:${basename}][${transcription}]] [[${url}][remote]]\" >> \"${outdir}/<>.org\" + fi + fi +done" + (if prefix + "true" + "false") + prefix) + :utils "jq" + :extensions "har")) + +;;;###autoload +(defun dwim-shell-extract-har-urls () + "Get all request URLs." + (interactive) + (dwim-shell-command-on-marked-files + "Extract har request URLs" + "jq -r '.log.entries[].request.url' '<>'" + :utils "jq")) + +;;;###autoload +(defun dwim-shell-view-open-ports-per-app () + "View open ports per app" + (interactive) + (dwim-shell-command-on-marked-files + "Ports per app" + ;; https://x.com/nurmiwilliam/status/1823228630664634695 + "sudo lsof -iTCP -sTCP:LISTEN -n -P | awk 'NR>1 {print $9, $1, $2}' | sed 's/.*://' | sort -u | while read port process pid; do echo \"Port $port: $(ps -p $pid -o command= | sed 's/^-//') (PID: $pid)\"; done | sort -n" + :utils "jq")) + +;;;###autoload +(defun dwim-shell-commands-open-clipboard-url () + "Open clipboard URL. Offer to stream if possible." + (interactive) + (let ((url (or (current-kill 0) + (user-error "Nothing in clipboard")))) + (dwim-shell-commands-url-browse url))) + +(defun dwim-shell-commands-url-browse (url &rest args) + "If URL is playable media, offer to open in mpv. Else browser. +Optional argument ARGS as per `browse-url-default-browser'" + (if (and (or (string-match-p "^http[s]?://.*youtube.com" url) + (string-match-p "^http[s]?://.*m.youtube.com" url) + (string-match-p "^http[s]?://.*youtu.be" url) + (string-match-p "^http[s]?://.*soundcloud.com" url) + (string-match-p "^http[s]?://.*redditmedia.com" url) + (string-match-p "^http[s]?://.*reddit.com" url) + (string-match-p "^http[s]?://.*bandcamp.com" url)) + (y-or-n-p "Stream from mpv? ")) + (dwim-shell-command-on-marked-files + "Streaming" + (format "mpv --geometry=30%%x30%%+100%%+0%% '%s'" url) + :utils "mpv" + :no-progress t + :error-autofocus t + :silent-success t) + (funcall #'browse-url-default-browser url args))) + +;;;###autoload +(defun dwim-shell-commands-stream-clipboard-url () + "Stream clipboard URL using mpv." + (interactive) + (cl-assert (string-match-p "^http[s]?://" (current-kill 0)) nil "Not a URL") + (dwim-shell-command-on-marked-files + "Streaming" + "mpv --geometry=30%x30%+100%+0% \"<>\"" + :utils "mpv" + :no-progress t + :error-autofocus t + :silent-success t)) + +;;;###autoload +(defun dwim-shell-commands-download-clipboard-stream-url () + "Download clipboard URL." + (interactive) + (cl-assert (string-match-p "^http[s]?://" (current-kill 0)) nil "Not a URL") + (dwim-shell-command-on-marked-files + "Downloading" + "youtube-dl --newline -o \"~/Downloads/%(title)s.%(ext)s\" \"<>\"" + :utils "youtube-dl" + :no-progress t + :error-autofocus t + :monitor-directory "~/Downloads" + :silent-success t)) + +;;;###autoload +(defun dwim-shell-commands-image-clear-exif-metadata () + "Clear EXIF metadata in image(s)." + (interactive) + (dwim-shell-command-on-marked-files + "View EXIF" + "cp '<>' '<>_cleared.<>' + exiftool -all:all= -overwrite_original '<>_cleared.<>'" + :utils "exiftool")) + +;;;###autoload +(defun dwim-shell-commands-image-scan-code () + "Scan any code (ie. qr, bar, etc) from image(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Scan code" + "zbarimg --quiet '<>'" + :utils "zbarimg")) + +;;;###autoload +(defun dwim-shell-commands-image-exif-metadata () + "View EXIF metadata in image(s)." + (interactive) + (dwim-shell-command-on-marked-files + "View EXIF" + "exiftool '<>'" + :utils "exiftool")) + +;;;###autoload +(defun dwim-shell-commands-tesseract-ocr-text-from-image () + "Extract text from image via tesseract." + (interactive) + (dwim-shell-command-on-marked-files + "Extract text from image via tesseract." + "tesseract '<>' -" + :utils "tesseract")) + +;;;###autoload +(defun dwim-shell-commands-image-view-location-in-openstreetmap () + "Open image(s) location in map/browser." + (interactive) + (dwim-shell-command-on-marked-files + "Browse location" + "lat=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<>' | tail -n 1 | cut -s -d',' -f2-2)\" + if [ -z \"$lat\" ]; then + echo \"no latitude\" + exit 1 + fi + lon=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<>' | tail -n 1 | cut -s -d',' -f3-3)\" + if [ -z \"$lon\" ]; then + echo \"no longitude\" + exit 1 + fi + if [[ $OSTYPE == darwin* ]]; then + open \"http://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}&layers=C\" + else + xdg-open \"http://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}&layers=C\" + fi" + :utils "exiftool" + :error-autofocus t + :silent-success t)) + +;;;###autoload +(defun dwim-shell-commands-image-reverse-geocode-location () + "Reverse geocode image(s) location." + (interactive) + (dwim-shell-command-on-marked-files + "Reverse geocode" + "lat=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<>' | tail -n 1 | cut -s -d',' -f2-2)\" + if [ -z \"$lat\" ]; then + echo \"no latitude\" + exit 1 + fi + lon=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<>' | tail -n 1 | cut -s -d',' -f3-3)\" + if [ -z \"$lon\" ]; then + echo \"no longitude\" + exit 1 + fi + json=$(curl \"https://nominatim.openstreetmap.org/reverse?format=json&accept-language=en&lat=${lat}&lon=${lon}&zoom=18&addressdetails=1\") + echo \"json_start $json json_end\"" + :utils '("exiftool" "curl") + :silent-success t + :error-autofocus t + :on-completion (lambda (buffer _process) + (with-current-buffer buffer + (goto-char (point-min)) + (let ((matches '())) + (while (re-search-forward "^json_start\\(.*?\\)json_end" nil t) + (push (match-string 1) matches)) + (message "%s" (string-join (seq-map (lambda (json) + (map-elt (json-parse-string json :object-type 'alist) 'display_name)) + matches) + "\n"))) + (kill-buffer buffer))))) + +;;;###autoload +(defun dwim-shell-commands-image-horizontal-flip () + "Horizontally flip image(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Image horizontal flip" + "convert -verbose -flop '<>' '<>_h_flipped.<>'" + :utils "convert")) + +;;;###autoload +(defun dwim-shell-commands-image-vertical-flip () + "Horizontally flip image(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Image vertical flip" + "convert -verbose -flip '<>' '<>_v_flipped.<>'" + :utils "convert")) + +;;;###autoload +(defun dwim-shell-commands-image-to-jpg () + "Convert all marked images to jpg(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to jpg" + "convert -verbose '<>' '<>.jpg'" + :utils "convert")) + +;;;###autoload +(defun dwim-shell-commands-image-to-png () + "Convert all marked images to png(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to png" + "convert -verbose '<>' '<>.png'" + :utils "convert")) + +;;;###autoload +(defun dwim-shell-commands-image-to-webp () + "Convert all marked images to webp(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to png" + "convert -verbose '<>' '<>.webp'" + :utils "convert")) + +;;;###autoload +(defun dwim-shell-commands-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 png -o '<>.png'" + :utils "rsvg-convert")) + +;;;###autoload +(defun dwim-shell-commands-svg-to-favicons () + "Convert svg to common favicons." + (interactive) + (dwim-shell-command-on-marked-files + "Create favicons" + "echo '>\">' + rsvg-convert -o '<>-16.png' -w 16 -h 16 '<>' + echo '>-16.png\" sizes=\"16x16\">' + rsvg-convert -o '<>-32.png' -w 32 -h 32 '<>' + echo '>-32.png\" sizes=\"32x32\">' + rsvg-convert -o '<>-48.png' -w 48 -h 48 '<>' + echo '>-48.png\" sizes=\"48x48\">' + convert '<>-16.png' '<>-32.png' '<>-48.png' '<>.ico' + echo '>.ico'\">' + rsvg-convert -o '<>-64.png' -w 64 -h 64 '<>' + echo '>-64.png\" sizes=\"64x64\">' + rsvg-convert -o '<>-180.png' -w 180 -h 180 '<>' + echo '>-180.png\" sizes=\"180x180\">' + read -n 1 -s -r" + :extensions "svg" + :utils '("convert" "rsvg-convert") + :focus-now t)) + +;;;###autoload +(defun dwim-shell-commands-make-transparent-png () + "Create a transparent png." + (interactive) + (dwim-shell-command-on-marked-files + "Create transparent png" + "convert -verbose -size <>x<> xc:none '<>x<>.png(u)>>'" + :utils "convert")) + +;;;###autoload +(defun dwim-shell-commands-join-as-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")) + +;;;###autoload +(defun dwim-shell-commands-keep-pdf-page () + "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 '<>' --pages . %d -- '<>_%d.<>'" page-num page-num) + :utils "qpdf"))) + +;;;###autoload +(defun dwim-shell-commands-join-images-horizontally () + "Join all marked images horizontally as a single image." + (interactive) + (let ((filename (format "joined.%s" + (or (seq-first (dwim-shell-command--file-extensions)) "png")))) + (dwim-shell-command-on-marked-files + "Join images horizontally" + (format "convert -verbose '<<*>>' +append '<<%s(u)>>'" + (dwim-shell-command-read-file-name + (format "Join as image named (default \"%s\"): " filename) + :default filename)) + :utils "convert"))) + +;;;###autoload +(defun dwim-shell-commands-join-images-vertically () + "Join all marked images vertically as a single image." + (interactive) + (let ((filename (format "joined.%s" + (or (seq-first (dwim-shell-command--file-extensions)) "png")))) + (dwim-shell-command-on-marked-files + "Join images vertically" + (format "convert -verbose '<<*>>' -gravity center -append '<<%s(u)>>'" + (dwim-shell-command-read-file-name + (format "Join as image named (default \"%s\"): " filename) + :default filename)) + :utils "convert"))) + +;;;###autoload +(defun dwim-shell-commands-image-to-grayscale () + "Convert all marked images to grayscale." + (interactive) + (dwim-shell-command-on-marked-files + "Convert image to grayscale" + "convert -verbose -type Grayscale '<>' '<>_grayscale.<>'" + :utils "convert")) + +;;;###autoload +(defun dwim-shell-commands-reorient-image () + "Reorient images." + (interactive) + (dwim-shell-command-on-marked-files + "Reorient image" + "convert -verbose -auto-orient '<>' '<>_reoriented.<>'" + :utils "convert")) + +;;;###autoload +(defun dwim-shell-commands-gif-to-video () + "Convert all marked gif(s) to video(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to gif" + "ffmpeg -i '<>' -movflags faststart -pix_fmt yuv420p -vf 'scale=trunc(iw/2)*2:trunc(ih/2)*2' '<>.mp4'" + :utils "ffmpeg")) + +;;;###autoload +(defun dwim-shell-commands-extract-first-video-frame () + "Extract first frame from video(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to gif" + "ffmpeg -i '<>' -vframes 1 -q:v 2 '<>.jpg'" + :utils "ffmpeg")) + +;;;###autoload +(defun dwim-shell-commands-set-video-framerate () + "Set framerate for marked video(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Set video framerate" + (let ((fps (read-number "Target framerate (fps): " 30))) + (format "ffmpeg -i '<>' -r %d -c:a copy '<>_%dfps.<>'" + fps fps)) + :utils "ffmpeg")) + +;;;###autoload +(defun dwim-shell-commands-macos-empty-trash () + "Empty macOS trash." + (interactive) + (when (y-or-n-p "Empty macOS trash? ") + (dwim-shell-command-on-marked-files + "Empty macOS trash" + "trash -e -y" + :silent-success t + :utils "trash"))) + +;;;###autoload +(defun dwim-shell-commands-macos-ocr-text-from-desktop-region () + "Select a macOS desktop area to OCR and copy recognized text to kill ring." + (interactive) + (dwim-shell-command-on-marked-files + "OCR area" + "macosrec --ocr" + ;; brew install xenodium/macosrec/macosrec + :utils "macosrec" + :on-completion + (lambda (buffer process) + (when-let ((success (= (process-exit-status process) 0)) + (text (with-current-buffer buffer + (string-trim (buffer-string))))) + (progn + (kill-new text) + (switch-to-buffer buffer) + (goto-char (point-min)) + (message "OCR copied to clipboard")))))) + +;;;###autoload +(defun dwim-shell-commands-macos-ocr-text-from-image () + "OCR file and copy recognized text to kill ring." + (interactive) + (dwim-shell-command-on-marked-files + "OCR area" + "macosrec --ocr --clipboard --input '<>'" + ;; brew install xenodium/macosrec/macosrec + :utils "macosrec" + :on-completion + (lambda (buffer process) + (when-let ((success (= (process-exit-status process) 0)) + (text (with-current-buffer buffer + (string-trim (buffer-string))))) + (progn + (kill-new text) + (switch-to-buffer buffer) + (goto-char (point-min)) + (message "OCR copied to clipboard")))))) + +;;;###autoload +(defun dwim-shell-commands-macos-convert-to-mp4 () + "Convert to mov to mp4" + (interactive) + (dwim-shell-command-on-marked-files + "Convert to mov to mp4" + ;; "ffmpeg -loglevel quiet -stats -y -i <>.mov -vcodec h264 -acodec copy <>.mp4" + ;; Found the encoder via ffmpeg -encoders | grep videotoolbox, + ;; source https://www.reddit.com/r/ffmpeg/comments/14pqeex/getting_0_gpu_utilization_with_apple_silicons/ + "ffmpeg -i '<>' -map_metadata 0 \ + -c:v hevc_videotoolbox -q:v 35 -preset fast -c:a aac -b:a 128k -tag:v hvc1 '<>'.mp4" + :utils "ffmpeg")) + +;;;###autoload +(defun dwim-shell-commands-video-to-gif () + "Convert all marked videos to gif(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to gif" + "ffmpeg -loglevel quiet -stats -y -i '<>' -pix_fmt rgb24 -r 15 '<>.gif'" + :utils "ffmpeg")) + +;;;###autoload +(defun dwim-shell-commands-video-to-mov () + "Convert all marked videos to mov(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to mov" + "ffmpeg -i '<>' -c:v libx264 -c:a aac '<>.mov'" + :utils "ffmpeg")) + +;;;###autoload +(defun dwim-shell-commands-video-to-webp () + "Convert all marked videos to webp(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to webp" + "ffmpeg -i '<>' -vcodec libwebp -filter:v fps=fps=10 -compression_level 3 -loop 0 -preset default -an -vsync 0 '<>'.webp" + :utils "ffmpeg")) + +;;;###autoload +(defun dwim-shell-commands-webp-to-video () + "Convert all marked webp(s) to video(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert webp to video" + "convert '<>' '<>/<>.gif' + ffmpeg -i '<>/<>.gif' -movflags faststart -pix_fmt yuv420p -vf 'scale=trunc(iw/2)*2:trunc(ih/2)*2' '<>.mp4'" + :utils '("ffmpeg" "convert") + :extensions "webp")) + +;;;###autoload +(defun dwim-shell-commands-webp-to-gif () + "Convert all marked webp(s) to gif(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert webp to video" + "convert '<>' '<>.gif'" + :utils '("convert") + :extensions "webp")) + +;;;###autoload +(defun dwim-shell-commands-video-to-hevc-mkv () + "Convert all marked videos to hevc mkv." + (interactive) + (dwim-shell-command-on-marked-files + "Convert video to h265 " + "REPO_DIR=/tmp/other_video_transcoding + if ! [ -d \"$REPO_DIR\" ] + then + git clone https://github.com/donmelton/other_video_transcoding.git $REPO_DIR + fi + pushd $REPO_DIR + git pull origin master || echo \"skipping repo update...\" + popd + ruby $REPO_DIR/bin/other-transcode --hevc '<>'" + :utils '("git" "ffmpeg" "mkvtoolnix" "mpv"))) + +;;;###autoload +(defun dwim-shell-commands-video-to-optimized-gif () + "Convert all marked videos to optimized gif(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to optimized gif" + "ffmpeg -loglevel quiet -stats -y -i '<>' -pix_fmt rgb24 -r 15 '<>.gif' + gifsicle -O3 '<>.gif' --lossy=80 -o '<>.gif'" + :utils '("ffmpeg" "gifsicle"))) + +;;;###autoload +(defun dwim-shell-commands-unzip () + "Unzip all marked archives (of any kind) using `atool'." + (interactive) + (dwim-shell-command-on-marked-files + "Unzip" "atool --extract --explain '<>'" + :utils "atool")) + +;;;###autoload +(defun dwim-shell-commands-zip () + "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 '<>.<>' '<>'" + "zip -r '<>' '<<*>>'") + :utils "zip")) + +;;;###autoload +(defun dwim-shell-commands-zip-password-protect () + "Protect/encrypt zip file(s) with password." + (interactive) + (dwim-shell-command-on-marked-files + "Add zip password" "zipcloak --output-file '<>_protected.<>' '<>'" + :extensions "zip" + :utils "zipcloak")) + +;;;###autoload +(defun dwim-shell-commands-optimize-gif () + "Convert all marked videos to optimized gif(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to optimized gif" + "gifsicle -O3 '<>' --lossy=90 -o '<>_optimized.gif'" + :utils '("ffmpeg" "gifsicle"))) + +;;;###autoload +(defun dwim-shell-commands-speed-up-gif () + "Speeds up gif(s)." + (interactive) + (let ((factor (string-to-number + (completing-read "Speed up x times: " '("1" "1.5" "2" "2.5" "3" "4"))))) + (dwim-shell-command-on-marked-files + "Speed up gif" + (format "gifsicle -U '<>' <> -O2 -o '<>_x%s.<>'" factor) + :extensions "gif" :utils '("gifsicle" "identify") + :post-process-template (lambda (script file) + (string-replace "<>" (dwim-shell-commands--gifsicle-frames-every factor file) script))))) + +;;;###autoload +(defun dwim-shell-commands-image-apply-ios-round-corners () + "Apply iOS round corners to image(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Speed up gif" + "set -o xtrace + width=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=noprint_wrappers=1:nokey=1 '<>') + height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 '<>') + corner=$((${width}/4)) + echo ${corner} + convert -size ${width}x${height} xc:none -fill white -draw \"roundRectangle 0,0 ${width},${height} ${corner},${corner}\" '<>' -compose SrcIn -composite '<>_ios_round.<>'" + :utils '("ffprobe" "convert"))) + +;;;###autoload +(defun dwim-shell-commands-clip-round-rect-gif () + "Clip gif(s) with round rectangle." + (interactive) + (dwim-shell-command-on-marked-files + "Clip round rect gif(s)" + "width=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=noprint_wrappers=1:nokey=1 '<>') + height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 '<>') + convert -quiet -size \"${width}x${height}\" xc:none -fill black -draw \"roundRectangle 0,0,${width},${height} <>,<>\" '<>/mask.png' + convert '<>' -coalesce -background black -alpha remove -alpha off '<>/no_alpha.<>' + # https://stackoverflow.com/a/66990135 + convert '<>/no_alpha.<>' -quiet -coalesce -alpha extract null: \\( '<>/mask.png' -alpha extract \\) -compose multiply -layers composite '<>/alpha.gif' + convert '<>/no_alpha.<>' null: '<>/alpha.gif' -quiet -alpha off -compose copy_opacity -layers composite '<>_rounded.<>' + # Turn looping on. + mogrify -loop 0 '<>_rounded.<>' + gifsicle -O3 '<>_rounded.<>' --lossy=80 -o '<>_rounded.<>'" + :extensions "gif" + :utils '("ffprobe" "convert"))) + +;;;###autoload +(defun dwim-shell-commands-resize-gif () + "Resize marked gif(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Resize marked gif(s)" + "gifsicle --scale <> '<>' -o '<>_x<>.gif'" + :extensions "gif" + :utils "gifsicle")) + +;;;###autoload +(defun 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 '<>' > '<>.org'" + :extensions "epub" + :utils "pandoc")) + +;;;###autoload +(defun dwim-shell-commands-docx-to-pdf () + "Convert docx(s) to pdf (via latex)." + (interactive) + (dwim-shell-command-on-marked-files + "docx to pdf (via latex)" + "pandoc -t latex '<>' -o '<>.pdf'" + :extensions "docx" ;; brew install mactex + :utils "pdflatex")) + +;;;###autoload +(defun dwim-shell-commands-kill-process () + "Select and kill process." + (interactive) + (let* ((pid-width 5) + (comm-width 25) + (user-width 10) + (processes (proced-process-attributes)) + (candidates + (mapcar (lambda (attributes) + (let* ((process (cdr attributes)) + (pid (format (format "%%%ds" pid-width) (map-elt process 'pid))) + (user (format (format "%%-%ds" user-width) + (truncate-string-to-width + (map-elt process 'user) user-width nil nil t))) + (comm (format (format "%%-%ds" comm-width) + (truncate-string-to-width + (map-elt process 'comm) comm-width nil nil t))) + (args-width (- (window-width) (+ pid-width user-width comm-width 3))) + (args (map-elt process 'args))) + (cons (if args + (format "%s %s %s %s" pid user comm (truncate-string-to-width args args-width nil nil t)) + (format "%s %s %s" pid user comm)) + process))) + processes)) + (selection (map-elt candidates + (completing-read "kill process: " + (seq-sort + (lambda (p1 p2) + (string-lessp (nth 2 (split-string (string-trim (car p1)))) + (nth 2 (split-string (string-trim (car p2)))))) + candidates) nil t))) + (prompt-title (format "%s %s %s" + (map-elt selection 'pid) + (map-elt selection 'user) + (map-elt selection 'comm)))) + (when (y-or-n-p (format "Kill %s?" prompt-title)) + (dwim-shell-command-on-marked-files + (format "Kill %s" prompt-title) + (format "kill -9 %d" (map-elt selection 'pid)) + :utils "kill" + :error-autofocus t + :silent-success t)))) + +;;;###autoload +(defun dwim-shell-commands-macos-add-to-photos () + "Add to Photos.app." + (interactive) + (dwim-shell-command-on-marked-files + "Add to Photos.app" + "osascript <>\" + end tell +EOF" + :silent-success t + :utils "osascript" + :on-completion (lambda (buffer process) + (if-let ((success (= (process-exit-status process) 0))) + (progn + (kill-buffer buffer) + (start-process "Open Photos" nil "open" "-a" "Photos")) + (switch-to-buffer buffer))))) + +;;;###autoload +(defun dwim-shell-commands-macos-toggle-bluetooth-device-connection () + "Toggle Bluetooth device connection." + (interactive) + (let* ((devices (seq-filter + (lambda (line) + ;; Keep lines like: af-8c-3b-b1-99-af - Device name + (string-match-p "^[0-9a-f]\\{2\\}" line)) + (with-current-buffer (get-buffer-create "*BluetoothConnector*") + (erase-buffer) + ;; BluetoothConnector exits with 64 if no param is given. + ;; Invoke with no params to get a list of devices. + (unless (eq 64 (call-process "BluetoothConnector" nil (current-buffer))) + (kill-buffer (current-buffer)) + (error (buffer-string))) + (let ((lines (split-string (buffer-string) "\n"))) + (kill-buffer (current-buffer)) + lines)))) + (candidates (mapcar (lambda (device) + ;; key (device name) : value (address) + (cons (nth 1 (split-string device " - ")) + (nth 0 (split-string device " - ")))) + devices)) + (selected-name (completing-read "Toggle BT connection: " + (seq-sort (lambda (a b) + (string-lessp (car a) (car b))) + candidates) nil t)) + (address (map-elt candidates selected-name))) + (dwim-shell-command-on-marked-files + (format "Toggle %s" selected-name) + (format "BluetoothConnector %s --notify" address) + :utils "BluetoothConnector" + ;; :error-autofocus t + ;; :silent-success t + ))) + +;;;###autoload +(defun dwim-shell-commands-macos-bin-plist-to-xml () + "Convert binary plist to xml." + (interactive) + (dwim-shell-command-on-marked-files + "Convert binary plist to xml" + "plutil -convert xml1 -o '<>.xml' '<>'" + :utils "plutil")) + +;;;###autoload +(defun dwim-shell-commands-macos-toggle-dark-mode () + "Toggle macOS dark mode." + (interactive) + (dwim-shell-command-on-marked-files + "Toggle dark mode" + "dark-mode" + :utils "dark-mode" ;; brew install dark-mode + :silent-success t)) + +;;;###autoload +(defun dwim-shell-commands-macos-toggle-menu-bar-autohide () + "Toggle macOS dark mode." + (interactive) + (dwim-shell-command-on-marked-files + "Toggle menu bar auto-hide." + "current_status=$(osascript -e 'tell application \"System Events\" to get autohide menu bar of dock preferences') + +if [ \"$current_status\" = \"true\" ]; then + osascript -e 'tell application \"System Events\" to set autohide menu bar of dock preferences to false' + echo \"Auto-hide disabled.\" +else + osascript -e 'tell application \"System Events\" to set autohide menu bar of dock preferences to true' + echo \"Auto-hide enabled.\" +fi" + :utils "osascript" + :shell-util "zsh" + :silent-success t)) + +;;;###autoload +(defun dwim-shell-commands-pdf-to-txt () + "Convert pdf to txt." + (interactive) + (dwim-shell-command-on-marked-files + "pdf to txt" + "pdftotext -layout '<>' '<>.txt'" + :utils "pdftotext")) + +;;;###autoload +(defun 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 '<>' '<>_x%.2f.<>'" + (* 100 factor) factor)) + :utils "convert")) + +;;;###autoload +(defun 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 '<>' '<>_x%d.<>'" width width)) + :utils "convert")) + +;;;###autoload +(defun 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 -- '<>' '<>_protected.<>'" + (read-passwd "user-password: ") + (read-passwd "owner-password: ")) + :utils "qpdf" + :extensions "pdf")) + +;;;###autoload +(defun 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' -- '<>' '<>_unprotected.<>'" + (read-passwd "password: ")) + :utils "qpdf" + :extensions "pdf")) + +(defun dwim-shell-commands--gifsicle-frames-every (skipping-every file) + "Generate frames SKIPPING-EVERY count for video FILE." + (string-join + (seq-map (lambda (n) (format "'#%d'" n)) + (number-sequence 0 (string-to-number + ;; Get total frames count. + (seq-first (process-lines "identify" "-format" "%n\n" file))) + skipping-every)) " ")) + +;;;###autoload +(defun dwim-shell-commands-video-to-mp3 () + "Convert video(s) to mp3." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to mp3" + "ffmpeg -i '<>' -vn -ab 128k -ar 44100 -y '<>.mp3'" + :utils "ffmpeg")) + +;;;###autoload +(defun dwim-shell-commands-video-to-mp3-with-artwork () + "Convert video(s) to mp3 (keep frame as artwork)." + (interactive) + (dwim-shell-command-on-marked-files + "Convert to mp3" + "ffmpeg -i '<>' -vf 'select=eq(n\\,0)' -q:v 3 cover.jpg -i '<>' -vn -ab 128k -ar 44100 -y -map_metadata 0 -id3v2_version 3 -write_id3v1 1 -metadata:s:v title='Album cover' -metadata:s:v comment='Cover (front)' '<>.mp3'" + :utils "ffmpeg")) + +;;;###autoload +(defun dwim-shell-commands-ndjson-to-org () + "Convert ndjson file to org." + (interactive) + (unless (eq (length (dwim-shell-command--files)) 1) + (error "Only 1 file supported")) + (let* ((emacs-bin (file-truename (expand-file-name invocation-name + invocation-directory))) + (source (nth 0 (dwim-shell-command--files))) + (destination (concat (file-name-sans-extension + source) ".org")) + (fields (with-temp-buffer + (insert-file-contents source) + (read-string "Fields: " (mapconcat 'identity (mapcar (lambda (item) + (symbol-name (car item))) + (json-read-from-string + (buffer-substring-no-properties + (point-min) (line-end-position)))) + " "))))) + (dwim-shell-command-on-marked-files + "Convert ndjson to org" + (format "%s --quick --batch --eval \"%s\"" emacs-bin + (replace-regexp-in-string + "\"" "\\\\\"" + (prin1-to-string + `(progn + (require 'org) + (require 'json) + (defun convert-to-org-table (ndjson) + (let ((rows (mapcar #'json-read-from-string + (split-string ndjson "\n" t)))) + (orgtbl-to-orgtbl + (append + (list (split-string ,fields)) + '(hline) + (mapcar (lambda (obj) + (mapcar (lambda (key) + (or (alist-get (intern key) obj) "")) + (split-string ,fields))) + rows)) nil))) + (with-temp-buffer + (insert-file-contents ,source) + (let ((org (convert-to-org-table (buffer-string)))) + (with-temp-file ,destination + (insert org)))))))) + :extensions "ndjson"))) + +;;;###autoload +(defun dwim-shell-commands-set-media-artwork-image-metadata () + "Set image artwork metadata for media file(s)." + (interactive) + (let ((artwork-file (file-name-unquote + (read-file-name "Select artwork image: " + nil nil t))) + (should-backup (y-or-n-p "Create backup files? "))) + (unless (file-regular-p artwork-file) + (user-error "Not a file")) + (unless should-backup + (unless (y-or-n-p "Override file(s)? ") + (user-error "Aborted"))) + (dwim-shell-command-on-marked-files + "Set album artwork" + (format (if should-backup + "ffmpeg -i '<>' -i '%s' -map_metadata 0 -map 0:a -map 1 -c copy -disposition:v:0 attached_pic '<>.tmp.<>' && mv -f '<>' '<>.bak' && mv '<>.tmp.<>' '<>'" + "ffmpeg -i '<>' -i '%s' -map_metadata 0 -map 0:a -map 1 -c copy -disposition:v:0 attached_pic '<>.tmp.<>' && mv -f '<>.tmp.<>' '<>'") + artwork-file) + :utils "AtomicParsley" + :silent-success t))) + +;;;###autoload +(defun dwim-shell-commands-video-trim-beginning () + "Drop audio from all marked videos." + (interactive) + (dwim-shell-command-on-marked-files + "Trim beginning" + "ffmpeg -i '<>' -y -ss <> -c:v copy -c:a copy '<>_trimmed.<>'" + :silent-success t + :utils "ffmpeg")) + +;;;###autoload +(defun dwim-shell-commands-video-trim-end () + "Drop audio from all marked videos." + (interactive) + (dwim-shell-command-on-marked-files + "Trim beginning" + "ffmpeg -sseof -<> -i '<>' -y -c:v copy -c:a copy '<>_trimmed.<>'" + :silent-success t + :utils "ffmpeg")) + +;;;###autoload +(defun dwim-shell-commands-video-to-thumbnail () + "Generate a thumbnail for marked video(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Thumbnail with ffmpeg" + "ffmpeg -i '<>' -ss 00:00:01.000 -vframes 1 '<>.jpg'" + :utils "ffmpeg")) + +;;;###autoload +(defun dwim-shell-commands-drop-video-audio () + "Drop audio from all marked videos." + (interactive) + (dwim-shell-command-on-marked-files + "Drop audio" + "ffmpeg -i '<>' -c copy -an '<>_no_audio.<>'" + :utils "ffmpeg")) + +;;;###autoload +(defun dwim-shell-commands-ping-google () + "Ping google.com." + (interactive) + (dwim-shell-command-on-marked-files + "Ping google.com" + "ping -c 3 google.com" + :utils "ping" + :focus-now t)) + +;;;###autoload +(defun dwim-shell-commands-speed-up-video () + "Speed up video(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Speed up video" + (let ((factor (read-number "Resize scaling factor: " 2))) + (format "ffmpeg -i '<>' -an -filter:v 'setpts=%s*PTS' '<>_x%s.<>'" + (/ 1 (float factor)) factor)) + :utils "ffmpeg")) + +;;;###autoload +(defun dwim-shell-commands-speed-up-video-fragment () + "Speed up fragment in video(s)." + (interactive) + (let ((start (read-number "Start (seconds): ")) + (end (read-number "End (seconds): ")) + (factor (read-number "Speed up factor: " 2))) + (dwim-shell-command-on-marked-files + "Speed up fragment in video" + (format "ffmpeg -i '<>' -filter_complex '[0:v]trim=start=0:end=%d,setpts=PTS-STARTPTS[v0];[0:v]trim=start=%d:end=%d,setpts=(PTS-1)/%d[v1];[0:v]trim=start=%d,setpts=PTS-STARTPTS[v2];[v0][v1][v2]concat=n=3:v=1:a=0' -preset fast '<>_%d:%dx%d.<>'" start start end factor end start end factor) + :utils "ffmpeg"))) + +(defun dwim-shell-commands-set-song-title () + "Set song(s) title." + (interactive) + (let ((title (replace-regexp-in-string "'" "\'" (read-string "New title: ")))) + (dwim-shell-command-on-marked-files + "Set song(s) title" + (format "ffmpeg -i '<>' -metadata title='%s' -codec copy '<>/<>_temp.<>' && mv -f '<>/<>_temp.<>' '<>'" + title) + :utils "ffmpeg"))) + +;;;###autoload +(defun dwim-shell-commands-resize-video () + "Resize marked images." + (interactive) + (dwim-shell-command-on-marked-files + "Resize video" + " +eval $(ffprobe -v quiet -show_format -of flat=s=_ -show_entries stream=width '<>'); +width=${streams_stream_0_width}; +zmodload zsh/mathfunc +width=$((rint($width * <>))); +# Make it even or face 'not divisible by 2' errors. +if [[ $((width%2)) -ne 0 ]] then + width=$(($width - 1)) +fi +ffmpeg -n -i '<>' -vf \"scale=$width:-2\" '<>_x<>.<>'" + :utils "ffmpeg")) + +;;;###autoload +(defun dwim-shell-commands-clipboard-to-qr () + "Generate a QR code from clipboard." + (interactive) + (let ((temp-file (concat (temporary-file-directory) "qr-code"))) + (dwim-shell-command-on-marked-files + "Generate a QR code from clipboard" + (format "qrencode -s10 -o %s %s" temp-file (shell-quote-argument (current-kill 0))) + :utils "qrencode" + :on-completion (lambda (buffer _process) + (kill-buffer buffer) + (switch-to-buffer (find-file-noselect temp-file t)))))) + +;;;###autoload +(defun dwim-shell-commands-sha-256-hash-file-at-clipboard-url () + "Download file at clipboard URL and generate SHA-256 hash." + (interactive) + (unless (string-match-p "^http[s]?://" (current-kill 0)) + (user-error "No URL in clipboard")) + (dwim-shell-command-on-marked-files + "Generate SHA-256 hash from clipboard URL." + "curl -s '<>' | sha256sum - | cut -d ' ' -f1" + :utils '("curl" "sha256sum") + :on-completion + (lambda (buffer process) + (if-let ((success (= (process-exit-status process) 0)) + (hash (with-current-buffer buffer + (string-trim (buffer-string))))) + (progn + (kill-buffer buffer) + (kill-new hash) + (message "Copied %s to clipboard" + (propertize hash 'face 'font-lock-string-face))) + (switch-to-buffer buffer))))) + +;;;###autoload +(defun dwim-shell-commands-view-sqlite-schema-diagram () + "View sqlite schema diagram." + (interactive) + (dwim-shell-command-on-marked-files + "View sqlite schema" + "set -e +temp_dir=\"${TMPDIR:-/tmp/}\" +file_name=\"sqlite-schema-diagram.sql\" +file_path=\"${temp_dir}/${file_name}\" +url=\"https://gitlab.com/Screwtapello/sqlite-schema-diagram/-/raw/main/sqlite-schema-diagram.sql\" + +if [[ ! -f \"$file_path\" ]]; then + curl -o \"$file_path\" \"$url\" +fi + +sqlite3 -list \"<>\" < $file_path > \"<>.dot\" +dot -Tsvg \"<>.dot\" > \"<>.svg\" +echo \"<>.svg\" +" + :utils '("dot" "sqlite3") + :on-completion + (lambda (buffer process) + (if (= (process-exit-status process) 0) + (with-current-buffer buffer + (let ((svg-file (string-trim (buffer-string)))) + (if (string-suffix-p "svg" svg-file) + (progn + (find-file svg-file) + (kill-buffer buffer)) + (switch-to-buffer buffer)))) + (switch-to-buffer buffer))))) + +;;;###autoload +(defun dwim-shell-commands-open-externally () + "Open file(s) externally." + (interactive) + (dwim-shell-command-on-marked-files + "Open externally" + (if (eq system-type 'darwin) + (if (derived-mode-p 'prog-mode) + (format "xed --line %d '<>'" + (line-number-at-pos (point))) + "open '<>'") + "xdg-open '<>'") + :shell-util "zsh" + :shell-args '("-x" "-c") + :silent-success t + :utils (if (eq system-type 'darwin) + "open" + "xdg-open"))) + +;;;###autoload +(defun dwim-shell-commands-macos-caffeinate () + "Invoke caffeinate to prevent mac from sleeping." + (interactive) + (dwim-shell-command-on-marked-files + "Caffeinate" + "caffeinate" + :utils "caffeinate" + :no-progress t + :focus-now t)) + +;;;###autoload +(defun dwim-shell-commands-macos-make-finder-alias () + "Make macOS Finder alias." + (interactive) + (let ((files (dwim-shell-command--files)) + (target-dir (read-directory-name "Select target dir: " "/Applications" nil t))) + (dwim-shell-command-on-marked-files + "Make macOS alias" + (format "osascript -e 'tell application \"Finder\" to make alias file to POSIX file \"<>\" at POSIX file \"%s\"'" + target-dir) + :utils "osascript" + :no-progress t + :silent-success t + :on-completion (lambda (buffer _process) + (kill-buffer buffer) + (dired-jump nil (file-name-concat target-dir (file-name-nondirectory (nth 0 files)))))))) + +;;;###autoload +(defun dwim-shell-commands-macos-version-and-hardware-overview-info () + "View macOS version and hardware overview info." + (interactive) + (dwim-shell-command-on-marked-files + "macOS hardware overview" + "sw_vers; system_profiler SPHardwareDataType" + :utils '("sw_vers" "system_profiler"))) + +;;;###autoload +(defun dwim-shell-commands-macos-reveal-in-finder () + "Reveal selected files in macOS Finder." + (interactive) + (dwim-shell-command-on-marked-files + "Reveal in Finder" + "import AppKit + NSWorkspace.shared.activateFileViewerSelecting([\"<<*>>\"].map{URL(fileURLWithPath:$0)})" + :silent-success t + :shell-pipe "swift -" + :join-separator ", " + :utils "swift")) + +(defun dwim-shell-commands--macos-sharing-services () + "Return a list of sharing services." + (let* ((source (format "import AppKit + NSSharingService.sharingServices(forItems: [ + %s + ]).forEach { + print(\"\\($0.title)\") + }" + (string-join (mapcar (lambda (file) + (format "URL(fileURLWithPath: \"%s\")" file)) + (dwim-shell-command--files)) + ", "))) + (services (split-string + (string-trim + ;; Remove anything that doesn't start with alpha characters + ;; There may be compilation warnings. + (replace-regexp-in-string "^[^[:alpha:]].*\n" "" + (shell-command-to-string (format "echo '%s' | swift -" source)))) + "\n"))) + (when (seq-empty-p services) + (error "No sharing services available")) + services)) + +;;;###autoload +(defun dwim-shell-commands-macos-share () + "Share selected files from macOS." + (interactive) + (let* ((services (dwim-shell-commands--macos-sharing-services)) + (service-name (completing-read "Share via: " services)) + (selection (seq-position services service-name #'string-equal))) + (dwim-shell-command-on-marked-files + "Share" + (format + "import AppKit + + _ = NSApplication.shared + + NSApp.setActivationPolicy(.regular) + + class MyWindow: NSWindow, NSSharingServiceDelegate { + func sharingService( + _ sharingService: NSSharingService, + didShareItems items: [Any] + ) { + NSApplication.shared.terminate(nil) + } + + func sharingService( + _ sharingService: NSSharingService, didFailToShareItems items: [Any], error: Error + ) { + let error = error as NSError + if error.domain == NSCocoaErrorDomain && error.code == NSUserCancelledError { + NSApplication.shared.terminate(nil) + } + exit(1) + } + } + + let window = MyWindow( + contentRect: NSRect(x: 0, y: 0, width: 0, height: 0), + styleMask: [], + backing: .buffered, + defer: false) + + let services = NSSharingService.sharingServices(forItems: [\"<<*>>\"].map{URL(fileURLWithPath:$0)}) + let service = services[%s] + service.delegate = window + service.perform(withItems: [\"<<*>>\"].map{URL(fileURLWithPath:$0)}) + + NSApp.run()" selection) + :silent-success t + :shell-pipe "swift -" + :join-separator ", " + :no-progress t + :utils "swift"))) + +;;;###autoload +(defun dwim-shell-commands-macos-toggle-display-rotation () + "Rotate display." + (interactive) + ;; # Display_ID Resolution ____Display_Bounds____ Rotation + ;; 2 0x2b347692 1440x2560 0 0 1440 2560 270 [main] + ;; From fb-rotate output, get the `current-rotation' from Column 7, row 1 zero-based. + (let ((current-rotation (nth 7 (split-string (nth 1 (process-lines "fb-rotate" "-i")))))) + (dwim-shell-command-on-marked-files + "macOS hardware overview" + (format "fb-rotate -d 1 -r %s" (if (equal current-rotation "270") "0" "270")) + :utils "fb-rotate"))) + +;;;###autoload +(defun dwim-shell-commands-make-swift-package-library () + "Create a swift package library" + (interactive) + (dwim-shell-command-on-marked-files + "Create a swift package library" + "swift package init --type library" + :utils "swift")) + +;;;###autoload +(defun dwim-shell-commands-make-swift-package-executable () + "Create a swift package executable" + (interactive) + (dwim-shell-command-on-marked-files + "Create a swift package executable" + "swift package init --type executable" + :utils "swift")) + +(defun dwim-shell-commands--macos-apps () + "Return alist of macOS apps (\"Emacs\" . \"/Applications/Emacs.app\")." + (mapcar (lambda (path) + (cons (file-name-base path) path)) + (seq-sort + #'string-lessp + (seq-mapcat (lambda (paths) + (directory-files-recursively + paths "\\.app$" t (lambda (path) + (not (string-suffix-p ".app" path))))) + '("/Applications" "~/Applications" "/System/Applications"))))) + +;;;###autoload +(defun dwim-shell-commands-macos-set-default-app () + "Set default app for file(s)." + (interactive) + (let* ((apps (dwim-shell-commands--macos-apps)) + (selection (progn + (cl-assert apps nil "No apps found") + (completing-read "Set default app: " apps nil t)))) + (dwim-shell-command-on-marked-files + "Set default app" + (format "duti -s \"%s\" '<>' all" + (string-trim + (shell-command-to-string (format "defaults read '%s/Contents/Info.plist' CFBundleIdentifier" + (map-elt apps selection))))) + :silent-success t + :no-progress t + :utils "duti"))) + +;;;###autoload +(defun dwim-shell-commands-macos-open-with () + "Open file(s) with specific external app." + (interactive) + (let* ((apps (dwim-shell-commands--macos-apps)) + (selection (progn + (cl-assert apps nil "No apps found") + (completing-read "Open with: " apps nil t)))) + (dwim-shell-command-on-marked-files + "Open with" + (format "open -a '%s' '<<*>>'" (map-elt apps selection)) + :silent-success t + :no-progress t + :utils "open"))) + +;;;###autoload +(defun dwim-shell-commands-macos-open-with-firefox () + "Open file(s) in Firefox." + (interactive) + (dwim-shell-command-on-marked-files + "Open in Firefox" + "open -a Firefox '<<*>>'" + :silent-success t + :no-progress t + :utils "open")) + +;;;###autoload +(defun dwim-shell-commands-macos-open-with-safari () + "Open file(s) in Safari." + (interactive) + (dwim-shell-command-on-marked-files + "Open in Safari" + "open -a Safari '<<*>>'" + :silent-success t + :no-progress t + :utils "open")) + +;;;###autoload +(defun dwim-shell-commands-macos-start-recording-window () + "Select and start recording a macOS window." + (interactive) + (let* ((window (dwim-shell-commands--macos-select-window)) + (path (dwim-shell-commands--generate-path "~/Screenshots" (car window) ".gif")) + (buffer-file-name path) ;; override so <> picks it up + (inhibit-message t)) + ;; Silence echo to avoid unrelated messages making into animation. + (cl-letf (((symbol-function 'dwim-shell-command--message) + (lambda (_fmt &rest _args) nil))) + (dwim-shell-command-on-marked-files + "Start recording a macOS window." + (format + "macosrec --record '%s' --gif --output '<>'" + (cdr window)) + :silent-success t + :focus-now nil + :monitor-directory "~/Screenshots" + :no-progress t + :utils '("ffmpeg" "macosrec") + :on-completion + (lambda (buffer process) + (if (= (process-exit-status process) 0) + (progn + "Saved recording" + (dired-jump nil path) + (kill-buffer buffer)) + (with-current-buffer buffer + (goto-char (point-min)) + (if (search-forward "Aborted" nil t) + (progn + (message "Aborted recording") + (kill-buffer buffer)) + (switch-to-buffer buffer))))))))) + +(defun dwim-shell-commands--generate-path (dir name ext) + "Generate a timestamped path with DIR, NAME, and EXT." + (concat (file-name-as-directory (expand-file-name dir)) + (format-time-string "%Y-%m-%d-%H:%M:%S-") + name ext)) + +(defun dwim-shell-commands--macos-select-window () + "Return a list of macOS windows." + (if-let* ((line (completing-read + "Select: " + (process-lines "macosrec" "--list") nil t)) + (window-info (split-string line " ")) + (window-number (string-to-number (nth 0 window-info))) + (window-app (nth 1 window-info)) + (valid (> window-number 0))) + (cons window-app window-number) + (user-error "No window found"))) + +;;;###autoload +(defun dwim-shell-commands-macos-end-recording-window () + "Stop recording a macOS window." + (interactive) + (let ((inhibit-message t)) + (cl-letf (((symbol-function 'dwim-shell-command--message) + (lambda (_fmt &rest _args) nil))) + (dwim-shell-command-on-marked-files + "End recording macOS window." + "macosrec --save" + :silent-success t + :no-progress t + :error-autofocus t + :utils "macosrec")))) + +;;;###autoload +(defun dwim-shell-commands-macos-abort-recording-window () + "Stop recording a macOS window." + (interactive) + (let ((inhibit-message t)) + (cl-letf (((symbol-function 'dwim-shell-command--message) + (lambda (_fmt &rest _args) nil))) + (dwim-shell-command-on-marked-files + "Abort recording macOS window." + "macosrec --abort" + :silent-success t + :no-progress t + :utils "macosrec")))) + +;;;###autoload +(defun dwim-shell-commands-macos-screenshot-window () + "Select and screenshot macOS window." + (interactive) + ;; Silence echo to avoid unrelated messages making into screenshot. + (let ((window (dwim-shell-commands--macos-select-window)) + (inhibit-message t)) + (dwim-shell-command-on-marked-files + "Start recording a macOS window." + (format "macosrec --screenshot %s" (cdr window)) + :silent-success t + :monitor-directory "~/Screenshots" + :no-progress t + :utils "macosrec"))) + +;;;###autoload +(defun dwim-shell-commands-files-combined-size () + "Get files combined file size." + (interactive) + (dwim-shell-command-on-marked-files + "Get files combined file size" + "du -csh '<<*>>'" + :utils "du" + :on-completion (lambda (buffer _process) + (with-current-buffer buffer + (message "Total size: %s" + (progn + (re-search-backward "\\(^[ 0-9.,]+[A-Za-z]+\\).*total$") + (match-string 1)))) + (kill-buffer buffer)))) + +;;;###autoload +(defun dwim-shell-commands-image-to-icns () + "Convert png to icns icon." + (interactive) + (dwim-shell-command-on-marked-files + "Convert png to icns icon." + " + # Based on http://stackoverflow.com/questions/12306223/how-to-manually-create-icns-files-using-iconutil + # Note: png must be 1024x1024 + mkdir '<>.iconset' + sips -z 16 16 '<>' --out '<>.iconset/icon_16x16.png' + sips -z 32 32 '<>' --out '<>.iconset/icon_16x16@2x.png' + sips -z 32 32 '<>' --out '<>.iconset/icon_32x32.png' + sips -z 64 64 '<>' --out '<>.iconset/icon_32x32@2x.png' + sips -z 128 128 '<>' --out '<>.iconset/icon_128x128.png' + sips -z 256 256 '<>' --out '<>.iconset/icon_128x128@2x.png' + sips -z 256 256 '<>' --out '<>.iconset/icon_256x256@2x.png' + sips -z 512 512 '<>' --out '<>.iconset/icon_512x512.png' + sips -z 512 512 '<>' --out '<>.iconset/icon_256x256@2x.png' + sips -z 1024 1024 '<>' --out '<>.iconset/icon_512x512@2x.png' + iconutil -c icns '<>.iconset'" + :utils '("sips" "iconutil") + :extensions "png")) + +;;;###autoload +(defun dwim-shell-commands-image-add-drop-shadow () + "Add a drop shadow." + (interactive) + (dwim-shell-command-on-marked-files + "Add a drop shadow." + "convert <> -bordercolor white -border 13 \\( +clone -background black -shadow 80x3+2+2 \\) +swap -background white -layers merge +repage <>-shadow.<>" + :utils "convert")) + +;;;###autoload +(defun dwim-shell-commands-image-trim-borders () + "Trim image(s) border (useful for video screenshots)." + (interactive) + (dwim-shell-command-on-marked-files + "Trim image border" + "magick convert -fuzz 3% -define trim:percent-background=0% -trim +repage '<>' '<>_trimmed.<>'" + :utils "magick")) + +;;;###autoload +(defun dwim-shell-commands-http-serve-dir () + "HTTP serve current directory." + (interactive) + (cond ((executable-find "python3") + (dwim-shell-command-on-marked-files + "HTTP serve current dir" + "python3 -m http.server" + :utils "python3" + :focus-now t + :no-progress t)) + ((executable-find "python2") + (dwim-shell-command-on-marked-files + "HTTP serve current dir" + "python2 -m SimpleHTTPServer" + :utils "python2" + :focus-now t + :no-progress t)) + ((executable-find "python") + (dwim-shell-command-on-marked-files + "HTTP serve current dir" + "python -m SimpleHTTPServer" + :utils "python" + :focus-now t + :no-progress t)) + (t + (error "No python found")))) + +(defcustom dwim-shell-commands-git-clone-dirs + '("~/Downloads" "~/Desktop") + "List of directories where git repositories can be cloned. +The first directory is used as the default." + :type '(repeat directory) + :group 'dwim-shell-commands) + +;;;###autoload +(defun dwim-shell-commands-git-clone-clipboard-url (&optional arg) + "Clone git URL in clipboard to a directory. +With C-u ARG, prompt for directory from +`dwim-shell-commands-git-clone-dirs'. + +With C-u C-u ARG, prompt for any directory. + +Without prefix, use the first directory in +`dwim-shell-commands-git-clone-dirs'." + (interactive "P") + (unless (or (string-match-p "^\\(?:http\\|https\\|ssh\\|git\\)://" (string-trim (current-kill 0))) + (string-match-p "^git@" (string-trim (current-kill 0)))) + (user-error "No URL in clipboard")) + (let* ((url (string-trim (current-kill 0))) + (current-dir (abbreviate-file-name + (directory-file-name default-directory))) + (fallback-dir (car dwim-shell-commands-git-clone-dirs)) + (candidates (append (list (cons fallback-dir fallback-dir)) + (list (cons (concat current-dir " (current)") current-dir)) + (mapcar (lambda (dir) + (cons dir dir)) + (cdr dwim-shell-commands-git-clone-dirs)))) + (target-dir (cond + ;; C-u prefix - Choose from `dwim-shell-commands-git-clone-dirs' + ((equal arg '(4)) + (expand-file-name + (map-elt candidates + (completing-read "Clone repo to: " candidates + nil t)))) + ;; C-u C-u prefix - Choose any directory + ((equal arg '(16)) + (expand-file-name + (read-directory-name "Clone repo to: " nil nil t))) + ;; Default to using first in `dwim-shell-commands-git-clone-dirs' + (t + (expand-file-name fallback-dir)))) + (base-name (file-name-base url)) + (project-dir (dwim-shell-command--unique-new-file-path + (concat (file-name-as-directory target-dir) base-name))) + (default-directory target-dir)) + (dwim-shell-command-on-marked-files + (format "Clone %s" (file-name-base url)) + (format "git clone %s %s" + (shell-quote-argument url) + (shell-quote-argument (file-name-nondirectory project-dir))) + :monitor-directory target-dir + :utils "git" + :on-completion (lambda (buffer _) + (kill-buffer buffer) + (dired project-dir) + (goto-char (point-min)) + (when (re-search-forward "README" nil t) + (beginning-of-line)))))) + +;;;###autoload +(defun dwim-shell-commands-pass-git-pull () + "Pass git pull." + (interactive) + (dwim-shell-command-on-marked-files + "pass git pull" + "pass git pull" + :utils '("pass" "git") + :silent-success t)) + +;;;###autoload +(defun dwim-shell-commands-git-list-untracked-files () + "List untracked git files in `default-directory'." + (interactive) + (dwim-shell-command-on-marked-files + "List untracked" + "git ls-files --others ." + :utils "git" + :focus-now t)) + +;;;###autoload +(defun dwim-shell-commands-git-delete-untracked-files () + "Delete untracked git files in `default-directory'." + (interactive) + (when (y-or-n-p (format "Clean '%s'? \n\n%s\n...\n\n" + default-directory + (string-join + (seq-take (process-lines "git" "ls-files" "--others" ".") 3) + "\n"))) + (dwim-shell-command-on-marked-files + "Clean untracked" + "git clean -f ." + :utils "git" + :silent-success t))) + +;;;###autoload +(defun dwim-shell-commands-external-ip () + "Copy external IP to kill ring." + (interactive) + (let ((ip (car (last (process-lines "curl" "ifconfig.me"))))) + (kill-new ip) + (message "Copied %s" ip))) + +;;;###autoload +(defun dwim-shell-commands-macos-install-iphone-device-ipa () + "Install iPhone device .ipa. +Needs ideviceinstaller and libmobiledevice installed." + (interactive) + (dwim-shell-command-on-marked-files + "Install .ipa" + "ideviceinstaller -i '<>'" + :utils "ideviceinstaller")) + +;;;###autoload +(defun dwim-shell-commands-copy-to-downloads () + "Copy file to ~/Downloads." + (interactive) + (dwim-shell-command-foreach + (lambda (file) + (copy-file file "~/Downloads/" 1) + (file-name-concat "~/Downloads" (file-name-nondirectory file))) + :monitor-directory "~/Downloads")) + +;;;###autoload +(defun dwim-shell-commands-duplicate (times) + "Duplicate file. + +With prefix, duplicate it n TIMES." + (interactive "p") + (dwim-shell-commands--duplicate (list times))) + +(defun dwim-shell-commands--duplicate (remaining) + "Helper using REMAINING as a list to share recursion state." + (let ((current-buffer (current-buffer))) + (dwim-shell-command-on-marked-files + "Duplicate file(s)." + "cp -R '<>' '<>'" + :utils "cp" + :on-completion + (lambda (buffer _process) + (kill-buffer buffer) + (setcar remaining (1- (car remaining))) + (if (> (car remaining) 0) + (with-current-buffer current-buffer + (dwim-shell-commands--duplicate remaining)) + (dired-jump nil (with-current-buffer current-buffer + default-directory))))))) + +;;;###autoload +(defun dwim-shell-commands-rename-all () + "Rename all marked file(s)." + (interactive) + (dwim-shell-command-on-marked-files + "Rename all" + "mv '<>' '<>(<>).<>'" + :utils "mv")) + +;;;###autoload +(defun dwim-shell-commands-move-to-downloads () + "Move file to ~/Downloads." + (interactive) + (dwim-shell-command-foreach + (lambda (file) + (rename-file file "~/Downloads/" 1) + (when (buffer-file-name) + (rename-buffer (file-name-nondirectory file)) + (set-visited-file-name + (file-name-concat "~/Downloads" (file-name-nondirectory file))) + (set-buffer-modified-p nil)) + (file-name-concat "~/Downloads" (file-name-nondirectory file))) + :monitor-directory "~/Downloads")) + +;;;###autoload +(defun dwim-shell-commands-copy-to-desktop () + "Copy file to ~/Desktop." + (interactive) + (dwim-shell-command-foreach + (lambda (file) + (copy-file file "~/Desktop/" 1) + (file-name-concat "~/Desktop" (file-name-nondirectory file))) + :monitor-directory "~/Desktop")) + +;;;###autoload +(defun dwim-shell-commands-move-to-desktop () + "Move file to ~/Desktop." + (interactive) + (dwim-shell-command-foreach + (lambda (file) + (rename-file file "~/Desktop/" 1) + (when (buffer-file-name) + (rename-buffer (file-name-nondirectory file)) + (set-visited-file-name + (file-name-concat "~/Desktop" (file-name-nondirectory file))) + (set-buffer-modified-p nil)) + (file-name-concat "~/Desktop" (file-name-nondirectory file))) + :monitor-directory "~/Desktop")) + +;;;###autoload +(defun 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)) + +(defun dwim-shell-commands-upload-to-tmpfiles-org () + "Upload the marked files to 0x0.st" + (interactive) + (dwim-shell-command-on-marked-files + "0x0 upload" + "curl -Ffile=@<> -Fsecret= https://tmpfiles.org/api/v1/upload" + :utils "curl" + :post-process-template + ;; The placement of required single quotes confuse the escaping + ;; mechanisms of dwim-shell-command, as it considers @ as the + ;; opening 'quote' because it appears in front of <>. + ;; + ;; What we want is: + ;; + ;; curl -F'file=@yourfile.png' -Fsecret= https://tmpfiles.org/api/v1/upload + (lambda (template path) + (string-replace "-Ffile" "-F'file" + (string-replace path (concat path "'") template))) + :on-completion + (lambda (buffer process) + (if (= (process-exit-status process) 0) + (with-current-buffer buffer + (if-let* ((json (condition-case nil + (json-read-from-string (buffer-string)) + (error nil))) + (success (equal (map-elt json 'status) "success"))) + (progn + (eww (map-nested-elt json '(data url))) + (kill-new (map-nested-elt json '(data url))) + (message "Copied: %s" (current-kill 0)) + (kill-buffer buffer)) + (switch-to-buffer buffer))) + (switch-to-buffer buffer))))) + +;; Based on +;; https://apps.bram85.nl/git/bram/gists/src/commit/31ac3363da925daafa2420b7f96c67612ca28241/gists/dwim-0x0-upload.el +;;;###autoload +(defun dwim-shell-commands-upload-to-0x0 () + "Upload the marked files to 0x0.st" + (interactive) + (dwim-shell-command-on-marked-files + "0x0 upload" + "curl -Ffile=@<> -Fsecret= https://0x0.st" + :utils "curl" + :post-process-template + ;; Insert the single quotes at the appropriate place according to + ;; 0x0.st example online: + ;; curl -F'file=@yourfile.png' -Fsecret= https://0x0.st + ;; + ;; The placement of these single quotes confuse the escaping + ;; mechanisms of dwim-shell-command, as it considers @ as the + ;; opening 'quote' as it appears right in front of <>. + (lambda (template path) + (string-replace "-Ffile" "-F'file" + (string-replace path (concat path "'") template))) + :on-completion + (lambda (buffer process) + (if (= (process-exit-status process) 0) + (with-current-buffer buffer + (let ((url (car (last (split-string (string-trim (buffer-string)) "\n"))))) + (eww url) + (kill-new url) + (message "Copied: %s" (current-kill 0))) + (kill-buffer buffer)) + (switch-to-buffer buffer))))) + +(provide 'dwim-shell-commands) + +;;; dwim-shell-commands.el ends here