;;; 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 <m.sakurai atmark kiwanami.net>
;; 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 <http://www.gnu.org/licenses/>.

;;; 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
