273 lines
8.6 KiB
EmacsLisp
273 lines
8.6 KiB
EmacsLisp
;;; whisper-posframe.el --- Record speech into a posframe -*- lexical-binding: t; -*-
|
|
|
|
(require 'whisper)
|
|
(require 'posframe)
|
|
|
|
(defgroup whisper-posframe nil
|
|
"Record speech into a temporary posframe buffer."
|
|
:group 'whisper)
|
|
|
|
(defcustom whisper-posframe-buffer-name "*whisper-capture*"
|
|
"Name of the temporary Whisper capture buffer."
|
|
:type 'string
|
|
:group 'whisper-posframe)
|
|
|
|
(defcustom whisper-posframe-width 80
|
|
"Width of the Whisper posframe in columns."
|
|
:type 'integer
|
|
:group 'whisper-posframe)
|
|
|
|
(defcustom whisper-posframe-height 12
|
|
"Height of the Whisper posframe in lines."
|
|
:type 'integer
|
|
:group 'whisper-posframe)
|
|
|
|
(defvar whisper-posframe--parent-frame nil)
|
|
|
|
(defvar whisper-posframe-mode-map
|
|
(let ((map (make-sparse-keymap)))
|
|
;; Finish recording and begin transcription.
|
|
(define-key map (kbd "C-c C-c") #'whisper-posframe-finish)
|
|
|
|
;; Cancel recording/transcription without copying anything.
|
|
(define-key map (kbd "C-c C-k") #'whisper-posframe-cancel)
|
|
|
|
;; Make C-g and Escape dismiss the capture.
|
|
(define-key map (kbd "C-g") #'whisper-posframe-cancel)
|
|
(define-key map (kbd "<escape>") #'whisper-posframe-cancel)
|
|
|
|
map)
|
|
"Keymap for `whisper-posframe-mode'.")
|
|
|
|
(defvar-local whisper-posframe--status-timer nil)
|
|
|
|
(defun whisper-posframe--recording-p ()
|
|
"Return non-nil when Whisper is recording."
|
|
(process-live-p whisper--recording-process))
|
|
|
|
(defun whisper-posframe--transcribing-p ()
|
|
"Return non-nil when Whisper is transcribing."
|
|
(process-live-p whisper--transcribing-process))
|
|
|
|
(defvar whisper-posframe--capture-buffer nil)
|
|
|
|
(defun whisper-posframe--set-status (status)
|
|
"Display STATUS at the beginning of the capture buffer."
|
|
(when (derived-mode-p 'whisper-posframe-mode)
|
|
(let ((inhibit-read-only t))
|
|
(save-excursion
|
|
(goto-char (point-min))
|
|
|
|
;; Remove the previous status line.
|
|
(when (looking-at "^\\[Whisper:.*\\]\n")
|
|
(delete-region (point)
|
|
(line-beginning-position 2)))
|
|
|
|
;; Insert the new status line as actual displayed buffer text.
|
|
(insert
|
|
(propertize
|
|
(format "[Whisper: %s]\n" status)
|
|
'face 'font-lock-keyword-face
|
|
'read-only t
|
|
'rear-nonsticky '(read-only face)))))))
|
|
|
|
(defun whisper-posframe--update-status ()
|
|
"Update the status line according to Whisper's current process."
|
|
(when (buffer-live-p (current-buffer))
|
|
(cond
|
|
((whisper-posframe--recording-p)
|
|
(whisper-posframe--set-status
|
|
"recording — C-c C-c to finish, C-g or Esc to cancel"))
|
|
|
|
((whisper-posframe--transcribing-p)
|
|
(whisper-posframe--set-status
|
|
"transcribing…"))
|
|
|
|
(t
|
|
;; This can occur briefly while whisper.el is compiling, checking
|
|
;; its model, or transitioning between recording and transcription.
|
|
(whisper-posframe--set-status
|
|
"preparing Whisper…")))))
|
|
|
|
(defun whisper-posframe--stop-status-timer ()
|
|
"Stop the status update timer."
|
|
(when (timerp whisper-posframe--status-timer)
|
|
(cancel-timer whisper-posframe--status-timer)
|
|
(setq whisper-posframe--status-timer nil)))
|
|
|
|
(defun whisper-posframe--copy-and-close ()
|
|
"Copy only the transcription and close the capture posframe."
|
|
(let ((buffer whisper-posframe--capture-buffer))
|
|
(when (buffer-live-p buffer)
|
|
(with-current-buffer buffer
|
|
(whisper-posframe--stop-status-timer)
|
|
|
|
(let ((inhibit-read-only t)
|
|
text)
|
|
;; Remove the status line from the actual capture buffer.
|
|
(save-excursion
|
|
(goto-char (point-min))
|
|
(when (looking-at "^\\[Whisper:.*\\]\n")
|
|
(delete-region (point)
|
|
(line-beginning-position 2))))
|
|
|
|
;; Extract only the remaining transcription.
|
|
(setq text
|
|
(string-trim
|
|
(buffer-substring-no-properties
|
|
(point-min)
|
|
(point-max))))
|
|
|
|
(when (> (length text) 0)
|
|
(kill-new text)
|
|
(message "Whisper transcription copied to the kill ring")))))
|
|
|
|
;; Clear Whisper's asynchronous insertion target.
|
|
(set-marker whisper--marker nil)
|
|
(setq whisper--point-buffer nil)
|
|
|
|
;; Delete the posframe and the known capture buffer.
|
|
(ignore-errors
|
|
(posframe-delete-frame whisper-posframe-buffer-name))
|
|
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer))
|
|
|
|
(setq whisper-posframe--capture-buffer nil)))
|
|
|
|
|
|
(define-derived-mode whisper-posframe-mode text-mode "Whisper"
|
|
"Major mode for a temporary Whisper capture buffer."
|
|
(use-local-map whisper-posframe-mode-map)
|
|
|
|
(setq-local whisper-insert-text-at-point t)
|
|
(setq-local whisper-display-transcription-buffer nil)
|
|
|
|
(whisper-posframe--set-status "preparing Whisper…")
|
|
|
|
;; This hook runs after whisper.el has inserted the final transcription.
|
|
(add-hook 'whisper-after-insert-hook
|
|
#'whisper-posframe--copy-and-close
|
|
nil
|
|
t))
|
|
|
|
(defun whisper-posframe--delete ()
|
|
"Delete the Whisper posframe, if it exists."
|
|
(when (buffer-live-p (get-buffer whisper-posframe-buffer-name))
|
|
(ignore-errors
|
|
(posframe-delete-frame whisper-posframe-buffer-name))))
|
|
|
|
(defun whisper-posframe-finish ()
|
|
"Stop recording and begin Whisper transcription."
|
|
(interactive)
|
|
(cond
|
|
((whisper-posframe--recording-p)
|
|
(whisper-posframe--set-status "transcribing…")
|
|
;; whisper-run toggles recording off and starts transcription.
|
|
(whisper-run))
|
|
|
|
((whisper-posframe--transcribing-p)
|
|
(message "Whisper is already transcribing"))
|
|
|
|
(t
|
|
(message "Whisper is not currently recording"))))
|
|
|
|
(defun whisper-posframe-cancel ()
|
|
"Cancel the active Whisper operation and close the posframe."
|
|
(interactive)
|
|
(when (or (whisper-recording-p)
|
|
(whisper-transcribing-p))
|
|
;; `whisper-run' interrupts recording or cancels transcription,
|
|
;; depending on Whisper's current state.
|
|
(whisper-run))
|
|
|
|
(whisper-posframe--stop-status-timer)
|
|
(set-marker whisper--marker nil)
|
|
(setq whisper--point-buffer nil)
|
|
|
|
(whisper-posframe--delete)
|
|
|
|
(let ((buffer (current-buffer)))
|
|
(when (buffer-live-p buffer)
|
|
(kill-buffer buffer)))
|
|
|
|
(message "Whisper recording canceled"))
|
|
|
|
|
|
(defun whisper-posframe--focus (buffer frame)
|
|
"Focus FRAME and select its window displaying BUFFER."
|
|
(when (frame-live-p frame)
|
|
(select-frame-set-input-focus frame)
|
|
(when-let ((window (get-buffer-window buffer frame)))
|
|
(select-window window))
|
|
(with-current-buffer buffer
|
|
(goto-char (point-max)))))
|
|
|
|
(defun whisper-posframe--start-whisper (buffer)
|
|
"Start Whisper with BUFFER as its explicit insertion target."
|
|
(with-current-buffer buffer
|
|
(setq-local whisper-insert-text-at-point t)
|
|
|
|
;; These are the variables whisper.el uses asynchronously when
|
|
;; inserting the completed transcription.
|
|
(setq whisper--point-buffer buffer)
|
|
(set-marker whisper--marker (point) buffer)
|
|
|
|
(whisper-run)))
|
|
|
|
;;;###autoload
|
|
(defun whisper-posframe-record ()
|
|
"Open a posframe and start recording with Whisper."
|
|
(interactive)
|
|
(require 'whisper)
|
|
(require 'posframe)
|
|
|
|
(when (or (whisper-recording-p)
|
|
(whisper-transcribing-p))
|
|
(user-error "Whisper is already busy"))
|
|
|
|
(let ((buffer (get-buffer-create whisper-posframe-buffer-name)))
|
|
(setq whisper-posframe--capture-buffer buffer)
|
|
(setq whisper-posframe--parent-frame (selected-frame))
|
|
|
|
(with-current-buffer buffer
|
|
(erase-buffer)
|
|
(whisper-posframe-mode)
|
|
|
|
;; Refresh the visible status every 200 ms.
|
|
(setq-local whisper-posframe--status-timer
|
|
(run-at-time
|
|
0
|
|
0.2
|
|
(lambda (buffer)
|
|
(when (buffer-live-p buffer)
|
|
(with-current-buffer buffer
|
|
(whisper-posframe--update-status))))
|
|
buffer)))
|
|
|
|
;; `:accept-focus t' ensures that C-g, Escape, and the other local
|
|
;; keybindings are received by the posframe.
|
|
(let ((posframe
|
|
(posframe-show
|
|
buffer
|
|
:parent-frame (selected-frame)
|
|
:position (point)
|
|
:poshandler #'posframe-poshandler-frame-center
|
|
:width whisper-posframe-width
|
|
:height whisper-posframe-height
|
|
:internal-border-color "gray50"
|
|
:border-width 2
|
|
:accept-focus t)))
|
|
|
|
;; Focus after posframe-show has created and displayed the child frame.
|
|
(run-at-time
|
|
0 nil
|
|
(lambda ()
|
|
(whisper-posframe--focus buffer posframe))))
|
|
|
|
;; Start recording. `whisper-run' records into the current buffer.
|
|
(whisper-posframe--start-whisper buffer)))
|
|
|
|
(provide 'whisper-posframe)
|
|
;;; whisper-posframe.el ends here
|