summaryrefslogtreecommitdiff
path: root/modules/custom-buffer-file.el
diff options
context:
space:
mode:
Diffstat (limited to 'modules/custom-buffer-file.el')
-rw-r--r--modules/custom-buffer-file.el376
1 files changed, 376 insertions, 0 deletions
diff --git a/modules/custom-buffer-file.el b/modules/custom-buffer-file.el
new file mode 100644
index 00000000..b5740cd2
--- /dev/null
+++ b/modules/custom-buffer-file.el
@@ -0,0 +1,376 @@
+;;; custom-buffer-file.el --- Custom Buffer and File Operations -*- coding: utf-8; lexical-binding: t; -*-
+;;
+;;; Commentary:
+;; This module provides custom buffer and file operations including PostScript
+;; printing capabilities.
+;;
+;; Functions include:
+;; - printing buffers or regions as PostScript to the default printer (with color support)
+;; - moving/renaming/deleting buffer files
+;; - diffing buffer contents with saved file version
+;; - copying file paths and file:// links to the kill ring
+;; - copying buffer contents (whole buffer, to top of buffer, to bottom of buffer)
+;; - clearing buffer contents from point to top or bottom.
+;;
+;; The PostScript printing auto-detects the system print spooler (lpr or lp)
+;; and prints with face/syntax highlighting.
+;;
+;; Keybindings under ~C-; b~:
+;; - ~C-; b k~ kill buffer and window (delete window, kill/bury buffer)
+;; - Copy buffer content submenu at ~C-; b c~
+;; - ~C-; b c w~ copy whole buffer
+;; - ~C-; b c t~ copy from beginning to point
+;; - ~C-; b c b~ copy from point to end
+;;
+;;; Code:
+
+;; cj/custom-keymap defined in keybindings.el
+(eval-when-compile (defvar cj/custom-keymap))
+(eval-when-compile (require 'ps-print)) ;; for ps-print variables
+(declare-function ps-print-buffer-with-faces "ps-print")
+(declare-function ps-print-region-with-faces "ps-print")
+
+;; ------------------------- Print Buffer As Postscript ------------------------
+
+(defvar cj/print-spooler-command 'auto
+ "Command used to send PostScript to the system print spooler.
+Set to a string to force a specific command (e.g., lpr or lp). Set to `auto' to
+auto-detect once per session.")
+
+(defvar cj/print--spooler-cache nil
+ "Cached spooler command detected for the current Emacs session.")
+
+(defun cj/print--resolve-spooler ()
+ "Return the spooler command to use, auto-detecting and caching if needed."
+ (cond
+ ;; User-specified command
+ ((and (stringp cj/print-spooler-command)
+ (> (length cj/print-spooler-command) 0))
+ (or (executable-find cj/print-spooler-command)
+ (user-error "Cannot print: spooler command '%s' not found in PATH"
+ cj/print-spooler-command))
+ cj/print-spooler-command)
+ ;; Auto-detect once per session
+ ((eq cj/print-spooler-command 'auto)
+ (or cj/print--spooler-cache
+ (let ((cmd (or (and (executable-find "lpr") "lpr")
+ (and (executable-find "lp") "lp"))))
+ (unless cmd
+ (user-error "Cannot print: neither 'lpr' nor 'lp' found in PATH"))
+ (setq cj/print--spooler-cache cmd)
+ cmd)))
+ (t
+ (user-error "Invalid value for cj/print-spooler-command: %S"
+ cj/print-spooler-command))))
+
+(defun cj/print-buffer-ps (&optional color)
+ "Print the buffer (or active region) as PostScript to the default printer.
+With prefix argument COLOR, print in color and skip confirmation; otherwise
+print in monochrome with confirmation prompt.
+Sends directly to the system spooler with no header."
+ (interactive "P")
+ (unless (require 'ps-print nil t)
+ (user-error "Cannot print: ps-print library not found"))
+ (let* ((spooler (cj/print--resolve-spooler))
+ (want-color (not (null color)))
+ (have-region (use-region-p))
+ (skip-confirm color)) ; C-u skips confirmation
+ ;; Confirm unless C-u was used
+ (when (and (not skip-confirm)
+ (not (y-or-n-p (format "Send %s to printer? "
+ (if have-region "region" "buffer")))))
+ (user-error "Printing cancelled"))
+ (let ((ps-lpr-command spooler)
+ (ps-printer-name nil) ; default system printer
+ (ps-lpr-switches nil)
+ (ps-print-color-p want-color)
+ (ps-use-face-background want-color)
+ (ps-print-header nil)) ; no headers
+ (if have-region
+ (ps-print-region-with-faces (region-beginning) (region-end))
+ (ps-print-buffer-with-faces)))
+ (message "Sent %s to default printer via %s (%s)"
+ (if have-region "region" "buffer")
+ spooler
+ (if want-color "color" "monochrome"))))
+
+;; ------------------------- Buffer And File Operations ------------------------
+
+(defun cj/--move-buffer-and-file (dir &optional ok-if-exists)
+ "Internal implementation: Move buffer and file to DIR.
+If OK-IF-EXISTS is nil and target exists, signal an error.
+If OK-IF-EXISTS is non-nil, overwrite existing file.
+Returns t on success, nil if buffer not visiting a file."
+ (let* ((name (buffer-name))
+ (filename (buffer-file-name))
+ (dir (expand-file-name dir))
+ (dir
+ (if (string-match "[/\\\\]$" dir)
+ (substring dir 0 -1) dir))
+ (newname (concat dir "/" name)))
+ (if (not filename)
+ (progn
+ (message "Buffer '%s' is not visiting a file!" name)
+ nil)
+ (progn (copy-file filename newname ok-if-exists)
+ (delete-file filename)
+ (set-visited-file-name newname)
+ (set-buffer-modified-p nil)
+ t))))
+
+(defun cj/move-buffer-and-file (dir)
+ "Move both current buffer and the file it visits to DIR.
+When called interactively, prompts for confirmation if target file exists."
+ (interactive (list (read-directory-name "Move buffer and file (to new directory): ")))
+ (let* ((target (expand-file-name (buffer-name) (expand-file-name dir))))
+ (condition-case _
+ (cj/--move-buffer-and-file dir nil)
+ (file-already-exists
+ (if (yes-or-no-p (format "File %s exists; overwrite? " target))
+ (cj/--move-buffer-and-file dir t)
+ (message "File not moved"))))))
+
+(defun cj/--rename-buffer-and-file (new-name &optional ok-if-exists)
+ "Internal implementation: Rename buffer and file to NEW-NAME.
+NEW-NAME can be just a basename or a full path to move to different directory.
+If OK-IF-EXISTS is nil and target exists, signal an error.
+If OK-IF-EXISTS is non-nil, overwrite existing file.
+Returns t on success, nil if buffer not visiting a file."
+ (let ((filename (buffer-file-name))
+ (new-basename (file-name-nondirectory new-name)))
+ (if (not filename)
+ (progn
+ (message "Buffer '%s' is not visiting a file!" (buffer-name))
+ nil)
+ ;; Check if a buffer with the new name already exists
+ (when (and (get-buffer new-basename)
+ (not (eq (get-buffer new-basename) (current-buffer))))
+ (error "A buffer named '%s' already exists" new-basename))
+ ;; Expand new-name to absolute path (preserves directory if just basename)
+ (let ((expanded-name (expand-file-name new-name
+ (file-name-directory filename))))
+ (rename-file filename expanded-name ok-if-exists)
+ (rename-buffer new-basename)
+ (set-visited-file-name expanded-name)
+ (set-buffer-modified-p nil)
+ t))))
+
+(defun cj/rename-buffer-and-file (new-name)
+ "Rename both current buffer and the file it visits to NEW-NAME.
+When called interactively, prompts for confirmation if target file exists."
+ (interactive
+ (list (if (not (buffer-file-name))
+ (user-error "Buffer '%s' is not visiting a file!" (buffer-name))
+ (read-string "Rename buffer and file (to new name): "
+ (file-name-nondirectory (buffer-file-name))))))
+ (condition-case err
+ (cj/--rename-buffer-and-file new-name nil)
+ (file-already-exists
+ (if (yes-or-no-p (format "File %s exists; overwrite? " new-name))
+ (cj/--rename-buffer-and-file new-name t)
+ (message "File not renamed")))
+ (error
+ ;; Handle buffer-already-exists and other errors
+ (message "%s" (error-message-string err)))))
+
+(defun cj/delete-buffer-and-file ()
+ "Kill the current buffer and delete the file it visits."
+ (interactive)
+ (let ((filename (buffer-file-name)))
+ (when filename
+ (if (vc-backend filename)
+ (vc-delete-file filename)
+ (progn
+ (delete-file filename t)
+ (message "Deleted file %s" filename)
+ (kill-buffer))))))
+
+(defun cj/copy-link-to-buffer-file ()
+ "Copy the full file:// path of the current buffer's source file to the kill ring."
+ (interactive)
+ (let ((file-path (buffer-file-name)))
+ (when file-path
+ (setq file-path (concat "file://" file-path))
+ (kill-new file-path)
+ (message "Copied file link to kill ring: %s" file-path))))
+
+(defun cj/copy-path-to-buffer-file-as-kill ()
+ "Copy the full path of the current buffer's file to the kill ring.
+Signal an error if the buffer is not visiting a file."
+ (interactive)
+ (let ((path (buffer-file-name)))
+ (if (not path)
+ (user-error "Current buffer is not visiting a file")
+ (kill-new path)
+ (message "Copied file path: %s" path)
+ path)))
+
+(defun cj/copy-whole-buffer ()
+ "Copy the entire contents of the current buffer to the kill ring.
+Point and mark are left exactly where they were. No transient region
+is created. A message is displayed when done."
+ (interactive)
+ (let ((contents (buffer-substring-no-properties (point-min) (point-max))))
+ (kill-new contents)
+ (message "Buffer contents copied to kill ring")))
+
+(defun cj/copy-to-bottom-of-buffer ()
+ "Copy text from point to the end of the buffer to the kill ring.
+Point and mark are left exactly where they were. No transient region
+is created. A message is displayed when done."
+ (interactive)
+ (let ((contents (buffer-substring-no-properties (point) (point-max))))
+ (kill-new contents)
+ (message "Copied from point to end of buffer")))
+
+(defun cj/copy-to-top-of-buffer ()
+ "Copy text from the beginning of the buffer to point to the kill ring.
+Point and mark are left exactly where they were. No transient region
+is created. A message is displayed when done."
+ (interactive)
+ (let ((contents (buffer-substring-no-properties (point-min) (point))))
+ (kill-new contents)
+ (message "Copied from beginning of buffer to point")))
+
+(defun cj/clear-to-bottom-of-buffer ()
+ "Delete all text from point to the end of the current buffer.
+This does not save the deleted text in the kill ring."
+ (interactive)
+ (delete-region (point) (point-max))
+ (message "Buffer contents removed to the end of the buffer."))
+
+(defun cj/clear-to-top-of-buffer ()
+ "Delete all text from point to the beginning of the current buffer.
+Do not save the deleted text in the kill ring."
+ (interactive)
+ (delete-region (point) (point-min))
+ (message "Buffer contents removed to the beginning of the buffer."))
+
+(defun cj/copy-buffer-name ()
+ "Copy current buffer name to kill ring."
+ (interactive)
+ (kill-new (buffer-name))
+ (message "Copied: %s" (buffer-name)))
+
+(require 'system-lib)
+(declare-function ansi-color-apply-on-region "ansi-color")
+
+(defun cj/--diff-with-difftastic (file1 file2 buffer)
+ "Run difftastic on FILE1 and FILE2, output to BUFFER.
+Applies ANSI color and sets up special-mode for navigation."
+ (with-current-buffer buffer
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (insert (format "Difftastic diff: %s (saved) vs buffer (modified)\n\n"
+ (file-name-nondirectory file1)))
+ (call-process "difft" nil t nil
+ "--color" "always"
+ "--display" "side-by-side-show-both"
+ file1 file2)
+ (require 'ansi-color)
+ (ansi-color-apply-on-region (point-min) (point-max))
+ (special-mode)
+ (goto-char (point-min)))))
+
+(defun cj/--diff-with-regular-diff (file1 file2 buffer)
+ "Run regular unified diff on FILE1 and FILE2, output to BUFFER.
+Sets up diff-mode for navigation."
+ (with-current-buffer buffer
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (insert (format "Unified diff: %s (saved) vs buffer (modified)\n\n"
+ (file-name-nondirectory file1)))
+ (call-process "diff" nil t nil "-u" file1 file2)
+ (diff-mode)
+ (goto-char (point-min)))))
+
+(defun cj/diff-buffer-with-file ()
+ "Compare the current modified buffer with the saved version.
+Uses difftastic if available for syntax-aware diffing, falls back to regular diff.
+Shows output in a separate buffer.
+Signal an error if the buffer is not visiting a file."
+ (interactive)
+ (unless (buffer-file-name)
+ (user-error "Current buffer is not visiting a file"))
+ (let* ((file (buffer-file-name))
+ (file-ext (file-name-extension file t)) ; includes the dot
+ (temp-file (make-temp-file "buffer-diff-" nil file-ext))
+ (buffer-content (buffer-string))) ; Capture BEFORE with-temp-file!
+ (unwind-protect
+ (progn
+ ;; Write current buffer content to temp file
+ (with-temp-file temp-file
+ (insert buffer-content))
+ ;; Check if there are any differences first
+ (if (zerop (call-process "diff" nil nil nil "-q" file temp-file))
+ (message "No differences between buffer and file")
+ ;; Run diff/difftastic and display in buffer
+ (let* ((using-difftastic (cj/executable-exists-p "difft"))
+ (buffer-name (if using-difftastic
+ "*Diff (difftastic)*"
+ "*Diff (unified)*"))
+ (diff-buffer (get-buffer-create buffer-name)))
+ (if using-difftastic
+ (cj/--diff-with-difftastic file temp-file diff-buffer)
+ (cj/--diff-with-regular-diff file temp-file diff-buffer))
+ (display-buffer diff-buffer))))
+ ;; Clean up temp file
+ (when (file-exists-p temp-file)
+ (delete-file temp-file)))))
+
+;; --------------------------- Buffer And File Keymap --------------------------
+
+;; Copy buffer content sub-keymap
+(defvar-keymap cj/copy-buffer-content-map
+ :doc "Keymap for copy buffer content operations."
+ "w" #'cj/copy-whole-buffer
+ "b" #'cj/copy-to-bottom-of-buffer
+ "t" #'cj/copy-to-top-of-buffer)
+
+;; Buffer & file operations prefix and keymap
+(defvar-keymap cj/buffer-and-file-map
+ :doc "Keymap for buffer and file operations."
+ "m" #'cj/move-buffer-and-file
+ "r" #'cj/rename-buffer-and-file
+ "p" #'cj/copy-path-to-buffer-file-as-kill
+ "d" #'cj/delete-buffer-and-file
+ "D" #'cj/diff-buffer-with-file
+ "c" cj/copy-buffer-content-map
+ "n" #'cj/copy-buffer-name
+ "l" #'cj/copy-link-to-buffer-file
+ "k" #'cj/kill-buffer-and-window
+ "P" #'cj/print-buffer-ps
+ "t" #'cj/clear-to-top-of-buffer
+ "b" #'cj/clear-to-bottom-of-buffer
+ "x" #'erase-buffer
+ "s" #'mark-whole-buffer
+ "S" #'write-file ;; save as
+ "g" #'revert-buffer)
+(keymap-set cj/custom-keymap "b" cj/buffer-and-file-map)
+
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-; b" "buffer and file menu"
+ "C-; b m" "move file"
+ "C-; b r" "rename file"
+ "C-; b p" "copy file path"
+ "C-; b d" "delete file"
+ "C-; b D" "diff buffer with file"
+ "C-; b c" "buffer copy menu"
+ "C-; b c w" "copy whole buffer"
+ "C-; b c b" "copy to bottom"
+ "C-; b c t" "copy to top"
+ "C-; b n" "copy buffer name"
+ "C-; b l" "copy file link"
+ "C-; b k" "kill buffer and window"
+ "C-; b P" "print to PS"
+ "C-; b t" "clear to top"
+ "C-; b b" "clear to bottom"
+ "C-; b x" "erase buffer"
+ "C-; b s" "select whole buffer"
+ "C-; b S" "save as"
+ "C-; b g" "revert buffer"))
+
+
+(provide 'custom-buffer-file)
+;;; custom-buffer-file.el ends here.