;;; rcd-hash-edit.el --- Edit hashes visually and collaboratively -*- lexical-binding: t; -*- ;; Copyright (C) 2021 by Jean Louis ;; Author: Jean Louis ;; Version: 0.2 ;; 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 . ;;; 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 nil t) (defvar-local rcd-hash-current-hash nil "Buffer local variable to designate the symbol of edited hash.") (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.") (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)))) (defun rcd-hash-edit-hash (rcd-hash) "Edit HASH with `rcd-hash-list-mode'." (let ((rcd-hash-current-hash rcd-hash) (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 '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) "Edit HASH value by using KEY." (let* ((hash (symbol-value hash)) (value (gethash key hash)) (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) (read-from-minibuffer prompt value nil nil nil value)) (t (error "Not recognized type %s" type))))) (puthash key new-value hash))) (defun rcd-hash-add-key () "Add key to edited hash." (interactive) (let* ((key (read-from-minibuffer "New 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") (read-from-minibuffer prompt)) ((string= type "Cons") (read--expression prompt)) (t (read-from-minibuffer prompt))))) (when (and key value) (let ((point (point)) (rcd-hash rcd-hash-current-hash)) (setf (gethash key (symbol-value rcd-hash-current-hash)) value) (kill-this-buffer) (rcd-hash-edit-hash rcd-hash) (goto-char point))))) (defun rcd-hash-report (title entries format rcd-hash &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 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-mode) (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)) (hash rcd-hash-current-hash) (point (point))) (when (and key rcd-hash-current-hash) (rcd-hash-edit-entry-1 rcd-hash-current-hash key) (kill-this-buffer) (rcd-hash-edit-hash hash) (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-current-hash)))) (when (y-or-n-p (format "Delete `%s'? " value)) (puthash key (rcd-hash-nullify-by-type value) (symbol-value rcd-hash-current-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") ;;; rcd-hash-edit.el ends here