Compare commits

...

44 Commits
v0.1 ... master

Author SHA1 Message Date
TEC 2770c0cfef
Wayland support 2024-04-01 22:42:40 +08:00
TEC ddb092d88d
Tweak infix definition again
I realised the prompt is really duplicating the docstring.
2023-03-28 23:54:57 +08:00
lunik1 5446e0f5a5 Move metadata setting to hook
This ensures metadata is set after pngquant, which will destroy any
metadata.
2023-03-28 23:54:43 +08:00
lunik1 ac49dff18d Optionally set image's "author" metadata
Adds option to set images "author" metadata to user-full-name
2023-03-28 23:54:43 +08:00
lunik1 2b17c6cf66 Optionally set image's "title" metadata
Adds option to set images "title" metadata to the name of the
screenshotted buffer
2023-03-28 23:54:43 +08:00
lunik1 b52211087d Optionally set image's "description" metadata
Adds option to embed text in screenshot region as the image's
"description" metadata
2023-03-28 23:54:43 +08:00
lunik1 3f8142d815 Set image's "software" metadata field
Record the version of Emacs and screenshot.el used to generate the
image
2023-03-28 23:54:43 +08:00
TEC bfde4578c8
Version 0.2 2023-01-20 19:25:32 +08:00
TEC 5d14360117
Update melpazoid action 2023-01-20 19:25:32 +08:00
TEC 7f24958efd
Add docstring to transient 2023-01-20 19:24:18 +08:00
TEC e08e342fe4
Don't rely on defun returning function symbol
This is undocumented behaviour.
2023-01-20 19:23:33 +08:00
TEC 6df63e7616
Further improve infix variable creation
The major change here is that now modified values are saved with
custom.el.
2023-01-20 19:23:21 +08:00
TEC 3846a772ab
Copy to the kill-ring not just system clipboard 2023-01-20 19:23:21 +08:00
TEC 731ac5a90d
Use call-process with imagemagick instead of shell 2023-01-20 19:23:19 +08:00
TEC 420eae2388
Propertize url in upload message as link 2023-01-20 00:09:36 +08:00
TEC e2a364c1a1
Add support for copying text content of screenshot
This is basically just useful because of the automatic de-indentation.
2023-01-19 23:59:13 +08:00
TEC d84a8581bd
Rework action definition and operation
Now actions can be called by themselves, outside the transient.
2023-01-19 23:59:09 +08:00
TEC d2cb601519
Better support removing common indent in region
With an assortment of related tweaks.
2023-01-19 23:59:07 +08:00
TEC 58c64eec6a
Tweak style of screenshot--define-infix 2023-01-19 01:09:01 +08:00
TEC 177dbd3996
Use make-indirect-buffer to inhibit buffer hooks 2022-10-18 20:05:59 +08:00
TEC 965375426e
Change some of the transient switches 2022-10-18 02:45:30 +08:00
TEC dac727e98e
Calculate lines including wrapping 2022-10-18 02:43:40 +08:00
TEC 36b7c50641
Completion agnostic font prompt 2022-10-18 02:42:52 +08:00
TEC 49490a4a23
Toggle hl-line-mode in original buf when cloning 2022-10-18 02:05:41 +08:00
TEC acfc52d09c
Determine frame width better 2022-10-18 01:50:29 +08:00
TEC 867bebe701
Winner mode is unhelpful 2022-10-18 01:50:15 +08:00
TEC 68ab27c2ab
Make the byte compiler happier 2022-10-18 01:50:07 +08:00
TEC e7a8941d9f
Eval macros at compile time for byte-compiler
This makes the byte-compiler aware of the variables defined by the macro
expansions.
2022-10-18 01:49:12 +08:00
TEC 053c9e856f
Update donation badge 2022-10-13 23:45:11 +08:00
TEC 7621e0cd17
Add stage badge to Readme 2021-11-12 02:34:57 +08:00
TEC 6faa48a988
More compact hl-line / solaire mode check 2021-09-11 15:58:17 +08:00
TEC fdd1692a4c
Ensure default font family is always valid
If the default face's font is unspecified, then just use "monospace".
2021-09-11 15:46:11 +08:00
TEC f8204e82dc
Disable solaire mode when its enabled 2021-09-09 22:08:08 +08:00
TEC b081bdca17
Update readme screenshot 2021-09-09 22:07:56 +08:00
TEC 3b03bdea89
Give people a way to caffeinate me 2021-08-24 18:18:53 +08:00
TEC 41b92a2a88
Use eval-when-compile to define vars for compiled
By using eval-when-compile, the macros defining the variables are
executed, preventing the "undefined variable" warnings.
I've also snuck in a few extra tweaks (three undefined global variables,
and one that needed moving) that should make the byte-compiler happier.
2021-06-04 18:20:37 +08:00
TEC 03335d3821
Add CI: Melpazoid 2021-06-02 22:41:09 +08:00
Sergi Ruiz 61c20c132b Clean byte-compile and checkdoc warnings 2021-06-02 22:35:37 +08:00
TEC c6061dd6b0
Fix incorrectly labelled option
Closes #5
2021-03-30 09:51:21 +08:00
TEC 6825ec53d2
ix.io upload: work with non-file buffers 2021-03-12 19:12:13 +08:00
TEC d8c2b4751d
Replace obsolete transient command 2021-03-09 16:18:48 +08:00
TEC e8191b4a99
Remove pointless newlines in func 2021-03-09 16:17:37 +08:00
TEC 76c53c11d9
More default tweaking 2021-03-09 16:16:51 +08:00
TEC c3103f66cc
Tweak defaults, add maintainer email. 2021-02-07 21:49:01 +08:00
5 changed files with 517 additions and 257 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,2 @@
github: tecosaur
liberapay: tec

25
.github/workflows/melpazoid.yml vendored Normal file
View File

@ -0,0 +1,25 @@
# melpazoid <https://github.com/riscy/melpazoid> build checks.
name: melpazoid
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v1
with: { python-version: 3.9 }
- name: Install
run: |
python -m pip install --upgrade pip
sudo apt-get install emacs && emacs --version
git clone https://github.com/riscy/melpazoid.git ~/melpazoid
pip install ~/melpazoid
- name: Run
env:
RECIPE: (screenshot :repo "tecosaur/screenshot" :fetcher github)
EXIST_OK: false
LOCAL_REPO: ${{ github.workspace }}
run: echo $GITHUB_REF && make -C ~/melpazoid

View File

@ -1,6 +1,12 @@
#+title: Screenshot.el
#+author: tecosaur
#+html: <p><img src="https://img.shields.io/badge/Emacs-27.1+-blueviolet.svg?style=flat-square&logo=GNU%20Emacs&logoColor=white">
#+html: <img src="https://img.shields.io/badge/stage-%CE%B2,%20refinement-orange?style=flat-square">
#+html: <img src="https://img.shields.io/badge/-Linux-fcc624?logo=linux&style=flat-square&logoColor=black">
#+html: <img src="https://img.shields.io/badge/-MacOS-lightgrey?logo=apple&style=flat-square&logoColor=black">
#+html: <a href="https://liberapay.com/tec"><img src="https://shields.io/badge/support%20my%20efforts-f6c915?logo=Liberapay&style=flat-square&logoColor=black"></a></p>
Ever wanted to take a screenshot of some code to share on /<insert IM platform
of choice here>/?
+ Yes: well then this package may just be for you!

View File

@ -3,9 +3,9 @@
;; Copyright (C) 2020 TEC
;; Author: TEC <http://github/tecosaur>
;; Maintainer: TEC <http://github/tecosaur>
;; Maintainer: TEC <tec@tecosaur.com>
;; Homepage: https://github.com/tecosaur/screenshot
;; Version: 0.1.0
;; Version: 0.2.0
;; Keywords: convenience, screenshot
;; Package-Requires: ((emacs "27") (transient "0.2.0") (posframe "0.8.3"))
@ -29,32 +29,201 @@
;;; Commentary:
;; Convenience package for creating images of the current region or buffer.
;; Requires `imagemagick' for some visual post-processing, and `xclip' for
;; copying images to the clipboard.
;; Requires `imagemagick' to set metadata and for some visual post-processing,
;; and `xclip' for copying images to the clipboard.
;;; Code:
(require 'transient)
(require 'posframe)
(require 'lisp-mnt)
(defgroup screenshot ()
"Customise group for Screenshot."
:group 'convenience)
(defvar screenshot--buffer nil
"The buffer last used to create a screenshot.")
(defcustom screenshot-buffer-creation-hook nil
"Hook run after creating a buffer for screenshots.
Run after hardcoded setup, but before the screenshot is captured."
:type 'hook
:group 'screenshot)
;;; Generated variables
(defvar screenshot--region-beginning nil
"Start of the region forming the screenshot.")
(defvar screenshot--region-end nil
"End of the region forming the screenshot.")
(defvar screenshot--tmp-file nil
"An intermediate target file for the screenshot.")
(defvar screenshot--first-line-number nil
"The first line contained in the screenshot.")
;;; Screenshot parameters
(eval-when-compile
(defmacro screenshot--define-infix (key name description type default
&rest reader)
"Define a defcustom screenshot-NAME and an associated transient infix setter.
The new variable screenshot-NAME takes the default value DEFAULT,
and is given the docstring DESCRIPTION, and declared to be of
TYPE.
The infix uses KEY and DESCRIPTION, modifies the variable
screenshot-NAME, and is set by a reader function with body
READER."
(declare (indent 5) (doc-string 3))
(let* ((infix-var (intern (format "screenshot-%s" name)))
(reader
(cond
((and (not reader)
(eq (cadr type) 'boolean))
`((not ,infix-var)))
((and (memq (cadr type) '(string color number integer float))
(not reader))
`((,(pcase (cadr type)
((or 'string 'color) #'read-string)
((or 'number 'integer 'float) #'read-number))
,(concat description ": ") ,infix-var)))
(t reader))))
`(progn
(defcustom ,infix-var ,default
,(concat description ".")
:type ,type
:group 'screenshot)
(transient-define-infix ,(intern (format "screenshot--set-%s" name)) ()
,(format "Set `screenshot--%s' from a popup buffer." name)
:class 'transient-lisp-variable
:variable ',infix-var
:key ,key
:description ,description
:argument ,(format "--%s" name)
:reader (lambda (&rest _)
(let ((new-value (progn ,@reader)))
(if (equal new-value ,infix-var)
(message "%s unchanged" ',infix-var)
(prog1
(customize-save-variable ',infix-var new-value)
(message "New %s value saved" ',infix-var)))
new-value)))))))
(screenshot--define-infix
"-n" line-numbers-p
"Show line numbers"
'boolean nil)
(screenshot--define-infix
"-r" relative-line-numbers-p
"Relative line numbers within the screenshot"
'boolean nil)
(screenshot--define-infix
"-t" text-only-p
"Use a text-only version of the buffer"
'boolean nil)
(screenshot--define-infix
"-x" truncate-lines-p
"Truncate lines beyond the screenshot width"
'boolean nil)
(screenshot--define-infix
"-i" remove-indent-p
"Remove indent in selection"
'boolean t)
(screenshot--define-infix
"-md" code-as-image-description-p
"Set \"description\" metadata to text in region"
'boolean t)
(screenshot--define-infix
"-mt" buffer-name-as-image-title-p
"Set image \"title\" metadata to `buffer-name'"
'boolean t)
(screenshot--define-infix
"-ma" user-full-name-as-image-author-p
"Set image \"author\" metadata to `user-full-name'"
'boolean nil)
(declare-function counsel-fonts "ext:counsel-fonts")
(declare-function ivy-read "ext:ivy-read")
(screenshot--define-infix
"-ff" font-family
"Font family to use"
'string (let ((font (face-attribute 'default :font)))
(if (eq font 'unspecified) "monospace"
(symbol-name (font-get font :family))))
(completing-read
"Font: "
(mapcar
(lambda (f) (propertize f 'face (list :family f)))
;; TODO strip non-ascii fonts
(delete-dups (font-family-list)))
nil t nil nil screenshot-font-family))
(screenshot--define-infix
"-fs" font-size
"Font size in pt"
'number 14)
;;;; Frame
(screenshot--define-infix
"-bw" border-width
"Border width in px"
'integer 20)
(screenshot--define-infix
"-br" radius
"Rounded corner radius in px"
'integer 10)
(screenshot--define-infix
"-w" min-width
"Minimum width, in columns"
'integer 40)
(screenshot--define-infix
"-W" max-width
"Maximum width, in columns"
'integer 120)
;;;; Shadow
(screenshot--define-infix
"-sr" shadow-radius
"Radius of the shadow in px"
'integer 12)
(screenshot--define-infix
"-si" shadow-intensity
"Intensity of the shadow (0-100)"
'integer 80)
(screenshot--define-infix
"-sc" shadow-color
"Color of the shadow (hex string)"
'color "#333")
(screenshot--define-infix
"-sx" shadow-offset-horizontal
"Shadow horizontal offset in px"
'integer -8)
(screenshot--define-infix
"-sy" shadow-offset-vertical
"Shadow vertical offset in px"
'integer 5)
;;; Main function
;;;###autoload
(defun screenshot (beg end &optional upload-text)
"Take a screenshot of the current region or buffer.
@ -63,47 +232,18 @@ Region included in screenshot is the active selection, interactively,
or given by BEG and END. Buffer is used if region spans 0-1 characters.
When a universal argument is given, UPLOAD-TEXT is non-nil.
Then the text of the region/buffer is uploaded, and the URL is copied to clipboard."
Then the text of the region/buffer is uploaded, and the URL is copied
to the clipboard."
(interactive (if (region-active-p)
(list (region-beginning) (region-end) (when current-prefix-arg t))
(list (point-min) (point-max) (when current-prefix-arg t))))
(if upload-text
(screenshot-text-upload beg end)
(deactivate-mark)
(screenshot--set-screenshot-region beg end)
(setq screenshot--tmp-file
(make-temp-file "screenshot-" nil ".png"))
(screenshot-transient)))
(defun screenshot-text-upload (beg end)
"Upload the region from BEG to END, and copy the upload URL to the clipboard."
(message "Uploading text...")
(let ((url
(funcall screenshot-text-upload-function beg end)))
(gui-select-text url)
(message "Screenshot uploaded, link copied to clipboard (%s)" url)))
(defun screenshot-ixio-upload (beg end)
"Upload the region from BEG to END to ix.io, and return the URL."
(let ((output (generate-new-buffer "ixio")) url)
(shell-command-on-region beg end
(format "curl -F 'ext:1=%s' -F 'f:1=<-' ix.io 2>/dev/null"
(file-name-extension (buffer-file-name)))
output)
(setq url (string-trim-right (with-current-buffer output (buffer-string))))
(kill-buffer output)
url))
(defvar screenshot-text-upload-function #'screenshot-ixio-upload
"Function to use to upload text.
Must take a start and end position for the current buffer, and
return a URL.")
(call-interactively #'screenshot-transient)))
;;; Screenshot capturing
@ -123,13 +263,14 @@ and the line number of the first line of the region."
(when (= beg (point))
(setq beg (line-beginning-position)))
(goto-char end)
(when (string-match-p "\\`\\s-*$" (thing-at-point 'line))
(forward-line -1)
(setq end (line-end-position))))
(setq end (1+ (re-search-backward "[^[:space:]\n]"))))
(setq screenshot--region-beginning beg
screenshot--region-end end
screenshot--first-line-number (line-number-at-pos beg)
screenshot--total-lines (- (line-number-at-pos end) (line-number-at-pos beg) -1)))
screenshot--first-line-number (line-number-at-pos beg)))
(declare-function solaire-mode "ext:solaire-mode")
(declare-function hl-line-mode "ext:hl-line")
(defun screenshot--setup-buffer ()
"Modify the current buffer to make it appropriate for screenshotting."
@ -137,8 +278,9 @@ and the line number of the first line of the region."
(show-paren-match nil)
(region nil))
line-spacing 0.1)
(when (and (featurep 'hl-line) hl-line-mode)
(hl-line-mode -1))
(when (bound-and-true-p hl-line-mode) (hl-line-mode -1))
(when (bound-and-true-p solaire-mode) (solaire-mode -1))
(when (bound-and-true-p winner-mode) (winner-mode -1))
(run-hooks 'screenshot-buffer-creation-hook))
(defvar screenshot--text-only-buffer
@ -150,52 +292,115 @@ and the line number of the first line of the region."
"A text-only buffer for use in creating screenshots.")
(defun screenshot--format-text-only-buffer (beg end)
"Insert text from BEG to END in the current buffer, into the screenshot text-only buffer."
"Insert text from BEG to END in the current buffer, into the screenshot buffer."
;; include indentation if `beg' is where indentation starts
(let ((s (string-trim-right (buffer-substring beg end))))
(with-current-buffer (setq screenshot--buffer screenshot--text-only-buffer)
(with-current-buffer screenshot--text-only-buffer
(buffer-face-set :family screenshot-font-family
:height (* 10 screenshot-font-size))
(erase-buffer)
(insert s)
(indent-rigidly (point-min) (point-max)
(- (indent-rigidly--current-indentation
(point-min) (point-max))))
(current-buffer))))
(defun screenshot--narrowed-clone-buffer (beg end)
"Create a clone of the current buffer, narrowed to the region from BEG to END.
This buffer then then set up to be used for a screenshot."
(with-current-buffer (clone-indirect-buffer " *screenshot-clone" nil t)
(narrow-to-region beg end)
(screenshot--setup-buffer)
(buffer-face-set :family screenshot-font-family
:height (* 10 screenshot-font-size))
(current-buffer)))
(let ((hl (bound-and-true-p hl-line-mode)))
(when hl (hl-line-mode -1))
(prog1
(with-current-buffer (make-indirect-buffer (current-buffer) " *screenshot-clone" t t)
(narrow-to-region beg end)
(screenshot--setup-buffer)
(buffer-face-set :family screenshot-font-family
:height (* 10 screenshot-font-size))
(current-buffer))
(when hl (hl-line-mode 1)))))
(defun screenshot--max-line-length (&optional buffer)
"Find the maximum line length in BUFFER."
(let ((max-line 0))
(with-current-buffer (or buffer (current-buffer))
(save-excursion
(goto-char (point-min))
(dotimes (line-1 (count-lines (point-min) (point-max)))
(setq max-line (max max-line
(- (line-end-position (1+ line-1))
(line-beginning-position (1+ line-1))))))))
max-line))
(defun screenshot--displayed-lines (&optional buffer)
"Count the number of (perhaps wrapped) lines displyed in BUFFER."
(with-current-buffer (or buffer (current-buffer))
(if screenshot-truncate-lines-p
(count-lines (point-min) (point-max))
(save-excursion
(goto-char (point-min))
(let ((displayed-lines 0))
(dotimes (line-1 (count-lines (point-min) (point-max)))
(setq displayed-lines
(+ displayed-lines
(max 1 (ceiling (- (line-end-position (1+ line-1))
(line-beginning-position (1+ line-1)))
screenshot-max-width)))))
displayed-lines)))))
;;; Screenshot processing
(defun screenshot--process ()
"Perform the screenshot process.
(defun screenshot--process (beg end)
"Perform the screenshot process on the region BEG to END.
Create a buffer for the screenshot, use `x-export-frames' to create the image,
and process it."
(setq screenshot--buffer
(if screenshot-text-only-p
(screenshot--format-text-only-buffer screenshot--region-beginning screenshot--region-end)
(screenshot--narrowed-clone-buffer screenshot--region-beginning screenshot--region-end)))
More specifically, this function will:
- Create a buffer for the screenshot
- Save it as an image
- Process the image"
(let ((ss-buf
(if screenshot-text-only-p
(screenshot--format-text-only-buffer beg end)
(screenshot--narrowed-clone-buffer beg end)))
(indent-level 0))
(when screenshot-remove-indent-p
(with-current-buffer ss-buf
(setq indent-level (indent-rigidly--current-indentation
(point-min) (point-max)))
(when (> indent-level 0)
(indent-rigidly (point-min) (point-max)
(- indent-level)))))
(unwind-protect
(progn
(screenshot--process-buffer ss-buf)
(screenshot--post-process screenshot--tmp-file))
(when (and screenshot-remove-indent-p
(not screenshot-text-only-p)
(> indent-level 0))
(with-current-buffer ss-buf
(indent-rigidly (point-min) (point-max)
indent-level)))
(unless (eq ss-buf screenshot--text-only-buffer)
(kill-buffer ss-buf)))))
(let ((frame (posframe-show
screenshot--buffer
:position (point-min)
:internal-border-width screenshot-border-width
:min-width screenshot-min-width
:width screenshot-max-width
:min-height screenshot--total-lines
:lines-truncate screenshot-truncate-lines-p
:poshandler #'posframe-poshandler-point-bottom-left-corner
:hidehandler #'posframe-hide)))
(with-current-buffer screenshot--buffer
(defun screenshot--process-buffer (ss-buf)
"Save a screenshot of SS-BUF to `screenshot--tmp-file' via `x-export-frames'."
(let* (before-make-frame-hook
delete-frame-functions
(width (max screenshot-min-width
(min screenshot-max-width
(screenshot--max-line-length
ss-buf))))
(height (screenshot--displayed-lines ss-buf))
(frame (posframe-show
ss-buf
:position (point-min)
:internal-border-width screenshot-border-width
:min-width width
:width width
:max-width width
:min-height height
:height height
:max-height height
:lines-truncate screenshot-truncate-lines-p
:poshandler #'posframe-poshandler-point-bottom-left-corner
:hidehandler #'posframe-hide)))
(with-current-buffer ss-buf
(setq-local display-line-numbers screenshot-line-numbers-p)
(when screenshot-text-only-p
(setq-local display-line-numbers-offset
@ -205,85 +410,163 @@ and process it."
(redraw-frame frame)
(with-temp-file screenshot--tmp-file
(insert (x-export-frames frame 'png))))
(posframe-hide screenshot--buffer))
(unless screenshot-text-only-p
(kill-buffer screenshot--buffer))
(screenshot--post-process screenshot--tmp-file))
(posframe-hide ss-buf)))
(defcustom screenshot-post-process-hook
(when (executable-find "pngquant")
(list (defun screenshot--compress-file (file)
(call-process "pngquant" nil nil nil "-f" "-o" file file))))
(append (and (executable-find "pngquant") (list #'screenshot--compress-file))
(list #'screenshot--set-metadata))
"Functions to be called on the output file after processing.
Must take a single argument, the file name, and operate in-place."
:type 'function
:group 'screenshot)
(defun screenshot--compress-file (file)
"Compress FILE with pngquant."
(call-process "pngquant" nil nil nil "--force" "--skip-if-larger" "--output" file file))
(defun screenshot--set-metadata (file)
"Set requested metadata on FILE."
(let ((result (apply #'call-process "convert" nil nil nil
(append (list file)
(and screenshot-code-as-image-description-p
(list "-set" "description" (buffer-substring
screenshot--region-beginning
screenshot--region-end)))
(and screenshot-buffer-name-as-image-title-p
(list "-set" "title" (buffer-name)))
(and screenshot-user-full-name-as-image-author-p
(list "-set" "author" user-full-name))
(list
"-set" "software" (format "Emacs %s; screenshot.el %s"
emacs-version
(lm-version (symbol-file 'screenshot)))
file)))))
(unless (zerop result)
(error "Could not apply imagemagick commands to image (exit code %d)" result))))
(defun screenshot--post-process (file)
"Apply any image post-processing to FILE."
(when (or (> screenshot-radius 0)
(> screenshot-shadow-radius 0))
(let ((result
(shell-command-to-string
(format "convert '%1$s' \\( +clone -alpha extract \\
\\( -size %2$dx%2$d xc:black -draw 'fill white circle %2$d,%2$d %2$d,0' -write mpr:arc +delete \\) \\
\\( mpr:arc \\) -gravity northwest -composite \\
\\( mpr:arc -flip \\) -gravity southwest -composite \\
\\( mpr:arc -flop \\) -gravity northeast -composite \\
\\( mpr:arc -rotate 180 \\) -gravity southeast -composite \\) \\
-alpha off -compose CopyOpacity -composite -compose over \\
\\( +clone -background '%3$s' -shadow %4$dx%5$d+%6$d+%7$d \\) \\
+swap -background none -layers merge '%1$s'"
file
screenshot-radius
screenshot-shadow-color
screenshot-shadow-intensity
screenshot-shadow-radius
screenshot-shadow-offset-horizontal
screenshot-shadow-offset-vertical))))
(unless (string= result "")
(error "Could not apply imagemagick commants to image:\n%s" result))))
(apply #'call-process
"convert"
nil nil nil
(delq
nil
(append
(list file)
(and (> screenshot-radius 0)
(list "(" "+clone" "-alpha" "extract"
"(" "-size" (format "%1$dx%1$d" screenshot-radius)
"xc:black"
"-draw" (format "fill white circle %1$d,%1$d %1$d,0"
screenshot-radius)
"-write" "mpr:arc" "+delete" ")"
"(" "mpr:arc" ")" "-gravity" "northwest" "-composite"
"(" "mpr:arc" "-flip" ")" "-gravity" "southwest" "-composite"
"(" "mpr:arc" "-flop" ")" "-gravity" "northeast" "-composite"
"(" "mpr:arc" "-rotate" "180" ")" "-gravity" "southeast" "-composite" ")"
"-alpha" "off"
"-compose" "CopyOpacity"
"-composite" "-compose" "over"))
(and (> screenshot-shadow-radius 0)
(list "(" "+clone" "-background" screenshot-shadow-color
"-shadow" (format "%dx%d+%d+%d"
screenshot-shadow-intensity
screenshot-shadow-radius
screenshot-shadow-offset-horizontal
screenshot-shadow-offset-vertical)
")" "+swap"))
(list
"-background" "none"
"-layers" "merge"
file))))))
(unless (eq result 0)
(error "Could not apply imagemagick commands to image (exit code %d)" result))))
(run-hook-with-args 'screenshot-post-process-hook file))
;;; Screenshot actions
(defmacro screenshot--def-action (name &rest body)
"Define an action that may be performed on a screenshot from the transient interface.
BODY is executed after `screenshot-process' is called."
`(defun ,(intern (concat "screenshot-" name)) (&optional args)
"Screenshot action to be performed from the transient interface."
(interactive
(list (transient-args 'screenshot-transient)))
(screenshot--process)
,@body))
(eval-when-compile
(defmacro screenshot--def-action (name &optional docstring &rest body)
"Define action NAME to be performed from the transient interface.
This defines a function screenshot-NAME with DOCSTRING which executes BODY after
determining (and binding) the region beg/end and calling `screenshot--process'.
If BODY starts with :no-img then `screenshot--process' is not called."
(declare (doc-string 2) (indent defun))
`(defun ,(intern (format "screenshot-%s" name)) (beg end)
,(concat
(if (stringp docstring)
(concat docstring "\n\n")
(push docstring body)
"")
"Screenshot action to be performed from the transient interface.")
(interactive
(progn
(unless (eq transient-current-command 'screenshot-transient)
(if (region-active-p)
(screenshot--set-screenshot-region (region-beginning) (region-end))
(screenshot--set-screenshot-region
(line-beginning-position) (line-end-position)))
,@(and (not (eq (car body) :no-img))
'((setq screenshot--tmp-file
(make-temp-file "screenshot-" nil ".png")))))
(list screenshot--region-beginning screenshot--region-end)))
,@(if (eq (car body) :no-img)
(progn (pop body) nil)
'((screenshot--process beg end)))
,@body)))
(screenshot--def-action
"save"
(rename-file
screenshot--tmp-file
(concat (file-name-sans-extension
(or (buffer-file-name)
(expand-file-name "screenshot")))
".png")
t)
(message "Screenshot saved"))
(screenshot--def-action save
"Save the current selection (BEG-END) as an image."
(rename-file
screenshot--tmp-file
(concat (file-name-sans-extension
(or (buffer-file-name)
(expand-file-name "screenshot")))
".png")
t)
(message "Screenshot saved"))
(screenshot--def-action
"save-as"
(rename-file
screenshot--tmp-file
(read-file-name "Save as: " (file-name-directory (or (buffer-file-name) default-directory)))
1)
(message "Screenshot saved"))
(screenshot--def-action save-as
"Save the current selection (BEG-END) as an image in the specified location."
(rename-file
screenshot--tmp-file
(read-file-name "Save as: " (file-name-directory (or (buffer-file-name) default-directory)))
1)
(message "Screenshot saved"))
(screenshot--def-action
"copy"
(call-process "xclip" nil nil nil
"-selection" "clipboard"
"-target" "image/png"
"-in" screenshot--tmp-file)
(delete-file screenshot--tmp-file)
(message "Screenshot copied"))
(screenshot--def-action copy
"Copy the current selection (BEG-END) as an image to the clipboard."
(let ((wayland-p (getenv "WAYLAND_DISPLAY")))
(cond
((and wayland-p (executable-find "wl-copy"))
(call-process "wl-copy" screenshot--tmp-file nil nil
"--type" "image/png"))
((and (not wayland-p) (executable-find "xclip"))
(call-process "xclip" nil nil nil
"-selection" "clipboard"
"-target" "image/png"
"-in" screenshot--tmp-file))
(t
(user-error "Missing `%s' executable, needed to copy images to the clipboard"
(if wayland-p "wl-copy" "xclip")))))
(delete-file screenshot--tmp-file)
(message "Screenshot copied"))
(screenshot--def-action text-copy
"Copy the current selection (BEG-END) as text to the clipboard."
:no-img
(let ((content (string-trim-right (buffer-substring beg end))))
(with-temp-buffer
(insert content)
(when screenshot-remove-indent-p
(indent-rigidly (point-min) (point-max)
(- (indent-rigidly--current-indentation
(point-min) (point-max)))))
(kill-new (buffer-substring-no-properties
(point-min) (point-max))))))
(defcustom screenshot-upload-fn nil
"Function or string which provides a method to upload a file.
@ -294,28 +577,66 @@ Note: you have to define this yourself, there is no default."
:type '(choice function string)
:group 'screenshot)
(screenshot--def-action
"upload"
(if (not screenshot-upload-fn)
(error "No upload function defined")
(message "Uploading...")
(let ((url
(pcase screenshot-upload-fn
((pred functionp) (funcall screenshot-upload-fn screenshot--tmp-file))
((pred stringp) (string-trim-right (shell-command-to-string (format screenshot-upload-fn screenshot--tmp-file))))
(_ (error "Upload function is not a function or string!")))))
(gui-select-text url)
(message "Screenshot uploaded, link copied to clipboard (%s)" url)))
(delete-file screenshot--tmp-file))
(screenshot--def-action upload
"Upload an image of the current selection (BEG-END) via `screenshot-upload-fn'."
(if (not screenshot-upload-fn)
(error "No upload function defined")
(message "Uploading...")
(let ((url
(pcase screenshot-upload-fn
((pred functionp) (funcall screenshot-upload-fn screenshot--tmp-file))
((pred stringp) (string-trim-right (shell-command-to-string (format screenshot-upload-fn screenshot--tmp-file))))
(_ (error "Upload function is not a function or string!")))))
(kill-new url)
(message "Screenshot uploaded, link copied to clipboard (%s)"
(propertize url 'face 'link))))
(delete-file screenshot--tmp-file))
(defcustom screenshot-text-upload-fn #'screenshot-ixio-upload
"Function to use to upload text.
Must take a start and end position for the current buffer, and
return a URL."
:type 'function
:group 'screenshot)
(defun screenshot-ixio-upload (beg end)
"Upload the region from BEG to END to ix.io, and return the URL."
(let ((output (generate-new-buffer "ixio")) url)
(shell-command-on-region beg end
(format "curl -F 'ext:1=%s' -F 'f:1=<-' ix.io 2>/dev/null"
(file-name-extension (or (buffer-file-name) " .txt")))
output)
(setq url (string-trim-right (with-current-buffer output (buffer-string))))
(kill-buffer output)
url))
(screenshot--def-action text-upload
"Upload the current selection (BEG-END) as text via `screenshot-text-upload-fn'."
(message "Uploading text...")
(let ((content (string-trim-right (buffer-substring beg end)))
url)
(with-temp-buffer
(insert content)
(when screenshot-remove-indent-p
(indent-rigidly (point-min) (point-max)
(- (indent-rigidly--current-indentation
(point-min) (point-max)))))
(setq url (funcall screenshot-text-upload-fn (point-min) (point-max)))
(kill-new url)
(message "Screenshot uploaded, link copied to clipboard (%s)"
(propertize url 'face 'link)))))
;;; Screenshot transient
(define-transient-command screenshot-transient ()
(transient-define-prefix screenshot-transient ()
"Transient that should only ever be invoked by `screenshot'."
["Code"
(screenshot--set-line-numbers-p)
(screenshot--set-relative-line-numbers-p)
(screenshot--set-text-only-p)
(screenshot--set-truncate-lines-p)
(screenshot--set-remove-indent-p)
(screenshot--set-font-family)
(screenshot--set-font-size)]
["Frame"
@ -329,114 +650,20 @@ Note: you have to define this yourself, there is no default."
(screenshot--set-shadow-color)
(screenshot--set-shadow-offset-horizontal)
(screenshot--set-shadow-offset-vertical)]
["Metadata"
(screenshot--set-code-as-image-description-p)
(screenshot--set-buffer-name-as-image-title-p)
(screenshot--set-user-full-name-as-image-author-p)]
["Action"
("s" "Save" screenshot-save)
("S" "Save as" screenshot-save-as)
("c" "Copy" screenshot-copy)
("u" "Upload" screenshot-upload)])
(defmacro screenshot--define-infix (key name description type default &rest reader)
`(progn
(defcustom ,(intern (concat "screenshot-" name)) ,default
,description
:type ,type
:group 'screenshot)
(transient-define-infix ,(intern (concat "screenshot--set-" name)) ()
"Set `screenshot--theme' from a popup buffer."
:class 'transient-lisp-variable
:variable ',(intern (concat "screenshot-" name))
:key ,key
:description ,description
:argument ,(concat "--" name)
:reader (lambda (&rest _) ,@reader))))
;;; Screenshot parameters
;;;; Code
(screenshot--define-infix
"-l" "line-numbers-p" "Show line numbers"
'boolean t
(not screenshot-line-numbers-p))
(screenshot--define-infix
"-L" "relative-line-numbers-p" "Relative line numbers within the screenshot"
'boolean nil
(not screenshot-relative-line-numbers-p))
(screenshot--define-infix
"-t" "text-only-p" "Use a text-only version of the buffer"
'boolean t
(not screenshot-text-only-p))
(screenshot--define-infix
"-T" "truncate-lines-p" "Truncate lines beyond the screenshot width"
'boolean nil
(not screenshot-truncate-lines-p))
(screenshot--define-infix
"-ff" "font-family" "Font family to use"
'string (symbol-name (font-get (face-attribute 'default :font) :family))
(if (fboundp #'counsel-fonts)
(ivy-read "Font: " (delete-dups (font-family-list))
:preselect screenshot-font-family
:require-match t
:history 'counsel-fonts-history
:caller 'counsel-fonts)
(completing-read "Font: " (delete-dups (font-family-list)))))
(screenshot--define-infix
"-fs" "font-size" "Font size (pt)"
'number 14
(read-number "Font size in pt: " screenshot-font-size))
;;;; Frame
(screenshot--define-infix
"-b" "border-width" "Border width in pixels"
'integer 20
(read-number "Border width in px: " screenshot-border-width))
(screenshot--define-infix
"-r" "radius" "Rounded corner radius"
'integer 10
(read-number "Border radius in px: " screenshot-radius))
(screenshot--define-infix
"-w" "min-width" "Minimum width, in columns"
'integer 40
(read-number "Minimum width (columns): " screenshot-min-width))
(screenshot--define-infix
"-W" "max-width" "Minimum width, in columns"
'integer 120
(read-number "Maximum width (columns): " screenshot-max-width))
;;;; Shadow
(screenshot--define-infix
"-s" "shadow-radius" "Radius of the shadow in pixels"
'integer 12
(read-number "Shadow width in px: " screenshot-shadow-radius))
(screenshot--define-infix
"-i" "shadow-intensity" "Intensity of the shadow"
'integer 100
(read-number "Shadow width in px: " screenshot-shadow-intensity))
(screenshot--define-infix
"-c" "shadow-color" "Color of the shadow"
'color "#333"
(read-string "Shadow color: " screenshot-shadow-color))
(screenshot--define-infix
"-x" "shadow-offset-horizontal" "Shadow horizontal offset"
'integer 8
(read-number "Shadow horizontal offset in px: " screenshot-shadow-offset-horizontal))
(screenshot--define-infix
"-y" "shadow-offset-vertical" "Shadow vertical offset"
'integer 5
(read-number "Shadow vertical offset in px: " screenshot-shadow-offset-vertical))
["Save"
("s" "Save image" screenshot-save)
("S" "Save image as" screenshot-save-as)]
["Upload"
("u" "Image" screenshot-upload)
("U" "Text" screenshot-text-upload)]
["Copy"
("c" "Image" screenshot-copy)
("C" "Text" screenshot-text-copy)]])
(provide 'screenshot)
;;; screenshot.el ends here

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 42 KiB