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.

365 lines
12KB

  1. ;;; pass.el --- Major mode for password-store.el -*- lexical-binding: t; -*-
  2. ;; Copyright (C) 2015-2016 Nicolas Petton & Damien Cassou
  3. ;; Author: Nicolas Petton <petton.nicolas@gmail.com>
  4. ;; Damien Cassou <damien@cassou.me>
  5. ;; Version: 1.6
  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 'imenu)
  25. (require 'f)
  26. (defgroup pass '()
  27. "Major mode for password-store."
  28. :group 'password-store)
  29. (defvar pass-buffer-name "*Password-Store*"
  30. "Name of the pass buffer.")
  31. (defvar pass-mode-map
  32. (let ((map (make-sparse-keymap)))
  33. (define-key map (kbd "n") #'pass-next-entry)
  34. (define-key map (kbd "p") #'pass-prev-entry)
  35. (define-key map (kbd "M-n") #'pass-next-directory)
  36. (define-key map (kbd "M-p") #'pass-prev-directory)
  37. (define-key map (kbd "k") #'pass-kill)
  38. (define-key map (kbd "s") #'isearch-forward)
  39. (define-key map (kbd "?") #'describe-mode)
  40. (define-key map (kbd "g") #'pass-update-buffer)
  41. (define-key map (kbd "i") #'pass-insert)
  42. (define-key map (kbd "I") #'pass-insert-generated)
  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. (define-derived-mode pass-mode nil "Password-Store"
  63. "Major mode for editing password-stores.
  64. \\{pass-mode-map}"
  65. (setq default-directory (password-store-dir))
  66. (setq-local imenu-generic-expression '((nil "├──\\ \\(.*\\)" 1)))
  67. (read-only-mode))
  68. (defun pass-setup-buffer ()
  69. "Setup the password-store buffer."
  70. (pass-mode)
  71. (pass-update-buffer))
  72. ;;;###autoload
  73. (defun pass ()
  74. "Open the password-store buffer."
  75. (interactive)
  76. (if (get-buffer pass-buffer-name)
  77. (progn
  78. (switch-to-buffer pass-buffer-name)
  79. (pass-update-buffer))
  80. (let ((buf (get-buffer-create pass-buffer-name)))
  81. (pop-to-buffer buf)
  82. (pass-setup-buffer))))
  83. (defmacro pass--with-writable-buffer (&rest body)
  84. "Evaluate BODY as if the current buffer was not in `read-only-mode'."
  85. (declare (indent 0) (debug t))
  86. `(let ((inhibit-read-only t))
  87. ,@body))
  88. (defmacro pass--save-point (&rest body)
  89. "Evaluate BODY and restore the point.
  90. Similar to `save-excursion' but only restore the point."
  91. (declare (indent 0) (debug t))
  92. (let ((point (make-symbol "point")))
  93. `(let ((,point (point)))
  94. ,@body
  95. (goto-char (min ,point (point-max))))))
  96. (defun pass-quit ()
  97. "Kill the buffer quitting the window."
  98. (interactive)
  99. (when (y-or-n-p "Kill all pass entry buffers? ")
  100. (dolist (buf (buffer-list))
  101. (with-current-buffer buf
  102. (when (eq major-mode 'pass-view-mode)
  103. (kill-buffer buf)))))
  104. (quit-window t))
  105. (defun pass-next-entry ()
  106. "Move point to the next entry found."
  107. (interactive)
  108. (pass--goto-next #'pass-entry-at-point))
  109. (defun pass-prev-entry ()
  110. "Move point to the previous entry."
  111. (interactive)
  112. (pass--goto-prev #'pass-entry-at-point))
  113. (defun pass-next-directory ()
  114. "Move point to the next directory found."
  115. (interactive)
  116. (pass--goto-next #'pass-directory-at-point))
  117. (defun pass-prev-directory ()
  118. "Move point to the previous directory."
  119. (interactive)
  120. (pass--goto-prev #'pass-directory-at-point))
  121. (defmacro pass--with-closest-entry (varname &rest body)
  122. "Bound VARNAME to the closest entry before point and evaluate BODY."
  123. (declare (indent 1) (debug t))
  124. `(let ((,varname (pass-closest-entry)))
  125. (if ,varname
  126. (progn ,@body)
  127. (message "No entry at point"))))
  128. (defun pass-rename (new-name)
  129. "Rename the entry at point to NEW-NAME."
  130. (interactive (list (read-string "Rename entry to: " (pass-closest-entry))))
  131. (pass--with-closest-entry entry
  132. (password-store-rename entry new-name)
  133. (pass-update-buffer)))
  134. (defun pass-kill ()
  135. "Remove the entry at point."
  136. (interactive)
  137. (pass--with-closest-entry entry
  138. (when (yes-or-no-p (format "Do you want remove the entry %s? " entry))
  139. (password-store-remove entry)
  140. (pass-update-buffer))))
  141. (defun pass-update-buffer ()
  142. "Update the current buffer contents."
  143. (interactive)
  144. (pass--save-point
  145. (pass--with-writable-buffer
  146. (delete-region (point-min) (point-max))
  147. (pass-display-data))))
  148. (defun pass-insert ()
  149. "Insert an entry to the password-store.
  150. The password is read from user input."
  151. (interactive)
  152. (call-interactively #'password-store-insert)
  153. (pass-update-buffer))
  154. (defun pass-insert-generated ()
  155. "Insert an entry to the password-store.
  156. Use a generated password instead of reading the password from
  157. user input."
  158. (interactive)
  159. (call-interactively #'password-store-generate)
  160. (pass-update-buffer))
  161. (defun pass-view ()
  162. "Visit the entry at point."
  163. (interactive)
  164. (pass--with-closest-entry entry
  165. (password-store-edit entry)))
  166. (defun pass-copy ()
  167. "Add the entry at point to kill ring."
  168. (interactive)
  169. (pass--with-closest-entry entry
  170. (password-store-copy entry)))
  171. (defun pass-display-data ()
  172. "Display the password-store data into the current buffer."
  173. (let ((items (pass--tree)))
  174. (pass-display-header)
  175. (pass-display-item items)))
  176. (defun pass-display-header ()
  177. "Display the header in to the current buffer."
  178. (insert "Password-store directory:")
  179. (put-text-property (point-at-bol) (point) 'face 'pass-mode-header-face)
  180. (newline)
  181. (newline))
  182. (defun pass-display-item (item &optional indent-level)
  183. "Display the directory or entry ITEM into the current buffer.
  184. If INDENT-LEVEL is specified, add enough spaces before displaying
  185. ITEM."
  186. (unless indent-level (setq indent-level 0))
  187. (let ((directory (listp item)))
  188. (pass-display-item-prefix indent-level)
  189. (if directory
  190. (pass-display-directory item indent-level)
  191. (pass-display-entry item))))
  192. (defun pass-display-entry (entry)
  193. "Display the password-store entry ENTRY into the current buffer."
  194. (let ((entry-name (f-filename entry)))
  195. (insert entry-name)
  196. (add-text-properties (point-at-bol) (point)
  197. `(face pass-mode-entry-face pass-entry ,entry))
  198. (newline)))
  199. (defun pass-display-directory (directory indent-level)
  200. "Display the directory DIRECTORY into the current buffer.
  201. DIRECTORY is a list, its CAR being the name of the directory and its CDR
  202. the entries of the directory. Add enough spaces so that each entry is
  203. indented according to INDENT-LEVEL."
  204. (let ((name (car directory))
  205. (items (cdr directory)))
  206. (when (not (string= name ".git"))
  207. (insert name)
  208. (add-text-properties (point-at-bol) (point)
  209. `(face pass-mode-directory-face pass-directory ,name))
  210. (newline)
  211. (dolist (item items)
  212. (pass-display-item item (1+ indent-level))))))
  213. (defun pass-display-item-prefix (indent-level)
  214. "Display some indenting text according to INDENT-LEVEL."
  215. (dotimes (_ (max 0 (* (1- indent-level) 4)))
  216. (insert " "))
  217. (unless (zerop indent-level)
  218. (insert "├── ")))
  219. (defun pass-entry-at-point ()
  220. "Return the `pass-entry' property at point."
  221. (get-text-property (point) 'pass-entry))
  222. (defun pass-directory-at-point ()
  223. "Return the `pass-directory' property at point."
  224. (get-text-property (point) 'pass-directory))
  225. (defun pass-closest-entry ()
  226. "Return the closest entry in the current buffer, looking backward."
  227. (save-excursion
  228. (unless (bobp)
  229. (or (pass-entry-at-point)
  230. (progn
  231. (forward-line -1)
  232. (pass-closest-entry))))))
  233. (defun pass--goto-next (pred)
  234. "Move point to the next match of PRED."
  235. (forward-line)
  236. (while (not (or (eobp) (funcall pred)))
  237. (forward-line)))
  238. (defun pass--goto-prev (pred)
  239. "Move point to the previous match of PRED."
  240. (forward-line -1)
  241. (while (not (or (bobp) (funcall pred)))
  242. (forward-line -1)))
  243. (defun pass--tree (&optional subdir)
  244. "Return a tree of all entries in SUBDIR.
  245. If SUBDIR is nil, return the entries of `(password-store-dir)'."
  246. (unless subdir (setq subdir ""))
  247. (let ((path (f-join (password-store-dir) subdir)))
  248. (delq nil
  249. (if (f-directory? path)
  250. (cons (f-filename path)
  251. (mapcar 'pass--tree
  252. (f-entries path)))
  253. (when (equal (f-ext path) "gpg")
  254. (password-store--file-to-entry path))))))
  255. ;;; major mode for viewing entries
  256. (defvar pass-view-mask "·············"
  257. "Mask used to hide passwords.")
  258. (defvar pass-view-mode-map
  259. (let ((map (make-sparse-keymap)))
  260. (define-key map (kbd "C-c C-c") #'pass-view-toggle-password)
  261. (define-key map (kbd "C-c C-w") #'pass-view-copy-password)
  262. map))
  263. (defun pass-view-toggle-password ()
  264. "Enable or disable password hiding."
  265. (interactive)
  266. (save-excursion
  267. (goto-char (point-min))
  268. (let ((buf-modified (buffer-modified-p)))
  269. (if (string= (get-text-property (point) 'display)
  270. pass-view-mask)
  271. (pass-view-unmask-password)
  272. (pass-view-mask-password))
  273. (set-buffer-modified-p buf-modified))))
  274. (defun pass-view-copy-password ()
  275. "Copy the password of the entry in the current buffer."
  276. (interactive)
  277. (save-excursion
  278. (goto-char (point-min))
  279. (copy-region-as-kill (point) (line-end-position))))
  280. (defun pass-view-mask-password ()
  281. "Mask the password of the current buffer."
  282. (let ((inhibit-read-only t))
  283. (save-excursion
  284. (goto-char (point-min))
  285. (set-text-properties (point-min) (line-end-position)
  286. `(display ,pass-view-mask)))))
  287. (defun pass-view-unmask-password ()
  288. "Show the password in the current buffer."
  289. (save-excursion
  290. (goto-char (point-min))
  291. (remove-text-properties (point-min) (line-end-position)
  292. '(display nil))))
  293. (defvar pass-view-font-lock-keywords '("^[^:\n]+:" . 'font-lock-keyword-face)
  294. "Font lock keywords for pass-view-mode.")
  295. (define-derived-mode pass-view-mode nil "Pass-View"
  296. "Major mode for viewing password-store entries.
  297. \\{pass-view-mode-map}"
  298. (pass-view-toggle-password)
  299. (setq-local font-lock-defaults '(pass-view-font-lock-keywords))
  300. (font-lock-mode 1)
  301. (message
  302. (substitute-command-keys
  303. "Press <\\[pass-view-toggle-password]> to display & edit the password")))
  304. (add-to-list 'auto-mode-alist '("\\.password-store/" . pass-view-mode))
  305. (provide 'pass)
  306. ;;; pass.el ends here