+++ /dev/null
-;;; 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)))
-
-
-\f
-;;; 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:
-
- <<f>> (file path)
- <<fne>> (file path without extension)
- <<e>> (extension)
- <<td>> (generate a temporary directory)
- <<*>> (all files joined)
- <<cb>> (clipboard)
- <<n>>, <<1n>>, or <<An>> (for current iteration)
-
- For example:
-
- With drawn files '(\"path/to/image1.png\" \"path/to/image2.png\")
-
- \"convert '<<f>>' '<<fne>>.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 '<<f>>' '<<fne>>.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-"))
-
-\f
-;;; 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-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-"))
-\f
-;;; 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
+++ /dev/null
-;; -*- no-byte-compile: t; lexical-binding: nil -*-
-(define-package "dwim-shell-command" "20241115.845"
- "Shell commands with DWIM behaviour."
- '((emacs "28.1"))
- :url "https://github.com/xenodium/dwim-shell-command"
- :commit "1fa8b9d361f01618bf65111ac0b253adb0599a09"
- :revdesc "1fa8b9d361f0")
+++ /dev/null
-;;; 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: 20241115.845
-;; Package-Revision: 1fa8b9d361f0
-
-;; 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 <https://www.gnu.org/licenses/>.
-
-;;; 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 (<<f>> <<fne>> <<e>>): "
- "`dwim-shell-command' prompt. Modify if shorter is preferred."
- :type 'string
- :group 'dwim-shell-command)
-
-(defcustom dwim-shell-command-default-command
- " '<<f>>'"
- "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:
-
- <<f>> (file path)
- <<fne>> (file path without extension)
- <<e>> (extension)
- <<td>> (generate a temporary directory)
- <<*>> (all files joined)
- <<cb>> (clipboard)
- <<n>>, <<1n>>, or <<An>> (for current iteration)
-
- For example:
-
- With drawn files '(\"path/to/image1.png\" \"path/to/image2.png\")
-
- \"convert '<<f>>' '<<fne>>.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 '<<f>>' '<<fne>>.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 '<<f>>' '<<fne>>.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:
-
- <<f>> (file path)
- <<fne>> (file path without extension)
- <<e>> (extension)
- <<td>> (generate a temporary directory)
- <<*>> (all files joined)
- <<cb>> (clipboard)
- <<n>>, <<1n>>, or <<An>> (for current iteration)
-
- For example:
-
- With drawn files '(\"path/to/image1.png\" \"path/to/image2.png\")
-
- \"convert '<<f>>' '<<fne>>.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 '<<f>>' '<<fne>>.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:
-
- <<f>> (file path)
- <<fne>> (file path without extension)
- <<e>> (extension)
- <<td>> (generate a temporary directory)
- <<*>> (all files joined)
- <<cb>> (clipboard)
- <<n>>, <<1n>>, or <<An>> (for current iteration)
-
- For example:
-
- Given :FILES '(\"path/to/image1.png\" \"path/to/image2.png\")
-
- \"convert '<<f>>' '<<fne>>.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 <<td>> 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 "\<\<td\>\>" 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 <<Width:100>> world <<Height:200>>\" =>
-
- ((\"<<Width:100>>\" . \"100\")
- (\"<<Height:200>>\" . \"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 <<f>>, <<fne>>, or <<e>>.
-
- 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(u)>>" -> 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))
-
- ;; "<<td>>" with TEMP-DIR -> "/var/folders/m7/ky091cp56d5g68nyhl4y7frc0000gn/T/dwim-shell-command-JNK4V5"
- (setq template (replace-regexp-in-string "\\(\<\<td\>\>\\)" temp-dir template nil nil 1))
-
- ;; "<<cb>>" with (current-kill 0) -> "whatever was in kill ring"
- (when (string-match "\<\<cb\>\>" template)
- (setq template (replace-regexp-in-string "\\(\<\<cb\>\>\\)" (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:
-
- \"\<\<fne\>\>\" \"before \"<<fne>>\" after\" => (\"\"\" \"\\\\\"\")
-
- \"\<\<fne\>\>\" \"before '<<fne>>' 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 <<f>> for FILE, <<fne>> for FILE without extension, and
-<<e>> for FILE extension. <<n>>, <<1n>>, or <<an>> is replaced with
-CURRENT. <<some.txt(u)>> expands to unique \"some(1).txt\".
-
-Note: This expander cannot be used to expand <<*>>.
-
- For example:
-
- Given FILE \"path/to/image.png\"
-
- \"convert '<<f>>' '<<fne>>.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)))
- ;; "<<fne>>" with "/path/tmp.txt" -> "/path/tmp"
- (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\<fne\>\>" template t))
- (unescaped-quote (nth 0 quoting))
- (escaped-quote (nth 1 quoting)))
- (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\<fne\>\>\\)"
- (string-replace unescaped-quote escaped-quote (file-name-sans-extension file))
- template nil nil 2))
- (setq template (replace-regexp-in-string "\\(\<\<fne\>\>\\)" (file-name-sans-extension file) template nil nil 1)))
-
- ;; "<<b>>" with "/path/tmp.txt" -> "tmp.txt"
- (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\<b\>\>" template t))
- (unescaped-quote (nth 0 quoting))
- (escaped-quote (nth 1 quoting)))
- (setq template (replace-regexp-in-string "\\(\<\<b\>\>\\)\\([^ ]\\)"
- (string-replace unescaped-quote escaped-quote (file-name-nondirectory file))
- template nil nil 1))
- (setq template (replace-regexp-in-string "\\(\<\<b\>\>\\)" (file-name-nondirectory file) template nil nil 1)))
-
- ;; "<<bne>>" with "/path/tmp.txt" -> "tmp"
- (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\<bne\>\>" template t))
- (unescaped-quote (nth 0 quoting))
- (escaped-quote (nth 1 quoting)))
- (setq template (replace-regexp-in-string "\\(\<\<bne\>\>\\)\\([^ ]\\)"
- (string-replace unescaped-quote escaped-quote
- (file-name-sans-extension (file-name-nondirectory file)))
- template nil nil 1))
- (setq template (replace-regexp-in-string "\\(\<\<bne\>\>\\)" (file-name-sans-extension
- (file-name-nondirectory file)) template nil nil 1)))
-
- ;; "<<e>>" with "/path/tmp.txt" -> "txt"
- (if (file-name-extension file)
- (setq template (replace-regexp-in-string "\\(\<\<e\>\>\\)" (file-name-extension file) template nil nil 1))
- ;; File had no extension. Attempt to remove .<<e>>.
- (setq template (replace-regexp-in-string "\\(\.\<\<e\>\>\\)" "" template nil nil 1)))
-
- ;; "<<f>>" with "/path/file.jpg" -> "/path/file.jpg"
- (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\<f\>\>" template))
- (unescaped-quote (nth 0 quoting))
- (escaped-quote (nth 1 quoting)))
- (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\<f\>\>\\)\\([^ ]\\)"
- (string-replace unescaped-quote escaped-quote file)
- template nil nil 2))
- (setq template (replace-regexp-in-string "\\(\<\<f\>\>\\)" file template nil nil 1)))
-
- ;; "<<f(u)>>" with "/path/file.jpg" -> "/path/file(1).jpg"
- (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\<f(u)\>\>" template))
- (unescaped-quote (nth 0 quoting))
- (escaped-quote (nth 1 quoting)))
- (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\<f\(u)>\>\\)\\([^ ]\\)"
- (string-replace unescaped-quote escaped-quote
- (dwim-shell-command--unique-new-file-path file))
- template nil nil 2))
- (setq template (replace-regexp-in-string "\\(\<\<f(u)\>\>\\)"
- (dwim-shell-command--unique-new-file-path file) template nil nil 1))))
-
- ;; "<<some.txt(u)>>" -> 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)))
-
- ;; "<<td>>" with TEMP-DIR -> "/var/folders/m7/ky091cp56d5g68nyhl4y7frc0000gn/T/dwim-shell-command-JNK4V5"
- (setq template (replace-regexp-in-string "\\(\<\<td\>\>\\)" temp-dir template nil nil 1))
-
- ;; "<<cb>>" with (current-kill 0) -> "whatever was in kill ring"
- (when (string-match "\<\<cb\>\>" template)
- (setq template (replace-regexp-in-string "\\(\<\<cb\>\>\\)" (current-kill 0)
- template nil nil 1)))
-
- ;; "<<n>>" or "<<an>" 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 <<f>>, <<fne>>, or <<e>> in TEMPLATE."
- (cond ((string-match "\<\<f\>\>" template)
- "<<f>>")
- ((string-match "\<\<fne\>\>" template)
- "<<fne>>")
- ((string-match "\<\<b\>\>" template)
- "<<b>>")
- ((string-match "\<\<bne\>\>" template)
- "<<bne>>")
- ((string-match "\<\<e\>\>" template)
- "<<e>>")))
-
-(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
+++ /dev/null
-;;; 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 <https://www.gnu.org/licenses/>.
-
-;;; 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 '<<f>>' -acodec libmp3lame '<<fne>>.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)
- (errro "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='<<fne>>'
-mkdir -p \"${outdir}\"
- : > \"${outdir}/<<bne>>.org\"
-jq -r '.log.entries[] | @base64' '<<f>>' | 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>.*)\") | .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}/<<bne>>.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' '<<f>>'"
- :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% \"<<cb>>\""
- :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\" \"<<cb>>\""
- :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 '<<f>>' '<<fne>>_cleared.<<e>>'
- exiftool -all:all= -overwrite_original '<<fne>>_cleared.<<e>>'"
- :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 '<<f>>'"
- :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 '<<f>>'"
- :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 '<<f>>' -"
- :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 '<<f>>' | tail -n 1 | cut -s -d',' -f2-2)\"
- if [ -z \"$lat\" ]; then
- echo \"no latitude\"
- exit 1
- fi
- lon=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<<f>>' | 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 '<<f>>' | tail -n 1 | cut -s -d',' -f2-2)\"
- if [ -z \"$lat\" ]; then
- echo \"no latitude\"
- exit 1
- fi
- lon=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<<f>>' | 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 '<<f>>' '<<fne>>_h_flipped.<<e>>'"
- :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 '<<f>>' '<<fne>>_v_flipped.<<e>>'"
- :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 '<<f>>' '<<fne>>.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 '<<f>>' '<<fne>>.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>>' -f png -o '<<fne>>.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 '<link rel=\"icon\" type=\"image/svg+xml\" href=\"<<f>>\">'
- rsvg-convert -o '<<fne>>-16.png' -w 16 -h 16 '<<f>>'
- echo '<link rel=\"icon\" type=\"image/png\" href=\"<<fne>>-16.png\" sizes=\"16x16\">'
- rsvg-convert -o '<<fne>>-32.png' -w 32 -h 32 '<<f>>'
- echo '<link rel=\"icon\" type=\"image/png\" href=\"<<fne>>-32.png\" sizes=\"32x32\">'
- rsvg-convert -o '<<fne>>-48.png' -w 48 -h 48 '<<f>>'
- echo '<link rel=\"icon\" type=\"image/png\" href=\"<<fne>>-48.png\" sizes=\"48x48\">'
- convert '<<fne>>-16.png' '<<fne>>-32.png' '<<fne>>-48.png' '<<fne>>.ico'
- echo '<link rel=\"icon\" type=\"image/x-icon\" href=\"/'<<fne>>.ico'\">'
- rsvg-convert -o '<<fne>>-64.png' -w 64 -h 64 '<<f>>'
- echo '<link rel=\"icon\" type=\"image/png\" href=\"<<fne>>-64.png\" sizes=\"64x64\">'
- rsvg-convert -o '<<fne>>-180.png' -w 180 -h 180 '<<f>>'
- echo '<link rel=\"apple-touch-icon\" href=\"<<fne>>-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 <<width:200>>x<<height:200>> xc:none '<<empty<<width:200>>x<<height:200>>.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 '<<f>>' --pages . %d -- '<<fne>>_%d.<<e>>'" 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 '<<f>>' '<<fne>>_grayscale.<<e>>'"
- :utils "convert"))
-
-;;;###autoload
-(defun dwim-shell-commands-reorient-image ()
- "Reorient images."
- (interactive)
- (dwim-shell-command-on-marked-files
- "Reorient image"
- "convert -verbose -auto-orient '<<f>>' '<<fne>>_reoriented.<<e>>'"
- :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 '<<f>>' -movflags faststart -pix_fmt yuv420p -vf 'scale=trunc(iw/2)*2:trunc(ih/2)*2' '<<fne>>.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 '<<f>>'"
- ;; 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 <<f>>.mov -vcodec h264 -acodec copy <<fne>>.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 '<<f>>' -map_metadata 0 \
- -c:v hevc_videotoolbox -q:v 35 -preset fast -c:a aac -b:a 128k -tag:v hvc1 '<<fne>>'.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 '<<f>>' -pix_fmt rgb24 -r 15 '<<fne>>.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 '<<f>>' -c:v libx264 -c:a aac '<<fne>>.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 '<<f>>' -vcodec libwebp -filter:v fps=fps=10 -compression_level 3 -lossless 1 -loop 0 -preset default -an -vsync 0 '<<fne>>'.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 '<<f>>' '<<td>>/<<bne>>.gif'
- ffmpeg -i '<<td>>/<<bne>>.gif' -movflags faststart -pix_fmt yuv420p -vf 'scale=trunc(iw/2)*2:trunc(ih/2)*2' '<<fne>>.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 '<<f>>' '<<fne>>.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 '<<f>>'"
- :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 '<<f>>' -pix_fmt rgb24 -r 15 '<<fne>>.gif'
- gifsicle -O3 '<<fne>>.gif' --lossy=80 -o '<<fne>>.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 '<<f>>'"
- :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 '<<fne>>.<<e>>' '<<f>>'"
- "zip -r '<<archive.zip(u)>>' '<<*>>'")
- :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 '<<fne>>_protected.<<e>>' '<<f>>'"
- :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 '<<f>>' --lossy=90 -o '<<fne>>_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 '<<f>>' <<frames>> -O2 -o '<<fne>>_x%s.<<e>>'" factor)
- :extensions "gif" :utils '("gifsicle" "identify")
- :post-process-template (lambda (script file)
- (string-replace "<<frames>>" (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 '<<f>>')
- height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 '<<f>>')
- corner=$((${width}/4))
- echo ${corner}
- convert -size ${width}x${height} xc:none -fill white -draw \"roundRectangle 0,0 ${width},${height} ${corner},${corner}\" '<<f>>' -compose SrcIn -composite '<<fne>>_ios_round.<<e>>'"
- :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 '<<f>>')
- height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 '<<f>>')
- convert -quiet -size \"${width}x${height}\" xc:none -fill black -draw \"roundRectangle 0,0,${width},${height} <<Width width:27>>,<<Width width:27>>\" '<<td>>/mask.png'
- convert '<<f>>' -coalesce -background black -alpha remove -alpha off '<<td>>/no_alpha.<<e>>'
- # https://stackoverflow.com/a/66990135
- convert '<<td>>/no_alpha.<<e>>' -quiet -coalesce -alpha extract null: \\( '<<td>>/mask.png' -alpha extract \\) -compose multiply -layers composite '<<td>>/alpha.gif'
- convert '<<td>>/no_alpha.<<e>>' null: '<<td>>/alpha.gif' -quiet -alpha off -compose copy_opacity -layers composite '<<fne>>_rounded.<<e>>'
- # Turn looping on.
- mogrify -loop 0 '<<fne>>_rounded.<<e>>'
- gifsicle -O3 '<<fne>>_rounded.<<e>>' --lossy=80 -o '<<fne>>_rounded.<<e>>'"
- :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 <<Scaling factor:0.5>> '<<f>>' -o '<<fne>>_x<<Scaling factor:0.5>>.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 '<<f>>' > '<<fne>>.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 '<<f>>' -o '<<fne>>.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 <<EOF
- tell application \"Photos\"
- import POSIX file \"<<f>>\"
- 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 '<<fne>>.xml' '<<f>>'"
- :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-pdf-to-txt ()
- "Convert pdf to txt."
- (interactive)
- (dwim-shell-command-on-marked-files
- "pdf to txt"
- "pdftotext -layout '<<f>>' '<<fne>>.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 '<<f>>' '<<fne>>_x%.2f.<<e>>'"
- (* 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 '<<f>>' '<<fne>>_x%d.<<e>>'" 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 -- '<<f>>' '<<fne>>_protected.<<e>>'"
- (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' -- '<<f>>' '<<fne>>_unprotected.<<e>>'"
- (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 '<<f>>' -vn -ab 128k -ar 44100 -y '<<fne>>.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 '<<f>>' -vf 'select=eq(n\\,0)' -q:v 3 cover.jpg -i '<<f>>' -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)' '<<fne>>.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 '<<f>>' -i '%s' -map_metadata 0 -map 0:a -map 1 -c copy -disposition:v:0 attached_pic '<<f>>.tmp.<<e>>' && mv -f '<<f>>' '<<f>>.bak' && mv '<<f>>.tmp.<<e>>' '<<f>>'"
- "ffmpeg -i '<<f>>' -i '%s' -map_metadata 0 -map 0:a -map 1 -c copy -disposition:v:0 attached_pic '<<f>>.tmp.<<e>>' && mv -f '<<f>>.tmp.<<e>>' '<<f>>'")
- 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 '<<f>>' -y -ss <<Seconds:5>> -c:v copy -c:a copy '<<fne>>_trimmed.<<e>>'"
- :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 -<<Seconds:5>> -i '<<f>>' -y -c:v copy -c:a copy '<<fne>>_trimmed.<<e>>'"
- :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 '<<f>>' -ss 00:00:01.000 -vframes 1 '<<fne>>.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 '<<f>>' -c copy -an '<<fne>>_no_audio.<<e>>'"
- :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 '<<f>>' -an -filter:v 'setpts=%s*PTS' '<<fne>>_x%s.<<e>>'"
- (/ 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 '<<f>>' -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 '<<fne>>_%d:%dx%d.<<e>>'" start start end factor end start end factor)
- :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 '<<f>>');
-width=${streams_stream_0_width};
-zmodload zsh/mathfunc
-width=$((rint($width * <<Scaling factor:0.5>>)));
-# Make it even or face 'not divisible by 2' errors.
-if [[ $((width%2)) -ne 0 ]] then
- width=$(($width - 1))
-fi
-ffmpeg -n -i '<<f>>' -vf \"scale=$width:-2\" '<<fne>>_x<<Scaling factor:0.5>>.<<e>>'"
- :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 '<<cb>>' | 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 \"<<f>>\" < $file_path > \"<<fne>>.dot\"
-dot -Tsvg \"<<fne>>.dot\" > \"<<fne>>.svg\"
-echo \"<<fne>>.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 '<<f>>'"
- (line-number-at-pos (point)))
- "open '<<f>>'")
- "xdg-open '<<f>>'")
- :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 \"<<f>>\" 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\" '<<e>>' 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 "~/Desktop" (car window) ".gif"))
- (buffer-file-name path) ;; override so <<f>> 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 '<<f>>'"
- (cdr window))
- :silent-success t
- :monitor-directory "~/Desktop"
- :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 "~/Desktop"
- :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 '<<fne>>.iconset'
- sips -z 16 16 '<<f>>' --out '<<fne>>.iconset/icon_16x16.png'
- sips -z 32 32 '<<f>>' --out '<<fne>>.iconset/icon_16x16NOSPAM2x.png'
- sips -z 32 32 '<<f>>' --out '<<fne>>.iconset/icon_32x32.png'
- sips -z 64 64 '<<f>>' --out '<<fne>>.iconset/icon_32x32NOSPAM2x.png'
- sips -z 128 128 '<<f>>' --out '<<fne>>.iconset/icon_128x128.png'
- sips -z 256 256 '<<f>>' --out '<<fne>>.iconset/icon_128x128NOSPAM2x.png'
- sips -z 256 256 '<<f>>' --out '<<fne>>.iconset/icon_256x256NOSPAM2x.png'
- sips -z 512 512 '<<f>>' --out '<<fne>>.iconset/icon_512x512.png'
- sips -z 512 512 '<<f>>' --out '<<fne>>.iconset/icon_256x256NOSPAM2x.png'
- sips -z 1024 1024 '<<f>>' --out '<<fne>>.iconset/icon_512x512NOSPAM2x.png'
- iconutil -c icns '<<fne>>.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 <<f>> -bordercolor white -border 13 \\( +clone -background black -shadow 80x3+2+2 \\) +swap -background white -layers merge +repage <<fne>>-shadow.<<e>>"
- :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 '<<f>>' '<<fne>>_trimmed.<<e>>'"
- :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 <<cb>>"
- :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 '<<f>>'"
- :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 '<<f>>' '<<f(u)>>'"
- :utils "cp"))
-
-;;;###autoload
-(defun dwim-shell-commands-rename-all ()
- "Rename all marked file(s)."
- (interactive)
- (dwim-shell-command-on-marked-files
- "Rename all"
- "mv '<<f>>' '<<New name:Renamed>>(<<n>>).<<e>>'"
- :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=@<<f>> -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 <<f>>.
- (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
--- /dev/null
+;;; 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)))
+
+
+\f
+;;; 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:
+
+ <<f>> (file path)
+ <<fne>> (file path without extension)
+ <<e>> (extension)
+ <<td>> (generate a temporary directory)
+ <<*>> (all files joined)
+ <<cb>> (clipboard)
+ <<n>>, <<1n>>, or <<An>> (for current iteration)
+
+ For example:
+
+ With drawn files '(\"path/to/image1.png\" \"path/to/image2.png\")
+
+ \"convert '<<f>>' '<<fne>>.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 '<<f>>' '<<fne>>.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-"))
+
+\f
+;;; 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-"))
+\f
+;;; 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
--- /dev/null
+;; -*- 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")
--- /dev/null
+;;; 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 <https://www.gnu.org/licenses/>.
+
+;;; 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 (<<f>> <<fne>> <<e>>): "
+ "`dwim-shell-command' prompt. Modify if shorter is preferred."
+ :type 'string
+ :group 'dwim-shell-command)
+
+(defcustom dwim-shell-command-default-command
+ " '<<f>>'"
+ "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:
+
+ <<f>> (file path)
+ <<fne>> (file path without extension)
+ <<e>> (extension)
+ <<td>> (generate a temporary directory)
+ <<*>> (all files joined)
+ <<cb>> (clipboard)
+ <<n>>, <<1n>>, or <<An>> (for current iteration)
+
+ For example:
+
+ With drawn files '(\"path/to/image1.png\" \"path/to/image2.png\")
+
+ \"convert '<<f>>' '<<fne>>.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 '<<f>>' '<<fne>>.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 '<<f>>' '<<fne>>.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:
+
+ <<f>> (file path)
+ <<fne>> (file path without extension)
+ <<e>> (extension)
+ <<td>> (generate a temporary directory)
+ <<*>> (all files joined)
+ <<cb>> (clipboard)
+ <<n>>, <<1n>>, or <<An>> (for current iteration)
+
+ For example:
+
+ With drawn files '(\"path/to/image1.png\" \"path/to/image2.png\")
+
+ \"convert '<<f>>' '<<fne>>.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 '<<f>>' '<<fne>>.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:
+
+ <<f>> (file path)
+ <<fne>> (file path without extension)
+ <<e>> (extension)
+ <<td>> (generate a temporary directory)
+ <<*>> (all files joined)
+ <<cb>> (clipboard)
+ <<n>>, <<1n>>, or <<An>> (for current iteration)
+
+ For example:
+
+ Given :FILES '(\"path/to/image1.png\" \"path/to/image2.png\")
+
+ \"convert '<<f>>' '<<fne>>.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 <<td>> 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 "\<\<td\>\>" 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 <<Width:100>> world <<Height:200>>\" =>
+
+ ((\"<<Width:100>>\" . \"100\")
+ (\"<<Height:200>>\" . \"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 <<f>>, <<fne>>, or <<e>>.
+
+ 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(u)>>" -> 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))
+
+ ;; "<<td>>" with TEMP-DIR -> "/var/folders/m7/ky091cp56d5g68nyhl4y7frc0000gn/T/dwim-shell-command-JNK4V5"
+ (setq template (replace-regexp-in-string "\\(\<\<td\>\>\\)" temp-dir template nil nil 1))
+
+ ;; "<<cb>>" with (current-kill 0) -> "whatever was in kill ring"
+ (when (string-match "\<\<cb\>\>" template)
+ (setq template (replace-regexp-in-string "\\(\<\<cb\>\>\\)" (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:
+
+ \"\<\<fne\>\>\" \"before \"<<fne>>\" after\" => (\"\"\" \"\\\\\"\")
+
+ \"\<\<fne\>\>\" \"before '<<fne>>' 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 <<f>> for FILE, <<fne>> for FILE without extension, and
+<<e>> for FILE extension. <<n>>, <<1n>>, or <<an>> is replaced with
+CURRENT. <<some.txt(u)>> expands to unique \"some(1).txt\".
+
+Note: This expander cannot be used to expand <<*>>.
+
+ For example:
+
+ Given FILE \"path/to/image.png\"
+
+ \"convert '<<f>>' '<<fne>>.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)))
+ ;; "<<fne>>" with "/path/tmp.txt" -> "/path/tmp"
+ (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\<fne\>\>" template t))
+ (unescaped-quote (nth 0 quoting))
+ (escaped-quote (nth 1 quoting)))
+ (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\<fne\>\>\\)"
+ (string-replace unescaped-quote escaped-quote (file-name-sans-extension file))
+ template nil nil 2))
+ (setq template (replace-regexp-in-string "\\(\<\<fne\>\>\\)" (file-name-sans-extension file) template nil nil 1)))
+
+ ;; "<<b>>" with "/path/tmp.txt" -> "tmp.txt"
+ (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\<b\>\>" template t))
+ (unescaped-quote (nth 0 quoting))
+ (escaped-quote (nth 1 quoting)))
+ (setq template (replace-regexp-in-string "\\(\<\<b\>\>\\)\\([^ ]\\)"
+ (string-replace unescaped-quote escaped-quote (file-name-nondirectory file))
+ template nil nil 1))
+ (setq template (replace-regexp-in-string "\\(\<\<b\>\>\\)" (file-name-nondirectory file) template nil nil 1)))
+
+ ;; "<<bne>>" with "/path/tmp.txt" -> "tmp"
+ (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\<bne\>\>" template t))
+ (unescaped-quote (nth 0 quoting))
+ (escaped-quote (nth 1 quoting)))
+ (setq template (replace-regexp-in-string "\\(\<\<bne\>\>\\)\\([^ ]\\)"
+ (string-replace unescaped-quote escaped-quote
+ (file-name-sans-extension (file-name-nondirectory file)))
+ template nil nil 1))
+ (setq template (replace-regexp-in-string "\\(\<\<bne\>\>\\)" (file-name-sans-extension
+ (file-name-nondirectory file)) template nil nil 1)))
+
+ ;; "<<e>>" with "/path/tmp.txt" -> "txt"
+ (if (file-name-extension file)
+ (setq template (replace-regexp-in-string "\\(\<\<e\>\>\\)" (file-name-extension file) template nil nil 1))
+ ;; File had no extension. Attempt to remove .<<e>>.
+ (setq template (replace-regexp-in-string "\\(\.\<\<e\>\>\\)" "" template nil nil 1)))
+
+ ;; "<<f>>" with "/path/file.jpg" -> "/path/file.jpg"
+ (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\<f\>\>" template))
+ (unescaped-quote (nth 0 quoting))
+ (escaped-quote (nth 1 quoting)))
+ (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\<f\>\>\\)\\([^ ]\\)"
+ (string-replace unescaped-quote escaped-quote file)
+ template nil nil 2))
+ (setq template (replace-regexp-in-string "\\(\<\<f\>\>\\)" file template nil nil 1)))
+
+ ;; "<<f(u)>>" with "/path/file.jpg" -> "/path/file(1).jpg"
+ (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\<f(u)\>\>" template))
+ (unescaped-quote (nth 0 quoting))
+ (escaped-quote (nth 1 quoting)))
+ (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\<f\(u)>\>\\)\\([^ ]\\)"
+ (string-replace unescaped-quote escaped-quote
+ (dwim-shell-command--unique-new-file-path file))
+ template nil nil 2))
+ (setq template (replace-regexp-in-string "\\(\<\<f(u)\>\>\\)"
+ (dwim-shell-command--unique-new-file-path file) template nil nil 1))))
+
+ ;; "<<some.txt(u)>>" -> 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)))
+
+ ;; "<<td>>" with TEMP-DIR -> "/var/folders/m7/ky091cp56d5g68nyhl4y7frc0000gn/T/dwim-shell-command-JNK4V5"
+ (setq template (replace-regexp-in-string "\\(\<\<td\>\>\\)" temp-dir template nil nil 1))
+
+ ;; "<<cb>>" with (current-kill 0) -> "whatever was in kill ring"
+ (when (string-match "\<\<cb\>\>" template)
+ (setq template (replace-regexp-in-string "\\(\<\<cb\>\>\\)" (current-kill 0)
+ template nil nil 1)))
+
+ ;; "<<n>>" or "<<an>" 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 <<f>>, <<fne>>, or <<e>> in TEMPLATE."
+ (cond ((string-match "\<\<f\>\>" template)
+ "<<f>>")
+ ((string-match "\<\<fne\>\>" template)
+ "<<fne>>")
+ ((string-match "\<\<b\>\>" template)
+ "<<b>>")
+ ((string-match "\<\<bne\>\>" template)
+ "<<bne>>")
+ ((string-match "\<\<e\>\>" template)
+ "<<e>>")))
+
+(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
--- /dev/null
+;;; 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 <https://www.gnu.org/licenses/>.
+
+;;; 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 '<<f>>' -acodec libmp3lame '<<fne>>.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='<<fne>>'
+mkdir -p \"${outdir}\"
+ : > \"${outdir}/<<bne>>.org\"
+jq -r '.log.entries[] | @base64' '<<f>>' | 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>.*)\") | .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}/<<bne>>.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' '<<f>>'"
+ :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% \"<<cb>>\""
+ :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\" \"<<cb>>\""
+ :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 '<<f>>' '<<fne>>_cleared.<<e>>'
+ exiftool -all:all= -overwrite_original '<<fne>>_cleared.<<e>>'"
+ :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 '<<f>>'"
+ :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 '<<f>>'"
+ :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 '<<f>>' -"
+ :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 '<<f>>' | tail -n 1 | cut -s -d',' -f2-2)\"
+ if [ -z \"$lat\" ]; then
+ echo \"no latitude\"
+ exit 1
+ fi
+ lon=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<<f>>' | 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 '<<f>>' | tail -n 1 | cut -s -d',' -f2-2)\"
+ if [ -z \"$lat\" ]; then
+ echo \"no latitude\"
+ exit 1
+ fi
+ lon=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<<f>>' | 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 '<<f>>' '<<fne>>_h_flipped.<<e>>'"
+ :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 '<<f>>' '<<fne>>_v_flipped.<<e>>'"
+ :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 '<<f>>' '<<fne>>.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 '<<f>>' '<<fne>>.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>>' -f png -o '<<fne>>.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 '<link rel=\"icon\" type=\"image/svg+xml\" href=\"<<f>>\">'
+ rsvg-convert -o '<<fne>>-16.png' -w 16 -h 16 '<<f>>'
+ echo '<link rel=\"icon\" type=\"image/png\" href=\"<<fne>>-16.png\" sizes=\"16x16\">'
+ rsvg-convert -o '<<fne>>-32.png' -w 32 -h 32 '<<f>>'
+ echo '<link rel=\"icon\" type=\"image/png\" href=\"<<fne>>-32.png\" sizes=\"32x32\">'
+ rsvg-convert -o '<<fne>>-48.png' -w 48 -h 48 '<<f>>'
+ echo '<link rel=\"icon\" type=\"image/png\" href=\"<<fne>>-48.png\" sizes=\"48x48\">'
+ convert '<<fne>>-16.png' '<<fne>>-32.png' '<<fne>>-48.png' '<<fne>>.ico'
+ echo '<link rel=\"icon\" type=\"image/x-icon\" href=\"/'<<fne>>.ico'\">'
+ rsvg-convert -o '<<fne>>-64.png' -w 64 -h 64 '<<f>>'
+ echo '<link rel=\"icon\" type=\"image/png\" href=\"<<fne>>-64.png\" sizes=\"64x64\">'
+ rsvg-convert -o '<<fne>>-180.png' -w 180 -h 180 '<<f>>'
+ echo '<link rel=\"apple-touch-icon\" href=\"<<fne>>-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 <<width:200>>x<<height:200>> xc:none '<<empty<<width:200>>x<<height:200>>.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 '<<f>>' --pages . %d -- '<<fne>>_%d.<<e>>'" 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 '<<f>>' '<<fne>>_grayscale.<<e>>'"
+ :utils "convert"))
+
+;;;###autoload
+(defun dwim-shell-commands-reorient-image ()
+ "Reorient images."
+ (interactive)
+ (dwim-shell-command-on-marked-files
+ "Reorient image"
+ "convert -verbose -auto-orient '<<f>>' '<<fne>>_reoriented.<<e>>'"
+ :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 '<<f>>' -movflags faststart -pix_fmt yuv420p -vf 'scale=trunc(iw/2)*2:trunc(ih/2)*2' '<<fne>>.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 '<<f>>'"
+ ;; 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 <<f>>.mov -vcodec h264 -acodec copy <<fne>>.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 '<<f>>' -map_metadata 0 \
+ -c:v hevc_videotoolbox -q:v 35 -preset fast -c:a aac -b:a 128k -tag:v hvc1 '<<fne>>'.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 '<<f>>' -pix_fmt rgb24 -r 15 '<<fne>>.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 '<<f>>' -c:v libx264 -c:a aac '<<fne>>.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 '<<f>>' -vcodec libwebp -filter:v fps=fps=10 -compression_level 3 -lossless 1 -loop 0 -preset default -an -vsync 0 '<<fne>>'.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 '<<f>>' '<<td>>/<<bne>>.gif'
+ ffmpeg -i '<<td>>/<<bne>>.gif' -movflags faststart -pix_fmt yuv420p -vf 'scale=trunc(iw/2)*2:trunc(ih/2)*2' '<<fne>>.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 '<<f>>' '<<fne>>.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 '<<f>>'"
+ :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 '<<f>>' -pix_fmt rgb24 -r 15 '<<fne>>.gif'
+ gifsicle -O3 '<<fne>>.gif' --lossy=80 -o '<<fne>>.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 '<<f>>'"
+ :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 '<<fne>>.<<e>>' '<<f>>'"
+ "zip -r '<<archive.zip(u)>>' '<<*>>'")
+ :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 '<<fne>>_protected.<<e>>' '<<f>>'"
+ :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 '<<f>>' --lossy=90 -o '<<fne>>_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 '<<f>>' <<frames>> -O2 -o '<<fne>>_x%s.<<e>>'" factor)
+ :extensions "gif" :utils '("gifsicle" "identify")
+ :post-process-template (lambda (script file)
+ (string-replace "<<frames>>" (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 '<<f>>')
+ height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 '<<f>>')
+ corner=$((${width}/4))
+ echo ${corner}
+ convert -size ${width}x${height} xc:none -fill white -draw \"roundRectangle 0,0 ${width},${height} ${corner},${corner}\" '<<f>>' -compose SrcIn -composite '<<fne>>_ios_round.<<e>>'"
+ :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 '<<f>>')
+ height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 '<<f>>')
+ convert -quiet -size \"${width}x${height}\" xc:none -fill black -draw \"roundRectangle 0,0,${width},${height} <<Width width:27>>,<<Width width:27>>\" '<<td>>/mask.png'
+ convert '<<f>>' -coalesce -background black -alpha remove -alpha off '<<td>>/no_alpha.<<e>>'
+ # https://stackoverflow.com/a/66990135
+ convert '<<td>>/no_alpha.<<e>>' -quiet -coalesce -alpha extract null: \\( '<<td>>/mask.png' -alpha extract \\) -compose multiply -layers composite '<<td>>/alpha.gif'
+ convert '<<td>>/no_alpha.<<e>>' null: '<<td>>/alpha.gif' -quiet -alpha off -compose copy_opacity -layers composite '<<fne>>_rounded.<<e>>'
+ # Turn looping on.
+ mogrify -loop 0 '<<fne>>_rounded.<<e>>'
+ gifsicle -O3 '<<fne>>_rounded.<<e>>' --lossy=80 -o '<<fne>>_rounded.<<e>>'"
+ :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 <<Scaling factor:0.5>> '<<f>>' -o '<<fne>>_x<<Scaling factor:0.5>>.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 '<<f>>' > '<<fne>>.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 '<<f>>' -o '<<fne>>.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 <<EOF
+ tell application \"Photos\"
+ import POSIX file \"<<f>>\"
+ 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 '<<fne>>.xml' '<<f>>'"
+ :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 '<<f>>' '<<fne>>.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 '<<f>>' '<<fne>>_x%.2f.<<e>>'"
+ (* 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 '<<f>>' '<<fne>>_x%d.<<e>>'" 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 -- '<<f>>' '<<fne>>_protected.<<e>>'"
+ (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' -- '<<f>>' '<<fne>>_unprotected.<<e>>'"
+ (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 '<<f>>' -vn -ab 128k -ar 44100 -y '<<fne>>.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 '<<f>>' -vf 'select=eq(n\\,0)' -q:v 3 cover.jpg -i '<<f>>' -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)' '<<fne>>.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 '<<f>>' -i '%s' -map_metadata 0 -map 0:a -map 1 -c copy -disposition:v:0 attached_pic '<<f>>.tmp.<<e>>' && mv -f '<<f>>' '<<f>>.bak' && mv '<<f>>.tmp.<<e>>' '<<f>>'"
+ "ffmpeg -i '<<f>>' -i '%s' -map_metadata 0 -map 0:a -map 1 -c copy -disposition:v:0 attached_pic '<<f>>.tmp.<<e>>' && mv -f '<<f>>.tmp.<<e>>' '<<f>>'")
+ 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 '<<f>>' -y -ss <<Seconds:5>> -c:v copy -c:a copy '<<fne>>_trimmed.<<e>>'"
+ :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 -<<Seconds:5>> -i '<<f>>' -y -c:v copy -c:a copy '<<fne>>_trimmed.<<e>>'"
+ :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 '<<f>>' -ss 00:00:01.000 -vframes 1 '<<fne>>.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 '<<f>>' -c copy -an '<<fne>>_no_audio.<<e>>'"
+ :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 '<<f>>' -an -filter:v 'setpts=%s*PTS' '<<fne>>_x%s.<<e>>'"
+ (/ 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 '<<f>>' -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 '<<fne>>_%d:%dx%d.<<e>>'" 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 '<<f>>' -metadata title='%s' -codec copy '<<td>>/<<bne>>_temp.<<e>>' && mv -f '<<td>>/<<bne>>_temp.<<e>>' '<<f>>'"
+ 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 '<<f>>');
+width=${streams_stream_0_width};
+zmodload zsh/mathfunc
+width=$((rint($width * <<Scaling factor:0.5>>)));
+# Make it even or face 'not divisible by 2' errors.
+if [[ $((width%2)) -ne 0 ]] then
+ width=$(($width - 1))
+fi
+ffmpeg -n -i '<<f>>' -vf \"scale=$width:-2\" '<<fne>>_x<<Scaling factor:0.5>>.<<e>>'"
+ :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 '<<cb>>' | 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 \"<<f>>\" < $file_path > \"<<fne>>.dot\"
+dot -Tsvg \"<<fne>>.dot\" > \"<<fne>>.svg\"
+echo \"<<fne>>.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 '<<f>>'"
+ (line-number-at-pos (point)))
+ "open '<<f>>'")
+ "xdg-open '<<f>>'")
+ :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 \"<<f>>\" 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\" '<<e>>' 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 <<f>> 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 '<<f>>'"
+ (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 '<<fne>>.iconset'
+ sips -z 16 16 '<<f>>' --out '<<fne>>.iconset/icon_16x16.png'
+ sips -z 32 32 '<<f>>' --out '<<fne>>.iconset/icon_16x16NOSPAM2x.png'
+ sips -z 32 32 '<<f>>' --out '<<fne>>.iconset/icon_32x32.png'
+ sips -z 64 64 '<<f>>' --out '<<fne>>.iconset/icon_32x32NOSPAM2x.png'
+ sips -z 128 128 '<<f>>' --out '<<fne>>.iconset/icon_128x128.png'
+ sips -z 256 256 '<<f>>' --out '<<fne>>.iconset/icon_128x128NOSPAM2x.png'
+ sips -z 256 256 '<<f>>' --out '<<fne>>.iconset/icon_256x256NOSPAM2x.png'
+ sips -z 512 512 '<<f>>' --out '<<fne>>.iconset/icon_512x512.png'
+ sips -z 512 512 '<<f>>' --out '<<fne>>.iconset/icon_256x256NOSPAM2x.png'
+ sips -z 1024 1024 '<<f>>' --out '<<fne>>.iconset/icon_512x512NOSPAM2x.png'
+ iconutil -c icns '<<fne>>.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 <<f>> -bordercolor white -border 13 \\( +clone -background black -shadow 80x3+2+2 \\) +swap -background white -layers merge +repage <<fne>>-shadow.<<e>>"
+ :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 '<<f>>' '<<fne>>_trimmed.<<e>>'"
+ :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 <<cb>>"
+ :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 '<<f>>'"
+ :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 '<<f>>' '<<f(u)>>'"
+ :utils "cp"))
+
+;;;###autoload
+(defun dwim-shell-commands-rename-all ()
+ "Rename all marked file(s)."
+ (interactive)
+ (dwim-shell-command-on-marked-files
+ "Rename all"
+ "mv '<<f>>' '<<New name:Renamed>>(<<n>>).<<e>>'"
+ :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=@<<f>> -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 <<f>>.
+ (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