Compare commits

...

4 commits

Author SHA1 Message Date
janis dd8f97d60d
lots innit 2026-10-16 23:45:31 +02:00
janis f96e613ca5
kotlin experiment 2026-08-25 21:34:45 +02:00
janis f779d171fb
minuet experiment 2026-08-25 21:34:26 +02:00
janis be949bbc1e
general config update, small fixes 2026-08-25 21:34:07 +02:00
4 changed files with 738 additions and 43 deletions

263
init.org
View file

@ -210,7 +210,8 @@ Add package repositories and rank them by priority
#+begin_src emacs-lisp #+begin_src emacs-lisp
(setq-default tab-width 4 (setq-default tab-width 4
fill-column 80 fill-column 80
indent-tabs-mode nil) indent-tabs-mode nil
lexical-binding t)
(setq use-short-answers t) (setq use-short-answers t)
(setq initial-major-mode 'org-mode) (setq initial-major-mode 'org-mode)
#+end_src #+end_src
@ -311,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.
@ -482,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
@ -497,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)
@ -529,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
@ -545,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
@ -558,7 +572,12 @@ TODO: figure out how to get which-key to correctly display the ‘m‘ label
"i" '("Edit init.org" . nemo/edit-init-org) "i" '("Edit init.org" . nemo/edit-init-org)
"s" '("Save" . save-buffer) "s" '("Save" . save-buffer)
"r" '("Ripgrep" . consult-ripgrep) "r" '("Ripgrep" . consult-ripgrep)
"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=
@ -645,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"
@ -907,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
@ -932,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
@ -952,7 +1001,6 @@ Also use =undo-fu=, which evil can use.
(global-set-key (kbd "<escape>") 'nemo/abort-minibuffer-if-active) (global-set-key (kbd "<escape>") 'nemo/abort-minibuffer-if-active)
#+end_src #+end_src
* Writing * Writing
** Olivetti ** Olivetti
#+begin_src emacs-lisp #+begin_src emacs-lisp
@ -961,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
@ -984,7 +1032,7 @@ Use =ispell= with company mode for completion in text-modes
:straight t :straight t
:defer t :defer t
:config :config
(setq ispell-dictionary "en_GB")) (setq ispell-dictionary "/etc/profiles/per-user/user/share/hunspell/en_GB"))
#+end_src #+end_src
** LaTeX ** LaTeX
@ -1033,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
@ -1095,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.
@ -1638,7 +1695,6 @@ Use =inheritenv= and =envrc= to load ~.envrc~ direnv files:
** Copilot 😭 ** Copilot 😭
*** Copilot.el *** Copilot.el
#+begin_src emacs-lisp #+begin_src emacs-lisp
(use-package copilot (use-package copilot
:straight (:host github :repo "copilot-emacs/copilot.el" :files ("*.el")) :straight (:host github :repo "copilot-emacs/copilot.el" :files ("*.el"))
@ -1697,6 +1753,45 @@ Use =inheritenv= and =envrc= to load ~.envrc~ direnv files:
) )
#+end_src #+end_src
*** COMMENT =minuet.el=
#+begin_src emacs-lisp
(use-package minuet
:straight t
:hook ((prog-mode markdown-mode conf-toml-mode) . minuet-auto-suggestion-mode)
:bind (("TAB" . minuet-accept-suggestion)
("C-<tab>" . minuet-accept-suggestion)
("M-<tab>" . minuet-accept-suggestion-line))
:init
(define-prefix-command 'nemo/minuet-map)
(setenv "DEEPSEEK_API_KEY" (s-chop-suffix "\n" (shell-command-to-string "rbw get 'Deepseek Minuet'")))
:general-config
(leader-def
"c" '("Minuet" . nemo/minuet-map))
(general-def
:keymaps 'nemo/minuet-map
"c" '("Show Completion" . minuet-show-suggestion)
"C" '("Show Completions" . minuet-complete-with-minibuffer)
"m" '("Configure" . minuet-configure-provider)
:map minuet-active-mode-map
"M-p" 'minuet-previous-suggestion
"M-n" 'minuet-next-suggestion
"M-RET" 'minuet-accept-suggestion
"M-DEL" 'minuet-reject-suggestion
"M-S-RET" 'minuet-accept-suggestion-line)
:config
(setq minuet-provider 'openai-fim-compatible
minuet-auto-suggestion-throttle-delay 0.5
minuet-auto-suggestion-debounce-delay 0.1
minuet-n-completions 1)
(plist-put minuet-openai-fim-compatible-options :end-point "https://api.deepseek.com/beta/completions")
(plist-put minuet-openai-fim-compatible-options :api-key "DEEPSEEK_API_KEY")
(plist-put minuet-openai-fim-compatible-options :model "deepseek-v4-flash")
(minuet-set-optional-options minuet-openai-fim-compatible-options :max_tokens 56)
(minuet-set-optional-options minuet-openai-fim-compatible-options :top_p 0.9))
#+end_src
*** COMMENT Ollama *** COMMENT Ollama
I feel so bad for this.. but its not awful.. I feel so bad for this.. but its not awful..
@ -1814,7 +1909,7 @@ I feel so bad for this.. but its not awful..
) )
#+end_src #+end_src
** COMMENT Yasnippet ** Yasnippet
Auto-completion requires yasnippet for some competions, such as function arguments and parens. Auto-completion requires yasnippet for some competions, such as function arguments and parens.
#+begin_src emacs-lisp #+begin_src emacs-lisp
(use-package yasnippet (use-package yasnippet
@ -1850,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)
@ -1864,22 +1970,44 @@ 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
'eglot-server-programs
'((kotlin-mode kotlin-ts-mode) .
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)
:allFeatures t :allFeatures t
: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
:inlayHints ( (:autoimport (:enable t)
;; :maxLength 20 :postfix (:enable t)
:bindingModeHints (:enable t) :snippets
:closureReturnTypeHints (:enable t)) (:custom
:procMacro (:enable t))) (: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))))
(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-request-timeout 60
eglot-extend-to-xref t
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)) completion-category-overrides '((eglot (styles orderless))
(eglot-capf (styles orderless))) (eglot-capf (styles orderless)))
flymake-show-diagnostics-at-end-of-line 'fancy flymake-show-diagnostics-at-end-of-line 'fancy
@ -1889,21 +2017,23 @@ Use =eglot= as an alternative to =lsp-mode=:
(advice-add 'eglot-completion-at-point :around #'cape-wrap-buster) (advice-add 'eglot-completion-at-point :around #'cape-wrap-buster)
:general-config :general-config
(general-def (general-def
:keymaps 'nemo/eglot-map :keymaps 'nemo/eglot-map
"r r" '("Rename" . eglot-rename) "r r" '("Rename" . eglot-rename)
"a a" '("Code Action" . eglot-code-action-quickfix) "a a" '("Code Action" . eglot-code-action-quickfix)
"a A" '("Code Action (all)" . eglot-code-actions) "a A" '("Code Action (all)" . eglot-code-actions)
"g d" '("Go to Definition" . xref-find-definitions) "g d" '("Go to Definition" . xref-find-definitions)
"g r" '("Go to References" . xref-find-references) "g r" '("Go to References" . xref-find-references)
"g g" '("Go to Declaration" . eglot-find-declaration) "g g" '("Go to Declaration" . eglot-find-declaration)
"g i" '("Go to Implementation" . eglot-find-implementation) "g i" '("Go to Implementation" . eglot-find-implementation)
"g t" '("Go to Type Definition" . eglot-find-typeDefinition) "g t" '("Go to Type Definition" . eglot-find-typeDefinition)
"h h" '("Help" . eldoc-box-help-at-point) "h h" '("Help" . eldoc-box-help-at-point)
"w r" '("Restart Server" . eglot-reconnect) "w r" '("Restart Server" . eglot-reconnect)
"w q" '("Shutdown Server" . eglot-shutdown) "w q" '("Shutdown Server" . eglot-shutdown)
"= =" '("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)
@ -1920,11 +2050,11 @@ Use =eglot= as an alternative to =lsp-mode=:
(use-package consult-eglot (use-package consult-eglot
:straight t :straight t
:after (consult eglot) :after eglot
:config :config
(general-define-key (general-def
:prefix-map 'nemo/eglot-command-map :keymaps 'nemo/eglot-map
"c s" '("Symbols" . consult-eglot-symbols)) "c s" '("Symbols" . consult-eglot-symbols))
) )
(use-package breadcrumb (use-package breadcrumb
@ -2471,6 +2601,42 @@ delimiters < and >'s."
:defer t) :defer t)
#+end_src #+end_src
** Kotlin
#+begin_src emacs-lisp
(defun nemo/eglot-uri-to-path-kotlin-advice (orig-fun uri &rest args)
(if (and (stringp uri) (string-prefix-p "jar:///" uri))
(apply orig-fun (replace-regexp-in-string "^jar:///" "jar:file:///" uri) args)
(apply orig-fun uri args)))
(defun nemo/eglot-path-to-uri-kotlin-advice (orig-fun path &rest args)
(if (and (stringp path) (string-prefix-p "jar:file:///" path))
(apply orig-fun (replace-regexp-in-string "^jar:file:///" "jar:///" path) args)
(apply orig-fun path args)))
(use-package kotlin-ts-mode
:straight t
:mode ("\\.kt\\'" . kotlin-ts-mode)
:config
(if (fboundp 'eglot-uri-to-path)
(progn
(advice-add 'eglot-uri-to-path :around #'nemo/eglot-uri-to-path-kotlin-advice)
(advice-add 'eglot-path-to-uri :around #'nemo/eglot-path-to-uri-kotlin-advice)
)
(progn
(advice-add 'eglot--uri-to-path :around #'nemo/eglot-uri-to-path-kotlin-advice)
(advice-add 'eglot--path-to-uri :around #'nemo/eglot-path-to-uri-kotlin-advice))
)
)
(use-package jarchive
:straight t
:config
(jarchive-mode))
#+end_src
** Web ** Web
#+begin_src emacs-lisp #+begin_src emacs-lisp
(use-package web-mode (use-package web-mode
@ -2568,12 +2734,12 @@ Use =nix-mode= to edit nix files:
(add-to-list 'eglot-server-programs '(scheme-mode . ("guile-lsp-server")))) (add-to-list 'eglot-server-programs '(scheme-mode . ("guile-lsp-server"))))
(use-package geiser (use-package geiser
:deter t :defer t
:custom :custom
(geiser-active-implementations '(guile))) (geiser-active-implementations '(guile)))
(use-package geiser-guile (use-package geiser-guile
:deter t :defer t
:after geiser :after geiser
:custom :custom
(geiser-guile-binary "guile")) (geiser-guile-binary "guile"))
@ -2645,6 +2811,17 @@ Open the current file with a =sudo= tramp:
) )
#+end_src #+end_src
#+begin_src emacs-lisp
(defun nemo/buffer-file-name-to-clipboard ()
"Copy the current buffer's file name to the user's clipboard"
(interactive)
(let* ((filename buffer-file-name))
(message filename)
(if filename
(kill-new (file-truename filename))))
)
#+end_src
* Keybinds * Keybinds
#+begin_src emacs-lisp #+begin_src emacs-lisp
(define-key custom-bindings-map (kbd "C-c l") 'org-store-link) (define-key custom-bindings-map (kbd "C-c l") 'org-store-link)

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