With the GNU Emacs package rcd-hash-edit.el one can visually edit Emacs Lisp hashes. Use it as a form editing convenience.

Tip
This file requires the GNU Emacs package: rcd-utilities.el:
https://gnu.support/gnu-emacs/packages/rcd-utilities-el.html
Caution
This package edits Emacs Lisp hashes created with the :test 'equal and is not well tested. Help is needed to expand package to handle hashes with keys as symbols. Maybe it works, maybe not. Who knows.

Yet another video demonstration:

Key bindings

  • Use D to remove key and value from hash

  • Use a to add new key and value to the hash

  • Use d to nullify the existing value

  • Use e to edit value of a key

  • Use g to refresh

  • Use s to save hash

  • Use h, j, k, l as in Vi editor bindings

  • Use q to burry buffer

Possible applications

  1. Create a hash or edit existing hash with command M-x rcd-hash-edit. You may as well invoke hash editing programmatically with the function rcd-hash-edit-hash.

  2. Use rcd-utilities.el to save hash or read hash from file. Send file to other people for collaboration, receive hash back by email or other communication line. Store your hash on a disk. Collaborate on Emacs Lisp hash. Send back and forth by using chat.

  3. For example M-x rcd-save-symbol-to-file may be used to save the symbol value to file. Or you may use M-x rcd-hash-save

  4. You may read the hash into memory by using: M-x rcd-read-symbol-from-file or M-x rcd-hash-load

  5. Convert a single database column to hash and send it to collaborator for editing or viewing, then receive it back modified if you wish.

  6. Use it as a mini and quick database, create fields and values, save it for later. Load again, save it again.

  7. Build a system for your personal notes. You may easily create bunch of notes by using this system, save it all in the file, read from file when you want. Use alist type of lists of hashes. Fetch information from database, let the user edit it and save it back into the database.

  8. Prepare one hash with empty values as a template, send it to collaborator to insert new information for you. Receive it back and continue processing your data. Use it for form editing.

Source for Emacs Lisp package rcd-hash-edit

;;; rcd-hash-edit.el --- Edit hashes visually and collaborate  -*- lexical-binding: t; -*-

;; Copyright (C) 2021-2022 by Jean Louis

;; Author: Jean Louis <bugs@gnu.support>
;; Version: 0.01
;; Package-Requires: (rcd-utilities)
;; Keywords: convenience
;; URL: https://hyperscope.link/3/7/6/6/1/Emacs-Lisp-package-rcd-hash-edit-37661.html

;; This file is not part of GNU Emacs.

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

;; 1. Create a hash any how or use M-x rcd-hash-edit

;; (setq hash (make-hash-table :test #'equal))
;; (puthash "ID" 29 hash)
;; (rcd-hash-edit-hash 'hash)
;;
;; M-x rcd-hash-edit
;; choose the hash to edit
;; if hash does not exist, new one can be created
;;
;; Key bindings:
;;
;; d - nullify the value
;; D - remove the key and value from hash
;; a - add new value to hash
;; e - edit value
;;
;; This file requires the GNU Emacs package: rcd-utilities.el:
;; https://gnu.support/gnu-emacs/packages/rcd-utilities-el.html
;;
;; Use `rcd-utilities' to save hash or read hash from file.  Send file
;; to other people to collaborate on the hash structure, let them send
;; it back by email.
;;
;; For example M-x rcd-save-symbol-to-file may be used to save the
;; symbol value to file.
;;
;; You may read the hash into memory by using:
;; M-x rcd-read-symbol-from-file
;;
;; Send file to your collaborators.  Receive the file from
;; collaborators.  Collaborate on Emacs Lisp hash.  Send back and forth
;; by using chat.
;;
;; Example uses:
;;
;; - Convert a single database column and send to collaborator to edit
;;   it and send it back.
;;
;; - Use it as a mini database, create fields and values, save it for
;;   later.  Load again, save it again.
;;
;; - Prepare one template empty hash, send it to collaborator to
;;   insert new information for you. Receive it back and continue
;;   processing your data.

;; RCD is acronym for Reach, Connect, Deliver, my personal
;; principle and formula for Wealth.

;;; Change Log:

;;; Code:

(require 'rcd-utilities)

(defvar-local rcd-hash-current-hash nil
  "Buffer local variable to designate the symbol of edited hash.")

(defvar-local rcd-hash-update-function nil
  "Buffer local variable to designate the symbol of the update function.")

(define-derived-mode rcd-hash-list-mode tabulated-list-mode "RCD Hash List"
  "RCD Hash List is derived from `tabulated-list-mode'.")

(defvar rcd-hash-mode-map
  (let ((map (make-sparse-keymap)))
    (set-keymap-parent map tabulated-list-mode-map)
    (define-key map "D" #'rcd-hash-delete-key-value)
    (define-key map "a" #'rcd-hash-add-key)
    (define-key map "d" #'rcd-hash-nullify-entry)
    (define-key map "e" #'rcd-hash-edit-entry)
    (define-key map "g" #'rcd-hash-refresh)
    (define-key map "h" #'backward-char)
    (define-key map "j" #'next-line)
    (define-key map "k" #'previous-line)
    (define-key map "l" #'forward-char)
    (define-key map "q" #'quit-window)
    (define-key map "s" #'(lambda () (interactive) (rcd-hash-save rcd-hash-current-hash)))
    map)
  "The RCD hash keymap.")

;; 2024-12-21 this was before
;; (defun rcd-hash-entries (hash)
;;   "Convert symbol or hash HASH to `tabulated-list-entries'."
;;   (let* ((entries '())
;; 	 (type (type-of hash))
;; 	 (hash (cond ((eq type 'symbol) (symbol-value hash))
;; 		     ((eq type 'hash-table) hash)
;; 		     (t hash))))
;;     (if (eq (type-of hash) 'hash-table)
;; 	(progn
;; 	  (maphash
;; 	   (lambda (key value)
;; 	     (push (list key (vector
;; 			      (string-or-empty-string key)
;; 			      (string-or-empty-string (prin1-to-string value))))
;; 		   entries))
;; 	   hash)
;; 	  (reverse entries))
;;       (error "%s is not hash or symbol for hash" hash))))

;; 2024-12-21 this is now
(defun rcd-hash-entries (hash)
  "Convert symbol or hash HASH to `tabulated-list-entries'."
  (let* ((entries '())
	 (hash-value (if (eq (type-of hash) 'symbol)
			 (symbol-value hash)
			 hash))
	 (hash-type (type-of hash-value)))
    (if (eq hash-type 'hash-table)
	(progn
	  (maphash
	   (lambda (key value)
	     (push (list key (vector
			      (string-or-empty-string key)
			      (string-or-empty-string (prin1-to-string value))))
		   entries))
	   hash-value)
	  (reverse entries))
      (error "%s is not hash or symbol for hash" hash))))

(defun rcd-hash-edit-hash (rcd-hash &optional update-function)
  "Edit HASH with `rcd-hash-list-mode'."
  (let ((rcd-hash-current-hash rcd-hash) ;; TODO is it really needed?
	(rcd-hash-update-function (or update-function 'ignore)) ;; TODO is it really needed?
	(entries (rcd-hash-entries rcd-hash)))
    (rcd-hash-report "Edit hash" entries [("Key" 20 t :right-align t :pad-right 3) ("Value" 40 t)] rcd-hash update-function 'rcd-hash-refresh)))

(defun rcd-hash-edit ()
  "Interactively choose a hash to edit.
Optionally provide symbol RCD-HASH for editing or creation."
  (interactive)
  (let ((rcd-hash (read--expression "Hash to edit: ")))
    (if (boundp rcd-hash)
	(if (eq (type-of (symbol-value rcd-hash)) 'hash-table)
	    (rcd-hash-edit-hash rcd-hash)
	  (message "Symbol is not a hash"))
      (progn
	(rcd-hash-create rcd-hash)
	;; (puthash "ID" "ID" (symbol-value rcd-hash))
	;; (remhash "ID" (symbol-value rcd-hash))
	(rcd-hash-edit-hash rcd-hash)))))

(defun rcd-hash-create (symbol)
  "Create hash NAME.
Argument SYMBOL will be created in global space."
  (eval `(defvar ,(intern (symbol-name symbol)) (make-hash-table :test #'equal))))

(defun rcd-hash-edit-entry-1 (hash key update-function)
  "Edit HASH value by using KEY."
  (let* ((hash1 (symbol-value hash))
	 (value (gethash key hash1))
	 (prompt (format "Value for key `%s': " (if (not (stringp key)) (prin1-to-string key) key)))
	 (type (type-of value))
	 (new-value (cond  ((eq type 'cons) (read--expression prompt (prin1-to-string value)))
			   ((or (eq type 'integer)
				(eq type 'float))
			    (read-number prompt value))
			   ((eq type 'string) (rcd-ask prompt value value))
			   (t (error "Not recognized type %s" type)))))
    (puthash key new-value hash1)
    (when update-function
      (funcall update-function hash1))))

(defun rcd-hash-add-key ()
  "Add key to edited hash."
  (interactive)
  (let* ((key (rcd-ask "New hash key: "))
	 (completion-ignore-case t)
	 (type (completing-read "Type: " '("Number" "String" "Cons") nil t))
	 (prompt (format "Value for key `%s': " key))
	 (value (cond ((string= type "Number") (read-number prompt))
		      ((string= type "String") (rcd-ask prompt))
		      ((string= type "Cons") (read--expression prompt))
		      (t (rcd-ask prompt)))))
    (when (and key value)
      (let ((point (point))
	    (rcd-hash rcd-hash-current-hash))
	(setf (gethash key (symbol-value rcd-hash)) value)
	(kill-this-buffer)
	(rcd-hash-edit-hash rcd-hash)
	(goto-char point)))))

(defun rcd-hash-report (title entries format rcd-hash update-function &optional refresh)
  "Handles visual HASH editing.
TITLE is used for the name of buffer.
ENTRIES is in the format of `tabulated-list-entries'.
FORMAT is is in the format of `tabulated-list-format'."
  (let* ((buffer (generate-new-buffer-name (concat "*RCD Hash Editing: " title "*"))))
    (let* ((buffer (get-buffer-create buffer)))
      (switch-to-buffer buffer)
      (setq tabulated-list-format format)
      (setq tabulated-list-entries entries)
      (setq rcd-tabulated-refresh-function refresh)
      (rcd-hash-list-mode)
      (use-local-map rcd-hash-mode-map)
      (hl-line-mode 1)
      (setq rcd-hash-current-hash rcd-hash)
      (setq rcd-hash-update-function update-function)
      (setq tabulated-list-padding 1)
      (tabulated-list-init-header))
    (tabulated-list-print t)))

(defun rcd-hash-delete-key-value ()
  "Delete the key and value from edited hash."
  (interactive)
  (let* ((key (tabulated-list-get-id)))
    (when key
      (let ((point (point))
	    (rcd-hash rcd-hash-current-hash))
	(when (y-or-n-p (format "Remove key `%s'? " key))
	  (remhash key (symbol-value rcd-hash))
	  (kill-this-buffer)
	  (rcd-hash-edit-hash rcd-hash)
	  (goto-char point))))))

(defun rcd-hash-refresh ()
  "Refresh the `rcd-hash-list-mode'."
  (interactive)
  (when (eq major-mode 'rcd-hash-list-moppde)
    (let ((rcd-hash rcd-hash-current-hash)
	  (point (point)))
      (kill-this-buffer)
      (rcd-hash-edit-hash rcd-hash)
      (goto-char point))))

(defun rcd-hash-edit-entry ()
  "Edit entry."
  (interactive)
  (let* ((key (tabulated-list-get-id))
	 (rcd-hash rcd-hash-current-hash)
	 (point (point))
	 (update-function rcd-hash-update-function))
    (when (and key rcd-hash-current-hash)
      (rcd-hash-edit-entry-1 rcd-hash key rcd-hash-update-function)
      (kill-this-buffer)
      (rcd-hash-edit-hash rcd-hash update-function)
      (goto-char point))))

(defun rcd-hash-nullify-entry ()
  "Nullify the entry for the specific hash key."
  (interactive)
  (let* ((key (tabulated-list-get-id))
	 (entry (tabulated-list-get-entry)))
    (when (and key entry)
      (let* ((point (point))
	     (rcd-hash rcd-hash-current-hash)
	     (value (gethash key (symbol-value rcd-hash))))
	(when (y-or-n-p (format "Delete `%s'? " value))
	  (puthash key (rcd-hash-nullify-by-type value) rcd-hash)
	  (kill-this-buffer)
	  (rcd-hash-edit-hash rcd-hash)
	  (goto-char point))))))

(defun rcd-hash-nullify-by-type (value)
  "Return the nullified value depending of the type of VALUE."
  (let ((type (type-of value)))
    (cond  ((or (eq type 'cons) (car (read-from-string "nil"))))
	   ((or (eq type 'symbol) (car (read-from-string "nil"))))
	   ((or (eq type 'integer)
		(eq type 'float))
	    0)
	   ((eq type 'string) "")
	   (t (error "Not recognized type %s" type)))))

(defalias 'rcd-hash-load 'rcd-read-symbol-from-file
  "Uses the package `rcd-utilities' to read hash from file.")

(defalias 'rcd-hash-save 'rcd-save-symbol-to-file
  "Uses the package `rcd-utilities' to save hash into file")

(provide 'rcd-hash-edit)

;;; rcd-hash-edit.el ends here

GNU General Public License Version 3

Copyright © 2021-05-18 14:13:47.580285+03 by Jean Louis

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 https://www.gnu.org/licenses/.