aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-04-30 01:11:27 -0500
committerCraig Jennings <c@cjennings.net>2026-04-30 01:11:27 -0500
commit5c7bb207081278e41122e62d8f6c282a18574665 (patch)
treefd49b588b2ce32f74e2cd170fd9a50938c38695b
parentd313c37f14511564849c70c564c14ca51bd4ae7c (diff)
downloadgloss-5c7bb207081278e41122e62d8f6c282a18574665.tar.gz
gloss-5c7bb207081278e41122e62d8f6c282a18574665.zip
feat: implement gloss secondary commands
Five interactive commands plus the supporting major mode and pure helpers for `gloss-add'. `gloss-add' opens a `*gloss-add: TERM*' buffer in `gloss-add-mode', a `text-mode' derivative with C-c C-c to save and C-c C-k to abort. The pure save side, `gloss--add-finish-internal', validates term and body, trims trailing whitespace, and delegates to `gloss-core-save' with source `manual' before showing the new entry. `gloss-edit' resolves a term to its source-buffer marker, jumps point there, unfolds the entry under both the legacy `org-show-entry' and the post-9.6 `org-fold-show-entry' (with-no-warnings on the fallback), and installs `gloss--after-save-refresh-cache' as a buffer-local after-save hook so manual edits keep the cache honest. `gloss-list-terms' prompts via `completing-read' over `gloss-core-list' and dispatches the chosen term to `gloss--lookup-flow'. Empty glossary raises a user-error. `gloss-stats' formats a multi-line report (total / by-source / drill-tagged / file size / cache mtime) via the pure helper `gloss--stats-text' and shows it in `*gloss-stats*' under `special-mode'. Drill counting walks the file via `org-map-entries' to be safe against tag-substring false positives. `gloss-reload' is a thin wrapper that resets the in-memory cache and re-ensures it from disk. `gloss-drill-export' is a thin wrapper around `gloss-drill-export-all'. Audit fold-in: renamed the stats-text missing-file test from `-reports-zero-and-never' to `-reports-zero' to match what the assertion actually checks (the file is auto-created on first call, so mtime is set, not "never"). Filed as a v1.1 follow-up: `gloss.el' reaches into `gloss-core--cache-reset' and `gloss-core--cache-ensure' double-dash-private functions. Decide whether to treat double-dash as "package-private" idiom or to expose public aliases. 125 tests pass in 0.25s — 111 prior plus 14 new across the six new files. No byte-compile warnings.
-rw-r--r--gloss.el168
-rw-r--r--tests/test-gloss--stats-text.el4
2 files changed, 157 insertions, 15 deletions
diff --git a/gloss.el b/gloss.el
index 66b069a..62ecba4 100644
--- a/gloss.el
+++ b/gloss.el
@@ -122,19 +122,93 @@ Returns a symbol naming the action taken: :show, :auto-save, :pick,
(interactive (list (read-string "Glossary lookup: " (thing-at-point 'word t))))
(gloss--lookup-flow term))
+(defvar gloss-add-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "C-c C-c") #'gloss-add-finish)
+ (define-key map (kbd "C-c C-k") #'gloss-add-abort)
+ map)
+ "Keymap for `gloss-add-mode'.")
+
+(define-derived-mode gloss-add-mode text-mode "GlossAdd"
+ "Major mode for entering a glossary entry's body.
+
+\\{gloss-add-mode-map}"
+ (setq header-line-format
+ (substitute-command-keys
+ "Type the body. \\[gloss-add-finish] saves; \\[gloss-add-abort] cancels.")))
+
+(defvar-local gloss-add--term nil
+ "Term being added in this `gloss-add-mode' buffer.")
+
+(defun gloss--add-finish-internal (term body)
+ "Save TERM with BODY as a manual entry, then show it.
+Returns the saved entry plist, or nil if `gloss-core-save' returned nil
+(e.g. user cancelled at the collision prompt). Trims surrounding
+whitespace from BODY before saving."
+ (when (string-empty-p (string-trim (or term "")))
+ (user-error "gloss-add: term cannot be empty"))
+ (let ((trimmed (string-trim (or body ""))))
+ (when (string-empty-p trimmed)
+ (user-error "gloss-add: body cannot be empty"))
+ (let ((saved (gloss-core-save term trimmed 'manual)))
+ (when saved
+ (gloss-display-show-entry term trimmed))
+ saved)))
+
+(defun gloss-add-finish ()
+ "Save the current `gloss-add-mode' buffer's body for the recorded term."
+ (interactive)
+ (unless gloss-add--term
+ (user-error "gloss-add: no term recorded for this buffer"))
+ (let ((term gloss-add--term)
+ (body (buffer-string))
+ (buf (current-buffer)))
+ (gloss--add-finish-internal term body)
+ (let ((kill-buffer-query-functions nil))
+ (kill-buffer buf))))
+
+(defun gloss-add-abort ()
+ "Abandon the current `gloss-add-mode' buffer without saving."
+ (interactive)
+ (let ((kill-buffer-query-functions nil))
+ (kill-buffer (current-buffer))))
+
;;;###autoload
(defun gloss-add (term)
- "Add TERM to the glossary manually."
+ "Add TERM to the glossary manually.
+Opens a side buffer for the body; \\[gloss-add-finish] saves,
+\\[gloss-add-abort] cancels."
(interactive (list (read-string "Add term: ")))
- (ignore term)
- (user-error "gloss-add: not yet implemented"))
+ (when (string-empty-p (string-trim (or term "")))
+ (user-error "gloss-add: term cannot be empty"))
+ (let ((buf (get-buffer-create (format "*gloss-add: %s*" term))))
+ (with-current-buffer buf
+ (erase-buffer)
+ (gloss-add-mode)
+ (setq gloss-add--term term))
+ (pop-to-buffer buf)))
+
+(defun gloss--after-save-refresh-cache ()
+ "Buffer-local `after-save-hook' that clears the gloss cache."
+ (gloss-core--cache-reset))
;;;###autoload
(defun gloss-edit (term)
- "Open the source org file at TERM's heading."
- (interactive (list (read-string "Edit term: ")))
- (ignore term)
- (user-error "gloss-edit: not yet implemented"))
+ "Open the source org file at TERM's heading.
+Installs a buffer-local `after-save-hook' that refreshes the gloss
+cache when the file is saved."
+ (interactive (list (read-string "Edit term: " (thing-at-point 'word t))))
+ (let ((marker (gloss-core-find-buffer-position term)))
+ (unless marker
+ (user-error "gloss: term not in glossary: %s" term))
+ (switch-to-buffer (marker-buffer marker))
+ (goto-char marker)
+ (when (derived-mode-p 'org-mode)
+ (if (fboundp 'org-fold-show-entry)
+ (org-fold-show-entry)
+ (with-no-warnings (org-show-entry))))
+ (add-hook 'after-save-hook #'gloss--after-save-refresh-cache nil t)
+ marker))
;;;###autoload
(defun gloss-fetch-online (term)
@@ -146,25 +220,93 @@ Returns a symbol naming the action taken: :show, :auto-save, :pick,
(defun gloss-drill-export ()
"Tag every entry as :drill: for `org-drill'."
(interactive)
- (user-error "gloss-drill-export: not yet implemented"))
+ (gloss-drill-export-all))
;;;###autoload
(defun gloss-list-terms ()
- "Browse glossary terms via `completing-read'."
+ "Browse glossary terms via `completing-read' and show the chosen one."
(interactive)
- (user-error "gloss-list-terms: not yet implemented"))
+ (let ((terms (gloss-core-list)))
+ (unless terms
+ (user-error "gloss: glossary is empty"))
+ (let ((chosen (completing-read "Term: " terms nil t)))
+ (when chosen
+ (gloss--lookup-flow chosen)))))
+
+(defun gloss--count-drill-tagged ()
+ "Return the number of top-level entries in `gloss-file' tagged :drill:.
+Returns 0 if `gloss-file' does not exist."
+ (if (and gloss-file (file-exists-p gloss-file))
+ (with-current-buffer (find-file-noselect gloss-file)
+ (unless (verify-visited-file-modtime (current-buffer))
+ (revert-buffer t t t))
+ (unless (derived-mode-p 'org-mode)
+ (let ((org-mode-hook nil)) (org-mode)))
+ (let ((count 0))
+ (org-map-entries
+ (lambda ()
+ (when (and (= 1 (org-current-level))
+ (member "drill" (org-get-tags nil t)))
+ (setq count (1+ count)))))
+ count))
+ 0))
+
+(defun gloss--stats-text ()
+ "Return a multi-line string summarizing glossary state."
+ (gloss-core--cache-ensure-or-init)
+ (let* ((terms (gloss-core-list))
+ (total (length terms))
+ (by-source (make-hash-table :test 'equal))
+ (drill-count (gloss--count-drill-tagged))
+ (file-size (when (and gloss-file (file-exists-p gloss-file))
+ (file-attribute-size (file-attributes gloss-file))))
+ (mtime gloss-core--cache-mtime))
+ (dolist (term terms)
+ (let* ((entry (gloss-core-lookup term))
+ (source (or (plist-get entry :source) 'unknown)))
+ (puthash source (1+ (or (gethash source by-source) 0)) by-source)))
+ (let (source-pairs)
+ (maphash (lambda (k v) (push (cons k v) source-pairs)) by-source)
+ (format "Glossary stats:
+ Total terms: %d
+ By source: %s
+ Drill-tagged: %d
+ File size: %s bytes
+ Cache mtime: %s
+"
+ total
+ (if source-pairs
+ (mapconcat (lambda (pair)
+ (format "%s=%d" (car pair) (cdr pair)))
+ source-pairs ", ")
+ "(none)")
+ drill-count
+ (or file-size 0)
+ (if mtime
+ (format-time-string "%Y-%m-%d %H:%M:%S" mtime)
+ "never")))))
;;;###autoload
(defun gloss-stats ()
- "Summarize the glossary state."
+ "Show glossary statistics in a side buffer."
(interactive)
- (user-error "gloss-stats: not yet implemented"))
+ (let ((buf (get-buffer-create "*gloss-stats*")))
+ (with-current-buffer buf
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (insert (gloss--stats-text)))
+ (special-mode)
+ (goto-char (point-min)))
+ (display-buffer buf)
+ buf))
;;;###autoload
(defun gloss-reload ()
"Force reload of the glossary cache from disk."
(interactive)
- (user-error "gloss-reload: not yet implemented"))
+ (gloss-core--cache-reset)
+ (gloss-core--cache-ensure)
+ (message "gloss: cache reloaded"))
;;;###autoload
(defun gloss-toggle-debug ()
diff --git a/tests/test-gloss--stats-text.el b/tests/test-gloss--stats-text.el
index 32093b1..d91a4e6 100644
--- a/tests/test-gloss--stats-text.el
+++ b/tests/test-gloss--stats-text.el
@@ -30,8 +30,8 @@
(should (string-match-p "File size:" text))
(should (string-match-p "Cache mtime:" text)))))
-(ert-deftest test-gloss-stats-text-missing-file-reports-zero-and-never ()
- "Error: missing glossary file reports zero terms and \"never\" mtime."
+(ert-deftest test-gloss-stats-text-missing-file-reports-zero ()
+ "Boundary: stats-text against a missing file creates it, reports zero terms."
(gloss-test--with-missing-glossary
(let ((text (gloss--stats-text)))
(should (string-match-p "Total terms: +0" text)))))