emacs: experiments with review workflow; update some packages

This commit is contained in:
Pavel Korytov 2025-01-23 12:15:30 +03:00
parent 327340a95c
commit 3e4efbae78
3 changed files with 643 additions and 453 deletions

View file

@ -8,7 +8,7 @@
"imagemagick"
"font-gnu-freefont"
"font-gnu-unifont"
"tdlib-1.8.16"
"emacs-telega-sever"
"yt-dlp"
"mpv"
"python-youtube-transcript-api"

View file

@ -1723,6 +1723,7 @@ targets."
(setq lsp-headerline-breadcrumb-enable nil)
(setq lsp-modeline-code-actions-enable nil)
(setq lsp-modeline-diagnostics-enable nil)
(setq lsp-volar-take-over-mode nil)
(add-to-list 'lsp-language-id-configuration '(svelte-mode . "svelte")))
(use-package lsp-ui
@ -2166,7 +2167,7 @@ Returns (<buffer> . <workspace-index>) or nil."
(use-package copilot
:straight (:host github :repo "copilot-emacs/copilot.el")
:commands (copilot-mode)
:if (not (or my/remote-server my/is-termux))
:disabled t
:init
(add-hook 'emacs-startup-hook
(lambda ()
@ -2310,11 +2311,13 @@ Returns (<buffer> . <workspace-index>) or nil."
(add-hook 'web-mode-hook #'my/web-mode-lsp)
(defun my/web-mode-vue-setup (&rest _)
(when (string-match-p (rx ".vue" eos) (buffer-file-name))
(setq-local web-mode-script-padding 0)
(setq-local web-mode-style-padding 0)
(setq-local create-lockfiles nil)
(setq-local web-mode-enable-auto-pairing nil)))
(let ((filename (buffer-file-name)))
(when (and (stringp filename)
(string-match-p (rx ".vue" eos) filename))
(setq-local web-mode-script-padding 0)
(setq-local web-mode-style-padding 0)
(setq-local create-lockfiles nil)
(setq-local web-mode-enable-auto-pairing nil))))
(add-hook 'web-mode-hook 'my/web-mode-vue-setup)
(add-hook 'editorconfig-after-apply-functions 'my/web-mode-vue-setup)
@ -2783,7 +2786,7 @@ Returns (<buffer> . <workspace-index>) or nil."
:config
(setq langtool-language-tool-server-jar "/home/pavel/bin/LanguageTool-6.4/languagetool-server.jar")
(setq langtool-mother-tongue "ru")
(setq langtool-default-language "en-US"))
(setq langtool-default-language "ru-RU"))
(my-leader-def
:infix "L"
@ -3557,6 +3560,7 @@ With ARG, repeats or can move backward if negative."
`((emacs-lisp . t)
(python . t)
(sql . t)
(sqlite . t)
;; (typescript .t)
(hy . t)
(shell . t)
@ -3946,6 +3950,9 @@ With ARG, repeats or can move backward if negative."
(with-eval-after-load 'org
(my-leader-def "ol" #'org-clock-agg))
:config
(setq org-clock-agg-node-format
"%-%(+ title-width)t %20c %8z %s/%S")
(setq org-clock-agg-node-title-width-delta 47)
(push
(cons "Agenda+Archive"
(append
@ -4063,6 +4070,21 @@ With ARG, repeats or can move backward if negative."
:infix "SPC"
"C" #'my/org-clock-recent))
(defun my/org-fix-task-kind ()
(interactive)
(let ((entries (org-ql-query
:select #'element-with-markers
:from (current-buffer)
:where '(and (olp "Tasks")
(not (property "TASK_KIND"))
(clocked)))))
(org-fold-show-all)
(dolist (entry entries)
(let ((marker (org-element-property :org-marker entry)))
(org-with-point-at marker
(let ((value (org-read-property-value "TASK_KIND")))
(org-set-property "TASK_KIND" value)))))))
(use-package org-super-agenda
:straight t
:after (org)
@ -4150,6 +4172,30 @@ TYPE may be `ts', `ts-active', `ts-inactive', `clocked', or
:sort '(priority todo deadline)
:super-groups '((:auto-outline-path-file t)))))
(defun my/org-ql-clocked-today ()
(interactive)
(let ((today (format-time-string
"%Y-%m-%d"
(days-to-time
(- (org-today) (time-to-days 0))))))
(org-ql-search (org-agenda-files) `(clocked :from ,today)
:title "Clocked today"
:sort '(todo priority date)
:super-groups '((:auto-outline-path-file t)
(:auto-todo t)))))
(defun my/org-ql-closed-today ()
(interactive)
(let ((today (format-time-string
"%Y-%m-%d"
(days-to-time
(- (org-today) (time-to-days 0))))))
(org-ql-search (org-agenda-files) `(closed :from ,today)
:title "Closed today"
:sort '(todo priority date)
:super-groups '((:auto-outline-path-file t)
(:auto-todo t)))))
(setq org-ql-views
(list
(cons "Overview: All TODO" #'my/org-ql-all-todo)
@ -4171,13 +4217,8 @@ TYPE may be `ts', `ts-active', `ts-inactive', `clocked', or
:sort '(todo priority date)
:super-groups '((:auto-outline-path-file t))))
(cons "Review: Recently timestamped" #'my/org-ql-view-recent-items)
(cons "Review: Unlinked to meetings"
(list :buffers-files #'org-agenda-files
:query '(and (todo "DONE" "NO")
(not (property "MEETING"))
(ts :from -7))
:super-groups '((:auto-outline-path-file t))))
(cons "Review: Meeting" #'my/org-ql-meeting-tasks)
(cons "Review: Clocked today" #'my/org-ql-clocked-today)
(cons "Review: Closed today" #'my/org-ql-closed-today)
(cons "Fix: tasks without TASK_KIND"
(lambda ()
(interactive)
@ -4522,7 +4563,7 @@ KEYS is a list of cons cells like (<label> . <time>)."
(thread-last
heading
(substring-no-properties)
(replace-regexp-in-string (rx (| "(" "[") (+ alnum) (| "]" ")")) "")
(replace-regexp-in-string (rx (| "(" "[") (+ nonl) (| "]" ")")) "")
(replace-regexp-in-string (rx " " (+ (or digit "."))) " ")
(replace-regexp-in-string (rx (+ " ")) " ")
(string-trim)))
@ -4774,6 +4815,11 @@ KEYS is a list of cons cells like (<label> . <time>)."
(add-hook 'org-journal-after-entry-create-hook
#'my/set-journal-header)
(defun my/org-journal-decrypt ()
"Decrypt the current org journal file."
(interactive)
(org-journal-tags--ensure-decrypted))
(use-package citar
:straight t
:init
@ -4841,21 +4887,61 @@ KEYS is a list of cons cells like (<label> . <time>)."
(setq org-roam-capture-templates
`(("d" "default" plain "%?"
:if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n")
:target (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n")
:unnarrowed t)
("f" "fleeting" plain "%?"
:target (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+filetags: :fleeting:\n")
:unnarrowed t)
("e" "encrypted" plain "%?"
:if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org.gpg" "#+title: ${title}\n")
:target (file+head "%<%Y%m%d%H%M%S>-${slug}.org.gpg" "#+title: ${title}\n")
:unnarrowed t)))
(use-package org-roam-ql
:straight t
:after (org-roam)
:config
(general-define-key
:states '(normal visual)
:keymaps '(org-roam-ql-mode-map)
"s" #'org-roam-ql-buffer-dispatch))
(defun my/org-roam-node-find-permanent (&optional other-window)
(interactive current-prefix-arg)
(org-roam-node-find
other-window
nil
(lambda (node)
(not
(seq-contains-p
"fleeting"
(org-roam-node-tags node))))))
(defun my/org-roam-node-insert-permanent ()
(interactive)
(org-roam-node-insert
(lambda (node)
(not
(seq-contains-p
(org-roam-node-tags node)
"fleeting")))))
(defun my/org-roam-ql-fleeting ()
(interactive)
(org-roam-ql-search
'(tags "fleeting")
"Fleeting notes"))
(with-eval-after-load 'org-roam
(my-leader-def
:infix "or"
"" '(:which-key "org-roam")
"i" #'org-roam-node-insert
"r" #'org-roam-node-find
"i" #'my/org-roam-node-insert-permanent
"r" #'my/org-roam-node-find-permanent
"g" #'org-roam-graph
"c" #'org-roam-capture
"b" #'org-roam-buffer-toggle)
"b" #'org-roam-buffer-toggle
"q" #'org-roam-ql-search
"f" #'my/org-roam-ql-fleeting)
(general-define-key
:keymaps 'org-roam-mode-map
:states '(normal)
@ -4876,7 +4962,8 @@ KEYS is a list of cons cells like (<label> . <time>)."
"a" #'org-roam-alias-add)
(general-define-key
:keymap 'org-mode-map
"C-c i" 'org-roam-node-insert))
"C-c i" #'my/org-roam-node-insert-permanent
"C-c I" #'org-roam-node-insert))
(defface my/org-roam-count-overlay-face
'((t :inherit tooltip))
@ -5122,6 +5209,8 @@ Review checklist:
- [ ] Reconcile ledger
- [ ] Clear [[file:~/Downloads][downloads]] and [[file:~/00-Scratch][scratch]] folders
- [ ] Process [[file:~/30-39 Life/35 Photos/35.00 Inbox/][photo inbox]]
- [ ] Process new [[elisp:(my/org-roam-ql-fleeting)][fleeting notes]] (skip if tired)
- [ ] Process new [[https://wallabag.sqrtminusone.xyz/tag/list/t:zk-inbox][zk-inbox]] (skip if tired)
- [ ] Process [[file:../inbox.org][inbox]]
- [ ] Create [[file:../recurring.org][recurring tasks]] for next week
- [ ] Check agenda (-1 / +2 weeks): priorities, deadlines
@ -5129,6 +5218,7 @@ Review checklist:
- [[org-ql-search:todo%3A?buffers-files=%22org-agenda-files%22&super-groups=%28%28%3Aauto-outline-path-file%20t%29%29&sort=%28priority%20todo%20deadline%29][org-ql-search: All TODOs]]
- [[org-ql-search:(and (todo) (not (tags \"nots\")) (not (ts :from -14)))?buffers-files=%22org-agenda-files%22&super-groups=%28%28%3Aauto-outline-path-file%20t%29%29&sort=%28priority%20todo%20deadline%29][org-ql-search: Stale tasks]]
- [[org-ql-search:todo%3AWAIT?buffers-files=%22org-agenda-files%22&super-groups=%28%28%3Aauto-outline-path-file%20t%29%29&sort=%28priority%20todo%20deadline%29][org-ql-search: WAIT]]
- [[org-ql-search:todo%3AMAYBE?buffers-files=%22org-agenda-files%22&super-groups=%28%28%3Aauto-outline-path-file%20t%29%29&sort=%28priority%20todo%20deadline%29][org-ql-search: MAYBE]]
- [ ] Run auto-archiving
- [ ] Review journal records
")
@ -5150,6 +5240,58 @@ TODO Write something, maybe? "))))
(with-eval-after-load 'org-journal
(my-leader-def "ojw" #'my/org-review-weekly))
(defun my/kill-messengers ()
(interactive)
(when (get-buffer telega-root-buffer-name)
(telega-kill t))
(call-process-shell-command "pkill -f rocketchat-desktop")
(call-process-shell-command "pkill -f 'bwrap --args 36 element'")
(call-process-shell-command "pkill -f element-desktop"))
(defun my/org-review-set-daily-record ()
(save-excursion
(org-journal-tags-prop-apply-delta :add '("review.daily"))
(insert "Daily Review")
(goto-char (point-max))
(insert "
Maintenance checklist (/delete this/):
- [ ] [[elisp:(my/kill-messengers)][Close all messengers]]
- [ ] Process [[file:../inbox.org][inbox]]
- [ ] Check if clocked tasks are properly annotated
- [[elisp:(my/org-ql-clocked-today)][Tasks clocked today]]
- [[elisp:(my/org-ql-closed-today)][Tasks closed today]]
- [ ] Check agenda for the current week
/Remember, all of the following headers are optional./
*** Happened today
Happened to me:
- /Anything interesting?/
Happened to the world:
- /Anything important?/
*** New ideas
/Write them down in org-roam with the \"fleeting\" tag; leave links here. Perhaps note what sparked that idea?/
*** Interactions today
/Any meaninginful interactions, conflicts or tensions?/
*** Emotions today
/How did I feel?/
")))
(defun my/org-review-daily ()
(interactive)
(let ((org-journal-after-entry-create-hook
`(,@org-journal-after-entry-create-hook
my/org-review-set-daily-record)))
(org-journal-new-entry nil)
(org-fold-show-subtree)))
(with-eval-after-load 'org-journal
(my-leader-def "ojd" #'my/org-review-daily))
(use-package org-contacts
:straight (:type git :repo "https://repo.or.cz/org-contacts.git")
:if (not my/remote-server)
@ -5400,7 +5542,7 @@ TODO Write something, maybe? "))))
"M-r" #'wdired-change-to-wdired-mode
"<left>" #'dired-up-directory
"<right>" #'dired-find-file
"M-<return>" #'dired-open-xdg))
"M-<return>" #'my/dired-open-xdg))
(defun my/dired-home ()
"Open dired at $HOME"
@ -5486,7 +5628,9 @@ TODO Write something, maybe? "))))
:hook (dired-mode . (lambda ()
(unless (or (file-remote-p default-directory)
(string-match-p "/gnu/store" default-directory))
(nerd-icons-dired-mode)))))
(nerd-icons-dired-mode))))
:config
(advice-add #'dired-create-empty-file :around #'nerd-icons-dired--refresh-advice))
(use-package dired-open
:straight t
@ -5522,11 +5666,18 @@ TODO Write something, maybe? "))))
:init
(my-leader-def "aa" #'avy-dired-goto-line))
(defun my/dired-rsync--refresh ()
(cl-loop for window being the windows
do (with-current-buffer (window-buffer window)
(when (derived-mode-p 'dired-mode)
(revert-buffer)))))
(use-package dired-rsync
:straight t
:after (dired)
:config
(add-to-list 'global-mode-string '(:eval dired-rsync-modeline-status))
(add-hook 'dired-rsync-success-hook #'my/dired-rsync--refresh)
(general-define-key
:states '(normal)
:keymaps '(dired-mode-map)
@ -5572,6 +5723,14 @@ TODO Write something, maybe? "))))
:keymaps 'dired-mode-map
"H" #'my/dired-goto-project-root))
(defun my/dired-open-xdg ()
"Try to run `xdg-open' to open the file under point."
(interactive)
(when (executable-find "xdg-open")
(let ((file (ignore-errors (dired-get-file-for-visit))))
(start-process "dired-open" nil
"xdg-open" (file-truename file)))))
(defun my/dired-bookmark-open ()
(interactive)
(let ((bookmarks
@ -6994,109 +7153,6 @@ by the `my/elfeed-youtube-subtitles' function."
(setq emms-volume-change-function #'my/set-volume)
(setq emms-volume-change-amount 5)
(use-package ytel
:straight t
:commands (ytel)
:config
(setq ytel-invidious-api-url "https://invidio.xamh.de/")
(general-define-key
:states '(normal)
:keymaps 'ytel-mode-map
"q" #'ytel-quit
"s" #'ytel-search
"L" #'ytel-search-next-page
"H" #'ytel-search-previous-page
"RET" #'my/ytel-add-emms))
(with-eval-after-load 'emms
(define-emms-source ytel (video)
(let ((track (emms-track
'url (concat "https://www.youtube.com/watch?v="
(ytel-video-id video)))))
(emms-track-set track 'info-title (ytel-video-title video))
(emms-track-set track 'info-artist (ytel-video-author video))
(emms-playlist-insert-track track))))
(defun my/ytel-add-emms ()
(interactive)
(emms-add-ytel (ytel-get-current-video)))
(setq my/invidious-instances-url
"https://api.invidious.io/instances.json?pretty=1&sort_by=health")
(defun my/ytel-instances-fetch-json ()
"Fetch list of invidious instances as json, sorted by health."
(let
((url-request-method "GET")
(url-request-extra-headers
'(("Accept" . "application/json"))))
(with-current-buffer
(url-retrieve-synchronously my/invidious-instances-url)
(goto-char (point-min))
(re-search-forward "^$")
(let* ((json-object-type 'alist)
(json-array-type 'list)
(json-key-type 'string))
(json-read)))))
(defun my/ytel-instances-alist-from-json ()
"Make the json of invidious instances into an alist."
(let ((jsonlist (my/ytel-instances-fetch-json))
(inst ()))
(while jsonlist
(push (concat "https://" (caar jsonlist)) inst)
(setq jsonlist (cdr jsonlist)))
(nreverse inst)))
(defun my/ytel-choose-instance ()
"Prompt user to choose an invidious instance to use."
(interactive)
(setq ytel-invidious-api-url
(or (condition-case nil
(completing-read "Using instance: "
(cl-subseq (my/ytel-instances-alist-from-json) 0 11) nil "confirm" "https://")
(error nil))
"https://invidious.synopyta.org")))
(defun my/ytel-draw--buffer-nil-videos-fix ()
(let ((inhibit-read-only t)
(current-line (line-number-at-pos)))
(erase-buffer)
(setf header-line-format
(concat "Search results for "
(propertize ytel-search-term 'face 'ytel-video-published-face)
", page "
(number-to-string ytel-current-page)))
(seq-do
(lambda (v)
(ytel--insert-video v)
(insert "\n"))
(seq-filter
(lambda (v)
(ytel-video-title v))
ytel-videos))
(goto-char (point-min))))
(with-eval-after-load 'ytel
(advice-add #'ytel--draw-buffer :override #'my/ytel-draw--buffer-nil-videos-fix))
(defun my/ytel--format-unknown-fix (fun &rest args)
(if (car args)
(apply fun args)
"unknown "))
(with-eval-after-load 'ytel
(advice-add #'ytel--format-video-length :around #'my/ytel--format-unknown-fix)
(advice-add #'ytel--format-video-published :around #'my/ytel--format-unknown-fix)
(advice-add #'ytel--format-video-views :around #'my/ytel--format-unknown-fix))
(defun my/ytel-kill-url ()
(interactive)
(kill-new
(concat
"https://www.youtube.com/watch?v="
(ytel-video-id (ytel-get-current-video)))))
(defun my/toggle-shr-use-fonts ()
"Toggle the shr-use-fonts variable in buffer"
(interactive)
@ -7206,25 +7262,31 @@ by the `my/elfeed-youtube-subtitles' function."
(setq mastodon-active-user "sqrtminusone")
(my/persp-add-rule mastodon-mode 0 "mastodon")
;; Hide spoilers by default
(setq-default mastodon-toot--content-warning t)
;; (setq-default mastodon-toot--content-warning nil)
(setq mastodon-media--avatar-height 40)
(setq mastodon-tl--timeline-posts-count "40")
(setq mastodon-tl--show-avatars t)
(setq mastodon-tl--horiz-bar
(make-string shr-max-width
(if (char-displayable-p ?―) ?― ?-)))
;; The default emojis take two characters for me
(setq mastodon-tl--symbols
'((reply "" . "R")
(boost "" . "B")
(favourite "" . "F")
(bookmark "" . "K")
(media "" . "[media]")
(verified "" . "V")
(locked "" . "[locked]")
(private "" . "[followers]")
(direct "" . "[direct]")
(edited "" . "[edited]"))))
(mapcar (lambda (item)
(setf (alist-get (car item) mastodon-tl--symbols)
(cdr item)))
'((reply "" . "R")
(boost "" . "B")
(favourite "" . "F")
(bookmark "" . "K")
(media "" . "[media]")
(verified "" . "V")
(locked "" . "[locked]")
(private "" . "[followers]")
(direct "" . "[direct]")
(edited "" . "[edited]"))))
(use-package mastodon-alt
:straight (:host github :repo "rougier/mastodon-alt")
:disabled t
:after (mastodon)
:config
(mastodon-alt-tl-activate))
@ -7362,12 +7424,15 @@ by the `my/elfeed-youtube-subtitles' function."
(lambda (toot)
(and
(or (not hide-replies)
;; Why is the original function inverted??
(mastodon-tl--is-reply toot))
(not (mastodon-tl--is-reply toot)))
(or (not hide-boosts)
(not (alist-get 'reblog toot)))))
toots)))
(mapc #'mastodon-tl--toot toots))))
toots))
(start-pos (point)))
(mapc #'mastodon-tl--toot toots)
(when mastodon-tl--display-media-p
(save-excursion
(mastodon-media--inline-images start-pos (point)))))))
(defun my/mastodon-tl--get-home (hide-replies hide-boosts)
(mastodon-tl--init
@ -7474,8 +7539,8 @@ base toot."
("o" "Thread" mastodon-tl--thread)
("w" "Browser" my/mastodon-toot--browse)
("le" "List edits" mastodon-toot--view-toot-edits)
("lf" "List favouriters" mastodon-toot--list-toot-favouriters)
("lb" "List boosters" mastodon-toot--list-toot-boosters)]
("lf" "List favouriters" mastodon-toot--list-favouriters)
("lb" "List boosters" mastodon-toot--list-boosters)]
["Toot Actions"
:class transient-row
("r" "Reply" mastodon-toot--reply)
@ -7677,6 +7742,8 @@ base toot."
(message "Scrolled %s" scrolled)))))
(use-package telega
;; :straight (:type built-in)
;; For now emacs-telega-server is compatible with the latest telega.el
:straight t
:if (not (or my/remote-server))
:commands (telega)
@ -7688,6 +7755,10 @@ base toot."
(telega-webpage-chat-link :foreground (my/color-value 'base0)
:background (my/color-value 'fg)))
:config
(when (file-directory-p "~/.guix-extra-profiles/emacs/")
(setq telega-server-command
(expand-file-name
"~/.guix-extra-profiles/emacs/emacs/bin/telega-server")))
(setq telega-emoji-use-images nil)
(setq telega-chat-fill-column 80)
(setq telega-completing-read-function #'completing-read)
@ -7744,7 +7815,7 @@ base toot."
(company-mode 1)
(setopt visual-fill-column-width
(+ telega-chat-fill-column
(if (display-graphic-p) 4 5)))
(if (display-graphic-p) 5 6)))
(setq-local split-width-threshold 1))
(add-hook 'telega-chat-mode-hook #'my/telega-chat-setup)
@ -7839,7 +7910,7 @@ base toot."
(setq biome-api-try-parse-error-as-response t))
:config
(add-to-list 'biome-query-coords
'("Saint-Petersburg, Russia" 59.93863 30.31413))
'("Saint-Petersburg, Russia" 59.942651 30.229930))
(add-to-list 'biome-query-coords
'("Tyumen, Russia" 57.15222 65.52722)))
@ -7971,20 +8042,31 @@ base toot."
(setq gptel-backend (gptel-make-ollama "Ollama"
:host "localhost:11434"
:stream t
:models '("llama3.1:latest" "llama3.1:instruct")))
:models '("llama3.1:8b" "deepseek-r1:32b"
"qwen2.5:32b" "qwen2.5-coder:32b"
"eva-qwen2.5-q4_k_l-32b:latest"
"t-pro-1.0-q4_k_m:latest")))
(gptel-make-openai "OpenRouter"
:host "openrouter.ai/api"
:key (lambda () (my/password-store-get-field
"My_Online/Accounts/openrouter" "api-key"))
:stream t
:models '("anthropic/claude-3.5-haiku"))
(setq gptel--known-backends
(seq-filter
(lambda (cell)
(not (equal (car cell) "ChatGPT")))
gptel--known-backends))
(setq gptel-response-prefix-alist
'((markdown-mode . "[Response] ")
(org-mode . "*** Response: ")
(text-mode . "[Response]")))
;; (my/gptel-switch-backend "llama3.1:latest")
(general-define-key
:keymaps '(gptel-mode-map)
:states '(insert normal)
"C-<return>" 'gptel-send)
(general-define-key
:keymaps '(gptel-mode-map)
:states '(normal)
"?" #'gptel-menu)
(gptel-make-gemini "Gemini"
:key (my/password-store-get-field "My_Online/Accounts/google-gemini" "api")
:stream t))
"C-<return>" 'gptel-send
"M-o" #'gptel-menu))
(use-package ellama
:straight t
@ -7995,19 +8077,26 @@ base toot."
(require 'llm-ollama)
;; I've looked for this option for 1.5 hours
(setq ellama-long-lines-length 100000)
(my-leader-def
"aie" '(:wk "ellama" :keymap ellama-command-map))
(setq ellama-provider (make-llm-ollama
:chat-model "llama3.1:instruct"
:embedding-model "llama3.1:instruct"))
:chat-model "qwen2.5:32b"
:embedding-model "qwen2.5:32b"))
(setq ellama-coding-provider (make-llm-ollama
:chat-model "qwen2.5-coder:32b"
:embedding-model "qwen2.5-coder:32b"))
(setq ellama-providers
`(("llama3.1:8b" . ,(make-llm-ollama
:chat-model "llama3.1:latest"
:embedding-model "llama3.1:latest"))
("llama3.1:instruct" . ,(make-llm-ollama
:chat-model "llama3.1:instruct"
:embedding-model "llama3.1:instruct")))))
:chat-model "llama3.1:latest"
:embedding-model "llama3.1:latest"))
("phi4:latest" . ,(make-llm-ollama
:chat-model "phi4:latest"
:embedding-model "phi4:latest"))
("qwen2.5:32b" . ,(make-llm-ollama
:chat-model "qwen2.5:32b"
:embedding-model "qwen2.5:32b"))
("qwen2.5-coder:32b" . ,(make-llm-ollama
:chat-model "qwen2.5-coder:32b"
:embedding-model "qwen2.5-coder:32b")))))
(with-eval-after-load 'ellama
(transient-define-prefix my/ellama-transient ()
@ -8024,9 +8113,7 @@ base toot."
("ci" "Improve" ellama-code-improve)]
["Natural Language"
:class transient-row
("np" "Proof-read" my/ellama-proof-read)
("nw" "Improve wording" my/ellama-improve-wording)
("nc" "Improve conciseness" my/ellama-improve-concise)]
("np" "Proof-read" my/ellama-proof-read)]
["Formatting"
:class transient-row
("ff" "Format" ellama-make-format)
@ -8051,7 +8138,6 @@ base toot."
("sr" "Rename ression" ellama-session-rename)
("sd" "Delete session" ellama-session-remove)]))
(defun my/ellama ()
(interactive)
(require 'ellama)
@ -8076,27 +8162,46 @@ base toot."
(delete-file file1)
(delete-file file2))))
(defun my/ellama-text-with-diff (text is-org-mode prompt)
(require 'ellama)
(defun my/ellama-proof-read--display (text is-org-mode prompt)
(llm-chat-async
ellama-provider
(llm-make-chat-prompt
(format prompt text))
(lambda (changed-text)
(when is-org-mode
(setq changed-text (ellama--translate-markdown-to-org-filter changed-text)))
(let ((buffer (generate-new-buffer "*ellama-diff*")))
(lambda (response)
(let* ((parts (split-string response "-FIXED TEXT ENDS-"))
(changed-text (nth 0 parts))
(comments (nth 1 parts))
(buffer (generate-new-buffer "*ellama-diff*")))
(when is-org-mode
(setq changed-text (ellama--translate-markdown-to-org-filter changed-text)))
(with-current-buffer buffer
(text-mode)
(insert changed-text)
(insert "\n\n")
(insert (my/diff-strings text changed-text)))
(insert
(propertize "Changed text:\n" 'face 'transient-heading)
(string-trim changed-text)
"\n\n"
(propertize "Comments:\n" 'face 'transient-heading)
(string-trim comments)
"\n\n"
(propertize "Diff:\n" 'face 'transient-heading)
(my/diff-strings text changed-text)))
(display-buffer buffer)))
(lambda (&rest err)
(message "Error: %s" err))))
(setq my/ellama-proof-read-prompt
"Proof-read the following text. Fix any errors but keep the original style and punctuation, including linebreaks. Print the changed text and nothing else, not even \"Here's the proof-read text\".\n\n %s")
"Proof-read the following text. Follow these rules:
- Fix all grammar errors
- Keep the original style and punctuation, including linebreaks.
- Use British spelling
- Do not replace ' with , and do not touch other such symbols
Output the following and nothing else:
- The fixed text
- The string -FIXED TEXT ENDS-
- List of found errors
- List of style suggestions
%s")
(defun my/ellama--text ()
(if (region-active-p)
@ -8105,28 +8210,17 @@ base toot."
(defun my/ellama-proof-read (text is-org-mode)
(interactive (list (my/ellama--text) (derived-mode-p 'org-mode)))
(my/ellama-text-with-diff text is-org-mode my/ellama-proof-read-prompt))
(setq my/ellama-improve-wording-prompt
"Proof-read the following text. Fix any errors and improve wording. Print the changed text and nothing else, not even \"Here's the improved text\".\n\n %s")
(defun my/ellama-improve-wording (text is-org-mode)
(interactive (list (my/ellama--text) (derived-mode-p 'org-mode)))
(my/ellama-text-with-diff text is-org-mode my/ellama-improve-wording-prompt))
(setq my/ellama-improve-concise-prompt
"Make the following text more concise. Print the changed text and nothing else, not even \"Here's the improved text\".\n\n %s")
(defun my/ellama-improve-concise (text is-org-mode)
(interactive (list (my/ellama--text) (derived-mode-p 'org-mode)))
(my/ellama-text-with-diff text is-org-mode my/ellama-improve-concise-prompt))
(require 'ellama)
(my/ellama-proof-read--display text is-org-mode my/ellama-proof-read-prompt))
(defun my/whisper--format-vtt-seconds (seconds)
(let* ((hours (/ (floor seconds) (* 60 60)))
(minutes (/ (- (floor seconds) (* hours 60 60)) 60))
(sec (% (floor seconds) 60))
(ms (floor (* 1000 (- seconds (floor seconds))))))
(format "%.2d:%.2d:%.2d.%.3d" hours minutes sec ms)))
(if (numberp seconds)
(let* ((hours (/ (floor seconds) (* 60 60)))
(minutes (/ (- (floor seconds) (* hours 60 60)) 60))
(sec (% (floor seconds) 60))
(ms (floor (* 1000 (- seconds (floor seconds))))))
(format "%.2d:%.2d:%.2d.%.3d" hours minutes sec ms))
""))
(defun my/whisper--save-chucks-vtt (path data)
(with-temp-file path

602
Emacs.org
View file

@ -44,6 +44,7 @@ I decided not to keep configs for features that I do not use anymore because thi
| Feature | Last commit |
|--------------------------+------------------------------------------|
| ytel | 327340a95c4ff9cffd171f6bd937c6041f63add7 |
| org-roam dailies | d2648918fcc338bd5c1cd6d5c0aa60a65077ccf7 |
| org-roam projects | 025278a1e180e86f3aade20242e4ac1cdc1a2f13 |
| treemacs | 3d87852745caacc0863c747f1fa9871d367240d2 |
@ -2639,6 +2640,7 @@ References:
(setq lsp-headerline-breadcrumb-enable nil)
(setq lsp-modeline-code-actions-enable nil)
(setq lsp-modeline-diagnostics-enable nil)
(setq lsp-volar-take-over-mode nil)
(add-to-list 'lsp-language-id-configuration '(svelte-mode . "svelte")))
(use-package lsp-ui
@ -3210,7 +3212,7 @@ A general-purpose package to run formatters on files. While the most popular for
(use-package copilot
:straight (:host github :repo "copilot-emacs/copilot.el")
:commands (copilot-mode)
:if (not (or my/remote-server my/is-termux))
:disabled t
:init
(add-hook 'emacs-startup-hook
(lambda ()
@ -3392,11 +3394,13 @@ Hooking this up with lsp.
Vue settings
#+begin_src emacs-lisp
(defun my/web-mode-vue-setup (&rest _)
(when (string-match-p (rx ".vue" eos) (buffer-file-name))
(setq-local web-mode-script-padding 0)
(setq-local web-mode-style-padding 0)
(setq-local create-lockfiles nil)
(setq-local web-mode-enable-auto-pairing nil)))
(let ((filename (buffer-file-name)))
(when (and (stringp filename)
(string-match-p (rx ".vue" eos) filename))
(setq-local web-mode-script-padding 0)
(setq-local web-mode-style-padding 0)
(setq-local create-lockfiles nil)
(setq-local web-mode-enable-auto-pairing nil))))
(add-hook 'web-mode-hook 'my/web-mode-vue-setup)
(add-hook 'editorconfig-after-apply-functions 'my/web-mode-vue-setup)
@ -3813,7 +3817,7 @@ References:
:config
(setq langtool-language-tool-server-jar "/home/pavel/bin/LanguageTool-6.4/languagetool-server.jar")
(setq langtool-mother-tongue "ru")
(setq langtool-default-language "en-US"))
(setq langtool-default-language "ru-RU"))
(my-leader-def
:infix "L"
@ -4950,6 +4954,7 @@ Enable languages
`((emacs-lisp . t)
(python . t)
(sql . t)
(sqlite . t)
;; (typescript .t)
(hy . t)
(shell . t)
@ -5510,6 +5515,9 @@ It's been somewhat complicated to integrate into my workflow, but I think it's b
(with-eval-after-load 'org
(my-leader-def "ol" #'org-clock-agg))
:config
(setq org-clock-agg-node-format
"%-%(+ title-width)t %20c %8z %s/%S")
(setq org-clock-agg-node-title-width-delta 47)
(push
(cons "Agenda+Archive"
(append
@ -5658,6 +5666,23 @@ And use the function to set the total clocked time.
:infix "SPC"
"C" #'my/org-clock-recent))
#+end_src
***** Fix tasks without TASK_KIND
#+begin_src emacs-lisp
(defun my/org-fix-task-kind ()
(interactive)
(let ((entries (org-ql-query
:select #'element-with-markers
:from (current-buffer)
:where '(and (olp "Tasks")
(not (property "TASK_KIND"))
(clocked)))))
(org-fold-show-all)
(dolist (entry entries)
(let ((marker (org-element-property :org-marker entry)))
(org-with-point-at marker
(let ((value (org-read-property-value "TASK_KIND")))
(org-set-property "TASK_KIND" value)))))))
#+end_src
**** org-super-agenda
[[https://github.com/alphapapa/org-super-agenda][org-super-agenda]] is alphapapa's extension to group items in org-agenda. I don't use it instead of the standard agenda, but =org-ql= uses it for some of its views.
@ -5766,6 +5791,37 @@ A view to return all TODOs in a category.
:super-groups '((:auto-outline-path-file t)))))
#+end_src
***** Items clocked or closed today
Some custom functions to account for =org-extend-today-until=. Needed because sometimes my daily reviews cross 00:00.
#+begin_src emacs-lisp
(defun my/org-ql-clocked-today ()
(interactive)
(let ((today (format-time-string
"%Y-%m-%d"
(days-to-time
(- (org-today) (time-to-days 0))))))
(org-ql-search (org-agenda-files) `(clocked :from ,today)
:title "Clocked today"
:sort '(todo priority date)
:super-groups '((:auto-outline-path-file t)
(:auto-todo t)))))
#+end_src
#+begin_src emacs-lisp
(defun my/org-ql-closed-today ()
(interactive)
(let ((today (format-time-string
"%Y-%m-%d"
(days-to-time
(- (org-today) (time-to-days 0))))))
(org-ql-search (org-agenda-files) `(closed :from ,today)
:title "Closed today"
:sort '(todo priority date)
:super-groups '((:auto-outline-path-file t)
(:auto-todo t)))))
#+end_src
***** Configuring views
Putting all the above in =org-ql-views=.
@ -5791,13 +5847,8 @@ Putting all the above in =org-ql-views=.
:sort '(todo priority date)
:super-groups '((:auto-outline-path-file t))))
(cons "Review: Recently timestamped" #'my/org-ql-view-recent-items)
(cons "Review: Unlinked to meetings"
(list :buffers-files #'org-agenda-files
:query '(and (todo "DONE" "NO")
(not (property "MEETING"))
(ts :from -7))
:super-groups '((:auto-outline-path-file t))))
(cons "Review: Meeting" #'my/org-ql-meeting-tasks)
(cons "Review: Clocked today" #'my/org-ql-clocked-today)
(cons "Review: Closed today" #'my/org-ql-closed-today)
(cons "Fix: tasks without TASK_KIND"
(lambda ()
(interactive)
@ -6221,7 +6272,7 @@ First, I need to find and group and such headers. =org-ql= can help with that:
(thread-last
heading
(substring-no-properties)
(replace-regexp-in-string (rx (| "(" "[") (+ alnum) (| "]" ")")) "")
(replace-regexp-in-string (rx (| "(" "[") (+ nonl) (| "]" ")")) "")
(replace-regexp-in-string (rx " " (+ (or digit "."))) " ")
(replace-regexp-in-string (rx (+ " ")) " ")
(string-trim)))
@ -6542,6 +6593,15 @@ And here's the function that creates a drawer with such information. At the mome
(add-hook 'org-journal-after-entry-create-hook
#'my/set-journal-header)
#+end_src
Also, a function to decrypt the current file:
#+begin_src emacs-lisp
(defun my/org-journal-decrypt ()
"Decrypt the current org journal file."
(interactive)
(org-journal-tags--ensure-decrypted))
#+end_src
*** Bibliography
I use [[https://www.zotero.org/][Zotero]] to manage my bibliograhy.
@ -6668,12 +6728,63 @@ Capture templates for =org-roam-capture=. As for now, nothing too complicated he
#+begin_src emacs-lisp
(setq org-roam-capture-templates
`(("d" "default" plain "%?"
:if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n")
:target (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n")
:unnarrowed t)
("f" "fleeting" plain "%?"
:target (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+filetags: :fleeting:\n")
:unnarrowed t)
("e" "encrypted" plain "%?"
:if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org.gpg" "#+title: ${title}\n")
:target (file+head "%<%Y%m%d%H%M%S>-${slug}.org.gpg" "#+title: ${title}\n")
:unnarrowed t)))
#+end_src
**** org-roam-ql
[[https://github.com/ahmed-shariff/org-roam-ql][org-roam-ql]] is a package to query =org-roam= files like =org-ql=.
#+begin_src emacs-lisp
(use-package org-roam-ql
:straight t
:after (org-roam)
:config
(general-define-key
:states '(normal visual)
:keymaps '(org-roam-ql-mode-map)
"s" #'org-roam-ql-buffer-dispatch))
#+end_src
**** Finding nodes
Find and insert permanent nodes:
#+begin_src emacs-lisp
(defun my/org-roam-node-find-permanent (&optional other-window)
(interactive current-prefix-arg)
(org-roam-node-find
other-window
nil
(lambda (node)
(not
(seq-contains-p
"fleeting"
(org-roam-node-tags node))))))
(defun my/org-roam-node-insert-permanent ()
(interactive)
(org-roam-node-insert
(lambda (node)
(not
(seq-contains-p
(org-roam-node-tags node)
"fleeting")))))
#+end_src
List unprocessed fleeting notes:
#+begin_src emacs-lisp
(defun my/org-roam-ql-fleeting ()
(interactive)
(org-roam-ql-search
'(tags "fleeting")
"Fleeting notes"))
#+end_src
**** Keybindings
A set of keybindings to quickly access things in Org Roam.
@ -6682,11 +6793,13 @@ A set of keybindings to quickly access things in Org Roam.
(my-leader-def
:infix "or"
"" '(:which-key "org-roam")
"i" #'org-roam-node-insert
"r" #'org-roam-node-find
"i" #'my/org-roam-node-insert-permanent
"r" #'my/org-roam-node-find-permanent
"g" #'org-roam-graph
"c" #'org-roam-capture
"b" #'org-roam-buffer-toggle)
"b" #'org-roam-buffer-toggle
"q" #'org-roam-ql-search
"f" #'my/org-roam-ql-fleeting)
(general-define-key
:keymaps 'org-roam-mode-map
:states '(normal)
@ -6707,7 +6820,8 @@ A set of keybindings to quickly access things in Org Roam.
"a" #'org-roam-alias-add)
(general-define-key
:keymap 'org-mode-map
"C-c i" 'org-roam-node-insert))
"C-c i" #'my/org-roam-node-insert-permanent
"C-c I" #'org-roam-node-insert))
#+end_src
**** Backlinks count display
Occasionally I want to see how many backlinks a particular page has.
@ -7007,7 +7121,7 @@ I'll use data from git to get the list of what I've been working on. The directo
(org-roam-node-title (cdr c)))))
(apply #'concat)))))
#+end_src
**** Org Journal integration
**** General review logic
#+begin_src emacs-lisp
(defun my/org-review-get-last-review-date (kind)
(let* ((start-of-day (- (time-convert nil #'integer)
@ -7027,9 +7141,7 @@ I'll use data from git to get the list of what I've been working on. The directo
(pcase kind
('weekly (- start-of-day (* 7 24 60 60)))))))
#+end_src
**** Weekly review
#+begin_src emacs-lisp
(defun my/org-review-set-weekly-record ()
(save-excursion
@ -7049,6 +7161,8 @@ Review checklist:
- [ ] Reconcile ledger
- [ ] Clear [[file:~/Downloads][downloads]] and [[file:~/00-Scratch][scratch]] folders
- [ ] Process [[file:~/30-39 Life/35 Photos/35.00 Inbox/][photo inbox]]
- [ ] Process new [[elisp:(my/org-roam-ql-fleeting)][fleeting notes]] (skip if tired)
- [ ] Process new [[https://wallabag.sqrtminusone.xyz/tag/list/t:zk-inbox][zk-inbox]] (skip if tired)
- [ ] Process [[file:../inbox.org][inbox]]
- [ ] Create [[file:../recurring.org][recurring tasks]] for next week
- [ ] Check agenda (-1 / +2 weeks): priorities, deadlines
@ -7056,6 +7170,7 @@ Review checklist:
- [[org-ql-search:todo%3A?buffers-files=%22org-agenda-files%22&super-groups=%28%28%3Aauto-outline-path-file%20t%29%29&sort=%28priority%20todo%20deadline%29][org-ql-search: All TODOs]]
- [[org-ql-search:(and (todo) (not (tags \"nots\")) (not (ts :from -14)))?buffers-files=%22org-agenda-files%22&super-groups=%28%28%3Aauto-outline-path-file%20t%29%29&sort=%28priority%20todo%20deadline%29][org-ql-search: Stale tasks]]
- [[org-ql-search:todo%3AWAIT?buffers-files=%22org-agenda-files%22&super-groups=%28%28%3Aauto-outline-path-file%20t%29%29&sort=%28priority%20todo%20deadline%29][org-ql-search: WAIT]]
- [[org-ql-search:todo%3AMAYBE?buffers-files=%22org-agenda-files%22&super-groups=%28%28%3Aauto-outline-path-file%20t%29%29&sort=%28priority%20todo%20deadline%29][org-ql-search: MAYBE]]
- [ ] Run auto-archiving
- [ ] Review journal records
")
@ -7079,6 +7194,70 @@ TODO Write something, maybe? "))))
(with-eval-after-load 'org-journal
(my-leader-def "ojw" #'my/org-review-weekly))
#+end_src
**** Daily review
My attempt at a daily review, or an end-of-day routine.
I try to keep it under 10-15 minutes.
#+begin_src emacs-lisp
(defun my/kill-messengers ()
(interactive)
(when (get-buffer telega-root-buffer-name)
(telega-kill t))
(call-process-shell-command "pkill -f rocketchat-desktop")
(call-process-shell-command "pkill -f 'bwrap --args 36 element'")
(call-process-shell-command "pkill -f element-desktop"))
#+end_src
#+begin_src emacs-lisp
(defun my/org-review-set-daily-record ()
(save-excursion
(org-journal-tags-prop-apply-delta :add '("review.daily"))
(insert "Daily Review")
(goto-char (point-max))
(insert "
Maintenance checklist (/delete this/):
- [ ] [[elisp:(my/kill-messengers)][Close all messengers]]
- [ ] Process [[file:../inbox.org][inbox]]
- [ ] Check if clocked tasks are properly annotated
- [[elisp:(my/org-ql-clocked-today)][Tasks clocked today]]
- [[elisp:(my/org-ql-closed-today)][Tasks closed today]]
- [ ] Check agenda for the current week
/Remember, all of the following headers are optional./
,*** Happened today
Happened to me:
- /Anything interesting?/
Happened to the world:
- /Anything important?/
,*** New ideas
/Write them down in org-roam with the \"fleeting\" tag; leave links here. Perhaps note what sparked that idea?/
,*** Interactions today
/Any meaninginful interactions, conflicts or tensions?/
,*** Emotions today
/How did I feel?/
")))
#+end_src
#+begin_src emacs-lisp
(defun my/org-review-daily ()
(interactive)
(let ((org-journal-after-entry-create-hook
`(,@org-journal-after-entry-create-hook
my/org-review-set-daily-record)))
(org-journal-new-entry nil)
(org-fold-show-subtree)))
#+end_src
#+begin_src emacs-lisp
(with-eval-after-load 'org-journal
(my-leader-def "ojd" #'my/org-review-daily))
#+end_src
*** Contacts
=org-contacts= is a package to store contacts in an org file.
@ -7521,7 +7700,7 @@ My config mostly follows ranger's and vifm's keybindings which I'm used to.
"M-r" #'wdired-change-to-wdired-mode
"<left>" #'dired-up-directory
"<right>" #'dired-find-file
"M-<return>" #'dired-open-xdg))
"M-<return>" #'my/dired-open-xdg))
(defun my/dired-home ()
"Open dired at $HOME"
@ -7630,7 +7809,9 @@ Display icons for files.
:hook (dired-mode . (lambda ()
(unless (or (file-remote-p default-directory)
(string-match-p "/gnu/store" default-directory))
(nerd-icons-dired-mode)))))
(nerd-icons-dired-mode))))
:config
(advice-add #'dired-create-empty-file :around #'nerd-icons-dired--refresh-advice))
#+end_src
Provides stuff like =dired-open-xdg=
@ -7682,13 +7863,20 @@ Display git info, such as the last commit for file and stuff. It's pretty useful
(my-leader-def "aa" #'avy-dired-goto-line))
#+end_src
[[https://github.com/stsquad/dired-rsync][dired-rsync]] allows using =rsync= instead of the default synchronous copy operation.
[[https://github.com/stsquad/dired-rsync][dired-rsync]] allows using =rsync= instead of the default synchronous copy operation. The only trouble is that it doesn't replace =dired-do-copy= completely, so...
#+begin_src emacs-lisp
(defun my/dired-rsync--refresh ()
(cl-loop for window being the windows
do (with-current-buffer (window-buffer window)
(when (derived-mode-p 'dired-mode)
(revert-buffer)))))
(use-package dired-rsync
:straight t
:after (dired)
:config
(add-to-list 'global-mode-string '(:eval dired-rsync-modeline-status))
(add-hook 'dired-rsync-success-hook #'my/dired-rsync--refresh)
(general-define-key
:states '(normal)
:keymaps '(dired-mode-map)
@ -7744,6 +7932,19 @@ Goto project root.
:keymaps 'dired-mode-map
"H" #'my/dired-goto-project-root))
#+end_src
Open a file with =xdg-open=. I used =dired-open= for this before, but I've had to abandon the package because it switched from =start-process= to =call-process=, which blocks my EXWM.
#+begin_src emacs-lisp
(defun my/dired-open-xdg ()
"Try to run `xdg-open' to open the file under point."
(interactive)
(when (executable-find "xdg-open")
(let ((file (ignore-errors (dired-get-file-for-visit))))
(start-process "dired-open" nil
"xdg-open" (file-truename file)))))
#+end_src
*** Bookmarks
A simple bookmark list for Dired, mainly to use with TRAMP. I may look into a proper bookmarking system later.
@ -9736,142 +9937,6 @@ The list will be in reverse order."
(setq alists (cons alist alists)))
alists)))
#+end_src
*** ytel
[[https://github.com/gRastello/ytel][ytel]] is a YouTube (actually Invidious) frontend, which lets one search YouTube (whereas the setup with elfeed just lets one view the pre-defined subscriptions).
**** Package config
The package doesn't provide evil bindings, so I define my own.
#+begin_src emacs-lisp
(use-package ytel
:straight t
:commands (ytel)
:config
(setq ytel-invidious-api-url "https://invidio.xamh.de/")
(general-define-key
:states '(normal)
:keymaps 'ytel-mode-map
"q" #'ytel-quit
"s" #'ytel-search
"L" #'ytel-search-next-page
"H" #'ytel-search-previous-page
"RET" #'my/ytel-add-emms))
#+end_src
**** EMMS integration
And here is the same kind of integration with EMMS as in the elfeed setup:
#+begin_src emacs-lisp
(with-eval-after-load 'emms
(define-emms-source ytel (video)
(let ((track (emms-track
'url (concat "https://www.youtube.com/watch?v="
(ytel-video-id video)))))
(emms-track-set track 'info-title (ytel-video-title video))
(emms-track-set track 'info-artist (ytel-video-author video))
(emms-playlist-insert-track track))))
(defun my/ytel-add-emms ()
(interactive)
(emms-add-ytel (ytel-get-current-video)))
#+end_src
**** Choosing instances
Invidious instances aren't particularly reliable, but there plenty of them, and there's an API at =invidious.io= that returns the available instances and their health, so we can use that.
Inspired by [[https://github.com/grastello/ytel/issues/17#issuecomment-801745429][this comment]].
#+begin_src emacs-lisp
(setq my/invidious-instances-url
"https://api.invidious.io/instances.json?pretty=1&sort_by=health")
#+end_src
#+begin_src emacs-lisp
(defun my/ytel-instances-fetch-json ()
"Fetch list of invidious instances as json, sorted by health."
(let
((url-request-method "GET")
(url-request-extra-headers
'(("Accept" . "application/json"))))
(with-current-buffer
(url-retrieve-synchronously my/invidious-instances-url)
(goto-char (point-min))
(re-search-forward "^$")
(let* ((json-object-type 'alist)
(json-array-type 'list)
(json-key-type 'string))
(json-read)))))
(defun my/ytel-instances-alist-from-json ()
"Make the json of invidious instances into an alist."
(let ((jsonlist (my/ytel-instances-fetch-json))
(inst ()))
(while jsonlist
(push (concat "https://" (caar jsonlist)) inst)
(setq jsonlist (cdr jsonlist)))
(nreverse inst)))
(defun my/ytel-choose-instance ()
"Prompt user to choose an invidious instance to use."
(interactive)
(setq ytel-invidious-api-url
(or (condition-case nil
(completing-read "Using instance: "
(cl-subseq (my/ytel-instances-alist-from-json) 0 11) nil "confirm" "https://")
(error nil))
"https://invidious.synopyta.org")))
#+end_src
**** Some fixes
At some point in the last 2 years, Invidious started to return videos with =null= fields. I have no idea what causes that, but I suspect it's related to YouTube Music.
=ytel= hasn't been updated in these two years, so it doesn't account for that change.
So, let's skip videos with null titles.
#+begin_src emacs-lisp
(defun my/ytel-draw--buffer-nil-videos-fix ()
(let ((inhibit-read-only t)
(current-line (line-number-at-pos)))
(erase-buffer)
(setf header-line-format
(concat "Search results for "
(propertize ytel-search-term 'face 'ytel-video-published-face)
", page "
(number-to-string ytel-current-page)))
(seq-do
(lambda (v)
(ytel--insert-video v)
(insert "\n"))
(seq-filter
(lambda (v)
(ytel-video-title v))
ytel-videos))
(goto-char (point-min))))
(with-eval-after-load 'ytel
(advice-add #'ytel--draw-buffer :override #'my/ytel-draw--buffer-nil-videos-fix))
#+end_src
And render other potentially =null= fields as "unknown".
#+begin_src emacs-lisp
(defun my/ytel--format-unknown-fix (fun &rest args)
(if (car args)
(apply fun args)
"unknown "))
(with-eval-after-load 'ytel
(advice-add #'ytel--format-video-length :around #'my/ytel--format-unknown-fix)
(advice-add #'ytel--format-video-published :around #'my/ytel--format-unknown-fix)
(advice-add #'ytel--format-video-views :around #'my/ytel--format-unknown-fix))
#+end_src
**** Some functions
Also, a function to copy a URL to the video under cursor.
#+begin_src emacs-lisp
(defun my/ytel-kill-url ()
(interactive)
(kill-new
(concat
"https://www.youtube.com/watch?v="
(ytel-video-id (ytel-get-current-video)))))
#+end_src
*** EWW
Emacs built-in web browser. +I wonder if anyone actually uses it.+
@ -10019,25 +10084,31 @@ The default UI is rather rough, but Nicolas Rougier's [[https://github.com/rougi
(setq mastodon-active-user "sqrtminusone")
(my/persp-add-rule mastodon-mode 0 "mastodon")
;; Hide spoilers by default
(setq-default mastodon-toot--content-warning t)
;; (setq-default mastodon-toot--content-warning nil)
(setq mastodon-media--avatar-height 40)
(setq mastodon-tl--timeline-posts-count "40")
(setq mastodon-tl--show-avatars t)
(setq mastodon-tl--horiz-bar
(make-string shr-max-width
(if (char-displayable-p ?―) ?― ?-)))
;; The default emojis take two characters for me
(setq mastodon-tl--symbols
'((reply "" . "R")
(boost "" . "B")
(favourite "" . "F")
(bookmark "" . "K")
(media "" . "[media]")
(verified "" . "V")
(locked "" . "[locked]")
(private "" . "[followers]")
(direct "" . "[direct]")
(edited "" . "[edited]"))))
(mapcar (lambda (item)
(setf (alist-get (car item) mastodon-tl--symbols)
(cdr item)))
'((reply "" . "R")
(boost "" . "B")
(favourite "" . "F")
(bookmark "" . "K")
(media "" . "[media]")
(verified "" . "V")
(locked "" . "[locked]")
(private "" . "[followers]")
(direct "" . "[direct]")
(edited "" . "[edited]"))))
(use-package mastodon-alt
:straight (:host github :repo "rougier/mastodon-alt")
:disabled t
:after (mastodon)
:config
(mastodon-alt-tl-activate))
@ -10201,12 +10272,15 @@ So here's a custom update function:
(lambda (toot)
(and
(or (not hide-replies)
;; Why is the original function inverted??
(mastodon-tl--is-reply toot))
(not (mastodon-tl--is-reply toot)))
(or (not hide-boosts)
(not (alist-get 'reblog toot)))))
toots)))
(mapc #'mastodon-tl--toot toots))))
toots))
(start-pos (point)))
(mapc #'mastodon-tl--toot toots)
(when mastodon-tl--display-media-p
(save-excursion
(mastodon-media--inline-images start-pos (point)))))))
#+end_src
In order to use it, the function has to be passed to =mastodon-tl--init=:
@ -10336,8 +10410,8 @@ And the prefix itself:
("o" "Thread" mastodon-tl--thread)
("w" "Browser" my/mastodon-toot--browse)
("le" "List edits" mastodon-toot--view-toot-edits)
("lf" "List favouriters" mastodon-toot--list-toot-favouriters)
("lb" "List boosters" mastodon-toot--list-toot-boosters)]
("lf" "List favouriters" mastodon-toot--list-favouriters)
("lb" "List boosters" mastodon-toot--list-boosters)]
["Toot Actions"
:class transient-row
("r" "Reply" mastodon-toot--reply)
@ -10577,14 +10651,16 @@ Or you can load up Element for a moment to see what the mention was, if that's e
*** Telega
[[https://github.com/zevlg/telega.el/][telega.el]] is a Telegam client for Emacs.
| Guix dependency |
|-------------------|
| tdlib-1.8.16 |
| font-gnu-unifont |
| font-gnu-freefont |
| Guix dependency |
|--------------------|
| emacs-telega-sever |
| font-gnu-unifont |
| font-gnu-freefont |
#+begin_src emacs-lisp
(use-package telega
;; :straight (:type built-in)
;; For now emacs-telega-server is compatible with the latest telega.el
:straight t
:if (not (or my/remote-server))
:commands (telega)
@ -10596,6 +10672,10 @@ Or you can load up Element for a moment to see what the mention was, if that's e
(telega-webpage-chat-link :foreground (my/color-value 'base0)
:background (my/color-value 'fg)))
:config
(when (file-directory-p "~/.guix-extra-profiles/emacs/")
(setq telega-server-command
(expand-file-name
"~/.guix-extra-profiles/emacs/emacs/bin/telega-server")))
(setq telega-emoji-use-images nil)
(setq telega-chat-fill-column 80)
(setq telega-completing-read-function #'completing-read)
@ -10661,7 +10741,7 @@ Configuring company backends for the chat buffer, as recommended in the manual:
(company-mode 1)
(setopt visual-fill-column-width
(+ telega-chat-fill-column
(if (display-graphic-p) 4 5)))
(if (display-graphic-p) 5 6)))
(setq-local split-width-threshold 1))
(add-hook 'telega-chat-mode-hook #'my/telega-chat-setup)
#+end_src
@ -10777,10 +10857,9 @@ References:
(setq biome-api-try-parse-error-as-response t))
:config
(add-to-list 'biome-query-coords
'("Saint-Petersburg, Russia" 59.93863 30.31413))
'("Saint-Petersburg, Russia" 59.942651 30.229930))
(add-to-list 'biome-query-coords
'("Tyumen, Russia" 57.15222 65.52722)))
#+end_src
** Reading documentation
*** tldr
@ -10914,7 +10993,7 @@ There is a package called =devdocs= that does more or less the same, but I like
(add-hook 'sx-question-list-mode-hook #'doom-modeline-mode))
#+end_src
** Not-an-AI
Workflows, which are sometimes referred as "AI", go in here.
Workflows, which are sometimes referred to as "AI", go in here.
I'm technically writing a PhD on a related topic, so I'm a bit more receptive towards the whole thing than most of the community. But I'm still not calling it AI.
@ -10939,20 +11018,31 @@ I don't have access to any proprietary APIs, but LLaMA 3.1 8b with [[https://oll
(setq gptel-backend (gptel-make-ollama "Ollama"
:host "localhost:11434"
:stream t
:models '("llama3.1:latest" "llama3.1:instruct")))
:models '("llama3.1:8b" "deepseek-r1:32b"
"qwen2.5:32b" "qwen2.5-coder:32b"
"eva-qwen2.5-q4_k_l-32b:latest"
"t-pro-1.0-q4_k_m:latest")))
(gptel-make-openai "OpenRouter"
:host "openrouter.ai/api"
:key (lambda () (my/password-store-get-field
"My_Online/Accounts/openrouter" "api-key"))
:stream t
:models '("anthropic/claude-3.5-haiku"))
(setq gptel--known-backends
(seq-filter
(lambda (cell)
(not (equal (car cell) "ChatGPT")))
gptel--known-backends))
(setq gptel-response-prefix-alist
'((markdown-mode . "[Response] ")
(org-mode . "*** Response: ")
(text-mode . "[Response]")))
;; (my/gptel-switch-backend "llama3.1:latest")
(general-define-key
:keymaps '(gptel-mode-map)
:states '(insert normal)
"C-<return>" 'gptel-send)
(general-define-key
:keymaps '(gptel-mode-map)
:states '(normal)
"?" #'gptel-menu)
(gptel-make-gemini "Gemini"
:key (my/password-store-get-field "My_Online/Accounts/google-gemini" "api")
:stream t))
"C-<return>" 'gptel-send
"M-o" #'gptel-menu))
#+end_src
**** ellama
@ -10968,19 +11058,26 @@ I don't have access to any proprietary APIs, but LLaMA 3.1 8b with [[https://oll
(require 'llm-ollama)
;; I've looked for this option for 1.5 hours
(setq ellama-long-lines-length 100000)
(my-leader-def
"aie" '(:wk "ellama" :keymap ellama-command-map))
(setq ellama-provider (make-llm-ollama
:chat-model "llama3.1:instruct"
:embedding-model "llama3.1:instruct"))
:chat-model "qwen2.5:32b"
:embedding-model "qwen2.5:32b"))
(setq ellama-coding-provider (make-llm-ollama
:chat-model "qwen2.5-coder:32b"
:embedding-model "qwen2.5-coder:32b"))
(setq ellama-providers
`(("llama3.1:8b" . ,(make-llm-ollama
:chat-model "llama3.1:latest"
:embedding-model "llama3.1:latest"))
("llama3.1:instruct" . ,(make-llm-ollama
:chat-model "llama3.1:instruct"
:embedding-model "llama3.1:instruct")))))
:chat-model "llama3.1:latest"
:embedding-model "llama3.1:latest"))
("phi4:latest" . ,(make-llm-ollama
:chat-model "phi4:latest"
:embedding-model "phi4:latest"))
("qwen2.5:32b" . ,(make-llm-ollama
:chat-model "qwen2.5:32b"
:embedding-model "qwen2.5:32b"))
("qwen2.5-coder:32b" . ,(make-llm-ollama
:chat-model "qwen2.5-coder:32b"
:embedding-model "qwen2.5-coder:32b")))))
#+end_src
The keybindings are a bit crazy to use even with =which-key=, so here goes transient.el.
@ -11000,9 +11097,7 @@ The keybindings are a bit crazy to use even with =which-key=, so here goes trans
("ci" "Improve" ellama-code-improve)]
["Natural Language"
:class transient-row
("np" "Proof-read" my/ellama-proof-read)
("nw" "Improve wording" my/ellama-improve-wording)
("nc" "Improve conciseness" my/ellama-improve-concise)]
("np" "Proof-read" my/ellama-proof-read)]
["Formatting"
:class transient-row
("ff" "Format" ellama-make-format)
@ -11027,7 +11122,6 @@ The keybindings are a bit crazy to use even with =which-key=, so here goes trans
("sr" "Rename ression" ellama-session-rename)
("sd" "Delete session" ellama-session-remove)]))
(defun my/ellama ()
(interactive)
(require 'ellama)
@ -11036,13 +11130,13 @@ The keybindings are a bit crazy to use even with =which-key=, so here goes trans
(my-leader-def "aie" #'my/ellama)
#+end_src
**** Change natural text & diff against the results
**** Change natural-language text & diff against the results
One pattern I often want is to change the given text and compare it to the old version.
LLMs aren't perfectly good at saying what changes they have done, so the pattern here is to query the model and show the changed text together with the diff.
So first, I need to diff two strings.
#+begin_src emacs-lisp :noweb yes
#+begin_src emacs-lisp
(defun my/diff-strings (str1 str2)
(let ((file1 (make-temp-file "diff1"))
(file2 (make-temp-file "diff2")))
@ -11062,22 +11156,30 @@ So first, I need to diff two strings.
#+end_src
And the function to do the prompting iself. Llama tends to output in Markdown, so I use a function from Ellama to convert the output back to Org-mode, if necessary.
#+begin_src emacs-lisp :noweb yes
(defun my/ellama-text-with-diff (text is-org-mode prompt)
(require 'ellama)
#+begin_src emacs-lisp
(defun my/ellama-proof-read--display (text is-org-mode prompt)
(llm-chat-async
ellama-provider
(llm-make-chat-prompt
(format prompt text))
(lambda (changed-text)
(when is-org-mode
(setq changed-text (ellama--translate-markdown-to-org-filter changed-text)))
(let ((buffer (generate-new-buffer "*ellama-diff*")))
(lambda (response)
(let* ((parts (split-string response "-FIXED TEXT ENDS-"))
(changed-text (nth 0 parts))
(comments (nth 1 parts))
(buffer (generate-new-buffer "*ellama-diff*")))
(when is-org-mode
(setq changed-text (ellama--translate-markdown-to-org-filter changed-text)))
(with-current-buffer buffer
(text-mode)
(insert changed-text)
(insert "\n\n")
(insert (my/diff-strings text changed-text)))
(insert
(propertize "Changed text:\n" 'face 'transient-heading)
(string-trim changed-text)
"\n\n"
(propertize "Comments:\n" 'face 'transient-heading)
(string-trim comments)
"\n\n"
(propertize "Diff:\n" 'face 'transient-heading)
(my/diff-strings text changed-text)))
(display-buffer buffer)))
(lambda (&rest err)
(message "Error: %s" err))))
@ -11086,7 +11188,18 @@ And the function to do the prompting iself. Llama tends to output in Markdown, s
As for prompts, I like the following prompt to proof-read text. It's pretty conservative, but good for fixing typos, missing commas, articles, etc.
#+begin_src emacs-lisp
(setq my/ellama-proof-read-prompt
"Proof-read the following text. Fix any errors but keep the original style and punctuation, including linebreaks. Print the changed text and nothing else, not even \"Here's the proof-read text\".\n\n %s")
"Proof-read the following text. Follow these rules:
- Fix all grammar errors
- Keep the original style and punctuation, including linebreaks.
- Use British spelling
- Do not replace ' with , and do not touch other such symbols
Output the following and nothing else:
- The fixed text
- The string -FIXED TEXT ENDS-
- List of found errors
- List of style suggestions
%s")
(defun my/ellama--text ()
(if (region-active-p)
@ -11095,27 +11208,8 @@ As for prompts, I like the following prompt to proof-read text. It's pretty cons
(defun my/ellama-proof-read (text is-org-mode)
(interactive (list (my/ellama--text) (derived-mode-p 'org-mode)))
(my/ellama-text-with-diff text is-org-mode my/ellama-proof-read-prompt))
#+end_src
The following is more expansive, but preserves less of the original text. For instance, it tends to replace my /id est/ and /exempli gratia/. But sometimes it has good ideas.
#+begin_src emacs-lisp
(setq my/ellama-improve-wording-prompt
"Proof-read the following text. Fix any errors and improve wording. Print the changed text and nothing else, not even \"Here's the improved text\".\n\n %s")
(defun my/ellama-improve-wording (text is-org-mode)
(interactive (list (my/ellama--text) (derived-mode-p 'org-mode)))
(my/ellama-text-with-diff text is-org-mode my/ellama-improve-wording-prompt))
#+end_src
Also, a prompt to make a text more concise.
#+begin_src emacs-lisp
(setq my/ellama-improve-concise-prompt
"Make the following text more concise. Print the changed text and nothing else, not even \"Here's the improved text\".\n\n %s")
(defun my/ellama-improve-concise (text is-org-mode)
(interactive (list (my/ellama--text) (derived-mode-p 'org-mode)))
(my/ellama-text-with-diff text is-org-mode my/ellama-improve-concise-prompt))
(require 'ellama)
(my/ellama-proof-read--display text is-org-mode my/ellama-proof-read-prompt))
#+end_src
*** Podcast transcripts
@ -11148,11 +11242,13 @@ First, some functions to process the output. These take a JSON formed by =insane
#+begin_src emacs-lisp
(defun my/whisper--format-vtt-seconds (seconds)
(let* ((hours (/ (floor seconds) (* 60 60)))
(minutes (/ (- (floor seconds) (* hours 60 60)) 60))
(sec (% (floor seconds) 60))
(ms (floor (* 1000 (- seconds (floor seconds))))))
(format "%.2d:%.2d:%.2d.%.3d" hours minutes sec ms)))
(if (numberp seconds)
(let* ((hours (/ (floor seconds) (* 60 60)))
(minutes (/ (- (floor seconds) (* hours 60 60)) 60))
(sec (% (floor seconds) 60))
(ms (floor (* 1000 (- seconds (floor seconds))))))
(format "%.2d:%.2d:%.2d.%.3d" hours minutes sec ms))
""))
(defun my/whisper--save-chucks-vtt (path data)
(with-temp-file path