You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
270 lines
11 KiB
270 lines
11 KiB
;;; pacfiles-mode.el --- pacnew and pacsave merging tool -*- lexical-binding: t; -*-
|
|
;;
|
|
;; Copyright (C) 2018 Carlos G. Cordero
|
|
;;
|
|
;; Author: Carlos G. Cordero <http://github/UndeadKernel>
|
|
;; Maintainer: Carlos G. Cordero <pacfiles@binarycharly.com>
|
|
;; Created: Oct 11, 2018
|
|
;; Modified: Oct 11, 2018
|
|
;; Version: 1.0
|
|
;; Keywords: files pacman arch pacnew pacsave update linux
|
|
;; URL: https://github.com/UndeadKernel/pacfiles-mode
|
|
;; Package-Requires: ((emacs "26") (cl-lib "0.5"))
|
|
;;
|
|
;; This file is not part of GNU Emacs.
|
|
;;
|
|
;;; Commentary:
|
|
;;
|
|
;; `pacfiles-mode' is an Emacs major mode to manage `.pacnew` and `.pacsave`
|
|
;; files left by Arch's pacman. To merge files, *pacfiles-mode* automatically
|
|
;; creates an Ediff merge session that a user can interact with. After finishing
|
|
;; the Ediff merge session, *pacfiles-mode* cleans up the mess that Ediff leaves
|
|
;; behind. *pacfiles-mode* also takes care of keeping the correct permissions of
|
|
;; merged files, and requests passwords (with TRAMP) to act as root when needed.
|
|
;;
|
|
;; Start the major mode using the command `pacfiles' or `pacfiles/start'.
|
|
;;
|
|
;;; Code:
|
|
|
|
(require 'pacfiles-buttons)
|
|
(require 'pacfiles-utils)
|
|
(require 'pacfiles-win)
|
|
|
|
(require 'cl-seq)
|
|
(require 'ediff)
|
|
(require 'outline)
|
|
(require 'time-date)
|
|
|
|
(defgroup pacfiles nil "Faces for the buttons used in pacfiles-mode."
|
|
:group 'tools)
|
|
|
|
(defvar pacfiles-updates-search-command "find /etc -name '*.pacnew' -o -name '*.pacsave' 2>/dev/null"
|
|
"Command to find .pacnew files.")
|
|
|
|
(defvar pacfiles--merge-search-command
|
|
(concat "find " pacfiles-merge-file-tmp-location " -name '*.pacmerge' 2>/dev/null")
|
|
"Command to search for temporarily merged files.")
|
|
|
|
(defvar pacfiles--ediff-conf '()
|
|
"Alist that stores ediff variables and its values.")
|
|
|
|
|
|
;;;###autoload
|
|
(defalias 'pacfiles 'pacfiles-start)
|
|
|
|
;;;###autoload
|
|
(defun pacfiles-start ()
|
|
"Find and manage pacman backup files in an Arch-based GNU/Linux system."
|
|
(interactive)
|
|
;; Save the current window configuration so that it can be restored when we are finished.
|
|
(pacfiles--push-window-conf)
|
|
;; Save ediff varaibles that we modify to later restore them to the uers's value.
|
|
(pacfiles--save-ediff-conf)
|
|
(let ((buffer (get-buffer-create pacfiles--files-buffer-name)))
|
|
(display-buffer buffer '(pacfiles--display-buffer-fullscreen))
|
|
(with-current-buffer buffer
|
|
(pacfiles-mode)
|
|
(pacfiles-revert-buffer t t))))
|
|
|
|
(defun pacfiles-quit ()
|
|
"Quit ‘pacfiles-mode’ and restore the previous window and ediff configuration."
|
|
(interactive)
|
|
(pacfiles--restore-ediff-conf)
|
|
;; Kill buffers we create which start with '*pacfiles:'
|
|
(kill-matching-buffers "^\\*pacfiles:.*" t t)
|
|
(pacfiles--pop-window-conf))
|
|
|
|
;; Main function that displays the contents of the PACFILES buffer.
|
|
(defun pacfiles-revert-buffer (&optional _ignore-auto noconfirm)
|
|
"Populate the ‘pacfiles-mode’ buffer with .pacnew and .pacsave files.
|
|
|
|
Ignore IGNORE-AUTO but take into account NOCONFIRM."
|
|
(interactive)
|
|
(with-current-buffer (get-buffer-create pacfiles--files-buffer-name)
|
|
(when (or noconfirm
|
|
(y-or-n-p (format "Reload list of backup pacman files? ")))
|
|
(run-hooks 'before-revert-hook)
|
|
;; The actual revert mechanism starts here
|
|
(let ((inhibit-read-only t)
|
|
(files (split-string (shell-command-to-string pacfiles-updates-search-command) "\n" t))
|
|
(merged-files (split-string (shell-command-to-string pacfiles--merge-search-command) "\n" t))
|
|
(pacnew-alist (list))
|
|
(pacsave-alist (list)))
|
|
(delete-region (point-min) (point-max))
|
|
(insert "* PACFILES MODE" "\n")
|
|
;; Split .pacnew and .pacsave files
|
|
(dolist (file files)
|
|
;; Associate each FILE in FILES with a file to hold the merge
|
|
(let ((merge-file (pacfiles--calculate-merge-file file pacfiles-merge-file-tmp-location)))
|
|
(cond
|
|
((string-match-p ".pacnew" file)
|
|
(push (cons file merge-file) pacnew-alist))
|
|
((string-match-p ".pacsave" file)
|
|
(push (cons file merge-file) pacsave-alist))
|
|
(t (user-error (format "Cannot process file %s" file))))))
|
|
;; --- Process .pacnew files ---
|
|
(insert "\n\n" "** PACNEW files" "\n")
|
|
(insert "\n" "*** pending" "\n")
|
|
;; Display the .pacnew files that need merging
|
|
(pacfiles--insert-pending-files pacnew-alist merged-files)
|
|
(insert "\n" "*** merged" "\n")
|
|
;; Display .pacnew files that have an associated merge file.
|
|
(pacfiles--insert-merged-files pacnew-alist merged-files)
|
|
;; --- Process .pacsave files ---
|
|
(insert "\n\n" "** PACSAVE files" "\n")
|
|
(insert "\n" "*** pending" "\n")
|
|
;; Display the .pacsave files that need merging
|
|
(pacfiles--insert-pending-files pacsave-alist merged-files)
|
|
(insert "\n" "*** merged" "\n")
|
|
(pacfiles--insert-merged-files pacsave-alist merged-files)
|
|
(insert "\n\n")
|
|
(pacfiles--insert-footer-buttons))))
|
|
(goto-char 0))
|
|
|
|
;;;###autoload
|
|
(defun pacfiles-revert-buffer-no-confirm ()
|
|
"Revert the pacfiles list buffer without asking for confirmation."
|
|
(interactive)
|
|
(pacfiles-revert-buffer t t))
|
|
|
|
(defun pacfiles--insert-pending-files (files-alist merged-files)
|
|
"Insert files in FILES-ALIST if their `cdr' is not in MERGED-FILES.
|
|
|
|
The FILE-TYPE specifies which type of update file we are processing."
|
|
;; Keep files in FILES-ALIST which don't have a cdr in MERGED-FILES.
|
|
(let ((pending-alist (cl-remove-if (lambda (i) (member (cdr i) merged-files)) files-alist)))
|
|
(if (null pending-alist)
|
|
(insert (propertize "--- no pending files ---\n" 'font-lock-face 'font-lock-comment-face))
|
|
(dolist (file-pair pending-alist)
|
|
(pacfiles--insert-merge-button file-pair)
|
|
(pacfiles--insert-diff-button (car file-pair))
|
|
(pacfiles--insert-delete-button file-pair)
|
|
(insert (car file-pair) " ")
|
|
(pacfiles--insert-days-old (car file-pair))
|
|
(insert "\n")))))
|
|
|
|
(defun pacfiles--insert-merged-files (files-alist merged-files)
|
|
"Insert files in FILES-ALIST that have an associated file in MERGED-FILES."
|
|
(let ((merged-alist (cl-remove-if-not (lambda (i) (member (cdr i) merged-files)) files-alist)))
|
|
(if (null merged-alist)
|
|
(insert (propertize "--- no merged files ---\n" 'font-lock-face 'font-lock-comment-face))
|
|
(dolist (file-pair merged-alist)
|
|
(pacfiles--insert-apply-button file-pair)
|
|
(pacfiles--insert-view-merge-button file-pair)
|
|
(pacfiles--insert-discard-button file-pair)
|
|
(insert (car file-pair) " ")
|
|
;; calculate how many days old is the merged file
|
|
(pacfiles--insert-days-created (cdr file-pair))
|
|
(insert "\n")))))
|
|
|
|
(defun pacfiles--insert-days-old (file)
|
|
"Insert how many days passed between FILE and FILE without its extension.
|
|
|
|
If REVERSE-ORDER is non-nil, calculate the time difference as
|
|
\(FILE without extension\) - FILE."
|
|
(let* ((base-file (file-name-sans-extension file))
|
|
(time-base-file
|
|
(time-to-seconds (file-attribute-modification-time (file-attributes base-file))))
|
|
(time-file
|
|
(time-to-seconds (file-attribute-modification-time (file-attributes file))))
|
|
(reverse-time (< time-file time-base-file)))
|
|
(when (file-exists-p base-file)
|
|
(insert
|
|
(propertize
|
|
(format "(%.1f %s)"
|
|
(time-to-number-of-days
|
|
(cond (reverse-time (time-subtract time-base-file time-file))
|
|
(t (time-subtract time-file time-base-file))))
|
|
(if reverse-time "day[s] old" "day[s] ahead"))
|
|
'font-lock-face 'font-lock-warning-face)))))
|
|
|
|
(defun pacfiles--insert-days-created (file)
|
|
"Insert the number of days since FILE was created."
|
|
(if (file-exists-p file)
|
|
(let ((time-file (file-attribute-modification-time (file-attributes file))))
|
|
(insert
|
|
(propertize
|
|
(format "(%.1f day[s] since created)" (time-to-number-of-days (time-since time-file)))
|
|
'font-lock-face 'font-lock-string-face)))
|
|
(error "File '%s' dosn't exist" file)))
|
|
|
|
(defun pacfiles--save-ediff-conf ()
|
|
"Save ediff variables we modify with the user's current values.
|
|
We restore the saved variables after ‘pacfiles-mode’ quits."
|
|
(require 'ediff)
|
|
(let ((vars-to-save
|
|
'(ediff-autostore-merges ediff-keep-variants ediff-window-setup-function
|
|
ediff-before-setup-hook ediff-quit-hook ediff-cleanup-hook ediff-quit-merge-hook
|
|
ediff-quit-hook ediff-split-window-function)))
|
|
(dolist (var vars-to-save)
|
|
(push (pacfiles--var-to-cons var) pacfiles--ediff-conf))))
|
|
|
|
(defun pacfiles--change-ediff-conf ()
|
|
"Change ediff's configuration variables to fit ‘pacfiles-mode’."
|
|
(setq ediff-autostore-merges nil
|
|
ediff-keep-variants t
|
|
ediff-window-setup-function #'ediff-setup-windows-plain
|
|
ediff-split-window-function #'split-window-horizontally)
|
|
(add-hook 'ediff-before-setup-hook #'pacfiles--push-window-conf)
|
|
(add-hook 'ediff-quit-hook #'pacfiles--pop-window-conf t)
|
|
(add-hook 'ediff-cleanup-hook #'pacfiles--clean-after-ediff)
|
|
(remove-hook 'ediff-quit-merge-hook #'ediff-maybe-save-and-delete-merge)
|
|
(add-hook 'ediff-quit-hook (lambda () (pacfiles-revert-buffer t t))))
|
|
|
|
(defun pacfiles--restore-ediff-conf ()
|
|
"Restore the ediff variables saved by `pacfiles--save-ediff-conf'."
|
|
(dolist (pair pacfiles--ediff-conf)
|
|
(pacfiles--cons-to-var pair))
|
|
(setq pacfiles--ediff-conf '()))
|
|
|
|
(defvar pacfiles-mode-map
|
|
(let ((map (make-sparse-keymap)))
|
|
(define-key map (kbd "q") #'pacfiles-quit)
|
|
(define-key map (kbd "g") #'pacfiles-revert-buffer-no-confirm)
|
|
(define-key map (kbd "r") #'pacfiles-revert-buffer-no-confirm)
|
|
(define-key map (kbd "TAB") #'outline-toggle-children)
|
|
(define-key map (kbd "C-c C-p") #'outline-previous-heading)
|
|
(define-key map (kbd "C-c C-n") #'outline-next-heading)
|
|
(define-key map (kbd "n") #'forward-button)
|
|
(define-key map (kbd "p") #'backward-button)
|
|
map)
|
|
"Keymap for ‘pacfiles-mode’.")
|
|
|
|
;; Tell emacs that, when creating new buffers, pacfiles-mode should not be used
|
|
;; ... as the major mode.
|
|
(put 'pacfiles-mode 'mode-class 'special)
|
|
|
|
;;;###autoload
|
|
(define-derived-mode pacfiles-mode outline-mode "pacfiles"
|
|
:syntax-table nil
|
|
:abbrev-table nil
|
|
"Major mode for managing .pacnew and .pacsave files."
|
|
;; If the buffer is not the one we create, do nothing and error out.
|
|
(unless (string= (buffer-name) pacfiles--files-buffer-name)
|
|
(user-error "Use the command `pacfiles' instead of `pacfiles-mode' to start pacfiles-mode"))
|
|
;; The buffer shall not be edited.
|
|
(read-only-mode)
|
|
;; No edits... no undo.
|
|
(buffer-disable-undo)
|
|
;; Disable showing parents locally by letting the mode think it's disabled.
|
|
(setq-local show-paren-mode nil)
|
|
(setq show-trailing-whitespace nil)
|
|
;; Disable lines numbers.
|
|
(when (bound-and-true-p global-linum-mode)
|
|
(linum-mode -1))
|
|
(when (and (fboundp 'nlinum-mode)
|
|
(bound-and-true-p global-nlinum-mode))
|
|
(nlinum-mode -1))
|
|
(when (and (fboundp 'display-line-numbers-mode)
|
|
(bound-and-true-p global-display-line-numbers-mode))
|
|
(display-line-numbers-mode -1))
|
|
;; Set the function used when reverting pacfile-mode buffers.
|
|
(setq-local revert-buffer-function #'pacfiles-revert-buffer)
|
|
;; configure ediff
|
|
(pacfiles--change-ediff-conf)
|
|
;; configure outline-mode
|
|
(setq-local outline-blank-line t))
|
|
|
|
|
|
(provide 'pacfiles-mode)
|
|
;;; pacfiles-mode.el ends here
|