A Self-Modifying-Source-Code-Password-Manager for Emacs

I’m posting my passwords on the Internet. No, this isn’t like the Lifelock CEO thing. If you aren’t an Emacs nerd, look away. Although even if you are not, reading the documentation (lines starting with ‘;;’) may allow you still to experience some of my witty prose. (Skip the MIT License – it’s not witty). If you have comments or use this, let me know!

;; -*- lexical-binding: t; -*-
;;
;; MIT License
;; Copyright 2020 Soren Telfer
;
;; Permission is hereby granted, free of charge, to any
;; person obtaining a copy of this software and associated
;; documentation files (the "Software"), to deal in the
;; Software without restriction, including without
;; limitation the rights to use, copy, modify, merge,
;; publish, distribute, sublicense, and/or sell copies of
;; the Software, and to permit persons to whom the Software
;; is furnished to do so, subject to the following
;; conditions:
;;
;; The above copyright notice and this permission notice
;; shall be included in all copies or substantial portions
;; of the Software.
;;
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
;; KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
;; THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
;; PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
;; THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
;; DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
;; CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
;; CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
;; IN THE SOFTWARE.
;;
;;
;;
;;
;;
;; A Self-Modifying-Source-Code-Password-Manager for Emacs
;;
;; If you aren't an Emacs weirdo, look away.
;;
;; This is a self-modifying file. Your passwords are stored
;; at the end of this file in a block delimited by
;; BEGIN-DATA/END-DATA. If you are clutching your pearls,
;; know that this thing uses the available GPG implementation
;; to encrypt the password data before it inserts itself
;; back into the file. So, you will also need a working GPG
;; Key.
;;
;; I had my own idea to do this, but s/o to
;;
;; https://www.masteringemacs.org/article/keeping-secrets-in-emacs-gnupg-auth-sources
;;
;; for another emacs weirdo using epg.
;;
;; If you're concerned about the length of this file over
;; time, I can tell you that I have quite a few passwords,
;; and I've been using this for quite a while, and the block
;; at the end of my file is about 17kB.
;;
;; Why have a self-modifying file for storing your
;; passwords? Well, who doesn't like self-modifying source
;; code! Seriously, it's just one thing to keep track of. No
;; worries about online security, someone else's (besides
;; mine) source code,etc. All of your secrets are stored
;; encrypted in this file. There are a number of ways that a
;; bad guy who had access to the memory of your emacs
;; session could get your password. But I'm not losing sleep
;; about that. This is the utopia of data and code cohabiting
;; in harmony and accreting crud together over time.
;;
;; Things to watch out for: This piece of software is
;; unforgiving about making changes. While in the entry
;; pane, almost everything you do is committed
;; immediately. Deletions require a y/n. If you are the type
;; of person who makes a lot of mistakes with your
;; passwords, I'd recommend putting this file in some kind
;; of version control or versioning system. I replicate this
;; file with a versioning file system, so I run the risk
;; that I might lose some changes in between version
;; syncs. But I can live with that. You can always appeal to
;; emacs undo.
;;
;; This thing was mega influenced by PasswordSafe. It has
;; the exact usage-flow. Only this is embedded in Elisp, and
;; is searchable with helm! Also multiple password versions
;; are stored. You can switch around which one is copied by the
;; 'Copy Password' button by clicking make-primary. It
;; technically has a version log, so you can inspect the log
;; to see what you did to your entry by clicking the History
;; button.
;;
;; I used to use the java version of PasswordSafe. I had to
;; keep the application, the data, and an installer for the
;; MacOS runtime junk on a usb key. Just too much of a pain.
;; Now, I put this thing on my replicated filesystem and
;; share it between my laptop and desktop and I only need to
;; worry about one file. If you run emacs on your mobile
;; device, you should be able to get this to work, but I
;; know nothing about that. But otherwise, I have my
;; passwords everywhere I have my emacs. And if I don't have
;; my emacs, I don't need my passwords!
;;
;; To use this, open this file and eval-buffer. There are
;; other more clever ways to invoke this from the command
;; line, etc, but for me I just keep this open in a buffer.
;;
;; You will be greeted by a Helm-based searchable index of
;; your passwords. Searching works on both the site, the ID
;; and description. If you select an entry it takes you to a 90's
;; inspired bunch of widgets in a buffer that let access
;; your password. Give it a try and you'll see. If you click
;; on the Open button, it launches a browser tab and also
;; copies the ID into the clipboard. Then you just need to
;; alt-tab back and copy the password. This is how PasswordSafe
;; works (used to work?). Obviously, since your password is
;; in your clipboard, don't be an idiot. It would be nice to
;; launch a task that zaps the Clipboard, but feels like too
;; much silliness could happen. We need a one-time
;; clipboard! Otherwise, like I said, don't be an
;; idiot. Also, open is bound to the return key, so in theory
;; you can just hit your site with a return.
;;
;; You'll need to install some stuff. The most challenging
;; of them is an OpenPGP implementation. Mostly, you need
;; something that works with epg. On Windows I use
;; Kleopatra:
;;
;; https://www.openpgp.org/software/kleopatra/
;;
;; On Mac I use GPGTools
;;
;; https://gpgtools.org/
;;
;; Many other options are possible. I can't help you install
;; any of them. I also can't help you generate and install a
;; GPG key for yourself. But the Internet abounds with help
;; for that.
;;
;; You need to enter your private key id here. Unless you
;; don't need to. In which case I guess we need to chat!
(defconst smscpwm/private-key-id "soren@memoize.me")
;;
;; Also, if you don't save this file as 'passwords.el',
;; you'll need to put the actual file name here
(defconst smscpwm/this-file-name "passwords.el")
;;
;; You need emacs stuff too, all available on ELPA. You can
;; also put your cursor at the end of each of the following
;; lines and C-x C-e:
;;
;; (package-install 'helm)
;; (package-install 'epg)
;; (package-install 'cl-lib)
;; (package-install 'widget)
;; (package-install 'cus-edit)
;; (package-install 'subr-x)
;;
;; Are they all necessary? I have no clue. I don't write
;; elisp for a living, so what you see is what you get. But
;; it works.
;;
;; There are many missing things. Like elegance in the
;; code. The functions are not documented but should be
;; self-explanatory by examining their names and knowing
;; that they do what they say they are going to do.
;;
;; There are features missing:
;;
;; - Keybinds for the entry screen
;; - A way to store Authenticator codes for a site, independent
;;   from passwords
;;
;; to name a few. These may or may not be added in the
;; future. I'm sure there are hardcoded things in here.
;;
;; This file comes with one password included! Here is how
;; it is generated (i.e. here is the api documentation)
;;
;; (setq b (smscpwm/create-block))
;; (setq id "me@me.me")
;; (setq site "http://me.me")
;; (setq password "password123")
;; (smscpwm/add-account b id site)
;; (smscpwm/add-password b id site password)
;; (smscpwm/write-block b)
;;
;;
;; Also, by eval-buffer'ing this you are really just running
;; smscpwn/start-helm. So feel free to keybind that, or
;; whatever. I typically just keep this thing running.
;;
;; Hope this is interesting and useful. But think of it more
;; as insipiration. Enjoy!
;;

(require 'helm)
(require 'epg)
(require 'cl-lib)
(require 'widget)
(require 'cus-edit)
(require 'subr-x)



(defconst smscpwm/results-buffer "*SMSCPWM MPW RESULTS*")
(defconst smscpwm/search-buffer "*SMSCPWM HELM SOURCES*")
(defvar smscpwm/source-buffer nil)

(cl-defstruct (smscpwm/block (:type list) :named)
  current-version
  versions
  accounts
  passwords
  )

(cl-defstruct (smscpwm/version (:type list) :named)
  version
  event-type
  (other-version nil)
  (key nil)
  (user-login-name (user-login-name))
  (system-name (smscpwm/system-name))
  (date (smscpwm/get-date))
  )

(cl-defstruct (smscpwm/account (:type list) :named)
  id
  site
  version
  (description nil)
  )

(cl-defstruct (smscpwm/password (:type list) :named)
  value
  version
  )

(define-hash-table-test 'contents-hash 'equal 'sxhash-equal)

(defun smscpwm/get-date ()
  (format-time-string "%Y-%m-%dT%T"))

(defun smscpwm/system-name ()
  (car (split-string (system-name) "\\." t)))


(defun smscpwm/create-block ()
  (let ((v (make-hash-table :test 'contents-hash)))
    (puthash 0 (make-smscpwm/version :version 0 :event-type 'create-block) v)
    (make-smscpwm/block :current-version 0
          :versions v
          :accounts (make-hash-table :test 'contents-hash)
          :passwords (make-hash-table :test 'contents-hash))))


(defun smscpwm/incr-version(b event-type &optional key other-version)
  (let* ((v (cl-incf (smscpwm/block-current-version b)))
     (vn (make-smscpwm/version :version v
                   :event-type event-type
                   :other-version other-version
                   :key key)))
    (puthash v vn (smscpwm/block-versions b))
    vn))

(defun smscpwm/get-account(id site b)
  (let ((k (cons id site))
     (a (smscpwm/block-accounts b)))
    (gethash k a)))

(defun smscpwm/add-account(b id site &optional descr)
  (let* ((k (cons id site))
     (a (smscpwm/block-accounts b))
     (x (gethash k a)))
    (if x x
      (puthash k
           (make-smscpwm/account :id id
                   :site site
                   :version (smscpwm/version-version (smscpwm/incr-version b 'add-account k))
                   :description descr) a))))

(defun smscpwm/add-account(b id site &optional descr)
  (let* ((k (cons id site))
     (a (smscpwm/block-accounts b))
     (x (gethash k a)))
    (if x x
      (puthash k
           (make-smscpwm/account :id id
                   :site site
                   :version (smscpwm/version-version (smscpwm/incr-version b 'add-account k))
                   :description descr) a))))

(defun smscpwm/change-account-descr (b id site descr)
  (let* ((k (cons id site))
     (a (smscpwm/block-accounts b))
     (ka (gethash k a)))
    (unless ka
      (error (format "Unknown key: %S" k)))
    (setf (smscpwm/account-description ka) descr)
    (smscpwm/incr-version b 'change-account-descr k (smscpwm/account-version ka))

    ))

(defun smscpwm/change-account-id(b id site new-id)
  (let* ((k (cons id site))
     (kn (cons new-id site))
     (a (smscpwm/block-accounts b))
     (p (smscpwm/block-passwords b))
     (ka (gethash k a)))
    (unless ka
      (error (format "Unknown key: %S" k)))
    (if (gethash kn a)
    (error (format "Key already exists: %S" k)))

    (smscpwm/add-account b new-id site (smscpwm/account-description ka))
    (dolist (pi (gethash k p))
      (smscpwm/add-password b new-id site (smscpwm/password-value pi)))
    (smscpwm/delete-account b id site (smscpwm/account-version ka) (smscpwm/account-description ka))
    ))

(defun smscpwm/delete-account(b id site version descr)
  (let* ((k (cons id site))
    (a (smscpwm/block-accounts b))
    (x (make-smscpwm/account :id id :site site :version version :description descr))
    (xa (gethash k a)))
    (unless xa
      (error (format "No matching account: %S" k)))
    (unless (equal xa x)
      (error (format "No match for account: %S" x)))
    (remhash k (smscpwm/block-passwords b))
    (remhash k a)
    (smscpwm/incr-version b 'delete-account version k)))


(defun smscpwm/add-password (b id site password)
  (let* ((other-version (smscpwm/account-version (smscpwm/add-account b id site)))
     (k (cons id site))
     (p (smscpwm/block-passwords b))
     (pl (gethash k p))
     (v (smscpwm/version-version (smscpwm/incr-version b 'add-password k other-version))))
    (push (make-smscpwm/password :value password
               :version v) pl)
    (puthash k pl p)
    pl))

(defun smscpwm/make-password-primary-s (b key p)
  (let* ((ph (smscpwm/block-passwords b))
     (pl (gethash key ph)))
    (setq pl (remove p pl))
    (push p pl)
    (puthash key pl ph)
    (smscpwm/incr-version b 'make-password-primary key)
    ))

(defun smscpwm/delete-password-s (b id site ps)
  (smscpwm/delete-password b id site (smscpwm/password-value ps) (smscpwm/password-version ps)))

(defun smscpwm/delete-password (b id site password version)
  (let* ((k (cons id site))
     (p (smscpwm/block-passwords b))
     (pl (gethash k p))
     (pn (make-smscpwm/password :value password :version version)))
    (unless (member pn pl)
      (error (format "No match for password: %S" pn)))
    (setq pl (remove pn pl))
    (puthash k pl p)
    (smscpwm/incr-version b 'delete-password)
    pl))

(defun smscpwm/select-pgp-key (id)
  (let ((context (epg-make-context 'OpenPGP)))
    (epg-list-keys context id)))

(defun smscpwm/get-pgp-size ()
  (- (smscpwm/get-pgp-end) (smscpwm/get-pgp-begin)))

(defun smscpwm/get-pgp-begin ()
  (save-excursion
    (goto-char (point-min))
    (search-forward ";; BEGIN-DATA\n;; " )
    (point)))

(defun smscpwm/get-pgp-end (&optional begin)
  (save-excursion
    (let ((begin (or begin (smscpwm/get-pgp-begin))))
      (goto-char begin))
    (search-forward "\n;; END-DATA" )
    (search-backward "\n;; END-DATA" )
    (point)))

(defun smscpwm/extract-pgp (&optional begin end)
  (save-excursion
    (let ((begin (or begin (smscpwm/get-pgp-begin)))
      (end (or end (smscpwm/get-pgp-end))))
      (buffer-substring-no-properties begin end)
    )))

(defun smscpwm/get-block ()
  (let* ((context (epg-make-context 'OpenPGP))
     (string (epg-decrypt-string context (smscpwm/extract-pgp))))
    (eval (car (read-from-string string)))))

(defun smscpwm/serialize-block (b)
  (prin1-to-string b))

(defun smscpwm/deserialize-block (string) 
  (car (read-from-string string)))

(defun smscpwm/encrypt-block (b &optional recipient)
  "Returns a Base64 encoding of the encrypted alist"
  (let* ((string (smscpwm/serialize-block b))
     (context (epg-make-context 'OpenPGP))
     (recipient (or recipient (smscpwm/select-pgp-key smscpwm/private-key-id))))
    (base64-encode-string
     (epg-encrypt-string context (encode-coding-string string 'utf-8) recipient) t)))


(defun smscpwm/write-block (b)
  (let* ((cipher (smscpwm/encrypt-block b))
     (begin (smscpwm/get-pgp-begin))
     (end (smscpwm/get-pgp-end)))
    (save-excursion
      (delete-region begin end)
      (goto-char begin)
      (insert cipher))))

(defun smscpwm/save-block (b)
  (with-current-buffer smscpwm/source-buffer
    (smscpwm/write-block b)
    (save-buffer)))

(defun smscpwm/load-block ()
  (with-current-buffer smscpwm/source-buffer
    (smscpwm/decrypt-string (smscpwm/extract-pgp))))

(defun smscpwm/decrypt-string (string)
  (let* ((context (epg-make-context 'OpenPGP))
     (cipher (base64-decode-string string))
     (string (epg-decrypt-string context cipher)))
    (smscpwm/deserialize-block string)))

(defun smscpwm/get-date ()
  (format-time-string "%Y-%m-%dT%T"))

(defun smscpwm/format-for-window (a1 a2 a3)
  (let* ((w (- (window-body-width) 1))
     (w1 (/ w 2) )
     (w2 (/ w 4) )
     (fmt (format "%%-%ds%%-%ds%%-%ds" w1 w2 w2)))
    (format fmt
        (truncate-string-to-width a1 (- w1 1))
        (truncate-string-to-width a2 (- w2 1))
        (truncate-string-to-width a3 (- w2 1)))))

(defun smscpwm/generate-password (&optional length command)
  (let ((cmd (format "%s %d" command length))
        (length (or length 12))
    (command (or command (command "openssl rand -base64"))))
    (string-trim (shell-command-to-string cmd))))

(defun smscpwm/copy-to-clipboard-darwin (text &optional push)
  "from https://gist.github.com/the-kenny/267162"
  (let ((process-connection-type nil))
    (let ((proc (start-process "pbcopy" "*Messages*" "pbcopy")))
      (process-send-string proc text)
      (process-send-eof proc))))

(defun smscpwm/copy-to-clipboard-default (text &optional push)
  (funcall interprogram-cut-function text))

(defun smscpwm/copy-to-clipboard (text &optional push)
  (if window-system
      (smscpwm/copy-to-clipboard-default text push)
    (cond ((string= system-type "darwin") (smscpwm/copy-to-clipboard-darwin text push))
      ((t (error "No support for system type"))))))

(defun smscpwm/copy-password (password record old-record buffer)
  (smscpwm/copy-to-clipboard (smscpwm/password-value password))
  (setf (smscpwm/password-access-date password) (smscpwm/get-date))
  (smscpwm/update-record record old-record buffer)
)

(defun smscpwm/validate-url (url)
  " Did I need to write my own url validator? I tried very hard to find one!"
  (let ((uri_regex "^\\(\\([^#/:?]+\\)://\\)?\\(\\([^#?]*\\)\\)?\\([^#?]*\\)"))
    (when (string-match uri_regex url)
      (let ((scheme (or (match-string 2 url) "http"))
            (authority (or (match-string 4 url) url)))
        (concat scheme "://" authority)))))

;; Basic sanity checking for our url parser
(dolist (a '("fubar.com" "1.2.3.4" "[2001:db8:0:1]"))
  (dolist (p '("" ":80"))
    (dolist (h '("" "/" "/fubar/index.html"))
      ; Default behavior
      (cl-assert (string= (smscpwm/validate-url (concat a p h)) (concat "http://" a p h)) t)
      ; Input matches defaul output
      (cl-assert (string= (smscpwm/validate-url (concat "http://" a p h)) (concat "http://" a p h)) t)
      ; Otherwise
      (cl-assert (string= (smscpwm/validate-url (concat "https://" a p h)) (concat "https://" a p h)) t))))


(defun smscpwm/get-candidates (b)
  (let (l)
    (maphash (lambda (k v)
           (let* ((descr (smscpwm/account-description v))
              (ll (smscpwm/format-for-window (cdr k) (car k) (or descr ""))))
         (setq l (cons (cons ll k) l)))) (smscpwm/block-accounts b))
    l))

(defun smscpwm/commit (b key args)
  (smscpwm/save-block b)
  (if (gethash key (smscpwm/block-accounts b))
      (apply 'smscpwm/render-key b key args)    ; rerender
    (smscpwm/restart-helm)
    ))

(defun smscpwm/render-buttons (account passwords block key args)
    (widget-create 'push-button
                   :notify (lambda (&rest ignore)
                     (smscpwm/copy-to-clipboard  (smscpwm/account-id account))
                     (browse-url (smscpwm/account-site account)))
                   :help-echo "Open URL and copy ID to Clipboard"
               :format "%[Open%]"
               )
    (widget-insert " ")
    (widget-create 'push-button
                   :notify (lambda (&rest ignore)
                     (let ((p (car passwords)))
                       (smscpwm/copy-to-clipboard (smscpwm/password-value p))
                   )
                 )
                   :help-echo "Copy current password to Clipboard"
               :format "%[Copy Password%]"
               )
    (widget-insert " ")
    (widget-create 'push-button
                   :notify (lambda (&rest ignore)
                     (smscpwm/copy-to-clipboard (smscpwm/account-site account)))
               :help-echo "Copy the URL to the clipboard"
                   :format "%[Copy URL%]")

    (widget-insert " ")
    (widget-create 'push-button
                   :notify (lambda (&rest ignore)
                     (smscpwm/copy-to-clipboard (smscpwm/account-id account)))
               :help-echo "Copy the user ID to the clipboard"
                   :format "%[Copy ID%]")

    (widget-insert "\n\n")    
    (widget-create 'push-button
                   :notify (lambda (&rest ignore)
                 (smscpwm/restart-helm)
                 )
                   :format "%[Done%]")

    (widget-insert " ")    
    (widget-create 'push-button
                   :notify (lambda (&rest ignore)
                 (when (smscpwm/confirm)
                   (smscpwm/delete-account block
                         (smscpwm/account-id account)
                         (smscpwm/account-site account)
                         (smscpwm/account-version account)
                         (smscpwm/account-description account))
                   (smscpwm/commit block key args))
                     )
                   :format "%[Delete%]"))

(defun smscpwm/confirm(&optional a s)
  (let* ((s (or s "Really? '%s' to continue "))
     (a (or a "Yes"))
     (q (format s a)))
    (string-equal a (read-from-minibuffer q))))

(defun smscpwm/render-key (b key &rest args)
  (let* ((id (car key))
     (site (cdr key))
     (account (gethash key (smscpwm/block-accounts b)))
     (passwords (gethash key (smscpwm/block-passwords b)))
     (account-version (gethash (smscpwm/account-version account) (smscpwm/block-versions b)))
     (show-passwords (or (plist-get args :show-passwords) nil))
     (show-history (or (plist-get args :show-history) nil))
     (buffer (or (plist-get args :buffer) (current-buffer)))
     (args (plist-put args :buffer buffer))
     (result-buffer (get-buffer-create smscpwm/results-buffer)))

    ;; Save this so we can ensure the we're the only changes being made
    (switch-to-buffer result-buffer)
    (Custom-mode)

    (kill-all-local-variables)
    (let ((inhibit-read-only t))
      (erase-buffer))
    (remove-overlays)
    (setq-local widget-button-face custom-button)
    (setq-local widget-button-pressed-face custom-button-pressed)
    (setq-local widget-mouse-face custom-button-mouse)
    (setq-local widget-button-click-moves-point t)

    (smscpwm/render-buttons account passwords b key args)

    (widget-insert "\n\n")
    (smscpwm/render-account account account-version
              (lambda (&rest ignore) ; change ID
            (let* ((input (read-from-minibuffer "Enter new ID: " id)))
              (smscpwm/change-account-id b id site input)
              (smscpwm/commit b (cons input site) args)))
              (lambda (&rest ignore)
            (let* ((input (read-from-minibuffer "Enter new description: " (smscpwm/account-description account))))
              (smscpwm/change-account-descr b id site input)
              (smscpwm/commit b key args))
            )
              )

    (smscpwm/render-password-header b id site show-passwords args)
    (dolist (p passwords)
      (let* ((v (smscpwm/password-version p))
        (version (gethash v (smscpwm/block-versions b))))
    (smscpwm/render-password p
               version
               (equal p (car passwords))
               show-passwords
               (lambda (&rest ignore)  ; delete
                 (when (smscpwm/confirm)
                 ;; (when (string-equal (read-from-minibuffer "Really (y/N): ") "y")
                 (smscpwm/delete-password-s b id site p)
                 (smscpwm/commit b key args)
                 )
               )
               (lambda (&rest ignore)  ; copy
                 (smscpwm/copy-to-clipboard (smscpwm/password-value p)))
               (lambda (&rest ignore) ; make-primary
                 (print (format "%S %S %S" b key p))
                 (smscpwm/make-password-primary-s b key p)
                 (smscpwm/commit b key args)
                 )
               )
    ))

    (widget-insert "\n\n")
    (smscpwm/render-history b key args)

    (use-local-map widget-keymap)
    (widget-setup)
    (goto-char (point-min))))

(defun smscpwm/render-history (b key args)
  (let ((show-history (plist-get args :show-history)))
    (widget-insert "History: ")
    (widget-create 'push-button
                   :notify (lambda (&rest ignore)
                     (let ((new-args (plist-put args :show-history
                                    (if show-history nil t))))
                       (apply 'smscpwm/render-key b key new-args)
                    ))
               :format (if show-history "%[Hide%]" "%[Show%]"))
    (widget-insert "\n")

    (when show-history
      (let ((history))
        (maphash (lambda (k v)
               (when (equal (smscpwm/version-key v) key)
                 (let ((date (smscpwm/version-date v)))
                   (push v history)))) (smscpwm/block-versions b))
        (dolist (h history)
          (smscpwm/render-version h))))))

(defun smscpwm/render-version (v)
  (widget-insert (format "%d %s %-30s %s@%s\n"
             (smscpwm/version-version v)
             (smscpwm/version-date v)
             (smscpwm/version-event-type v)
             (smscpwm/version-user-login-name v)
             (smscpwm/version-system-name v)
             )))

(defun smscpwm/render-account (account version change-id-notify change-descr-notify)
    (widget-insert (format "SITE\t%s\n" (smscpwm/account-site account)))
    (widget-insert "ID\t")
    (widget-create 'push-button :notify change-id-notify :help-echo "Change" :format "%[Change%]")    
    (widget-insert (format "\t%s" (smscpwm/account-id account)))
    (widget-insert "\n")
    (widget-insert "DESCR\t")
    (widget-create 'push-button :notify change-descr-notify :help-echo "Change" :format "%[Change%]")    
    (widget-insert (format "\t%s"(or (smscpwm/account-description account) "")))
    (widget-insert "\n")
  )

(defun smscpwm/render-password-header (b id site show-passwords args)
    (widget-insert "\nPasswords: ")
    (widget-create 'push-button
                   :notify (lambda (&rest ignore)
                     (let* ((input (read-from-minibuffer "Enter new password: "
                                         (smscpwm/generate-password))))
                   (smscpwm/add-password b id site input)
                   (smscpwm/commit b (cons id site) args)))
               :format "%[Add%]")
    (widget-insert " ")
    (widget-create 'push-button
                   :notify (lambda (&rest ignore)
                     (let ((new-args (plist-put args :show-passwords
                                    (if show-passwords nil t))))
                       (apply 'smscpwm/render-key b (cons id site) new-args)
                   )
                 )
               :format (concat "%[" (if show-passwords "Hide" "Show") "%]"))
    (widget-insert "\n")
    (widget-insert (format "%-30s %-20s%-20s" "" "Created" "By")))

(defun smscpwm/render-password (password version primary show-passwords delete-notify copy-notify make-primary-notify)
  (let* ((display (propertize (if show-passwords
             (smscpwm/password-value password)
             (make-string (length (smscpwm/password-value password)) ?*))
                  'face '(widget-field default)))
     (date (smscpwm/version-date version))
     (user-login-name (smscpwm/version-user-login-name version))
     (system-name (smscpwm/version-system-name version)))
    (widget-insert  (format "\n%-30s %s %s@%s" display date user-login-name system-name))
    (widget-insert " ")
    (widget-create 'push-button :notify copy-notify :help-echo "Copy password to Clipboard" :format "%[Copy%]")
    (widget-insert " ")
    (widget-create 'push-button :notify delete-notify :help-echo "Delete password" :format "%[Delete%]")
    (unless primary
      (widget-insert " ")
      (widget-create 'push-button :notify make-primary-notify :help-echo "Make Primary" :format "%[Make Primary%]"))))

(defun smscpwm/restart-helm()
  (kill-buffer smscpwm/results-buffer)
  (kill-buffer smscpwm/search-buffer)
  (if (active-minibuffer-window)
      (abort-recursive-edit))
  (smscpwm/start-helm))

(defun smscpwm/start-helm ()
  (interactive)

  (unless smscpwm/source-buffer
    (let ((buffer (find-file-noselect smscpwn/this-file-name)))
      (when buffer
    (setq smscpwm/source-buffer buffer))))

  (if (buffer-live-p (get-buffer smscpwm/results-buffer))
      (switch-to-buffer smscpwm/results-buffer)
    (let* ((block (smscpwm/load-block))
       (candidates (smscpwm/get-candidates block))
       (helm-full-frame t)
       (result (helm :sources (helm-build-sync-source
                      (smscpwm/format-for-window "URL" "ID" "DESCRIPTION")
                    :candidates candidates
                    :filtered-candidate-transformer
                    (lambda (candidates _source)
                      (let ((url (smscpwm/validate-url helm-input)))
                    (or candidates
                        (list
                         (cons
                          (format "Create %s" url) (cons nil url)))))))
             :buffer smscpwm/search-buffer)))
      (when (cdr result)
    (unless (car result)
      (let ((input (read-from-minibuffer (format "Enter new ID for %s: " (cdr result)))))
        (smscpwm/add-account block input (cdr result))
        (smscpwm/save-block block)
        (save-buffer)
        (setq result (cons input (cdr result)))))
    (smscpwm/render-key block result)))))


(smscpwm/start-helm)

;; BEGIN-DATA
;; hQEMA9eBn5Ss/79YAQf9H1d5NVIBWBnmltuf+fFZYbepM1CmKka5koF3JtuvS7mRWK6Pw+mugZT+xmvS3fQuMVUA0Xm+qnDTuFEvdSmnvcJHn0mGRsEkuCr1fjc071BtixJFGxJgGhxnSiuPrHOLrKJLo6ZGQfCAC9c1vT9qPnyWbV1Ppg7UEb9I8Q355ZxdIwx2DmzxAcyk/da6KW9WFaC9DF+EcfupNItsRXJGz350Ek5FDxxTz4RTX9yvcHjI7iExLwg/SJI4nAew3cs21uXC41K0oKNNqFLIK+8bxLIhrv6kZhxtw+s5owUOZHR3W7CTLyQI7U7odxJ4qeJpCEjdRD8012fIYbbQWmgL5tLAdQFLqiVszyyOlsSEGm5NVQRj7fB87RvWrSP3lKH8YWTx0QOYi7S1RM3nitilFmgWvg4DImTfOX0bO1YtLXRlSDkGIK9LQIsvE5o8fp7ctZltpOu1uAmSntyoDWWtlgPBak4JNMTyg4NXwMPWCdDN4JTAMEoFq6T+5q3yOyeP2nWkng5p33N3GEKIy3ULvJJ+QFdPGtzikM1b2Rjvdop4Po67YbWdewSQWPXrMBp5NEC7Uwg7EbA6vPbT9eyIxqgeYU2/WZThkyk6zSmGIVtZLfEy18XLH3elC6jotKhHS5rlCOuUKCgRmVMKvdBI6BYc7jtNAmW0bSKc7oktBJPSS09FA7azc++SKtD+OuSLs9V8qEgK2rtXuEJolSLf8ArYEvsqY9R6q+sC/W1ao/jh1IXgHH7I+g==
;; END-DATA

Read more in …

COVID-19  Math and Science  Society  Systems  Technology and Policy  Writing 

Reach me

on Twitter or email.
If you subscribe, I'll send you an email next time I post:
I'm Soren Telfer. From 2012-2020, I ran AT&T's Silicon Valley innovation lab, where we delivered hundreds of projects that impacted almost every part of the business. Now I'm consulting on technology and writing about what I find interesting. I've been a CTO, written a ton of software, and have been kicking around physics for the last twenty years.