aboutsummaryrefslogtreecommitdiff
path: root/gloss-display.el
blob: 1e2b92b025712d89fad2994a359fca43cb1c6521 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
;;; gloss-display.el --- Side-buffer UI for gloss -*- lexical-binding: t -*-

;; Copyright (C) 2026 Craig Jennings
;; Author: Craig Jennings <c@cjennings.net>
;; SPDX-License-Identifier: GPL-3.0-or-later

;;; Commentary:

;; UI layer for `gloss'.  Defines the side buffer's major mode and
;; renders entries; also owns the picker shown when an online fetch
;; returns multiple candidate definitions.
;;
;; Public API:
;;   `gloss-display-show-entry' TERM BODY
;;   `gloss-display-pick-definition' TERM DEFINITIONS -> chosen plist
;;
;; Pure helpers (full N/B/E test coverage):
;;   `gloss-display--format-candidate' PLIST -> "[source] text..."
;;   `gloss-display--render-entry' TERM BODY -> rendered string
;;
;; `gloss-mode' is derived from `special-mode': `q' quits the window.
;;
;; See `docs/design/gloss.org' for the full design.

;;; Code:

(defconst gloss-display--candidate-max-length 80
  "Maximum total length of a formatted candidate row.
Rows longer than this are truncated and end in an ellipsis.")

(defconst gloss-display--buffer-name-format "*gloss: %s*"
  "Format string for the side buffer name.  Single %s receives the term.")

(defconst gloss-display--side-window-alist
  '((display-buffer-in-side-window)
    (side . right)
    (window-width . 0.4))
  "Display action used by `gloss-display-show-entry'.")

(define-derived-mode gloss-mode special-mode "Gloss"
  "Major mode for the gloss side buffer.

\\{gloss-mode-map}"
  (setq-local truncate-lines nil))

(defun gloss-display--format-candidate (plist)
  "Format definition PLIST as a single-line `completing-read' row.
Return \"[source] text\".  Embedded newlines and runs of whitespace
collapse to a single space.  When the row would exceed
`gloss-display--candidate-max-length', the text portion is truncated
and the row ends in \"...\".  A missing :source falls back to the
literal tag \"unknown\"."
  (unless (listp plist)
    (signal 'wrong-type-argument (list 'listp plist)))
  (let* ((source (or (plist-get plist :source) 'unknown))
         (text (or (plist-get plist :text) ""))
         (single-line (replace-regexp-in-string "[ \t\n]+" " " text))
         (prefix (format "[%s] " source))
         (room (- gloss-display--candidate-max-length (length prefix))))
    (if (<= (length single-line) room)
        (concat prefix single-line)
      (concat prefix (substring single-line 0 (max 0 (- room 3))) "..."))))

(defun gloss-display--render-entry (term body)
  "Return the rendered string for TERM and BODY.
The format is the term, an underline of `=' matching the term length,
a blank line, then the body verbatim."
  (format "%s\n%s\n\n%s" term (make-string (length term) ?=) body))

(defun gloss-display-pick-definition (term definitions)
  "Prompt the user to pick one of DEFINITIONS for TERM.
DEFINITIONS is a list of plists shaped (:source SYM :text STRING).
Return the chosen plist, or nil if DEFINITIONS is empty or the user
cancelled with \\[keyboard-quit]."
  (when definitions
    (let* ((alist (mapcar (lambda (def)
                            (cons (gloss-display--format-candidate def) def))
                          definitions))
           (prompt (format "Pick definition for %s: " term))
           (choice (condition-case nil
                       (completing-read prompt alist nil t)
                     (quit nil))))
      (and choice (cdr (assoc choice alist))))))

(defun gloss-display-show-entry (term body)
  "Display TERM's BODY in the gloss side buffer and return the buffer."
  (let ((buf (get-buffer-create
              (format gloss-display--buffer-name-format term))))
    (with-current-buffer buf
      (let ((inhibit-read-only t))
        (erase-buffer)
        (insert (gloss-display--render-entry term body))
        (goto-char (point-min)))
      (unless (derived-mode-p 'gloss-mode)
        (gloss-mode)))
    (display-buffer buf gloss-display--side-window-alist)
    buf))

(provide 'gloss-display)
;;; gloss-display.el ends here