;;; -*- lexical-binding: t -*- (require 'whisper) ;; Optional: install/load posframe if you want floating display. ;; `nemo-whisper-use-posframe' controls whether it is used. (require 'posframe nil t) (defgroup nemo-whisper nil "Whisper recording utilities." :group 'whisper) (defcustom nemo-whisper-use-posframe t "Display the Whisper buffer in a floating posframe when non-nil." :type 'boolean :group 'nemo-whisper) (defcustom nemo-whisper-hide-posframe-after-transcription t "Hide the posframe after copying the transcription to the kill ring." :type 'boolean :group 'nemo-whisper) (defcustom nemo-whisper-posframe-width 80 "Width of the Whisper posframe." :type 'integer :group 'nemo-whisper) (defcustom nemo-whisper-posframe-height 8 "Height of the Whisper posframe." :type 'integer :group 'nemo-whisper) (defvar nemo-whisper--buffer nil "Buffer currently used for Whisper recording.") (defvar nemo-whisper--previous-buffer nil "Buffer that was selected before Whisper capture started.") (defvar nemo-whisper--recording-p nil "Whether Whisper is currently recording.") (defvar nemo-whisper--transcribing-p nil "Whether Whisper is currently transcribing.") (defvar nemo-whisper--posframe-active-p nil "Whether the current capture buffer is displayed in a posframe.") (defvar nemo-whisper--capture-map (let ((map (make-sparse-keymap))) ;; Bind both forms because terminal and GUI Emacs can report Escape ;; differently. (define-key map (kbd "ESC") #'nemo-whisper-escape) (define-key map [escape] #'nemo-whisper-escape) map) "Keymap for a Whisper capture buffer.") (define-minor-mode nemo-whisper-capture-mode "Minor mode used by the Whisper capture buffer." :init-value nil :lighter " Whisper" :keymap nemo-whisper--capture-map) (defun nemo-whisper--display-buffer (buffer) "Display BUFFER normally or in a posframe." (if (and nemo-whisper-use-posframe (fboundp 'posframe-show) (display-graphic-p)) (progn (setq nemo-whisper--posframe-active-p t) (posframe-show buffer :position nil :poshandler #'posframe-poshandler-frame-center :width nemo-whisper-posframe-width :height nemo-whisper-posframe-height :border-width 2 :internal-border-width 8 :internal-border-color "gray50" :accept-focus t) ;; Explicitly focus the child frame. Without this, keyboard input ;; may continue going to the previously selected Emacs frame. (let ((window (get-buffer-window buffer t))) (when (window-live-p window) (select-frame-set-input-focus (window-frame window)) (select-window window)))) (setq nemo-whisper--posframe-active-p nil) (pop-to-buffer buffer))) (defun nemo-whisper--remove-recording-preview () "Remove the recording preview before Whisper inserts its result." (let ((inhibit-read-only t)) (erase-buffer) (goto-char (point-min)))) (defun nemo-whisper--stop-recording () "Stop recording and start Whisper transcription." (when (and nemo-whisper--buffer (buffer-live-p nemo-whisper--buffer) nemo-whisper--recording-p) (setq nemo-whisper--recording-p nil nemo-whisper--transcribing-p t) (with-current-buffer nemo-whisper--buffer ;; Remove “Recording...” so the transcription is the only text ;; inserted into the buffer. (nemo-whisper--remove-recording-preview) (whisper-run)) (message "Recording stopped; transcribing..."))) (defun nemo-whisper--copy-result-to-kill-ring () "Copy the completed transcription to the kill ring." (let ((text (string-trim (buffer-substring-no-properties (point-min) (point-max))))) (unless (string-empty-p text) (kill-new text) (message "Whisper transcription copied to the kill ring")) (setq nemo-whisper--transcribing-p nil) ;; Select the completed transcription. This makes a subsequent ESC ;; return to the previous buffer when the capture buffer remains visible. (goto-char (point-min)) (push-mark (point-max) t t) (when (and nemo-whisper--posframe-active-p nemo-whisper-hide-posframe-after-transcription (fboundp 'posframe-hide)) (posframe-hide (buffer-name))))) (defun nemo-whisper--return-to-previous-buffer () "Return to the buffer active before Whisper capture started." (when (and nemo-whisper--posframe-active-p (fboundp 'posframe-hide) nemo-whisper--buffer) (posframe-hide (buffer-name nemo-whisper--buffer))) (let ((previous-buffer nemo-whisper--previous-buffer)) (setq nemo-whisper--buffer nil nemo-whisper--previous-buffer nil nemo-whisper--recording-p nil nemo-whisper--transcribing-p nil nemo-whisper--posframe-active-p nil) (when (buffer-live-p previous-buffer) (pop-to-buffer previous-buffer)))) (defun nemo-whisper-escape () "Stop recording, or return to the previous buffer after transcription." (interactive) (cond (nemo-whisper--recording-p (nemo-whisper--stop-recording)) (nemo-whisper--transcribing-p (message "Whisper is still transcribing; please wait")) (t (nemo-whisper--return-to-previous-buffer)))) (defun nemo-whisper-record-to-kill-ring () "Record speech and copy the transcription to the kill ring. Invoke this command to create a capture buffer and start recording. Press ESC to stop recording. After transcription completes, press ESC again to return to the previously selected buffer." (interactive) (cond ;; If called while recording, treat the command as a stop command. (nemo-whisper--recording-p (nemo-whisper--stop-recording)) ;; Do not interrupt an active transcription. (nemo-whisper--transcribing-p (message "Whisper is still transcribing; please wait")) ;; Start a new capture. (t (setq nemo-whisper--previous-buffer (window-buffer (selected-window)) nemo-whisper--buffer (generate-new-buffer "*Whisper capture*") nemo-whisper--recording-p t nemo-whisper--transcribing-p nil) (with-current-buffer nemo-whisper--buffer (text-mode) (nemo-whisper-capture-mode 1) ;; Insert the temporary preview text. (insert (propertize "Recording...\n" 'face 'font-lock-warning-face)) ;; Ask Whisper to insert its result into this buffer. (setq-local whisper-insert-text-at-point t) ;; Prevent Whisper from opening another transcription buffer. (setq-local whisper-display-transcription-buffer nil) ;; This hook is buffer-local, so it will not run in Whisper's ;; separate transcription buffer. (add-hook 'whisper-after-insert-hook #'nemo-whisper--copy-result-to-kill-ring nil t)) (nemo-whisper--display-buffer nemo-whisper--buffer) ;; The first invocation starts recording. (with-current-buffer nemo-whisper--buffer (whisper-run)) (message "Recording started; press ESC when finished speaking")))) (provide 'nemo-whisper)