diff --git a/init.org b/init.org index 636e87d..54aa6e5 100644 --- a/init.org +++ b/init.org @@ -312,7 +312,11 @@ Helper function for switching themes. ** emacs-related #+begin_src emacs-lisp (setq help-window-select t - help-window-keep-selected t) + help-window-keep-selected t + scroll-conservatively 20 + scroll-margin 3 + scroll-preserve-screen-position t + mouse-wheel-progressive-speed nil) #+end_src ** Fonts Font size [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Face-Attributes.html][(height)]] in emacs works in $\frac{1}{10}$ths of points, so ~110~ is the same as ~11~ points. @@ -483,7 +487,14 @@ Honestly not very happy with this at the moment, but it's kind of hacked togethe esup-depth 1)) #+end_src +** =xref= +#+begin_src emacs-lisp +(use-package xref + :straight (:type built-in) + ) +#+end_src ** =general.el = + TODO: figure out how to get which-key to correctly display the ‘m‘ label #+begin_src emacs-lisp @@ -498,9 +509,10 @@ TODO: figure out how to get which-key to correctly display the ‘m‘ label :straight t :init (define-prefix-command 'nemo/xref-command 'nemo/xref-map "xref") - (define-prefix-command 'nemo/buffers-command 'nemo/buffers-map "Buffers..") - (define-prefix-command 'nemo/windows-command 'nemo/windows-map "Windows..") - (define-prefix-command 'nemo/files-command 'nemo/files-map "Files..") + (define-prefix-command 'nemo/buffers-command 'nemo/buffers-map "Buffers") + (define-prefix-command 'nemo/windows-command 'nemo/windows-map "Windows") + (define-prefix-command 'nemo/files-command 'nemo/files-map "Files") + (define-prefix-command 'nemo/custom-command 'nemo/custom-map "Custom") :config (general-define-key :states '(emacs normal) @@ -530,7 +542,8 @@ TODO: figure out how to get which-key to correctly display the ‘m‘ label "g" '("xref" . nemo/xref-command) "b" '("Buffers" . nemo/buffers-command) "w" '("Windows" . nemo/windows-command) - "f" '("Files" . nemo/files-command)) + "f" '("Files" . nemo/files-command) + "c" '("Custom" . nemo/custom-command)) (general-def :keymaps 'nemo/xref-map @@ -546,7 +559,7 @@ TODO: figure out how to get which-key to correctly display the ‘m‘ label "n" '("Switch to next buffer" . switch-to-next-buffer) "b" '("Consult buffer" . consult-buffer) "d" '("Kill current buffer" . kill-current-buffer) - "s" '("Scratch buffer" . scratch-buffer)) + "s" '("Scratch buffer" . scratch)) (general-def :keymaps 'nemo/windows-map @@ -561,6 +574,10 @@ TODO: figure out how to get which-key to correctly display the ‘m‘ label "r" '("Ripgrep" . consult-ripgrep) "y" '("Yank Current Filename" . nemo/buffer-file-name-to-clipboard) "R" '("Ripgrep in dir" . nemo/consult-ripgrep-dir)) + + (general-def + :keymaps 'nemo/custom-map + "d" '("Whisper record" . whisper-run)) ) #+end_src ** =dired= @@ -647,6 +664,7 @@ Todo: figure out how best to use =image-dired=. :keymaps 'nemo/dired-map "p" '("Preview Files" . dired-preview-mode) "+" '("Create File/Directory" . nemo/dired-create-file-or-directory) + "f" '("Yank File Name" . dired-copy-filename-as-kill) "#" '("Toggle Dotfiles" . dired-omit-mode)) (setopt dired-compress-directory-default-suffix ".tar.zst" @@ -909,7 +927,7 @@ Also use =undo-fu=, which evil can use. "p" '(:keymap project-prefix-map :wk "Projectile") ) :config - (setq projectile-project-search-path '("~/code/")) + (setq projectile-project-search-path '("/var/code/")) (projectile-mode)) #+end_src @@ -934,7 +952,36 @@ Also use =undo-fu=, which evil can use. :straight t) #+end_src + * Stuff + +** =scratch.el= + +#+begin_src emacs-lisp +(use-package scratch + :straight t) +#+end_src + +** Dictation +I want to test out dictation using whisper.el, which uses OpenAI’s whisper model locally. + +#+begin_src emacs-lisp +(use-package whisper + :straight (:type git :host github :repo "natrys/whisper.el") + :bind + (("" . whisper-run)) + :config + (setopt whisper-model "small.en" + whisper-language "en" + ;; whisper--mode-line-recording-indicator "⏺" + whisper-isntall-directory (concat user-emacs-directory "whisper") + whisper-translate nil + whisper-return-cursor 'end + whisper-use-threads (/ (num-processors) 2) + ) + ) +#+end_src + ** Mini-buffer escape #+begin_src emacs-lisp :tangle no (setq minibuffer-prompt-properties @@ -962,7 +1009,7 @@ Also use =undo-fu=, which evil can use. :defer t ;; :bind (:map custom-bindings-map ("C-c o" . olivetti-mode)) :hook (org-mode . olivetti-mode) - :hook (olivetti-mode-on . (lambda () (olivetti-set-width 88))) + :hook (olivetti-mode-on . (lambda () (olivetti-set-width 120))) :config (setq olivetti-style t)) #+end_src @@ -1034,6 +1081,15 @@ PDF tools for latex previewing: ) #+end_src +** Markdown +#+begin_src emacs-lisp +(use-package markdown-mode + :ensure t + :mode ("\\.md\\'" . markdown-mode) + :config + (setq markdown-command "multimarkdown")) +#+end_src + ** Org Taken from [[https://sophiebos.io/posts/prettifying-emacs-org-mode/][ here]]. #+begin_src emacs-lisp @@ -1096,7 +1152,7 @@ Inline images as well. In case LaTeX previews are too small, use this to increase them. #+begin_src emacs-lisp -(plist-put org-format-latex-options :scale 1.35) +(plist-put org-format-latex-options :scale 0.5) #+end_src =pretty-entities= allows for latex symbols to be embedded into org-mode. @@ -1889,6 +1945,17 @@ build formatting on save functionality: ** Eglot Use =eglot= as an alternative to =lsp-mode=: #+begin_src emacs-lisp +(defun nemo/kotlin-lsp-socket (interactively) + "Start kotlin-lsp in socket mode." + (let ((port 9998) + (host "127.0.0.1")) + (unless (ignore-errors (delete-process (open-network-stream "test" nil host port))) + (message "Starting kotlin-lsp in socket mode on %s:%d" host port) + (start-process "kotlin-lsp" "*kotlin-lsp*" + "kotlin-lsp" "--socket" "127.0.0.1:9998") + (sleep-for 5)) + (list host port))) + (use-package eglot :straight t :hook ((rust-mode nix-mode prog-mode) . eglot-ensure) @@ -1903,10 +1970,10 @@ Use =eglot= as an alternative to =lsp-mode=: 'eglot-server-programs '(just-mode "just-lsp")) - (add-to-list - 'eglot-server-programs - '((kotlin-mode kotlin-ts-mode) . - ("kotlin-lsp" "--stdio"))) + (add-to-list + 'eglot-server-programs + '((kotlin-mode kotlin-ts-mode) . + nemo/kotlin-lsp-socket)) (setq-default eglot-workspace-configuration '(:rust-analyzer (:cargo (:buildScripts (:enable t) @@ -1914,21 +1981,31 @@ Use =eglot= as an alternative to =lsp-mode=: :features "all" :extraEnv (:RUSTFLAGS "-Clinker=clang -Clink-arg=-fuse-ld=mold")) :check (:command "clippy") - :completion (:autoimport (:enable t)) + :completion + (:autoimport (:enable t) + :postfix (:enable t) + :snippets + (:custom + (:mem_replace + (:postfix "replace" + :body "mem::replace(&mut ${receiver}, ${0:new_value})" + :description "Replace the value and return the old value" + :requires "core::mem" + :scope "expr")))) :inlayHints ( ;; :maxLength 20 :bindingModeHints (:enable t) :closureReturnTypeHints (:enable t)) - :procMacro (:enable t))) - ) + :procMacro (:enable t)))) (setq eglot-code-action-indications '(margin mode-line) eglot-extend-to-xref t eglot-connect-timeout 60 eglot-request-timeout 60 eglot-extend-to-xref t - eglot-sync-connect 0 + eglot-sync-connect 1 eglot-report-progress t eglot-autoshutdown t + eglot-events-buffer-size 0 ;; eglot-events-buffer-config '(:size 5000 :format short) eglot-max-file-watches 5000 completion-category-overrides '((eglot (styles orderless)) @@ -1955,6 +2032,8 @@ Use =eglot= as an alternative to =lsp-mode=: "= =" '("Format Buffer" . eglot-format-buffer) "= r" '("Format Region" . eglot-format-region) ) + (general-def '(emacs normal) :keymaps 'eglot-mode-map + "C-" 'xref-find-definitions-at-mouse) (leader-major-def :keymaps 'eglot-mode-map "l" '("Eglot" . nemo/eglot-map) @@ -2536,8 +2615,6 @@ delimiters < and >'s." (use-package kotlin-ts-mode :straight t - :after eglot - :after (inheritenv envrc) :mode ("\\.kt\\'" . kotlin-ts-mode) :config (if (fboundp 'eglot-uri-to-path) diff --git a/local-lisp/dired-hide-dotfiles.el b/local-lisp/dired-hide-dotfiles.el new file mode 100644 index 0000000..8e10a7e --- /dev/null +++ b/local-lisp/dired-hide-dotfiles.el @@ -0,0 +1,25 @@ +;;; dired-hide-dotfiles.el --- Hide dotfiles in dired buffers -*- lexical-binding: t; -*- + +(require 'dired) + +(defgroup dired-hide-dotfiles nil + :group 'dired) + +(define-minor-mode dired-hide-dotfiles-mode + :init-value nil + :group 'dired + (if dired-hide-dotfiles-mode + (progn + (add-hook 'dired-after-readin-hook #'dired-hide-dotfiles) + (dired-hide-dotfiles)) + (remove-hook 'dired-after-readin-hook #'dired-hide-dotfiles) + (revert-buffer)) + + (defun dired-hide-dotfiles () + "Hide dotfiles in dired buffer." + (let ((inhibit-message t)) + (dired-mark-files-regexp "^\\.|^.*/\\.") + (dired-do-kill-lines nil "Hid %d dotfiles.")))) + + +(provide 'dired-hide-dotfiles) diff --git a/local-lisp/nemo-whisper.el b/local-lisp/nemo-whisper.el new file mode 100644 index 0000000..0c639a1 --- /dev/null +++ b/local-lisp/nemo-whisper.el @@ -0,0 +1,221 @@ +;;; -*- 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) diff --git a/local-lisp/whisper-posframe.el b/local-lisp/whisper-posframe.el new file mode 100644 index 0000000..55b9799 --- /dev/null +++ b/local-lisp/whisper-posframe.el @@ -0,0 +1,272 @@ +;;; 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 "") #'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