66; ; Maintainer: Shen, Jen-Chieh <[email protected] >77; ; URL: https://github.com/emacs-openai/chatgpt
88; ; Version: 0.1.0
9- ; ; Package-Requires: ((emacs "26.1") (openai "0.1.0") (lv "0.0") (ht "2.0") (markdown-mode "2.1"))
9+ ; ; Package-Requires: ((emacs "26.1") (openai "0.1.0") (lv "0.0") (ht "2.0") (markdown-mode "2.1") (spinner "1.7.4") )
1010; ; Keywords: comm openai
1111
1212; ; This file is not part of GNU Emacs.
3838(require 'lv )
3939(require 'ht )
4040(require 'markdown-mode )
41+ (require 'spinner )
4142
4243(defgroup chatgpt nil
4344 " Use ChatGPT inside Emacs."
7172 :type 'boolean
7273 :group 'chatgpt )
7374
75+ (defcustom chatgpt-spinner-type 'moon
76+ " The type of the spinner."
77+ :type '(choice (const :tag " Key to variable `spinner-types' " symbol)
78+ (const :tag " Vector of characters" vector))
79+ :group 'openai )
80+
81+ (defcustom chatgpt-display-tokens-info t
82+ " Non-nil we display tokens infomration for each request."
83+ :type 'boolean
84+ :group 'chatgpt )
85+
86+ (defcustom chatgpt-priority 100
87+ " Overlays' priority."
88+ :type 'integer
89+ :group 'chatgpt )
90+
7491(defconst chatgpt-buffer-name-format " *ChatGPT: <%s>*"
7592 " Name of the buffer to use for the `chatgpt' instance." )
7693
86103(defvar-local chatgpt-requesting-p nil
87104 " Non-nil when requesting; waiting for the response." )
88105
106+ (defvar-local chatgpt-spinner-counter 0
107+ " Spinner counter." )
108+
109+ (defvar-local chatgpt-spinner-timer nil
110+ " Spinner timer." )
111+
89112(defvar-local chatgpt-data (ht-create)
90113 " Store other information other than messages." )
91114
97120 " Face used for user."
98121 :group 'chatgpt )
99122
123+ (defface chatgpt-tip
124+ '((t :foreground " #848484" ))
125+ " Face used for tip."
126+ :group 'chatgpt )
127+
128+ (defface chatgpt-info
129+ '((t :height 0.8 :foreground " #999999" ))
130+ " Face added to codemetrics display."
131+ :group 'chatgpt )
132+
100133; ;
101134; ;; Util
102135
114147 window-state-change-hook)
115148 ,@body ))
116149
150+ ; ; TODO: Use function `string-pixel-width' after 29.1
151+ (defun chatgpt--string-pixel-width (str )
152+ " Return the width of STR in pixels."
153+ (if (fboundp #'string-pixel-width )
154+ (string-pixel-width str)
155+ (require 'shr )
156+ (shr-string-pixel-width str)))
157+
158+ (defun chatgpt--str-len (str )
159+ " Calculate STR in pixel width."
160+ (let ((width (frame-char-width ))
161+ (len (chatgpt--string-pixel-width str)))
162+ (+ (/ len width)
163+ (if (zerop (% len width)) 0 1 )))) ; add one if exceeed
164+
165+ (defun chatgpt--align (&rest lengths )
166+ " Align sideline string by LENGTHS from the right of the window."
167+ (list (* (window-font-width )
168+ (+ (apply #'+ lengths) (if (display-graphic-p ) 1 2 )))))
169+
117170(defun chatgpt--kill-buffer (buffer-or-name )
118171 " Like function `kill-buffer' (BUFFER-OR-NAME) but in the safe way."
119172 (when-let ((buffer (get-buffer buffer-or-name)))
@@ -133,14 +186,33 @@ Display buffer from BUFFER-OR-NAME."
133186 " user" ; this is free?
134187 openai-user))
135188
189+ ; ;
190+ ; ;; Spinner
191+
192+ (defun chatgpt-mode--cancel-timer ()
193+ " Cancel spinner timer."
194+ (when (timerp chatgpt-spinner-timer)
195+ (cancel-timer chatgpt-spinner-timer)))
196+
197+ (defun chatgpt--start-spinner ()
198+ " Start spinner."
199+ (chatgpt-mode--cancel-timer)
200+ (setq chatgpt-spinner-counter 0
201+ chatgpt-spinner-timer (run-with-timer (/ spinner-frames-per-second 60.0 )
202+ (/ spinner-frames-per-second 60.0 )
203+ (lambda ()
204+ (cl-incf chatgpt-spinner-counter)
205+ (force-mode-line-update )))))
206+
136207; ;
137208; ;; Instances
138209
139210(defmacro chatgpt-with-instance (instance &rest body )
140211 " Execute BODY within instance."
141212 (declare (indent 1 ))
142- `(when-let ((buffer (and , instance
143- (get-buffer (cdr , instance )))))
213+ `(when-let* ((buffer (and , instance
214+ (get-buffer (cdr , instance ))))
215+ ((buffer-live-p buffer)))
144216 (with-current-buffer buffer
145217 (let ((inhibit-read-only t ))
146218 ,@body ))))
@@ -195,6 +267,30 @@ Display buffer from BUFFER-OR-NAME."
195267; ;
196268; ;; Core
197269
270+ (defun chatgpt--get-face-height ()
271+ " Make sure we get the face height."
272+ (let ((height (face-attribute 'chatgpt-info :height )))
273+ (if (numberp height) height
274+ 1 )))
275+
276+ (defun chatgpt--create-tokens-overlay (prompt-tokens completion-tokens total-tokens )
277+ " Display tokens information."
278+ (when chatgpt-display-tokens-info
279+ (let* ((ov (make-overlay (1- (point )) (1- (point )) nil t t ))
280+ (content (format " prompt %s , completion %s , total: %s "
281+ prompt-tokens completion-tokens total-tokens))
282+ (content-len (* (chatgpt--str-len content)
283+ (chatgpt--get-face-height)))
284+ (str (concat
285+ (propertize " " 'display
286+ `((space :align-to (- right ,(chatgpt--align (1- content-len))))
287+ (space :width 0 ))
288+ `cursor t )
289+ (propertize content 'face 'chatgpt-info ))))
290+ (overlay-put ov 'chatgpt t )
291+ (overlay-put ov 'priority chatgpt-priority)
292+ (overlay-put ov 'after-string str))))
293+
198294(defun chatgpt--add-tokens (data )
199295 " Record all tokens information from DATA."
200296 (let-alist data
@@ -206,7 +302,9 @@ Display buffer from BUFFER-OR-NAME."
206302 ; ; Then we add it up!
207303 (ht-set chatgpt-data 'prompt_tokens (+ prompt-tokens .prompt_tokens))
208304 (ht-set chatgpt-data 'completion_tokens (+ completion-tokens .completion_tokens))
209- (ht-set chatgpt-data 'total_tokens (+ total-tokens .total_tokens))))))
305+ (ht-set chatgpt-data 'total_tokens (+ total-tokens .total_tokens))
306+ ; ; Render it!
307+ (chatgpt--create-tokens-overlay .prompt_tokens .completion_tokens .total_tokens)))))
210308
211309(defun chatgpt--add-message (role content )
212310 " Add a message to history.
@@ -252,6 +350,8 @@ The data is consist of ROLE and CONTENT."
252350
253351(defun chatgpt--display-messages ()
254352 " Display all messages to latest response."
353+ (when (zerop chatgpt--display-pointer) ; clear up the tip message
354+ (erase-buffer ))
255355 (while (< chatgpt--display-pointer (length chatgpt-chat-history))
256356 (let ((message (elt chatgpt-chat-history chatgpt--display-pointer)))
257357 (let-alist message
@@ -280,13 +380,16 @@ The data is consist of ROLE and CONTENT."
280380 (chatgpt-with-instance instance
281381 (chatgpt--display-messages)) ; display it
282382 (setq chatgpt-requesting-p t )
383+ (chatgpt--start-spinner)
283384 (openai-chat chatgpt-chat-history
284385 (lambda (data )
285386 (chatgpt-with-instance instance
286387 (setq chatgpt-requesting-p nil )
287- (chatgpt--add-tokens data)
288- (chatgpt--add-response-messages data)
289- (chatgpt--display-messages)))
388+ (chatgpt-mode--cancel-timer)
389+ (unless openai-error
390+ (chatgpt--add-response-messages data)
391+ (chatgpt--display-messages)
392+ (chatgpt--add-tokens data))))
290393 :model chatgpt-model
291394 :max-tokens chatgpt-max-tokens
292395 :temperature chatgpt-temperature
@@ -366,7 +469,7 @@ The data is consist of ROLE and CONTENT."
366469
367470(defun chatgpt-input-header-line ()
368471 " The display for input header line."
369- (format " Session: %s " (cdr chatgpt-input-instance)))
472+ (format " [ Session] %s " (cdr chatgpt-input-instance)))
370473
371474(defvar chatgpt-input-mode-map
372475 (let ((map (make-sparse-keymap )))
@@ -375,7 +478,6 @@ The data is consist of ROLE and CONTENT."
375478 map)
376479 " Keymap for `chatgpt-mode' ." )
377480
378- ;;;### autoload
379481(define-derived-mode chatgpt-input-mode fundamental-mode " ChatGPT Input"
380482 " Major mode for `chatgpt-input-mode' .
381483
@@ -426,6 +528,8 @@ The data is consist of ROLE and CONTENT."
426528
427529(defun chatgpt-mode--kill-buffer-hook ()
428530 " Kill buffer hook."
531+ (ht-clear chatgpt-data)
532+ (chatgpt-mode--cancel-timer)
429533 (let ((instance chatgpt-instances))
430534 (when (get-buffer chatgpt-input-buffer-name)
431535 (with-current-buffer chatgpt-input-buffer-name
@@ -435,7 +539,16 @@ The data is consist of ROLE and CONTENT."
435539
436540(defun chatgpt-header-line ()
437541 " The display for header line."
438- (format " Session: %s , History: %s , User: %s (M-x chatgpt-info) "
542+ (format " %s [Session] %s [History] %s [User] %s "
543+ (if chatgpt-requesting-p
544+ (let* ((spinner (if (symbolp chatgpt-spinner-type)
545+ (cdr (assoc chatgpt-spinner-type spinner-types))
546+ chatgpt-spinner-type))
547+ (len (length spinner)))
548+ (when (<= len chatgpt-spinner-counter)
549+ (setq chatgpt-spinner-counter 0 ))
550+ (format " %s " (elt spinner chatgpt-spinner-counter)))
551+ " " )
439552 (cdr chatgpt-instance)
440553 (length chatgpt-chat-history)
441554 (chatgpt-user)))
@@ -446,6 +559,17 @@ The data is consist of ROLE and CONTENT."
446559 map)
447560 " Keymap for `chatgpt-mode' ." )
448561
562+ (defun chatgpt-mode-insert-tip ()
563+ " Insert tip to output buffer."
564+ (when (string-empty-p (buffer-string ))
565+ (let ((inhibit-read-only t )
566+ (tip " Press <return> to start asking questions
567+
568+ `M-x chatgpt-info` will print out more information about the current session!
569+ " ))
570+ (add-face-text-property 0 (length tip) 'chatgpt-tip nil tip)
571+ (insert tip))))
572+
449573;;;### autoload
450574(define-derived-mode chatgpt-mode fundamental-mode " ChatGPT"
451575 " Major mode for `chatgpt-mode' .
@@ -454,7 +578,9 @@ The data is consist of ROLE and CONTENT."
454578 (setq-local buffer-read-only t )
455579 (font-lock-mode -1 )
456580 (add-hook 'kill-buffer-hook #'chatgpt-mode--kill-buffer-hook nil t )
457- (setq-local header-line-format `((:eval (chatgpt-header-line)))))
581+ (setq-local header-line-format `((:eval (chatgpt-header-line))))
582+ (setq-local chatgpt-data (ht-create))
583+ (chatgpt-mode-insert-tip))
458584
459585(defun chatgpt-register-instance (index buffer-or-name )
460586 " Register BUFFER-OR-NAME with INDEX as an instance.
0 commit comments