lots innit

This commit is contained in:
janis 2026-10-16 23:45:31 +02:00
parent f96e613ca5
commit dd8f97d60d
Signed by: janis
SSH key fingerprint: SHA256:bB1qbbqmDXZNT0KKD5c2Dfjg53JGhj7B3CFcLIzSqq8
4 changed files with 614 additions and 19 deletions

115
init.org
View file

@ -312,7 +312,11 @@ Helper function for switching themes.
** emacs-related ** emacs-related
#+begin_src emacs-lisp #+begin_src emacs-lisp
(setq help-window-select t (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 #+end_src
** Fonts ** 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. 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)) esup-depth 1))
#+end_src #+end_src
** =xref=
#+begin_src emacs-lisp
(use-package xref
:straight (:type built-in)
)
#+end_src
** =general.el = ** =general.el =
TODO: figure out how to get which-key to correctly display the ‘m‘ label TODO: figure out how to get which-key to correctly display the ‘m‘ label
#+begin_src emacs-lisp #+begin_src emacs-lisp
@ -498,9 +509,10 @@ TODO: figure out how to get which-key to correctly display the ‘m‘ label
:straight t :straight t
:init :init
(define-prefix-command 'nemo/xref-command 'nemo/xref-map "xref") (define-prefix-command 'nemo/xref-command 'nemo/xref-map "xref")
(define-prefix-command 'nemo/buffers-command 'nemo/buffers-map "Buffers..") (define-prefix-command 'nemo/buffers-command 'nemo/buffers-map "Buffers")
(define-prefix-command 'nemo/windows-command 'nemo/windows-map "Windows..") (define-prefix-command 'nemo/windows-command 'nemo/windows-map "Windows")
(define-prefix-command 'nemo/files-command 'nemo/files-map "Files..") (define-prefix-command 'nemo/files-command 'nemo/files-map "Files")
(define-prefix-command 'nemo/custom-command 'nemo/custom-map "Custom")
:config :config
(general-define-key (general-define-key
:states '(emacs normal) :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) "g" '("xref" . nemo/xref-command)
"b" '("Buffers" . nemo/buffers-command) "b" '("Buffers" . nemo/buffers-command)
"w" '("Windows" . nemo/windows-command) "w" '("Windows" . nemo/windows-command)
"f" '("Files" . nemo/files-command)) "f" '("Files" . nemo/files-command)
"c" '("Custom" . nemo/custom-command))
(general-def (general-def
:keymaps 'nemo/xref-map :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) "n" '("Switch to next buffer" . switch-to-next-buffer)
"b" '("Consult buffer" . consult-buffer) "b" '("Consult buffer" . consult-buffer)
"d" '("Kill current buffer" . kill-current-buffer) "d" '("Kill current buffer" . kill-current-buffer)
"s" '("Scratch buffer" . scratch-buffer)) "s" '("Scratch buffer" . scratch))
(general-def (general-def
:keymaps 'nemo/windows-map :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) "r" '("Ripgrep" . consult-ripgrep)
"y" '("Yank Current Filename" . nemo/buffer-file-name-to-clipboard) "y" '("Yank Current Filename" . nemo/buffer-file-name-to-clipboard)
"R" '("Ripgrep in dir" . nemo/consult-ripgrep-dir)) "R" '("Ripgrep in dir" . nemo/consult-ripgrep-dir))
(general-def
:keymaps 'nemo/custom-map
"d" '("Whisper record" . whisper-run))
) )
#+end_src #+end_src
** =dired= ** =dired=
@ -647,6 +664,7 @@ Todo: figure out how best to use =image-dired=.
:keymaps 'nemo/dired-map :keymaps 'nemo/dired-map
"p" '("Preview Files" . dired-preview-mode) "p" '("Preview Files" . dired-preview-mode)
"+" '("Create File/Directory" . nemo/dired-create-file-or-directory) "+" '("Create File/Directory" . nemo/dired-create-file-or-directory)
"f" '("Yank File Name" . dired-copy-filename-as-kill)
"#" '("Toggle Dotfiles" . dired-omit-mode)) "#" '("Toggle Dotfiles" . dired-omit-mode))
(setopt dired-compress-directory-default-suffix ".tar.zst" (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") "p" '(:keymap project-prefix-map :wk "Projectile")
) )
:config :config
(setq projectile-project-search-path '("~/code/")) (setq projectile-project-search-path '("/var/code/"))
(projectile-mode)) (projectile-mode))
#+end_src #+end_src
@ -934,7 +952,36 @@ Also use =undo-fu=, which evil can use.
:straight t) :straight t)
#+end_src #+end_src
* Stuff * 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
(("<f9>" . 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 ** Mini-buffer escape
#+begin_src emacs-lisp :tangle no #+begin_src emacs-lisp :tangle no
(setq minibuffer-prompt-properties (setq minibuffer-prompt-properties
@ -962,7 +1009,7 @@ Also use =undo-fu=, which evil can use.
:defer t :defer t
;; :bind (:map custom-bindings-map ("C-c o" . olivetti-mode)) ;; :bind (:map custom-bindings-map ("C-c o" . olivetti-mode))
:hook (org-mode . 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 :config
(setq olivetti-style t)) (setq olivetti-style t))
#+end_src #+end_src
@ -1034,6 +1081,15 @@ PDF tools for latex previewing:
) )
#+end_src #+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 ** Org
Taken from [[https://sophiebos.io/posts/prettifying-emacs-org-mode/][ here]]. Taken from [[https://sophiebos.io/posts/prettifying-emacs-org-mode/][ here]].
#+begin_src emacs-lisp #+begin_src emacs-lisp
@ -1096,7 +1152,7 @@ Inline images as well.
In case LaTeX previews are too small, use this to increase them. In case LaTeX previews are too small, use this to increase them.
#+begin_src emacs-lisp #+begin_src emacs-lisp
(plist-put org-format-latex-options :scale 1.35) (plist-put org-format-latex-options :scale 0.5)
#+end_src #+end_src
=pretty-entities= allows for latex symbols to be embedded into org-mode. =pretty-entities= allows for latex symbols to be embedded into org-mode.
@ -1889,6 +1945,17 @@ build formatting on save functionality:
** Eglot ** Eglot
Use =eglot= as an alternative to =lsp-mode=: Use =eglot= as an alternative to =lsp-mode=:
#+begin_src emacs-lisp #+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 (use-package eglot
:straight t :straight t
:hook ((rust-mode nix-mode prog-mode) . eglot-ensure) :hook ((rust-mode nix-mode prog-mode) . eglot-ensure)
@ -1903,10 +1970,10 @@ Use =eglot= as an alternative to =lsp-mode=:
'eglot-server-programs 'eglot-server-programs
'(just-mode '(just-mode
"just-lsp")) "just-lsp"))
(add-to-list (add-to-list
'eglot-server-programs 'eglot-server-programs
'((kotlin-mode kotlin-ts-mode) . '((kotlin-mode kotlin-ts-mode) .
("kotlin-lsp" "--stdio"))) nemo/kotlin-lsp-socket))
(setq-default eglot-workspace-configuration (setq-default eglot-workspace-configuration
'(:rust-analyzer '(:rust-analyzer
(:cargo (:buildScripts (:enable t) (:cargo (:buildScripts (:enable t)
@ -1914,21 +1981,31 @@ Use =eglot= as an alternative to =lsp-mode=:
:features "all" :features "all"
:extraEnv (:RUSTFLAGS "-Clinker=clang -Clink-arg=-fuse-ld=mold")) :extraEnv (:RUSTFLAGS "-Clinker=clang -Clink-arg=-fuse-ld=mold"))
:check (:command "clippy") :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 ( :inlayHints (
;; :maxLength 20 ;; :maxLength 20
:bindingModeHints (:enable t) :bindingModeHints (:enable t)
:closureReturnTypeHints (:enable t)) :closureReturnTypeHints (:enable t))
:procMacro (:enable t))) :procMacro (:enable t))))
)
(setq eglot-code-action-indications '(margin mode-line) (setq eglot-code-action-indications '(margin mode-line)
eglot-extend-to-xref t eglot-extend-to-xref t
eglot-connect-timeout 60 eglot-connect-timeout 60
eglot-request-timeout 60 eglot-request-timeout 60
eglot-extend-to-xref t eglot-extend-to-xref t
eglot-sync-connect 0 eglot-sync-connect 1
eglot-report-progress t eglot-report-progress t
eglot-autoshutdown t eglot-autoshutdown t
eglot-events-buffer-size 0
;; eglot-events-buffer-config '(:size 5000 :format short) ;; eglot-events-buffer-config '(:size 5000 :format short)
eglot-max-file-watches 5000 eglot-max-file-watches 5000
completion-category-overrides '((eglot (styles orderless)) completion-category-overrides '((eglot (styles orderless))
@ -1955,6 +2032,8 @@ Use =eglot= as an alternative to =lsp-mode=:
"= =" '("Format Buffer" . eglot-format-buffer) "= =" '("Format Buffer" . eglot-format-buffer)
"= r" '("Format Region" . eglot-format-region) "= r" '("Format Region" . eglot-format-region)
) )
(general-def '(emacs normal) :keymaps 'eglot-mode-map
"C-<down-mouse-1>" 'xref-find-definitions-at-mouse)
(leader-major-def (leader-major-def
:keymaps 'eglot-mode-map :keymaps 'eglot-mode-map
"l" '("Eglot" . nemo/eglot-map) "l" '("Eglot" . nemo/eglot-map)
@ -2536,8 +2615,6 @@ delimiters < and >'s."
(use-package kotlin-ts-mode (use-package kotlin-ts-mode
:straight t :straight t
:after eglot
:after (inheritenv envrc)
:mode ("\\.kt\\'" . kotlin-ts-mode) :mode ("\\.kt\\'" . kotlin-ts-mode)
:config :config
(if (fboundp 'eglot-uri-to-path) (if (fboundp 'eglot-uri-to-path)

View file

@ -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)

221
local-lisp/nemo-whisper.el Normal file
View file

@ -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)

View file

@ -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 "<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