;;; id-manager.el --- id-password management ;; Copyright (C) 2009, 2010, 2011, 2013 SAKURAI Masashi ;; Time-stamp: <2015-06-06 12:38:31 sakurai> ;; Author: SAKURAI Masashi ;; Keywords: password, convenience ;; Package-Version: 20160425.216 ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; ID-password management utility. ;; This utility manages ID-password list and generates passwords. ;; The ID-password DB is saved in the tab-separated file. The default ;; file name of the DB `idm-database-file' is "~/.idm-db.gpg". ;; The file format is following: ;; (name)^t(ID)^t(password)^t(Update date "YYYY/MM/DD")[^t(memo)] ;; . One can prepare an initial data or modify the data by hand or ;; the Excel. ;; ;; Implicitly, this elisp program expects that the DB file is ;; encrypted by the some GPG encryption elisp, such as EasyPG or ;; alpaca. ;; ;; Excuting the command `idm-open-list-command', you can open the ;; ID-password list buffer. Check the function `describe-bindings'. ;;; Installation: ;; To use this program, locate this file to load-path directory, ;; and add the following code to your .emacs. ;; ------------------------------ ;; (require 'id-manager) ;; ------------------------------ ;; If you have helm.el, bind `id-manager' to key, ;; like (global-set-key (kbd "M-7") 'id-manager). ;;; Setting example: ;; For EasyPG users: ;; ;; (autoload 'id-manager "id-manager" nil t) ;; (global-set-key (kbd "M-7") 'id-manager) ; helm UI ;; (setq epa-file-cache-passphrase-for-symmetric-encryption t) ; saving password ;; (setenv "GPG_AGENT_INFO" nil) ; non-GUI password dialog. ;; For alpaca users: ;; ;; (autoload 'id-manager "id-manager" nil t) ;; (global-set-key (kbd "M-7") 'id-manager) ; helm UI ;; (setq idm-db-buffer-save-function ; adjustment for alpaca.el ;; (lambda (file) ;; (set-visited-file-name file) ;; (alpaca-save-buffer)) ;; idm-db-buffer-password-var ; if you are using `alpaca-cache-passphrase'. ;; 'alpaca-passphrase) ;;; Current implementation: ;; This program generates passwords by using external command: ;; `idm-gen-password-cmd'. If you have some better idea, please let me ;; know. ;; ;; I think that this program makes lazy password management more ;; securely. But I'm not sure that this program is secure enough. ;; I'd like many people to check and advice me. ;;; Code: (eval-when-compile (require 'cl)) (require 'widget) (eval-when-compile (require 'wid-edit)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Setting (defvar idm-database-file "~/.idm-db.gpg" "Encripted id-password database file. The file name may end with '.gpg' for encryption by the GnuPG.") (defvar idm-gen-password-cmd "head -c 10 < /dev/random | uuencode -m - | tail -n 2 |head -n 1 | head -c10" "[String] Password generation command. If a function symbol or lambda whose receive no parameter is set to this variable, id-manager calls the function to generate the password.") ;; "openssl rand 32 | uuencode -m - | tail -n 2 |head -n 1 | head -c10" ;; ...any other password generation ? (defvar idm-copy-action (lambda (text) (x-select-text text)) "Action for copying a password text into clipboard.") (defvar idm-db-buffer-load-function 'find-file-noselect "File loading function. This function has one argument FILENAME and returns a buffer, like `find-file-noselect'. Some decryption should work at this function.") (defvar idm-db-buffer-save-function 'write-file "File saving function. This function has one arguments FILENAME, like `write-file'. Some encryption should work at this function.") (defvar idm-db-buffer-password-var nil "Password variable. See the text of settings for alpaca.el. ") (defvar idm-clipboard-expire-time-sec 5 "Expire time for the clipboard content.") (defvar idm-clipboard-expire-timer nil "The timer object that will expire the clipboard content.") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Macros (defmacro idm--aif (test-form then-form &rest else-forms) `(let ((it ,test-form)) (if it ,then-form ,@else-forms))) (put 'idm--aif 'lisp-indent-function 2) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Management API (defun idm-gen-password () "Generate a password." (cond ((functionp idm-gen-password-cmd) (funcall idm-gen-password-cmd)) ((stringp idm-gen-password-cmd) (let ((buf (get-buffer-create " *idm-work*")) ret) (call-process-shell-command idm-gen-password-cmd nil buf nil) (with-current-buffer buf (setq ret (buffer-string))) (kill-buffer buf) ret)) (t (error "idm-gen-password-cmd is set to wrong value. [%S]" idm-gen-password-cmd)))) ;; record struct (defstruct (idm-record (:constructor make-idm-record-bylist (name account-id password update-time &optional memo))) name account-id password update-time memo) (defun idm-load-db () "Load the DB file `idm-database-file' and make a DB object." (let* ((coding-system-for-read 'utf-8) (tmpbuf (funcall idm-db-buffer-load-function (expand-file-name idm-database-file))) db-object) (unwind-protect (let ((db (idm--make-db tmpbuf))) (when idm-db-buffer-password-var (with-current-buffer tmpbuf (funcall db 'file-password (symbol-value idm-db-buffer-password-var)))) db) (kill-buffer tmpbuf)))) (defun idm--save-db (records file-vars &optional password) "Save RECORDS into the DB file `idm-database-file'. This function is called by a DB object." (let ((coding-system-for-write 'utf-8) (tmpbuf (get-buffer-create " *idm-tmp*"))) (with-current-buffer tmpbuf (erase-buffer) (goto-char (point-min)) (loop for (sym . v) in file-vars do (set (make-local-variable sym) v)) (insert (format ";; -*- %s -*-" (loop for (n . v) in file-vars concat (format "%s: %S; " n v))) "\n") (dolist (i records) (insert (concat (idm-record-name i) "\t" (idm-record-account-id i) "\t" (idm-record-password i) "\t" (idm-record-update-time i) (idm--aif (idm-record-memo i) (concat "\t" it)) "\n"))) (when password (set idm-db-buffer-password-var password)) (funcall idm-db-buffer-save-function idm-database-file) (kill-buffer tmpbuf)))) (defun idm--make-db (tmpbuf) "Build a database management object from the given buffer text. The object is a dispatch function. One can access the methods `funcall' with the method name symbol and some method arguments." (lexical-let (records (db-modified nil) file-vars ; file variables file-password) ; password for alpaca (setq file-vars (buffer-local-value 'file-local-variables-alist tmpbuf)) (idm--each-line tmpbuf (lambda (line) (cond ((string-match "^;; " line) ; file variables ) ; ignore (t ; entry lines (let ((cols (split-string line "\t"))) (if (or (= 4 (length cols)) (= 5 (length cols))) (push (apply 'make-idm-record-bylist cols) records))))))) (lambda (method &rest args) (cond ((eq method 'get) ; get record object by name (lexical-let ((name (car args)) ret) (mapc (lambda (i) (if (equal name (idm-record-name i)) (setq ret i))) records) ret)) ((eq method 'get-all-records) records) ; get-all-records ((eq method 'add-record) ; add-record (progn (lexical-let* ((record (car args)) (name (idm-record-name record))) (setf records (loop for i in records unless (equal (idm-record-name i) name) collect i)) (push record records) (setq db-modified t)))) ((eq method 'delete-record-by-name) ; delete-record-by-name (lexical-let ((name (car args))) (setf records (loop for i in records unless (equal (idm-record-name i) name) collect i)) (setq db-modified t))) ((eq method 'set-modified) ; set-modified (setq db-modified t)) ((eq method 'save) ; save (when db-modified (idm--save-db records file-vars file-password) (setq db-modified nil))) ((eq method 'file-password) ; file-password (setq file-password (car args)) nil) (t (error "Unknown method [%s]" method)))))) (defun idm--each-line (buf task) "Execute the function TASK with each lines in the buffer `buf'. This function is called by `idm--make-db'." (with-current-buffer buf (goto-char (point-min)) (unless (eobp) (while (let ((line (buffer-substring-no-properties (line-beginning-position) (line-end-position)))) (funcall task line) (forward-line 1) (not (eobp))))))) (defun idm--strtime (time) "Translate emacs time to formatted string." (format-time-string "%Y/%m/%d" time)) (defun idm--parsetime (str) "Translate formatted string to emacs time." (when (string-match "\\([0-9]+\\)\\/\\([0-9]+\\)\\/\\([0-9]+\\)" str) (apply 'encode-time (let (ret) (dotimes (i 6) (push (string-to-number (match-string (+ i 1) str)) ret)) ret)))) (defun idm--message (&rest args) "Show private text in the echo area without message buffer recording." (let (message-log-max) (apply 'message args))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; GUI (defvar idm-show-password nil "Display passwords switch. If this variable is non-nil, some functions show the password as plain text.") (defun idm-toggle-show-password () "Toggle the switch for display passwords. This function does not update views." (interactive) (setq idm-show-password (not idm-show-password))) (defun idm-add-record-dialog (db on-ok-func) "Make an account record interactively and register it with DB." (lexical-let ((db db) (on-ok-func on-ok-func)) (idm-edit-record-dialog (make-idm-record) (lambda (r) (cond ((funcall db 'get (idm-record-name r)) (idm-edit-record-dialog r on-ok-func nil "Record [%s] exists!")) (t (funcall on-ok-func r))))))) (defun idm-edit-record-dialog (record on-ok-func &optional password-show error-msg) "Pop up the edit buffer for the given record. If the user pushes the `ok' button, the function `idm-edit-record-dialog-commit' is called." (let ((before-win-num (length (window-list))) (main-buf (current-buffer)) (edit-buf (idm--edit-record-dialog-buffer record on-ok-func password-show error-msg))) (with-current-buffer edit-buf (set (make-local-variable 'idm-before-win-num) before-win-num) (set (make-local-variable 'idm-main-buf) main-buf)) (pop-to-buffer edit-buf))) (defun idm--edit-record-dialog-buffer (record on-ok-func &optional password-show error-msg) "Return the editing buffer for the given record." (let ((buf (get-buffer-create "*idm-record-dialog*"))) (with-current-buffer buf (let ((inhibit-read-only t)) (erase-buffer)) (kill-all-local-variables) (remove-overlays) (widget-insert (format "Record: %s\n\n" (idm--aif (idm-record-name record) it "(new record)"))) (when error-msg (widget-insert (let ((text (substring-no-properties error-msg))) (put-text-property 0 (length text) 'face 'font-lock-warning-face text) text)) (widget-insert "\n\n")) (lexical-let ((record record) (on-ok-func on-ok-func) (error-msg error-msg) fname fid fpassword fmemo cbshow bgenerate fields) ;; create dialog fields (setq fname (widget-create 'editable-field :size 20 :format " Account Name: %v \n" :value (or (idm-record-name record) "")) fid (widget-create 'editable-field :size 20 :format " Account ID : %v \n" :value (or (idm-record-account-id record) "")) fpassword (widget-create 'editable-field :size 20 :format " Password: %v \n" :secret (and (not password-show) ?*) :value (or (idm-record-password record) ""))) (widget-insert " (show password ") (setq cbshow (widget-create 'checkbox :value password-show)) (widget-insert " ) ") (setq bgenerate (widget-create 'push-button "Generate")) (widget-insert "\n") (setq fmemo (widget-create 'editable-field :size 20 :format " Memo : %v \n" :value (or (idm-record-memo record) ""))) (setq fields (list 'name fname 'id fid 'password fpassword 'memo fmemo 'password-show cbshow)) ;; OK / Cancel (widget-insert "\n") (widget-create 'push-button :notify (lambda (&rest ignore) (idm-edit-record-dialog-commit record fields on-ok-func)) "Ok") (widget-insert " ") (widget-create 'push-button :notify (lambda (&rest ignore) (idm-edit-record-kill-buffer)) "Cancel") (widget-insert "\n") ;; add event actions (widget-put cbshow :notify (lambda (&rest ignore) (let ((current-record (make-idm-record :name (widget-value fname) :account-id (widget-value fid) :password (widget-value fpassword) :memo (widget-value fmemo))) (password-show (widget-value cbshow))) (message "CLICK : %s" password-show) (idm-edit-record-kill-buffer) (idm-edit-record-dialog current-record on-ok-func password-show error-msg) (widget-forward 3)))) (widget-put bgenerate :notify (lambda (&rest ignore) (widget-value-set fpassword (idm-gen-password)) (widget-setup))) ;; setup widgets (use-local-map widget-keymap) (widget-setup) (goto-char (point-min)) (widget-forward 1))) buf)) (defun idm-edit-record-dialog-commit (record fields on-ok-func) "edit-record-dialog-commit" (let ((name-value (widget-value (plist-get fields 'name)))) (cond ((or (null name-value) (string-equal "" name-value)) (idm-edit-record-kill-buffer) (idm-edit-record-dialog record on-ok-func (widget-value (plist-get fields 'password-show)) "Should not be empty!")) (t (setf (idm-record-name record) name-value (idm-record-account-id record) (widget-value (plist-get fields 'id)) (idm-record-password record) (widget-value (plist-get fields 'password)) (idm-record-memo record) (widget-value (plist-get fields 'memo)) (idm-record-update-time record) (idm--strtime (current-time))) (idm-edit-record-kill-buffer) (funcall on-ok-func record))))) (defun idm-edit-record-kill-buffer () "edit-record-kill-buffer" (interactive) (let ((cbuf (current-buffer)) (win-num (length (window-list))) (next-win (get-buffer-window idm-main-buf))) (when (and (not (one-window-p)) (> win-num idm-before-win-num)) (delete-window)) (kill-buffer cbuf) (when next-win (select-window next-win)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; id-password list buffer (defun idm-open-list (db) "Open id-password list buffer." (lexical-let ((buf (get-buffer-create "ID-Password List")) (db db)) (with-current-buffer buf (idm--layout-list db) (idm--set-list-mode db) ) (set-buffer buf))) (defun idm--put-text-property (text attr val) "Put a text property on the whole text." (put-text-property 0 (length text) attr val text) text) (defun idm--put-record-id (text id) "Put the record id with the text property `idm-record-id'." (idm--put-text-property text 'idm-record-id id)) (defun idm--get-record-id () "Get the record id on the current point." (get-text-property (point) 'idm-record-id)) (defun idm--layout-list (db &optional order) "Erase the content in the current buffer and insert record lines. ORDER is sort key, which can be `time', `name' and `id'." (unless order (setq order 'name)) (let* ((name-max (length "Account Name")) (id-max (length "ID")) (pw-max (length "Password")) (pw-mask "********") (pw-getter (lambda (record) (if idm-show-password (idm-record-password record) pw-mask))) (cut (lambda (str) (substring str 0 (min (length str) 20)))) numcolm (count 1) (line-format "%%-%ds|%%-10s | %%-%ds | %%-%ds : %%-%ds : %%s\n") (records (funcall db 'get-all-records))) (when records (setq numcolm (fceiling (log10 (length records)))) (dolist (i records) (setq name-max (min 20 (max name-max (length (idm-record-name i)))) id-max (min 20 (max id-max (length (idm-record-account-id i)))) pw-max (max pw-max (length (funcall pw-getter i))))) (setq line-format (format line-format numcolm name-max id-max pw-max)) (let ((buffer-read-only nil) (prev-line (line-number-at-pos))) (erase-buffer) (goto-char (point-min)) (insert (format line-format " " "Time" "Name" "ID" "Password" "Memo")) (insert (make-string (- (window-width) 1) ?-) "\n") (dolist (i (idm--sort-records records order)) (insert (idm--put-record-id (format line-format count (idm-record-update-time i) (funcall cut (idm-record-name i)) (funcall cut (idm-record-account-id i)) (funcall pw-getter i) (idm-record-memo i)) (idm-record-name i))) (incf count)) (goto-char (point-min)) (when (< 1 prev-line) (ignore-errors (forward-line (1- prev-line)))))))) (defun idm--sort-records (records order) "Sort records by the key ORDER, which can be `time', `name', `memo' and `id'." (let* ((comparator (lambda (ref) (lexical-let ((ref ref)) (lambda (i j) (let ((ii (funcall ref i)) (jj (funcall ref j))) (cond ((string= ii jj) 0) ((string< ii jj) -1) (t 1))))))) (to-bool (lambda (f) (lexical-let ((f f)) (lambda (i j) (< (funcall f i j) 0))))) (cmp-id (funcall comparator 'idm-record-account-id)) (cmp-name (funcall comparator 'idm-record-name)) (cmp-time (funcall comparator 'idm-record-update-time)) (cmp-memo (funcall comparator 'idm-record-memo)) (chain (lambda (a b) (lexical-let ((a a) (b b)) (lambda (i j) (let ((v (funcall a i j))) (if (= 0 v) (funcall b i j) v))))))) (sort (loop for i in records collect i) ; copy-list (cond ((eq order 'id) ; id -> id, name (funcall to-bool (funcall chain cmp-id cmp-name))) ((eq order 'name) ; name -> name (funcall to-bool cmp-name)) ((eq order 'time) ; time -> time, name (funcall to-bool (funcall chain cmp-time cmp-name))) ((eq order 'memo) ; memo -> time, name (funcall to-bool (funcall chain cmp-memo cmp-name))) (t ; default (funcall to-bool cmp-name)))))) (defvar idm-list-mode-map nil "Keymap for `idm-list-mode'.") (setq idm-list-mode-map nil) ; for debug (unless idm-list-mode-map (setq idm-list-mode-map (make-sparse-keymap)) (mapc (lambda (i) (define-key idm-list-mode-map (car i) (cdr i))) `(("q" . idm-list-mode-quit) ("Q" . idm-list-mode-quit-without-save) ("n" . next-line) ("p" . previous-line) ("j" . next-line) ("k" . previous-line) ("d" . idm-list-mode-delete) ("-" . idm-list-mode-delete) ("e" . idm-list-mode-edit-dialog) ("m" . idm-list-mode-edit-dialog) ("a" . idm-list-mode-add) ("+" . idm-list-mode-add) ("u" . idm-list-mode-reload) ("r" . idm-list-mode-reload) ("T" . idm-list-mode-sortby-time) ("N" . idm-list-mode-sortby-name) ("I" . idm-list-mode-sortby-id) ("M" . idm-list-mode-sortby-memo) ("S" . idm-list-mode-toggle-show-password) ("s" . idm-list-mode-show-password) ([return] . idm-list-mode-copy) ))) (defun idm-list-mode-copy () (interactive) (idm--aif (idm--get-record-id) (let ((record (funcall idm-db 'get it))) (when record (idm--copy-password-action record))))) (defun idm--copy-password-action (record) (interactive) (message (concat "Copied the password for the account ID: " (idm-record-account-id record))) (funcall idm-copy-action (idm-record-password record)) (idm-set-clipboard-expiry)) (defun idm--copy-id-action (record) (interactive) (message (concat "Copied account ID: " (idm-record-account-id record))) (funcall idm-copy-action (idm-record-account-id record)) (idm-set-clipboard-expiry)) (defun idm-set-clipboard-expiry () (when idm-clipboard-expire-timer (cancel-timer idm-clipboard-expire-timer)) (when idm-clipboard-expire-time-sec (setq idm-clipboard-expire-timer (run-at-time idm-clipboard-expire-time-sec nil 'idm-expire-clipboard)))) (defun idm-expire-clipboard () "Clear clipboard" (funcall idm-copy-action "") (idm--message "ID Manager: expired.")) (defun idm-list-mode-sortby-id () (interactive) (idm--layout-list idm-db 'id)) (defun idm-list-mode-sortby-name () (interactive) (idm--layout-list idm-db 'name)) (defun idm-list-mode-sortby-time () (interactive) (idm--layout-list idm-db 'time)) (defun idm-list-mode-sortby-memo () (interactive) (idm--layout-list idm-db 'memo)) (defun idm-list-mode-reload () "Reload the id-password database file." (interactive) (setq idm-db (idm-load-db)) (idm--layout-list idm-db)) (defun idm-list-mode-toggle-show-password () "Toggle whether to show passwords." (interactive) (idm-toggle-show-password) (idm--layout-list idm-db)) (defun idm-list-mode-show-password () "Show password of the selected record." (interactive) (idm--aif (idm--get-record-id) (let ((record (funcall idm-db 'get it))) (if record (idm--message (concat "ID: " (idm-record-account-id record) " / PW: "(idm-record-password record))))))) (defun idm--set-list-mode (db) "Set up major mode for id-password list mode." (kill-all-local-variables) (make-local-variable 'idm-db) (setq idm-db db) (setq truncate-lines t) (use-local-map idm-list-mode-map) (setq major-mode 'idm-list-mode mode-name "ID-Password List") (hl-line-mode 1)) (defun idm-list-mode-quit () "Save the DB and kill buffer." (interactive) (funcall idm-db 'save) (kill-buffer (current-buffer))) (defun idm-list-mode-quit-without-save () "Kill buffer without saving the DB." (interactive) (kill-buffer (current-buffer))) (defun idm-list-mode-delete () "Delete a selected record from the DB. After deleting, update the list buffer." (interactive) (idm--aif (idm--get-record-id) (progn (when (y-or-n-p (format "Delete this record[%s] ?" it)) (funcall idm-db 'delete-record-by-name it) (idm--layout-list idm-db))))) (defun idm-list-mode-add () "Add a new record. After adding, update the list buffer." (interactive) (lexical-let ((db idm-db) (curbuf (current-buffer))) (idm-add-record-dialog db (lambda (r) (with-current-buffer curbuf (funcall db 'add-record r) (idm--layout-list db)))))) (defun idm-list-mode-edit-dialog () "Edit the selected record. After editting, update the list buffer." (interactive) (idm--aif (idm--get-record-id) (let ((record (funcall idm-db 'get it))) (if record (lexical-let ((db idm-db) (prev record) (curbuf (current-buffer))) (idm-edit-record-dialog record (lambda (r) (with-current-buffer curbuf (funcall db 'delete-record-by-name (idm-record-name prev)) (funcall db 'add-record r) (idm--layout-list db))))))))) ;;;###autoload (defun idm-open-list-command (&optional db) "Load the id-password DB and open a list buffer." (interactive) (unless db (setq db (idm-load-db))) (switch-to-buffer (idm-open-list db))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Helm UI (defun idm--helm-add-dialog (db) "Add a new record by the helm interface." (interactive) (lexical-let ((db db)) (idm-add-record-dialog db (lambda (r) (funcall db 'add-record r) (funcall db 'save) (when (eq major-mode 'idm-list-mode) (idm--layout-list db)))))) (defun idm--helm-edit-dialog (db record) "Edit a record selected by the helm interface." (interactive) (lexical-let ((db db) (prev record)) (idm-edit-record-dialog record (lambda (r) (funcall db 'delete-record-by-name (idm-record-name prev)) (funcall db 'add-record r) (funcall db 'save) (when (eq major-mode 'idm-list-mode) (idm--layout-list db)))))) ;;;###autoload (defun idm-helm-command () "Helm interface for id-manager." (interactive) (let* ((db (idm-load-db)) (source-commands (helm-build-sync-source "id-manager-global-command" :name "Global Command : " :candidates '(("Add a record" . (lambda () (idm--helm-add-dialog db))) ("Show all records" . (lambda () (idm-open-list-command db)))) :action (helm-make-actions "Execute" (lambda (i) (funcall i))))) (souce-records (helm-build-sync-source "id-manager-source-commands" :name "Accounts : " :candidates (lambda () (mapcar (lambda (record) (cons (concat (idm-record-name record) " (" (idm-record-account-id record) ") " " " (idm-record-memo record)) record)) (funcall db 'get-all-records))) :action (helm-make-actions "Copy password" (lambda (record) (idm--copy-password-action record)) "Copy id" (lambda (record) (idm--copy-id-action record)) "Show ID / Password" (lambda (record) (idm--message (concat "ID: " (idm-record-account-id record) " / PW: "(idm-record-password record)))) "Edit fields" (lambda (record) (idm--helm-edit-dialog db record))) :migemo t))) (helm :sources '(source-commands souce-records) :buffer "ID-Password Management : "))) (defalias 'id-manager 'idm-open-list-command) (eval-after-load "helm" '(defalias 'id-manager 'idm-helm-command)) (provide 'id-manager) ;;; id-manager.el ends here