A major mode for password-store
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.

299 lines
9.3 KiB

7 years ago
7 years ago
7 years ago
  1. ;;; pass.el --- Major mode for password-store.el -*- lexical-binding: t; -*-
  2. ;; Copyright (C) 2015 Nicolas Petton & Damien Cassou
  3. ;; Author: Nicolas Petton <petton.nicolas@gmail.com>
  4. ;; Damien Cassou <damien@cassou.me>
  5. ;; Version: 0.1
  6. ;; GIT: https://github.com/NicolasPetton/pass
  7. ;; Package-Requires: ((emacs "24") (password-store "0.1") (f "0.17"))
  8. ;; Created: 09 Jun 2015
  9. ;; Keywords: password-store, password, keychain
  10. ;; This program is free software; you can redistribute it and/or modify
  11. ;; it under the terms of the GNU General Public License as published by
  12. ;; the Free Software Foundation, either version 3 of the License, or
  13. ;; (at your option) any later version.
  14. ;; This program is distributed in the hope that it will be useful,
  15. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. ;; GNU General Public License for more details.
  18. ;; You should have received a copy of the GNU General Public License
  19. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. ;;; Commentary:
  21. ;; Major mode for password-store.el
  22. ;;; Code:
  23. (require 'password-store)
  24. (require 'f)
  25. (defgroup pass '()
  26. "Major mode for password-store."
  27. :group 'password-store)
  28. (defvar pass-buffer-name "*Password-Store*"
  29. "Name of the pass buffer.")
  30. (defvar pass-mode-hook nil
  31. "Mode hook for `pass-mode'.")
  32. (defvar pass-mode-map
  33. (let ((map (make-sparse-keymap)))
  34. (define-key map (kbd "n") #'pass-next-entry)
  35. (define-key map (kbd "p") #'pass-prev-entry)
  36. (define-key map (kbd "M-n") #'pass-next-directory)
  37. (define-key map (kbd "M-p") #'pass-prev-directory)
  38. (define-key map (kbd "k") #'pass-kill)
  39. (define-key map (kbd "s") #'isearch-forward)
  40. (define-key map (kbd "?") #'describe-mode)
  41. (define-key map (kbd "g") #'pass-update-buffer)
  42. (define-key map (kbd "i") #'pass-insert)
  43. (define-key map (kbd "w") #'pass-copy)
  44. (define-key map (kbd "v") #'pass-view)
  45. (define-key map (kbd "r") #'pass-rename)
  46. (define-key map (kbd "RET") #'pass-view)
  47. (define-key map (kbd "q") #'pass-quit)
  48. map)
  49. "Keymap for `pass-mode'.")
  50. (defface pass-mode-header-face '((t . (:inherit font-lock-keyword-face)))
  51. "Face for displaying the header of the pass buffer."
  52. :group 'pass)
  53. (defface pass-mode-entry-face '((t . ()))
  54. "Face for displaying pass entry names."
  55. :group 'pass)
  56. (defface pass-mode-directory-face '((t . (:inherit
  57. font-lock-function-name-face
  58. :weight
  59. bold)))
  60. "Face for displaying password-store directory names."
  61. :group 'pass)
  62. (defun pass-mode ()
  63. "Major mode for editing password-stores.
  64. \\{pass-mode-map}"
  65. (kill-all-local-variables)
  66. (setq major-mode 'pass-mode
  67. mode-name 'Password-Store)
  68. (read-only-mode)
  69. (use-local-map pass-mode-map)
  70. (run-hooks 'pass-mode-hook))
  71. (defun pass-setup-buffer ()
  72. "Setup the password-store buffer."
  73. (pass-mode)
  74. (pass-update-buffer))
  75. ;;;###autoload
  76. (defun pass ()
  77. "Open the password-store buffer."
  78. (interactive)
  79. (if (get-buffer pass-buffer-name)
  80. (switch-to-buffer pass-buffer-name)
  81. (let ((buf (get-buffer-create pass-buffer-name)))
  82. (pop-to-buffer buf)
  83. (pass-setup-buffer))))
  84. (defun pass-quit ()
  85. "Kill the buffer quitting the window."
  86. (interactive)
  87. (quit-window t))
  88. (defun pass-next-entry ()
  89. "Move point to the next entry found."
  90. (interactive)
  91. (pass--goto-next #'pass-entry-at-point))
  92. (defun pass-prev-entry ()
  93. "Move point to the previous entry."
  94. (interactive)
  95. (pass--goto-prev #'pass-entry-at-point))
  96. (defun pass-next-directory ()
  97. "Move point to the next directory found."
  98. (interactive)
  99. (pass--goto-next #'pass-directory-at-point))
  100. (defun pass-prev-directory ()
  101. "Move point to the previous directory."
  102. (interactive)
  103. (pass--goto-prev #'pass-directory-at-point))
  104. (defmacro pass--with-closest-entry (varname &rest body)
  105. "Bound VARNAME to the closest entry before point and evaluate BODY."
  106. (declare (indent 1) (debug t))
  107. `(let ((,varname (pass-closest-entry)))
  108. (if ,varname
  109. ,@body
  110. (message "No entry at point"))))
  111. (defun pass-rename (new-name)
  112. "Rename the entry at point to NEW-NAME."
  113. (interactive (list (read-string "Rename entry to: " (pass-closest-entry))))
  114. (pass--with-closest-entry entry
  115. (password-store-rename entry new-name)
  116. (pass-update-buffer)))
  117. (defun pass-kill ()
  118. "Remove the entry at point."
  119. (interactive)
  120. (pass--with-closest-entry entry
  121. (when (yes-or-no-p (format "Do you want remove the entry %s? " entry))
  122. (password-store-remove entry)
  123. (pass-update-buffer))))
  124. (defun pass-update-buffer ()
  125. "Update the current buffer contents."
  126. (interactive)
  127. (pass--save-point
  128. (pass--with-writable-buffer
  129. (delete-region (point-min) (point-max))
  130. (pass-display-data))))
  131. (defun pass-insert (&optional arg)
  132. "Insert an entry to the password-store.
  133. When called with a prefix argument ARG, use a generated password
  134. instead of reading the password from user input."
  135. (interactive "P")
  136. (if arg
  137. (call-interactively #'password-store-generate)
  138. (call-interactively #'password-store-insert))
  139. (pass-update-buffer))
  140. (defun pass-view ()
  141. "Visit the entry at point."
  142. (interactive)
  143. (pass--with-closest-entry entry
  144. (password-store-edit entry)))
  145. (defun pass-copy ()
  146. "Visit the entry at point."
  147. (interactive)
  148. (pass--with-closest-entry entry
  149. (password-store-copy entry)))
  150. (defun pass-display-data ()
  151. "Display the password-store data into the current buffer."
  152. (let ((items (pass--tree)))
  153. (pass-display-header)
  154. (pass-display-item items)))
  155. (defun pass-display-header ()
  156. "Display the header in to the current buffer."
  157. (insert "Password-store directory:")
  158. (put-text-property (point-at-bol) (point) 'face 'pass-mode-header-face)
  159. (newline)
  160. (newline))
  161. (defun pass-display-item (item &optional indent-level)
  162. "Display the directory or entry ITEM into the current buffer.
  163. If INDENT-LEVEL is specified, add enough spaces before displaying
  164. ITEM."
  165. (unless indent-level (setq indent-level 0))
  166. (let ((directory (listp item)))
  167. (pass-display-item-prefix indent-level)
  168. (if directory
  169. (pass-display-directory item indent-level)
  170. (pass-display-entry item))))
  171. (defun pass-display-entry (entry)
  172. "Display the password-store entry ENTRY into the current buffer."
  173. (let ((entry-name (f-filename entry)))
  174. (insert entry-name)
  175. (add-text-properties (point-at-bol) (point)
  176. `(face pass-mode-entry-face pass-entry ,entry))
  177. (newline)))
  178. (defun pass-display-directory (directory indent-level)
  179. "Display the directory DIRECTORY into the current buffer.
  180. DIRECTORY is a list, its CAR being the name of the directory and its CDR
  181. the entries of the directory. Add enough spaces so that each entry is
  182. indented according to INDENT-LEVEL."
  183. (let ((name (car directory))
  184. (items (cdr directory)))
  185. (when (not (string= name ".git"))
  186. (insert name)
  187. (add-text-properties (point-at-bol) (point)
  188. `(face pass-mode-directory-face pass-directory ,name))
  189. (newline)
  190. (dolist (item items)
  191. (pass-display-item item (1+ indent-level))))))
  192. (defun pass-display-item-prefix (indent-level)
  193. "Display some indenting text according to INDENT-LEVEL."
  194. (dotimes (_ (max 0 (* (1- indent-level) 4)))
  195. (insert " "))
  196. (unless (zerop indent-level)
  197. (insert "├── ")))
  198. (defun pass-entry-at-point ()
  199. "Return the `pass-entry' property at point."
  200. (get-text-property (point) 'pass-entry))
  201. (defun pass-directory-at-point ()
  202. "Return the `pass-directory' property at point."
  203. (get-text-property (point) 'pass-directory))
  204. (defun pass-closest-entry ()
  205. "Return the closest entry in the current buffer, looking backward."
  206. (save-excursion
  207. (unless (bobp)
  208. (or (pass-entry-at-point)
  209. (progn
  210. (forward-line -1)
  211. (pass-closest-entry))))))
  212. (defun pass--goto-next (pred)
  213. "Move point to the next match of PRED."
  214. (forward-line)
  215. (while (not (or (eobp) (funcall pred)))
  216. (forward-line)))
  217. (defun pass--goto-prev (pred)
  218. "Move point to the previous match of PRED."
  219. (forward-line -1)
  220. (while (not (or (bobp) (funcall pred)))
  221. (forward-line -1)))
  222. (defmacro pass--with-writable-buffer (&rest body)
  223. "Evaluate BODY with the current buffer not in `read-only-mode'."
  224. (declare (indent 0) (debug t))
  225. (let ((read-only (make-symbol "ro")))
  226. `(let ((,read-only buffer-read-only))
  227. (read-only-mode -1)
  228. ,@body
  229. (when ,read-only
  230. (read-only-mode 1)))))
  231. (defmacro pass--save-point (&rest body)
  232. "Evaluate BODY and restore the point.
  233. Similar to `save-excursion' but only restore the point."
  234. (declare (indent 0) (debug t))
  235. (let ((point (make-symbol "point")))
  236. `(let ((,point (point)))
  237. ,@body
  238. (goto-char (min ,point (point-max))))))
  239. (defun pass--tree (&optional subdir)
  240. "Return a tree of all entries in SUBDIR.
  241. If SUBDIR is nil, return the entries of `(password-store-dir)'."
  242. (unless subdir (setq subdir ""))
  243. (let ((path (f-join (password-store-dir) subdir)))
  244. (delq nil
  245. (if (f-directory? path)
  246. (cons (f-filename path)
  247. (mapcar 'pass--tree
  248. (f-entries path)))
  249. (when (equal (f-ext path) "gpg")
  250. (password-store--file-to-entry path))))))
  251. (provide 'pass)
  252. ;;; pass.el ends here