A conversion of major Emacs packages/functions, especially ones that I rely on, to single defuns to try to avoid the dependence on external packages.
The name is obviously from “Do It Yourself,” as this configuration is implementing Emacs package functionality. The bit on the end? Well, my surname is Dyer, and it also implies that you are a person who likes to go truly vanilla and do everything yourself!
It is also a general comment on how I have moved to a more vanilla-based Emacs setup and rely much less on external packages, as I think I now have enough Elisp knowledge to replace a lot of them.
Perfect for lightweight usage in VMs or basic tasks.
Used in concert with https://github.com/captainflasmr/Emacs-vanilla
Added evil-mode replacement with some tweaks to the built-in view-mode
Added Emacs Everywhere replacement, although this will be for Wayland
Added diminish replacement
Added attempt at a native orderless implementation in the minibuffer!
Added enhancements from Emacs-solo
- Incorporated nerdfont and dired icon replacements.
- Added Git gutter status overlay.
- Added ollama alternative directly from ansi-term
- Hide completion buffer when icomplete in buffer
Added elfeed replacement with newsticker
Added =csv-parse-buffer=/package csv replacement
Added LLM client replacement with simple local ollama communication/package
Improve vc-dir header to highlight current branch
- Added an optional `separator` argument to customize branch display.
- Current branch is now highlighted using text properties.
- Default separator set to `” | “` for better readability.
Here is a kanban of the features that will be (hopefully) converted to core/enhanced Emacs features and visually demonstrating their current status via a kanban board
| DOING | DONE |
|---|---|
| consult | elfeed |
| consult-imenu | csv parsing |
| consult-outline | minibuffer completion |
consult-history for eshell | ace-window |
consult-history for shell | recentf-open |
| embark | rainbow-mode |
| completion - corfu / company | visual-fill-column-mode |
| ox-hugo | find-name-dired |
| eglot | magit |
| orderless | tempel |
| dired-async-mode | image-dired |
| selected-window-accent-mode | |
| deadgrep | |
| jinx / powerthesaurus | |
| kurecolor | |
| popper | |
| pandoc md to org conversion | |
capf for eshell | |
capf for shell | |
| org-kanban | |
| open-with | |
| capf-autosuggest | |
| all-the-icons-dired | |
| LLMs | |
| diminish | |
| emacs-everywhere |
| TODO | Title | Parent Title |
|---|---|---|
| TODO | Apply css | ox-hugo |
| DOING | Generate RSS xml | ox-hugo |
| DOING | Test completion through programming modes | completion - corfu / company |
| TODO | push buffer to popup buffer and back | popper |
Here are the features that will be (hopefully) converted to core.
Well this is a bit of a turn up!, wandering around the general Emacs feeds and someone mentions newsticker as an elfeed replacement, and guess what!, it is built-in. Well lets see how this goes, as it turns out the syntax is pretty similar:
;; Set custom variables
(setq newsticker-retrieval-interval 3600) ; Update every hour
(setq newsticker-treeview-treeview-face-fn 'ignore)
(setq newsticker-treeview-date-format "%Y-%m-%d %H:%M")
(setq newsticker-url-list
'(("Emacs Dyer Dwelling"
"https://www.emacs.dyerdwelling.family/index.xml" nil nil nil)))
;; Key bindings
(with-eval-after-load 'newsticker
(define-key newsticker-treeview-mode-map (kbd "n") 'newsticker-treeview-next-item)
(define-key newsticker-treeview-mode-map (kbd "p") 'newsticker-treeview-prev-item)
(define-key newsticker-treeview-mode-map (kbd "m") 'newsticker-treeview-mark-item))
;; Configuration that runs after newsticker is loaded
(with-eval-after-load 'newsticker
(newsticker-start)
(defun my-newsticker-treeview-custom-filter ()
"Custom filter to show items from the last month."
(let ((one-month-ago (time-subtract (current-time) (days-to-time 30))))
(lambda (item)
(time-less-p one-month-ago (newsticker--age item)))))
(setq newsticker-treeview-filter-functions (list #'my-newsticker-treeview-custom-filter)))
(define-key my-jump-keymap (kbd "t") #'newsticker-show-news)
I have relied on the package csv for quite a while now for my csv parsing needs, what is most useful is the csv-parse-buffer function to convert a buffer into an alist, extracting the csv data out.
Well now I have created my bank-buddy package I have a really good framework for testing my own replacement, and here it is (and its faster too!)
(defun csv-parse-buffer (first-line-contains-keys &optional buffer coding-system)
"Parse a buffer containing CSV data, return data as a list of alists or lists.
The first line in the buffer is interpreted as a header line
if FIRST-LINE-CONTAINS-KEYS is non-nil, resulting in a list of alists.
Otherwise, return a list of lists.
If BUFFER is non-nil it gives the buffer to be parsed. If it is
nil the current buffer is parsed.
CODING-SYSTEM gives the coding-system for reading the buffer."
(with-current-buffer (or buffer (current-buffer))
(save-excursion
(goto-char (point-min))
(let ((lines (csv-parse-lines))
header result)
(when lines
(if first-line-contains-keys
(progn
(setq header (car lines)
lines (cdr lines))
(dolist (line lines)
(when line
(push (csv-combine-with-header header line) result))))
(setq result (reverse lines))))
result))))
(defun csv-parse-lines ()
"Parse CSV lines in current buffer, returning a list of parsed lines.
Each line is represented as a list of field values."
(let ((lines nil)
(begin-pos (point))
(in-quoted nil)
(current-line nil)
(current-field "")
(previous-char nil))
(while (not (eobp))
(let ((char (char-after)))
(cond
;; Handle quoted field
((and (eq char ?\") (not (and in-quoted (eq previous-char ?\"))))
(if in-quoted
(setq in-quoted nil)
(setq in-quoted t)))
;; Handle escaped quote within quoted field
((and (eq char ?\") in-quoted (eq previous-char ?\"))
(setq current-field (concat current-field "\""))
(setq previous-char nil) ;; Reset to avoid triple quote issue
(forward-char)
(continue))
;; Handle field separator (comma)
((and (eq char ?,) (not in-quoted))
(push current-field current-line)
(setq current-field "")
(setq begin-pos (1+ (point))))
;; Handle end of line
((and (eq char ?\n) (not in-quoted))
(push current-field current-line)
(push (reverse current-line) lines)
(setq current-field "")
(setq current-line nil)
(setq begin-pos (1+ (point))))
;; Handle carriage return (part of CRLF)
((and (eq char ?\r) (not in-quoted))
;; Just skip it, we'll handle the newline next
nil)
;; Accumulate characters for the current field
(t
(when (> (point) begin-pos)
(setq current-field (concat current-field (buffer-substring-no-properties begin-pos (point)))))
(setq current-field (concat current-field (char-to-string char)))
(setq begin-pos (1+ (point)))))
(setq previous-char char)
(forward-char)))
;; Handle any remaining content
(when (and (not (string-empty-p current-field)) (not current-line))
(push current-field current-line)
(when current-line
(push (reverse current-line) lines)))
(reverse lines)))
(defun csv-combine-with-header (header line)
"Combine HEADER and LINE into an alist."
(let ((result nil))
(dotimes (i (min (length header) (length line)))
(push (cons (nth i header) (nth i line)) result))
(reverse result)))
(defun my-icomplete-exit-minibuffer-with-input ()
"Exit the minibuffer with the current input, without forcing completion."
(interactive)
(exit-minibuffer))
(define-key icomplete-minibuffer-map (kbd "M-RET") 'my-icomplete-exit-minibuffer-with-input)
Solved with the code below:
(defun my/quick-window-jump ()
"Jump to a window by typing its assigned character label.
If there is only a single window, split it horizontally.
If there are only two windows, jump directly to the other window.
Side windows are ignored."
(interactive)
(let* ((window-list (seq-filter (lambda (w)
(not (window-parameter w 'window-side)))
(window-list nil 'no-mini))))
(cond
((= (length window-list) 1)
(split-window-horizontally)
(other-window 1))
((= (length window-list) 2)
(let ((other-window (if (eq (selected-window) (nth 0 window-list))
(nth 1 window-list)
(nth 0 window-list))))
(select-window other-window)))
(t
(let* ((my/quick-window-overlays nil)
(sorted-windows (sort window-list
(lambda (w1 w2)
(let ((edges1 (window-edges w1))
(edges2 (window-edges w2)))
(or (< (car edges1) (car edges2))
(and (= (car edges1) (car edges2))
(< (cadr edges1) (cadr edges2))))))))
(window-keys (seq-take '("j" "k" "l" ";" "a" "s" "d" "f")
(length sorted-windows)))
(window-map (cl-pairlis window-keys sorted-windows)))
(setq my/quick-window-overlays
(mapcar (lambda (entry)
(let* ((key (car entry))
(window (cdr entry))
(start (window-start window))
(overlay (make-overlay start start (window-buffer window))))
(overlay-put overlay 'after-string
(propertize (format "[%s]" key)
'face 'highlight))
(overlay-put overlay 'window window)
overlay))
window-map))
(let ((key (read-key (format "Select window [%s]: " (string-join window-keys ", ")))))
(mapc #'delete-overlay my/quick-window-overlays)
(message ".")
(setq my/quick-window-overlays nil)
(when-let ((selected-window (cdr (assoc (char-to-string key) window-map))))
(select-window selected-window))))))))Solved with the code below:
(defun my/rainbow-mode ()
"Overlay colors represented as hex values in the current buffer."
(interactive)
(remove-overlays (point-min) (point-max))
(let ((hex-color-regex "#[0-9a-fA-F]\\{3,6\\}"))
(save-excursion
(goto-char (point-min))
(while (re-search-forward hex-color-regex nil t)
(let* ((color (match-string 0))
(overlay (make-overlay (match-beginning 0) (match-end 0)))
(fg-color (if (string-greaterp color "#888888") "black" "white")))
(overlay-put overlay 'face `(:background ,color :foreground ,fg-color))))))
(when (derived-mode-p 'org-mode)
(org-set-startup-visibility)))
(defun my/rainbow-mode-clear ()
"Remove all hex color overlays in the current buffer."
(interactive)
(remove-overlays (point-min) (point-max)))Solved with the code below:
(defun toggle-centered-buffer ()
"Toggle center alignment of the buffer by adjusting window margins based on the fill-column."
(interactive)
(let* ((current-margins (window-margins))
(margin (if (or (equal current-margins '(0 . 0))
(null (car (window-margins))))
(/ (- (window-total-width) fill-column) 2)
0)))
(visual-line-mode 1)
(set-window-margins nil margin margin)))Currently, the file type jump key functionality for core is limited to find-name-dired. However, it might be better to implement a more flexible version that defaults to find-name-dired but also presents additional options if tools like ripgrep or find are available. This would offer a potentially more modern and versatile approach.
Being solved with the following code:
(defun my/find-file ()
"Find file from current directory in many different ways."
(interactive)
(let* ((find-options (delq nil
(list (when (executable-find "rg")
'("rg --follow --files --null" . :string))
(when (executable-find "find")
'("find -type f -printf \"$PWD/%p\\0\"" . :string))
(when (executable-find "fd")
'("fd --absolute-path --type f -0" . :string))
(when (fboundp 'find-name-dired)
'("find-name-dired" . :command)))))
(selection (completing-read "Select: " find-options))
file-list
file)
(pcase (alist-get selection find-options nil nil #'string=)
(:command
(call-interactively (intern selection)))
(:string
(setq file-list (split-string (shell-command-to-string selection) "\0" t))
(setq file (completing-read
(format "Find file in %s: "
(abbreviate-file-name default-directory))
file-list))))
(when file (find-file (expand-file-name file)))))Replaced by built-in VC
Just need to be able to push using ssh
The following instructions seem to work for now, but should really be doing a little better:
Are you getting the following issue when trying to push to github from Emacs in vc-dir mode?
Running "git push"... ssh_askpass: exec(/usr/lib/ssh/ssh-askpass): No such file or directory [email protected]: Permission denied (publickey). fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists.
Well the ssh-askpass is not installed and doesn’t exist in /usr/lib/ssh/ssh-askpass
Is there a way to point to a different name in Emacs?, not sure
But perform the following as a current workaround
Install the following:
openssh-askpass
Which make available the following:
/usr/bin/qt4-ssh-askpass
Emacs is looking for:
/usr/lib/ssh/ssh-askpass
So why not provide a symbolic link as root!?, seems to work:
su - cd /usr/lib/ssh ln -s /usr/bin/qt4-ssh-askpass ssh-askpass
Although still raises the following:
Running "git push"... ErrorHandler::Throw - warning: QFSFileEngine::open: No file name specified file: line: 0 function: To github.com:captainflasmr/Emacs-DIYer.git 6735e12..4766e6c main -> main
and for some vc-mode enhancements?
lets firstly try and show the branches in vc-dir
(defun my/vc-dir-show-branches-and-tags (&optional branch-separator tag-separator)
"Show Git branches and tags in the header line of the *vc-dir* buffer.
The current branch is highlighted. If BRANCH-SEPARATOR or TAG-SEPARATOR
are provided, they are used to separate the branches or tags in the display."
(interactive)
(when (and (boundp 'vc-dir-backend) (eq vc-dir-backend 'Git))
(let* ((default-directory (if (boundp 'vc-dir-directory)
vc-dir-directory
default-directory))
;; Get branches
(branches (split-string (shell-command-to-string "git branch") "\n" t "\\s-*"))
;; Get tags
(tags (split-string (shell-command-to-string "git tag") "\n" t))
;; Get current commit hash
(current-commit (string-trim (shell-command-to-string "git rev-parse HEAD")))
;; Default separators
(branch-sep (or branch-separator " | "))
(tag-sep (or tag-separator ", "))
;; Format branches
(styled-branches (mapconcat
(lambda (branch)
(if (string-prefix-p "* " branch)
(propertize (concat "*" (string-trim-left branch "* "))
'face '(:weight bold))
branch))
branches branch-sep))
;; Check which tags point to the current commit
(current-tags '())
(tag-part ""))
;; Find tags pointing to current commit
(dolist (tag tags)
(when (string-prefix-p
current-commit
(string-trim (shell-command-to-string (format "git rev-parse %s" tag))))
(push tag current-tags)))
;; Format tag display if we have any
(when current-tags
(setq tag-part
(concat " [Tags: "
(propertize
(mapconcat 'identity current-tags tag-sep)
'face '(:slant italic :foreground "goldenrod"))
"]")))
;; Set the header line
(setq-local header-line-format
(concat " Branches: " styled-branches tag-part)))))
;; Add the function to vc-dir-mode-hook
(add-hook 'vc-dir-mode-hook #'my/vc-dir-show-branches-and-tags)
;; Define advice function for refreshing branches and tags after switching
(defun my/after-vc-switch-branch (&rest _args)
"Update branch and tag display in all vc-dir buffers after switching branches."
(dolist (buf (buffer-list))
(with-current-buffer buf
(when (derived-mode-p 'vc-dir-mode)
(my/vc-dir-show-branches-and-tags)))))
;; Add the advice to vc-git-branch function (handles git checkout)
(advice-add 'vc-create-branch :after #'my/after-vc-switch-branch)
(advice-add 'vc-switch-branch :after #'my/after-vc-switch-branch)
;; Let's also add a command to show all tags
(defun my/vc-dir-show-all-tags ()
"Display all Git tags in a separate buffer."
(interactive)
(when (and (derived-mode-p 'vc-dir-mode)
(eq vc-dir-backend 'Git))
(let* ((default-directory (if (boundp 'vc-dir-directory)
vc-dir-directory
default-directory))
(buffer (get-buffer-create "*git-tags*"))
(tags (shell-command-to-string "git tag -n"))) ; -n shows annotations
(with-current-buffer buffer
(erase-buffer)
(insert "Git Tags:\n\n")
(insert tags)
(goto-char (point-min))
(special-mode))
(switch-to-buffer buffer))))
;; Lets show tracked files in Git!!
(defun my/vc-dir-show-tracked-files ()
"Show all tracked files in the current vc-dir buffer."
(interactive)
(when (and (derived-mode-p 'vc-dir-mode)
(eq vc-dir-backend 'Git))
(let* ((default-directory (if (boundp 'vc-dir-directory)
vc-dir-directory
default-directory))
(files (split-string
(shell-command-to-string "git ls-files")
"\n" t)))
(vc-dir-refresh)
(dolist (file files)
(let ((full-path (expand-file-name file default-directory)))
(vc-dir-show-fileentry file))))))
;; Bind keys in vc-dir-mode
(with-eval-after-load 'vc-dir
(define-key vc-dir-mode-map (kbd "B") 'my/vc-dir-show-branches-and-tags)
(define-key vc-dir-mode-map (kbd "T") 'my/vc-dir-show-all-tags) ; New key for showing all tags
(define-key vc-dir-mode-map (kbd "F") 'my/vc-dir-show-tracked-files)) ; Changed from T to FFor an extra bonus, lets try and put some git gutter status in dired, taken from Emacs-solo, its not really replacing anything from magit, but who cares!!
(setq emacs-solo-dired-gutter-enabled t)
(defvar emacs-solo/dired-git-status-overlays nil
"List of active overlays in Dired for Git status.")
(defun emacs-solo/dired--git-status-face (code)
"Return a cons cell (STATUS . FACE) for a given Git porcelain CODE."
(let* ((git-status-untracked "??")
(git-status-modified " M")
(git-status-modified-alt "M ")
(git-status-deleted "D ")
(git-status-added "A ")
(git-status-renamed "R ")
(git-status-copied "C ")
(git-status-ignored "!!")
(status (cond
((string-match-p "\\?\\?" code) git-status-untracked)
((string-match-p "^ M" code) git-status-modified)
((string-match-p "^M " code) git-status-modified-alt)
((string-match-p "^D" code) git-status-deleted)
((string-match-p "^A" code) git-status-added)
((string-match-p "^R" code) git-status-renamed)
((string-match-p "^C" code) git-status-copied)
((string-match-p "\\!\\!" code) git-status-ignored)
(t " ")))
(face (cond
((string= status git-status-ignored) 'shadow)
((string= status git-status-untracked) 'warning)
((string= status git-status-modified) 'font-lock-function-name-face)
((string= status git-status-modified-alt) 'font-lock-function-name-face)
((string= status git-status-deleted) 'error)
((string= status git-status-added) 'success)
(t 'font-lock-keyword-face))))
(cons status face)))
(defun emacs-solo/dired-git-status-overlay ()
"Overlay Git status indicators on the first column in Dired."
(interactive)
(require 'vc-git)
(let ((git-root (ignore-errors (vc-git-root default-directory))))
(when (and git-root
(not (file-remote-p default-directory))
emacs-solo-dired-gutter-enabled)
(setq git-root (expand-file-name git-root))
(let* ((git-status (vc-git--run-command-string nil "status" "--porcelain" "--ignored" "--untracked-files=normal"))
(status-map (make-hash-table :test 'equal)))
(mapc #'delete-overlay emacs-solo/dired-git-status-overlays)
(setq emacs-solo/dired-git-status-overlays nil)
;; Add this check to prevent the error
(when git-status ; Only process if git-status is not nil
(dolist (line (split-string git-status "\n" t))
(when (string-match "^\\(..\\) \\(.+\\)$" line)
(let* ((code (match-string 1 line))
(file (match-string 2 line))
(fullpath (expand-file-name file git-root))
(status-face (emacs-solo/dired--git-status-face code)))
(puthash fullpath status-face status-map)))))
(save-excursion
(goto-char (point-min))
(while (not (eobp))
(let* ((file (ignore-errors (expand-file-name (dired-get-filename nil t)))))
(when file
(setq file (if (file-directory-p file) (concat file "/") file))
(let* ((status-face (gethash file status-map (cons " " 'font-lock-keyword-face)))
(status (car status-face))
(face (cdr status-face))
(status-str (propertize (format " %s " status) 'face face))
(ov (make-overlay (line-beginning-position) (1+ (line-beginning-position)))))
(overlay-put ov 'before-string status-str)
(push ov emacs-solo/dired-git-status-overlays))))
(forward-line 1)))))))
(add-hook 'dired-after-readin-hook #'emacs-solo/dired-git-status-overlay)
(defun my-git-diff-stash (stash-ref)
"Diff working directory against specified stash"
(interactive "sStash reference: ")
(let ((buffer (get-buffer-create "*git-stash-diff*")))
(with-current-buffer buffer
(erase-buffer)
(call-process "git" nil buffer t "diff" (format "stash@{%s}" stash-ref))
(diff-mode)
(goto-char (point-min)))
(switch-to-buffer buffer)))
;; Bind to C-x v S (capital S for stash diff)
(define-key vc-prefix-map (kbd "S") 'my-git-diff-stash)
I use pretty simple configurations (no yasnippet complexity here) so adapting abbrev with some predefined functions for the most common completion replacements.
Adapting to use abbrev-mode, the syntax for abbrev_defs is very similar to Tempel configuration files, making it easy to adapt.
Replaced tempel with abbrev, will have to write a blog post about this but replacing the following tempel template :
fundamental-mode ;; Available everywhere
;;
(ja (format-time-string "<%Y-%m-%d>"))
(jT (format-time-string "%Y%m%d%H%M%S"))
(jt (format-time-string "%Y%m%d"))
(ji "(interactive)")
(jl "(lambda ()")
;;
org-mode
;;
(jm "#+hugo: more")
(jg "#+attr_org: :width 300px" n "#+attr_html: :width 100%")
(je "#+attr_org: :width 300px" n "#+attr_html: :class emacs-img")
(jo "---" n "#+TOC: headlines 1 local" n "---")
(jk "#+begin: kanban :layout (\"...\" . 40) :scope nil :range (\"TODO\" . \"DONE\") :sort \"O\" :depth 2 :compressed t" n "#+end:")
(jp "~--APT--~ ")
;;
sh-mode
(jd n "echo \"poop: " p "\"" n)
;;
emacs-lisp-mode
(jd n "(message \"poop: " p "\"\)" n)
;;
ada-mode
(jd n> "Ada.Text_Io.Put_Line \( \"poop: " p "\"\);" n)
;;
c++-mode
(jd n> "std::cout << \"poop: " p "\" << std::endl;" n)
;;
c-mode
(jd n> "fprintf(stderr, \"poop: " p "\"\);" n)
with the following abbrev abbrev_defs:
;;-*-coding: utf-8;-*-
(define-abbrev-table 'ada-mode-abbrev-table
'(
("jd" "Ada.Text_Io.Put_Line (\"poop: \");" nil :count 0)
))
(define-abbrev-table 'c++-mode-abbrev-table
'(
("jd" "std::cout << \"poop: \" << std::endl;" nil :count 0)
))
(define-abbrev-table 'c-mode-abbrev-table
'(
("jd" "printf(stderr, \"poop: \");" nil :count 0)
))
(define-abbrev-table 'emacs-lisp-mode-abbrev-table
'(
("jd" "(message \"poop: \")" nil :count 0)
))
(define-abbrev-table 'global-abbrev-table
'(
("jT" "" (lambda nil (interactive) (insert (format-time-string "%Y%m%d%H%M%S"))) :count 0)
("ja" "" (lambda nil (interactive) (insert (format-time-string "<%Y-%m-%d>"))) :count 1)
("ji" "(interactive)" nil :count 1)
("jl" "(lambda ()" nil :count 0)
("jt" "" (lambda nil (interactive) (insert (format-time-string "%Y%m%d"))) :count 0)
))
(define-abbrev-table 'org-mode-abbrev-table
'(
("je" "#+attr_org: :width 300px
#+attr_html: :class emacs-img" nil :count 0)
("jg" "#+attr_org: :width 300px
#+attr_html: :width 100%" nil :count 0)
("jk" "#+begin: kanban :layout (\"...\") :scope nil :range (\"TODO\" . \"DONE\") :sort \"O\" :depth 2 :compressed t
#+end:" nil :count 0)
("jm" "#+hugo: more" nil :count 0)
("jo" "---
#+TOC: headlines 1 local
---" nil :count 0)
("jp" "~--APT--~" nil :count 0)
))
(define-abbrev-table 'sh-mode-abbrev-table
'(
("jd" "echo \"poop: \"" nil :count 0)
))
The only downside is the lack of positional cursor options that are easily defined in Tempel, but if I really wanted to, I could just include lambda functions to move the cursor. However, I don’t think I’m too bothered; I’ll just use the usual Emacs navigation keys.
This is mainly enhancements to provide a more comfortable Desktop feel to image navigation.
My package of highlighting the selected window/tabs, which actually I find very useful and of course due to my familiarity I could code up a more simple version.
Pretty much covered by where the user will be prompted for a colour and the faces adapted accordingly:
(defun my/sync-ui-accent-color (&optional color)
"Synchronize various Emacs UI elements with a chosen accent color.
Affects mode-line, cursor, tab-bar, and other UI elements for a coherent theme.
If COLOR is not provided, prompts for color selection interactively.
The function adjusts:
- Mode-line (active and inactive states)
- Cursor
- Tab-bar (active and inactive tabs)
- Window borders and dividers
- Highlighting
- Fringes"
(interactive (list (when current-prefix-arg (read-color "Color: "))))
(let* ((accent-color (or color (read-color "Select accent color: ")))
(bg-color (face-background 'default))
(fg-color (face-foreground 'default))
(hl-color (face-background 'highlight))
(inactive-fg-color (face-foreground 'mode-line-inactive))
(is-dark-theme (not (string-greaterp bg-color "#888888")))
(adjusted-bg-color (if is-dark-theme
(adjust-color bg-color 20)
(adjust-color bg-color -5))))
;; Mode-line configuration
(set-face-attribute 'mode-line nil
:height 140
:underline nil
:overline nil
:box nil
:background accent-color
:foreground "#000000")
(set-face-attribute 'mode-line-inactive nil
:height 140
:underline nil
:overline nil
:background adjusted-bg-color
:foreground "#aaaaaa")
;; Other UI elements configuration
(if is-dark-theme
(custom-set-faces
`(cursor ((t (:background ,accent-color))))
`(hl-line ((t (:background ,adjusted-bg-color))))
`(vertical-border ((t (:foreground ,(adjust-color fg-color -60)))))
`(window-divider ((t (:foreground ,(adjust-color fg-color -60)))))
`(fringe ((t (:foreground ,bg-color :background ,bg-color))))
`(tab-bar ((t (:inherit default :background ,bg-color :foreground ,fg-color))))
`(tab-bar-tab ((t (:inherit 'highlight :background ,accent-color :foreground "#000000"))))
`(tab-bar-tab-inactive ((t (:inherit default :background ,bg-color :foreground ,fg-color
:box (:line-width 1 :color ,bg-color :style flat-button))))))
(custom-set-faces
`(cursor ((t (:background ,accent-color))))
`(hl-line ((t (:background ,adjusted-bg-color))))
`(vertical-border ((t (:foreground ,(adjust-color fg-color -60)))))
`(window-divider ((t (:foreground ,(adjust-color fg-color -60)))))
`(fringe ((t (:foreground ,bg-color :background ,bg-color))))
`(tab-bar ((t (:inherit default :background "#000000" :foreground ,bg-color))))
;; `(tab-bar-tab ((t (:inherit 'highlight :box (:line-width 6 :color ,accent-color :style flat-button)))))
`(tab-bar-tab ((t (:inherit 'highlight :background ,accent-color))))
`(tab-bar-tab-inactive ((t (:inherit default :background ,bg-color :foreground ,fg-color
:box (:line-width 1 :color ,bg-color :style flat-button)))))))
))Would rgrep be potentially good enough?, maybe, or perhaps implement ripgrep through a simple interface while reusing `grep-mode`. Essentially, it would look similar to rgrep’s output but include more detailed information from the ripgrep search, similar to the style of deadgrep. For example:
- directory
- search term
- glob
And, like deadgrep, have some local keybindings that can input the directory, search term, or glob.
Being solved with the following code:
(defun my/grep (search-term &optional directory glob)
"Run ripgrep (rg) with SEARCH-TERM and optionally DIRECTORY and GLOB.
If ripgrep is unavailable, fall back to Emacs's rgrep command. Highlights SEARCH-TERM in results.
By default, only the SEARCH-TERM needs to be provided. If called with a
universal argument, DIRECTORY and GLOB are prompted for as well."
(interactive
(let* ((univ-arg current-prefix-arg)
;; Prefer region, then symbol-at-point, then word-at-point, then empty string
(default-search-term
(cond
((use-region-p)
(buffer-substring-no-properties (region-beginning) (region-end)))
((thing-at-point 'symbol t))
((thing-at-point 'word t))
(t ""))))
(list
(read-string (if (string-empty-p default-search-term)
"Search for: "
(format "Search for (default `%s`): " default-search-term))
nil nil default-search-term)
(when univ-arg (read-directory-name "Directory: "))
(when univ-arg (read-string "File pattern (glob, default: ): " nil nil "")))))
(let* ((directory (expand-file-name (or directory default-directory)))
(glob (or glob ""))
(buffer-name "*grep*"))
(if (executable-find "rg")
(let* ((rg-command (format "rg --color=never --max-columns=500 --column --line-number --no-heading --smart-case -e %s --glob %s %s"
(shell-quote-argument search-term)
(shell-quote-argument glob)
directory))
(debug-output (shell-command-to-string (format "rg --debug --files %s" directory)))
(ignore-files (when (string-match "ignore file: \\(.*?\\.ignore\\)" debug-output)
(match-string 1 debug-output)))
(raw-output (shell-command-to-string rg-command))
(formatted-output
(concat
(format "[S] Search: %s\n[D] Directory: %s\n" search-term directory)
(format "[o] Glob: %s\n" glob)
(if ignore-files (format "%s\n" ignore-files) "")
"\n"
(if (string-empty-p raw-output)
"No results found.\n"
(replace-regexp-in-string (concat "\\(^" (regexp-quote directory) "\\)") "./" raw-output)))))
(when (get-buffer buffer-name)
(kill-buffer buffer-name))
(with-current-buffer (get-buffer-create buffer-name)
(setq default-directory directory)
(erase-buffer)
(insert formatted-output)
(insert "\nripgrep finished.")
(goto-char (point-min))
(unless (string-empty-p raw-output)
(let ((case-fold-search t))
(while (search-forward search-term nil t)
(overlay-put (make-overlay (match-beginning 0) (match-end 0))
'face '(:slant italic :weight bold :underline t)))))
(grep-mode)
(setq-local my/grep-search-term search-term)
(setq-local my/grep-directory directory)
(setq-local my/grep-glob glob)
(local-set-key (kbd "D") (lambda ()
(interactive)
(my/grep my/grep-search-term
(read-directory-name "New search directory: ")
my/grep-glob)))
(local-set-key (kbd "S") (lambda ()
(interactive)
(my/grep (read-string "New search term: "
nil nil my/grep-search-term)
my/grep-directory
my/grep-glob)))
(local-set-key (kbd "o") (lambda ()
(interactive)
(my/grep my/grep-search-term
my/grep-directory
(read-string "New glob: "))))
(local-set-key (kbd "g") (lambda ()
(interactive)
(my/grep my/grep-search-term my/grep-directory my/grep-glob)))
(pop-to-buffer buffer-name)
(goto-char (point-min))
(message "ripgrep finished.")))
(progn
(setq default-directory directory)
(message (format "%s : %s : %s" search-term glob directory))
(rgrep search-term (if (string= "" glob) "*" glob) directory)))))
(defun my-org-reveal-on-next-error ()
"Reveal the location of search results in an Org file."
(when (derived-mode-p 'org-mode)
(org-reveal)))
(add-hook 'next-error-hook 'my-org-reveal-on-next-error)I think I can probably just use flyspell-buffer, and do I really need a thesaurus? Probably not, I can just rely on dictionary-lookup-definition.
Solution is the following configuration:
(setq ispell-local-dictionary "en_GB")
(setq ispell-program-name "hunspell")
(setq dictionary-default-dictionary "*")
(setq dictionary-server "dict.org")
(setq dictionary-use-single-buffer t)
(defun my/flyspell-add-word-to-dict ()
"Add the word under point to the personal dictionary and refresh the errors list."
(interactive)
(let* ((button (button-at (point)))
(word (button-label button))
(target-buffer (button-get button 'buffer))
(target-pos (button-get button 'position)))
;; Switch to the source buffer, go to the word, and add it to dictionary
(with-current-buffer target-buffer
(save-excursion
(goto-char target-pos)
;; Use ispell to add the word to the personal dictionary
(ispell-send-string (concat "*" word "\n"))
;; Tell ispell we're done and the buffer hasn't changed
(ispell-send-string "#\n")
(sit-for 0.1) ; Wait for ispell to process
(message "Added '%s' to the dictionary." word)))
(with-current-buffer target-buffer
(pop-to-buffer target-buffer)
(my/collect-flyspell-errors))))
(defun my/collect-flyspell-errors ()
"Collect all flyspell errors in the current buffer and display them in a separate buffer with clickable links."
(interactive)
;; Store the buffer name and buffer itself for later reference
(let* ((source-buffer (current-buffer))
(source-buffer-name (buffer-name))
(error-list nil))
;; Ensure the buffer is fully spell-checked
(flyspell-buffer)
;; Collect all misspelled words and their positions
(save-excursion
(goto-char (point-min))
(while (not (eobp))
(let ((overlays (overlays-at (point)))
(moved nil))
(dolist (overlay overlays)
(when (overlay-get overlay 'flyspell-overlay)
(let ((start (overlay-start overlay))
(end (overlay-end overlay))
(word (buffer-substring-no-properties
(overlay-start overlay)
(overlay-end overlay)))
(line-num (line-number-at-pos (overlay-start overlay))))
;; Store the buffer name string rather than buffer object
(push (list word start end line-num) error-list)
(goto-char end)
(setq moved t))))
(unless moved
(forward-char 1)))))
;; Sort by position in buffer
(setq error-list (nreverse error-list))
;; Create and populate the errors buffer
(let ((errors-buffer (get-buffer-create "*Flyspell Errors*")))
(with-current-buffer errors-buffer
(let ((inhibit-read-only t))
(pop-to-buffer errors-buffer)
(visual-line-mode 1)
(erase-buffer)
(insert (format "Flyspell Errors in %s (%d found)\n\n"
source-buffer-name
(length error-list)))
;; Add all the errors with buttons
(dolist (error-info error-list)
(let ((word (nth 0 error-info))
(start (nth 1 error-info)))
;; Store position as a text property for the button
(insert-button word
'follow-link t
'help-echo "Click to jump to this misspelled word"
'buffer source-buffer
'position start
'action (lambda (button)
(switch-to-buffer (button-get button 'buffer))
(goto-char (button-get button 'position))
(recenter)))
(insert " ")))
(special-mode)
;; keybindings
(local-set-key (kbd "g")
(lambda ()
(interactive)
(let ((button (button-at (point))))
(with-current-buffer (target-buffer (button-get button 'buffer))
(my/collect-flyspell-errors)))))
(local-set-key (kbd "a") 'my/flyspell-add-word-to-dict)
(local-set-key (kbd "q") 'quit-window))))))
(defun spelling-menu ()
"Menu for spelling."
(interactive)
(let ((key (read-key
(propertize
"------- Spelling [q] Quit: -------
[s] Spelling
[l] Summary"
'face 'minibuffer-prompt))))
(pcase key
;; Spelling
(?s (progn
(flyspell-buffer)
(call-interactively 'flyspell-mode)))
(?l (call-interactively 'my/collect-flyspell-errors))
;; Quit
(?q (message "Quit menu."))
(?\C-g (message "Quit menu."))
;; Default Invalid Key
(_ (message "Invalid key: %c" key)))))
(global-set-key (kbd "C-c s") #'spelling-menu)
(global-set-key (kbd "C-0") #'ispell-word)Note that at the moment, I don’t really care about spell-checking efficiency (which Jinx was very good at). I am quite happy to wait a few seconds for the flyspell-buffer to run, and in a narrowed region, it won’t take that long anyway.
Also, as a bonus, I recently discovered the shortcut key `C-.`, which cycles through autocorrect suggestions for a word. This makes life much simpler.
(flyspell-auto-correct-word)
Correct the current word. This command proposes various successive corrections for the current word. If invoked repeatedly on the same position, it cycles through the possible corrections of the current word.
I have always found this very useful when customizing my system or webpage to incrementally tweak colours.
Testing with the following code:
(require 'cl-lib)
(require 'color)
(defun my/color-hex-to-rgb (hex-color)
"Convert a HEX-COLOR string to a list of RGB values."
(unless (string-match "^#[0-9a-fA-F]\\{6\\}$" hex-color)
(error "Invalid hex color: %s" hex-color))
(mapcar (lambda (x) (/ (string-to-number x 16) 255.0))
(list (substring hex-color 1 3)
(substring hex-color 3 5)
(substring hex-color 5 7))))
(defun my/color-rgb-to-hex (rgb)
"Convert a list of RGB values to a hex color string."
(format "#%02x%02x%02x"
(round (* 255 (nth 0 rgb)))
(round (* 255 (nth 1 rgb)))
(round (* 255 (nth 2 rgb)))))
(defun my/color-adjust-brightness (hex-color delta)
"Adjust the brightness of HEX-COLOR by DELTA (-1.0 to 1.0)."
(let* ((rgb (my/color-hex-to-rgb hex-color))
(adjusted-rgb (mapcar (lambda (c) (min 1.0 (max 0.0 (+ c delta)))) rgb)))
(my/color-rgb-to-hex adjusted-rgb)))
(defun my/color-adjust-saturation (hex-color delta)
"Adjust the saturation of HEX-COLOR by DELTA (-1.0 to 1.0)."
(let* ((rgb (my/color-hex-to-rgb hex-color))
(max (apply 'max rgb))
(adjusted-rgb (mapcar
(lambda (c)
(if (= max 0.0)
c
(+ (* c (+ 1 delta)) (* max (- delta)))))
rgb)))
(my/color-rgb-to-hex adjusted-rgb)))
(defun my/color-adjust-hue (hex-color delta)
"Adjust the hue of HEX-COLOR by DELTA (in degrees)."
(let* ((rgb (my/color-hex-to-rgb hex-color))
(hsl (color-rgb-to-hsl (nth 0 rgb) (nth 1 rgb) (nth 2 rgb)))
(new-h (mod (+ (nth 0 hsl) (/ delta 360.0)) 1.0)) ;; Wrap hue around
(new-rgb (apply 'color-hsl-to-rgb (list new-h (nth 1 hsl) (nth 2 hsl)))))
(my/color-rgb-to-hex new-rgb)))
(defun my/replace-color-at-point (transform-fn &rest args)
"Replace the hex color code at point using TRANSFORM-FN with ARGS."
(let ((bounds (bounds-of-thing-at-point 'sexp))
(original (thing-at-point 'sexp t)))
(if (and bounds (string-match "^#[0-9a-fA-F]\\{6\\}$" original))
(let ((new-color (apply transform-fn original args)))
(delete-region (car bounds) (cdr bounds))
(insert new-color))
(error "No valid hex color code at point"))))
(global-set-key (kbd "M-<up>")
(lambda ()
(interactive)
(my/replace-color-at-point 'my/color-adjust-brightness 0.02)
(my/rainbow-mode)))
(global-set-key (kbd "M-<down>")
(lambda ()
(interactive)
(my/replace-color-at-point 'my/color-adjust-brightness -0.02)
(my/rainbow-mode)))
(global-set-key (kbd "M-<prior>")
(lambda ()
(interactive)
(my/replace-color-at-point 'my/color-adjust-saturation 0.02)
(my/rainbow-mode)))
(global-set-key (kbd "M-<next>")
(lambda ()
(interactive)
(my/replace-color-at-point 'my/color-adjust-saturation -0.02)
(my/rainbow-mode)))
(global-set-key (kbd "M-<left>")
(lambda ()
(interactive)
(my/replace-color-at-point 'my/color-adjust-hue -5)
(my/rainbow-mode)))
(global-set-key (kbd "M-<right>")
(lambda ()
(interactive)
(my/replace-color-at-point 'my/color-adjust-hue 5)
(my/rainbow-mode)))
(global-set-key (kbd "M-<home>") 'my/insert-random-color-at-point)Originally I had the following keybindings mapped :
(global-set-key (kbd "M-g i") 'consult-imenu)
(global-set-key (kbd "M-g o") 'consult-outline)
(define-key eshell-hist-mode-map (kbd "M-r") #'consult-history)The first one is easy. I am happy to replace it with imenu; the interface brings up a simple minibuffer completing-read. I don’t dynamically jump to the headline, but I’m not a fan of that approach anyway.
The second one I think I can replace by using org-goto with a couple of tweaks
(global-set-key (kbd "M-g o") #'org-goto)
(setq org-goto-interface 'outline-path-completionp)
(setq org-outline-path-complete-in-steps nil)This transforms the awkward org-goto interface into a better, easier, completing-read one, more akin to consult-outline.
The third one can be roughly accomplished by passing eshell history through completing-read
(let ((bash-history-file "~/.bash_history")
(eshell-history-file (expand-file-name "eshell/history" user-emacs-directory)))
(when (file-exists-p bash-history-file)
(with-temp-buffer
(insert-file-contents bash-history-file)
(append-to-file (buffer-string) nil eshell-history-file))))
(defun my/eshell-history-completing-read ()
"Search eshell history using completing-read"
(interactive)
(insert
(completing-read "Eshell history: "
(delete-dups
(ring-elements eshell-history-ring)))))
(setq eshell-history-size 10000)
(setq eshell-save-history-on-exit t)
(setq eshell-hist-ignoredups t)
(defun my/setup-eshell-keybindings ()
"Setup eshell keybindings with version compatibility checks and fallbacks."
;; Try modern mode-specific maps first
(with-eval-after-load 'em-hist
(if (boundp 'eshell-hist-mode-map)
(progn
(define-key eshell-hist-mode-map (kbd "M-r") #'my/eshell-history-completing-read)
(define-key eshell-hist-mode-map (kbd "M-s") nil))
;; Fallback to eshell-mode-map if specific mode maps don't exist
(when (boundp 'eshell-mode-map)
(define-key eshell-mode-map (kbd "M-r") #'my/eshell-history-completing-read)
(define-key eshell-mode-map (kbd "M-s") nil))))
(with-eval-after-load 'em-cmpl
;; Add completion category overrides
(add-to-list 'completion-category-overrides
'(eshell-history (styles basic substring initials)))
;; Try modern completion map first, fallback to general map
(if (boundp 'eshell-cmpl-mode-map)
(define-key eshell-cmpl-mode-map (kbd "C-M-i") #'completion-at-point)
(when (boundp 'eshell-mode-map)
(define-key eshell-mode-map (kbd "C-M-i") #'completion-at-point)))))
(add-hook 'eshell-mode-hook #'my/setup-eshell-keybindings)Note: I needed to transfer the local shell history into eshell for a better history experience.
This overall setup is similar to eshell.
(defun my/load-bash-history ()
"Load commands from .bash_history into shell history ring."
(interactive)
(let* ((bash-history-file (expand-file-name "~/.bash_history"))
(existing-history (ring-elements comint-input-ring))
(bash-history
(when (file-exists-p bash-history-file)
(with-temp-buffer
(insert-file-contents bash-history-file)
(split-string (buffer-string) "\n" t)))))
;; Add bash history entries to comint history ring
(when bash-history
(dolist (cmd (reverse bash-history))
(unless (member cmd existing-history)
(comint-add-to-input-history cmd))))))
(add-hook 'shell-mode-hook 'my/load-bash-history)
(defun my/shell-history-complete ()
"Search shell history with completion."
(interactive)
(let* ((history (ring-elements comint-input-ring))
(selection (completing-read "Shell history: "
(delete-dups history)
nil
t)))
(when selection
(delete-region (comint-line-beginning-position)
(line-end-position))
(insert selection))))
(define-key shell-mode-map (kbd "M-r") #'my/shell-history-complete)I am not using too many aspects mainly the following:
- copy command from the minibuffer
- find file at point
Solved with the code below:
(defun my-icomplete-copy-candidate ()
"Copy the current Icomplete candidate to the kill ring."
(interactive)
(let ((candidate (car completion-all-sorted-completions)))
(when candidate
(kill-new (substring-no-properties candidate))
(let ((copied-text candidate))
(run-with-timer 0 nil (lambda ()
(message "Copied: %s" copied-text)))
(abort-recursive-edit)))))
(global-set-key (kbd "C-c ,") 'find-file-at-point)
(define-key minibuffer-local-completion-map (kbd "C-c ,") 'my-icomplete-copy-candidate)collect/export could be solved with a TAB showing completions buffer
Mainly used for popping in popping out shells, testing the following implementation:
(defun my/popper-matching-buffers ()
"Return a list of buffers matching pop-up patterns but excluding specific buffers."
(let ((popup-patterns '("\\*\.*shell\.*\\*"
"\\*\.*term\.*\\*"
"\\*eldoc\.*\\*"
"\\*Flymake\.*"))
(exclusion-patterns '("\\*shell\\*-comint-indirect")))
(seq-filter (lambda (buf)
(let ((bufname (buffer-name buf)))
(and (seq-some (lambda (pattern)
(string-match-p pattern bufname))
popup-patterns)
(not (seq-some (lambda (pattern)
(string-match-p pattern bufname))
exclusion-patterns)))))
(buffer-list))))
(defun my/popper-handle-popup (buffer)
"Display BUFFER as a popup, setting it as the current popup."
(pop-to-buffer buffer
'((display-buffer-reuse-window display-buffer-at-bottom)
(inhibit-same-window . t)
(window-height . 0.3)))
(message "Displayed pop-up buffer: %s" (buffer-name buffer)))
(defun my/popper-cycle-popup ()
"Cycle visibility of pop-up buffers."
(interactive)
(let* ((popup-buffers (my/popper-matching-buffers))
(current-popup-window (car (seq-filter (lambda (win)
(member (window-buffer win) popup-buffers))
(window-list)))))
(when current-popup-window
(let ((buf (window-buffer current-popup-window)))
(delete-window current-popup-window)
(bury-buffer buf)
(setq popup-buffers (my/popper-matching-buffers))
(message "Hid pop-up buffer: %s" (buffer-name buf))))
(if popup-buffers
(my/popper-handle-popup (car popup-buffers))
(message "No pop-up buffers to display!"))))
(defun my/popper-toggle-current ()
"Toggle visibility of pop-up buffers."
(interactive)
(let* ((popup-buffers (my/popper-matching-buffers))
(current-popup-window (car (seq-filter (lambda (win)
(member (window-buffer win) popup-buffers))
(window-list)))))
(if current-popup-window
(let ((buf (window-buffer current-popup-window)))
(delete-window current-popup-window)
(message "Hid pop-up buffer: %s" (buffer-name buf)))
(if popup-buffers
(my/popper-handle-popup (car popup-buffers))
(message "No pop-up buffers to display!")))))
;; Toggle the currently selected popup.
(global-set-key (kbd "C-x j") #'my/popper-toggle-current)
(global-set-key (kbd "C-'") #'my/popper-toggle-current)
;; Cycle through popups or show the next popup.
(global-set-key (kbd "M-L") #'my/popper-cycle-popup)Replacing the external tool Pandoc for converting Markdown (md) to Org format is especially useful when copying and pasting from AI chats.
Potentially solved with the following, probably requires more testing:
(defun my/md-to-org-convert-buffer ()
"Convert the current buffer from Markdown to Org-mode format."
(interactive)
(save-excursion
;; First, protect code blocks by replacing them with placeholders
(let ((code-blocks '())
(counter 0))
;; Collect and replace code blocks with placeholders
(goto-char (point-min))
(while (re-search-forward "^\\([ \t]*\\)```\\([^\n]*\\)\n\\(\\(?:.*\n\\)*?\\)\\1```" nil t)
(let ((indent (match-string 1))
(lang (match-string 2))
(code (match-string 3)))
(push (list counter indent lang code) code-blocks)
(replace-match (format "<<<CODE-BLOCK-%d>>>" counter))
(setq counter (1+ counter))))
;; Also protect inline code
(let ((inline-codes '())
(inline-counter 0))
(goto-char (point-min))
(while (re-search-forward "`\\([^`\n]+\\)`" nil t)
(push (cons inline-counter (match-string 1)) inline-codes)
(replace-match (format "<<<INLINE-CODE-%d>>>" inline-counter))
(setq inline-counter (1+ inline-counter)))
;; Now do all the conversions safely
;; Headers: # -> * (must come before list conversion)
(goto-char (point-min))
(while (re-search-forward "^\\(#+\\) " nil t)
(replace-match (concat (make-string (length (match-string 1)) ?*) " ")))
;; Lists: -, *, + -> -
(goto-char (point-min))
(while (re-search-forward "^\\([ \t]*\\)[*+-] " nil t)
(replace-match "\\1- "))
;; Bold: **text** -> *text*
(goto-char (point-min))
(while (re-search-forward "\\*\\*\\(.+?\\)\\*\\*" nil t)
(replace-match "*\\1*"))
;; Italics: _text_ or *text* (single) -> /text/
;; Only match _text_ to avoid conflicting with bold
(goto-char (point-min))
(while (re-search-forward "\\_<_\\([^_]+\\)_\\_>" nil t)
(replace-match "/\\1/"))
;; Images:  -> [[url]] (must come before links)
(goto-char (point-min))
(while (re-search-forward "!\\[\\(?:[^]]*\\)\\](\\([^)]+\\))" nil t)
(replace-match "[[\\1]]"))
;; Links: [text](url) -> [[url][text]]
(goto-char (point-min))
(while (re-search-forward "\\[\\([^]]+\\)\\](\\([^)]+\\))" nil t)
(replace-match "[[\\2][\\1]]"))
;; Horizontal rules
(goto-char (point-min))
(while (re-search-forward "^[ \t]*\\(-\\{3,\\}\\|\\*\\{3,\\}\\|_\\{3,\\}\\)[ \t]*$" nil t)
(replace-match "-----"))
;; Blockquotes: > text -> : text (simple version)
(goto-char (point-min))
(while (re-search-forward "^> \\(.*\\)$" nil t)
(replace-match ": \\1"))
;; Fix encoding issues
(goto-char (point-min))
(while (re-search-forward "â€" nil t)
(replace-match "—"))
;; Restore inline code as =code=
(dolist (item inline-codes)
(goto-char (point-min))
(when (search-forward (format "<<<INLINE-CODE-%d>>>" (car item)) nil t)
(replace-match (format "=%s=" (cdr item)) t t))))
;; Restore code blocks as #+begin_src ... #+end_src
(dolist (item code-blocks)
(let ((n (nth 0 item))
(indent (nth 1 item))
(lang (nth 2 item))
(code (nth 3 item)))
(goto-char (point-min))
(when (search-forward (format "<<<CODE-BLOCK-%d>>>" n) nil t)
(replace-match (format "%s#+begin_src %s\n%s%s#+end_src"
indent lang code indent)
t t)))))))
(defun my/md-to-org-convert-file (input-file output-file)
"Convert a Markdown file INPUT-FILE to an Org-mode file OUTPUT-FILE."
(with-temp-buffer
(insert-file-contents input-file)
(md-to-org-convert-buffer)
(write-file output-file)))
(defun my/convert-markdown-clipboard-to-org ()
"Convert Markdown content from clipboard to Org format and insert it at point."
(interactive)
(let ((markdown-content (current-kill 0))
(original-buffer (current-buffer)))
(with-temp-buffer
(insert markdown-content)
(my/md-to-org-convert-buffer)
(let ((org-content (buffer-string)))
(with-current-buffer original-buffer
(insert org-content))))))
(defun my/org-promote-all-headings (&optional arg)
"Promote all headings in the current Org buffer along with their subheadings."
(interactive "p")
(org-map-entries
(lambda ()
(dotimes (_ arg) (org-promote)))))Can these in buffer completion systems be replaced by a simple in-built icomplete solution?
Lets give it a go!, here is the general in buffer completion setup:
(define-key icomplete-minibuffer-map (kbd "C-n") #'icomplete-forward-completions)
(define-key icomplete-minibuffer-map (kbd "C-p") #'icomplete-backward-completions)
(define-key icomplete-minibuffer-map (kbd "RET") #'icomplete-force-complete-and-exit)
(add-hook 'after-init-hook (lambda () (fido-mode 1)))
(setq completion-styles '(flex basic substring))
(setq tab-always-indent 'complete)
(setq icomplete-delay-completions-threshold 0)
(setq icomplete-max-delay-chars 0)
(setq icomplete-compute-delay 0)
(setq icomplete-show-matches-on-no-input t)
(setq icomplete-separator " | ")
(add-hook 'buffer-list-update-hook
(lambda ()
(unless (minibufferp)
(setq-local icomplete-separator "\n"))))
(setq icomplete-in-buffer t)
(setq completion-auto-help t)
(define-key minibuffer-local-completion-map (kbd "TAB")
(lambda ()
(interactive)
(let ((completion-auto-help t))
(minibuffer-complete))))
(setq completion-show-help nil)
(setq icomplete-with-completion-tables t)
(setq icomplete-prospects-height 2)
(setq icomplete-scroll t)
(setq icomplete-hide-common-prefix t)
(if icomplete-in-buffer
(advice-add 'completion-at-point
:after #'minibuffer-hide-completions))
Note that the completion-styles variable is globally set to include flex because, by default, Icomplete is the completion engine that operates in the buffer. Since Fido mode, which is enabled by default, does not support flex (something I have now grown accustomed to), this adjustment is necessary.
Also note that when completion-in-buffer is turned on I have globally turned off the display of the Completions buffer through completion-auto-help except in the minibuffer as sometimes I would like to bring up the full list of completions, like maybe embark collect or export.
Note that setting completion-auto-help to nil means the help header in the completions buffer will not be shown, which helps to tidy things up.
Note that the buffer-list-update-hook allows for vertical Icomplete completion in the buffer! Of course, “\n” could generally be globally enabled if you would like simple Icomplete vertical completion, but I prefer vertical completion only in the buffer, as with Corfu or Company.
Note that icomplete-prospects-height allows for a form of in-buffer candidate height adjustment, but it is not an exact solution since the height is based on a horizontal setup. However, it does provide some level of control. Here, I have explicitly set it as a global setting, but in-buffer vertical completion can be tailored accordingly.
As another option, how about a simple defun leveraging completion-in-region or completing-read :
(defun my/simple-completion-at-point ()
"Use completing-read-in-buffer for completion at point."
(interactive)
(let* ((completion-data (run-hook-with-args-until-success
'completion-at-point-functions))
(beg (nth 0 completion-data))
(end (nth 1 completion-data))
(table (nth 2 completion-data))
(pred (plist-get (nthcdr 3 completion-data) :predicate))
(prefix (buffer-substring-no-properties beg end))
(completion (completing-read-default
"Complete: "
table
pred
nil ; no require-match
prefix)))
(when completion
(delete-region beg end)
(insert completion))))
(global-set-key (kbd "C-c TAB") #'my/simple-completion-at-point)More bonus points here for in buffer completion in shells, this includes eshell and shell
(defun my/eshell-history-capf ()
"Completion-at-point function for eshell history."
(let* ((beg (save-excursion
(eshell-bol)
(point)))
(end (point))
(prefix (buffer-substring-no-properties beg end))
(candidates (delete-dups
(ring-elements eshell-history-ring))))
(list beg end candidates
:exclusive 'no
:annotation-function
(lambda (_) " (history)"))))
(defun my/setup-eshell-history-completion ()
"Setup eshell history completion."
(add-hook 'completion-at-point-functions #'my/eshell-history-capf nil t))
(add-hook 'eshell-mode-hook #'my/setup-eshell-history-completion)(defun my/shell-history-capf ()
"Completion-at-point function for shell history completion."
(let* ((beg (comint-line-beginning-position))
(end (point))
(prefix (buffer-substring-no-properties beg end))
(history (ring-elements comint-input-ring))
(matching-history
(cl-remove-if-not
(lambda (cmd)
(string-prefix-p prefix cmd))
history)))
(list beg end matching-history
:exclusive 'no
:annotation-function
(lambda (_) " (history)"))))
(defun my/setup-shell-history-completion ()
"Setup shell history completion."
(add-hook 'completion-at-point-functions #'my/shell-history-capf nil t))
(add-hook 'shell-mode-hook #'my/setup-shell-history-completion)
(with-eval-after-load 'shell
(add-to-list 'completion-category-overrides
'(shell-history (styles basic substring initials))))Lets try and see how far we can get going through the org-publish mechanism for publishing a web-site!
(require 'ox-publish)
(defun my/org-html-src-block-filter (text backend info)
(when (org-export-derived-backend-p backend 'html)
(replace-regexp-in-string "\n\\s-*\n" "<br>\n" text)))
(defun my/org-setup-src-block-filter (backend)
"Set `org-export-filter-src-block-functions` dynamically based on BACKEND."
(message "Exporting with backend: %s" backend) ;; For debugging
(cond
((eq backend 'hugo) ;; Clear the filter for ox-hugo
(setq-local org-export-filter-src-block-functions nil))
((eq backend 'html) ;; Apply filter for ox-html/ox-publish
(setq-local org-export-filter-src-block-functions
'(my/org-html-src-block-filter)))))
(add-hook 'org-export-before-processing-functions #'my/org-setup-src-block-filter)
(setq org-publish-project-alist
'(("split-emacs"
:base-directory "~/DCIM/content"
:base-extension "org"
:publishing-directory "~/DCIM/content/split/emacs"
:exclude ".*"
:include ("emacs--all.org")
:publishing-function my-org-publish-split-headings
:recursive nil)
("blog-posts-emacs"
:base-directory "~/DCIM/content/split/emacs"
:base-extension "org"
:publishing-directory "~/publish/hugo-emacs/site/static/public_html"
:publishing-function org-html-publish-to-html
:recursive t
:section-numbers nil
:with-toc nil
:html-preamble t
:html-postamble t
:auto-sitemap t
:sitemap-filename "index.org"
:sitemap-title "the DyerDwelling"
:html-head "<link rel=\"stylesheet\"
href=\"../assets/css//bootstrap.css\"
type=\"text/css\"/>\n
<link rel=\"stylesheet\"
href=\"../assets/css//style-ignore.css\"
type=\"text/css\"/>"
:sitemap-function my-sitemap-format
:sitemap-sort-files alphabetically)
("images-emacs"
:base-directory "~/DCIM/content/emacs"
:base-extension "jpg\\|gif\\|png"
:recursive t
:publishing-directory "~/publish/hugo-emacs/site/static/public_html/emacs"
:publishing-function org-publish-attachment)
("blog" ;; Meta-project to combine phases
:components ("split-emacs" "images-emacs" "blog-posts-emacs"))))
(defun my-org-publish-split-headings (plist filename pub-dir)
"Split an Org file into separate files, each corresponding to a top-level heading
that is marked as DONE.
Each file name is prefixed with the date in YYYYMMDD format extracted from the
:EXPORT_HUGO_LASTMOD: property. PLIST is the property list for the publishing
process, FILENAME is the input Org file, and PUB-DIR is the publishing directory."
(with-temp-buffer
(insert-file-contents filename) ;; Load the content of the current Org file
(goto-char (point-min))
(let ((heading-level 1) ;; Level of the top-level heading to split by
prev-start heading-title sanitized-title output-file lastmod-date)
;; Iterate over all top-level headings
(while (re-search-forward (format "^\\*\\{%d\\} \\(?:\\([[:upper:]]+\\) \\)?\\(.*\\)" heading-level) nil t)
(let ((todo-keyword (match-string 1)) ;; Extract the TODO keyword (if it exists)
(heading-title (match-string 2))) ;; Extract the title of the heading
;; Process only headings marked as DONE
(when (and todo-keyword (string-equal todo-keyword "DONE"))
(setq prev-start (match-beginning 0)) ;; Start of the current heading
(setq sanitized-title (when heading-title
(replace-regexp-in-string "[^a-zA-Z0-9_-]" "_" heading-title))) ;; Sanitize title
;; Extract the :EXPORT_HUGO_LASTMOD: property for the current section
(save-excursion
(when (re-search-forward ":EXPORT_HUGO_LASTMOD: +\\(<.+>\\)" (save-excursion (re-search-forward "^\\* " nil t) (point)) t)
(let* ((raw-lastmod (match-string 1)) ;; Extract the timestamp string (e.g., "<2024-12-08 08:37>")
(date-elements (when (string-match "<\\([0-9]+\\)-\\([0-9]+\\)-\\([0-9]+\\)" raw-lastmod)
(list (match-string 1 raw-lastmod) ;; Year
(match-string 2 raw-lastmod) ;; Month
(match-string 3 raw-lastmod))))) ;; Day
(setq lastmod-date (when date-elements
(apply #'concat date-elements))))))
;; Default to "00000000" if no valid lastmod-date is found
(setq lastmod-date (or lastmod-date "00000000"))
;; Find the end of this section (right before the next top-level heading)
(let ((section-end (save-excursion
(or (re-search-forward (format "^\\*\\{%d\\} " heading-level) nil t)
(point-max))))) ;; End of current section or end of file
;; Only proceed if sanitized title exists and is valid
(when (and sanitized-title (not (string-empty-p sanitized-title)))
;; Create the output file name (prepend the date)
(setq output-file (expand-file-name (format "%s-%s.org" lastmod-date sanitized-title) pub-dir))
;; Write the section content (from prev-start to section-end)
(write-region prev-start section-end output-file)
(message "Wrote %s" output-file)))))))
;; Return nil to indicate successful processing
nil))
(defun my-sitemap-format (title list)
"Generate a sitemap with TITLE and reverse-sorted LIST of files."
(setq list (nreverse (cdr list)))
(concat "#+TITLE: " title "\n\n"
"* Blog Posts\n"
(mapconcat
(lambda (entry)
(format "- %s\n" (car entry)))
list)
"\n"))Starting with the following and adapting, it is a decent starting point:
(defun my-generate-rss-feed ()
"Generate a detailed RSS feed for Org-published blog posts."
(interactive)
(let* ((rss-file (expand-file-name "index.xml" "/home/jdyer/publish/hugo-emacs/site/static/public_html"))
(base-url "https://www.emacs.dyerdwelling.family/public_html/")
(self-link "https://www.emacs.dyerdwelling.family/public_html/index.xml") ;; Self-referencing link for Atom feeds
(last-build-date (format-time-string "%a, %d %b %Y %H:%M:%S %z")) ;; Current time as lastBuildDate
(org-directory "/home/jdyer/source/test/elisp")
(static-author "[email protected] (James Dyer)") ;; Static author
;; (org-directory "/home/jdyer/DCIM/content/split/emacs")
(rss-items ""))
;; Iterate over all Org files in the directory
(dolist (org-file (directory-files org-directory t "\\.org$"))
(let* ((html-file (concat (file-name-sans-extension
(file-name-nondirectory org-file)) ".html"))
(url (concat base-url html-file))
(heading-level 1)
(guid url) ;; Default GUID as the post URL
title
content
html-content
raw-pubdate
pubdate)
;; Read and process the org file
(with-temp-buffer
(insert-file-contents org-file)
(goto-char (point-min))
;; Extract the title from the first heading
(when (re-search-forward (format "^\\*\\{%d\\} \\(?:\\([[:upper:]]+\\) \\)?\\(.*\\)" heading-level) nil t)
(setq title (match-string 2)))
;; Extract the :EXPORT_HUGO_LASTMOD: property value
(when (re-search-forward "^.*EXPORT_HUGO_LASTMOD: *<\\([^>]+\\)>" nil t)
(setq raw-pubdate (match-string 1)))
;; Convert the raw-pubdate to the RFC 822 format for <pubDate>
(when raw-pubdate
(setq pubdate (format-time-string
"%a, %d %b %Y %H:%M:%S %z"
(org-time-string-to-time (concat "<" raw-pubdate ">")))))
;; Move to the end of :END: and extract the remaining contents
(when (re-search-forward "^:END:\n" nil t)
(setq content (buffer-substring-no-properties (point) (point-max)))
;; Convert the content to HTML
(setq html-content (org-export-string-as content 'html t '(:with-toc nil)))
;; (setq html-content (xml-escape-string html-content))
))
;; Add an item to the RSS feed
(setq rss-items
(concat rss-items (format "
<item>
<title>%s</title>
<link>%s</link>
<guid>%s</guid>
<pubDate>%s</pubDate>
<author>%s</author>
<description><![CDATA[%s]]></description>
</item>"
(or title "Untitled Post")
url
guid ;; Use the generated GUID
(or pubdate last-build-date) ;; Fallback to lastBuildDate if missing
static-author ;; Static author name
(or html-content "No content available"))))))
;; Write the RSS feed to the file
(with-temp-file rss-file
(insert "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>
<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">
<channel>
<title>Emacs@Dyerdwelling</title>
<image>
<url>/images/banner/favicon-james.png</url>
<title>Emacs@Dyerdwelling</title>
<link>https://emacs.dyerdwelling.family/public_html/</link>
<width>32</width>
<height>32</height>
</image>
<link>" base-url "</link>
<description>Recent content on Emacs@Dyerdwelling</description>
<language>en</language>
<managingEditor>[email protected] (James Dyer)</managingEditor>
<webMaster>[email protected] (James Dyer)</webMaster>
<lastBuildDate>" last-build-date "</lastBuildDate>
<atom:link href=\"" self-link "\" rel=\"self\" type=\"application/rss+xml\" />"
rss-items "
</channel>
</rss>"))
(message "RSS feed generated at %s" rss-file)))Note: a more modern version would have eglot built-in (29.1)
In the mean time lets leverage etags as much as possible, initially a bash script to generate a TAGS file for as many programming language extensions possible:
#!/bin/bash
TAGF=$PWD/TAGS
rm -f "$TAGF"
for src in `find $PWD \( -path \*/.cache -o \
-path \*/.gnupg -o \
-path \*/.local -o \
-path \*/.mozilla -o \
-path \*/.thunderbird -o \
-path \*/.wine -o \
-path \*/Games -o \
-path \*/cache -o \
-path \*/chromium -o \
-path \*/elpa -o \
-path \*/nas -o \
-path \*/syncthing -o \
-path \*/Image-Line -o \
-path \*/.cargo -o \
-path \*/.git -o \
-path \*/.svn -o \
-path \*/.themes -o \
-path \*/themes -o \
-path \*/objs -o \
-path \*/ArtRage \) \
-prune -o -type f -print`;
do
case "${src}" in
*.ad[absm]|*.[CFHMSacfhlmpsty]|*.def|*.in[cs]|*.s[as]|*.src|*.cc|\
*.hh|*.[chy]++|*.[ch]pp|*.[chy]xx|*.pdb|*.[ch]s|*.[Cc][Oo][Bb]|\
*.[eh]rl|*.f90|*.for|*.java|*.[cem]l|*.clisp|*.lisp|*.[Ll][Ss][Pp]|\
[Mm]akefile*|*.pas|*.[Pp][LlMm]|*.psw|*.lm|*.pc|*.prolog|*.oak|\
*.p[sy]|*.sch|*.scheme|*.[Ss][Cc][Mm]|*.[Ss][Mm]|*.bib|*.cl[os]|\
*.ltx|*.sty|*.TeX|*.tex|*.texi|*.texinfo|*.txi|*.x[bp]m|*.yy|\
*.[Ss][Qq][Ll])
etags --append "${src}" -o "$TAGF";
echo ${src}
;;
*)
FTYPE=`file ${src}`;
case "${FTYPE}" in
*script*text*)
etags --append "${src}" -o "$TAGF";
echo ${src}
;;
*text*)
if head -n1 "${src}" | grep '^#!' >/dev/null 2>&1;
then
etags --append "${src}" -o "$TAGF";
echo ${src}
fi;
;;
esac;
;;
esac;
done
echo
echo "Finished!"
echoor maybe the more elisp based approach:
(defun my/etags-load ()
"Load TAGS file from the first it can find up the directory stack."
(interactive)
(let ((my-tags-file (locate-dominating-file default-directory "TAGS")))
(when my-tags-file
(message "Loading tags file: %s" my-tags-file)
(visit-tags-table my-tags-file))))
(when (executable-find "my-generate-etags.sh")
(defun my/etags-update ()
"Call external bash script to generate new etags for all languages it can find."
(interactive)
(async-shell-command "my-generate-etags.sh" "*etags*")))
(defun predicate-exclusion-p (dir)
"exclusion of directories"
(not
(or
(string-match "/home/jdyer/examples/CPPrograms/nil" dir)
)))
(defun my/generate-etags ()
"Generate TAGS file for various source files in `default-directory` and its subdirectories."
(interactive)
(message "Getting file list...")
(let ((all-files
(append
(directory-files-recursively default-directory "\\(?:\\.cpp$\\|\\.c$\\|\\.h$\\)" nil 'predicate-exclusion-p)
(directory-files-recursively default-directory "\\(?:\\.cs$\\|\\.cs$\\)" nil 'predicate-exclusion-p)
(directory-files-recursively default-directory "\\(?:\\.ads$\\|\\.adb$\\)" nil 'predicate-exclusion-p)))
(tags-file-path (expand-file-name (concat default-directory "TAGS"))))
(unless (file-directory-p default-directory)
(error "Default directory does not exist: %s" default-directory))
;; Generate TAGS file
(dolist (file all-files)
(message file)
(shell-command (format "etags --append \%s -o %s" file tags-file-path)))))
(global-set-key (kbd "C-x p l") 'my/etags-load)
(global-set-key (kbd "C-x p u") 'my/etags-update)A bit of a bonus this one, I was watching one of System Crafters videos and there was talk around using built-in functionality and how it would be nice if there was an orderless implementation to allow minibuffer completion on an any word basis.
Well I thought I would take up the challenge and came up with this:
(defun simple-orderless-completion (string table pred point)
"Enhanced orderless completion with better partial matching."
(let* ((words (split-string string "[-, ]+"))
(patterns (mapcar (lambda (word)
(concat "\\b.*" (regexp-quote word) ".*"))
words))
(full-regexp (mapconcat 'identity patterns "")))
(if (string-empty-p string)
(all-completions "" table pred)
(cl-remove-if-not
(lambda (candidate)
(let ((case-fold-search completion-ignore-case))
(and (cl-every (lambda (word)
(string-match-p
(concat "\\b.*" (regexp-quote word))
candidate))
words)
t)))
(all-completions "" table pred)))))
;; Register the completion style
(add-to-list 'completion-styles-alist
'(simple-orderless simple-orderless-completion
simple-orderless-completion))
;; Set different completion styles for minibuffer vs other contexts
(defun setup-minibuffer-completion-styles ()
"Use orderless completion in minibuffer, regular completion elsewhere."
;; For minibuffer: use orderless first, then fallback to flex and basic
(setq-local completion-styles '(basic simple-orderless flex substring)))
;; Hook into minibuffer setup
(add-hook 'minibuffer-setup-hook #'setup-minibuffer-completion-styles)Opening a file, generally from dired, a solution as below:
(with-eval-after-load 'dired
(define-key dired-mode-map (kbd "W") 'dired-do-async-shell-command)
(setq dired-guess-shell-alist-user
'(("\\.\\(jpg\\|jpeg\\|png\\|gif\\|bmp\\)$" "gthumb")
("\\.\\(mp4\\|mkv\\|avi\\|mov\\|wmv\\|flv\\|mpg\\)$" "mpv")
("\\.\\(mp3\\|wav\\|ogg\\|\\)$" "mpv")
("\\.\\(kra\\)$" "krita")
("\\.\\(xcf\\)$" "gimp")
("\\.\\(odt\\|ods\\|doc\\|docx\\)$" "libreoffice")
("\\.\\(html\\|htm\\)$" "firefox")
("\\.\\(pdf\\|epub\\)$" "xournalpp"))))Does dired actions asynchronously, originally I thought this was built-in but I think you require the following for activation:
(require async)
(require 'dired-async)
(dired-async-mode 1)Could I just call out to async-shell-command, something like:
(defun my/rsync (dest)
"Rsync copy."
(interactive
(list
(expand-file-name (read-file-name "rsync to:"
(dired-dwim-target-directory)))))
(let ((files (dired-get-marked-files nil current-prefix-arg))
(command "rsync -arvz --progress --no-g "))
(dolist (file files)
(setq command (concat command (shell-quote-argument file) " ")))
(setq command (concat command (shell-quote-argument dest)))
(async-shell-command command "*rsync*")
(dired-unmark-all-marks)
(other-window 1)
(sleep-for 1)
(dired-revert)
(revert-buffer nil t nil)))Some elisp for some simple predictive inline completion, maybe take a look at how capf-autosuggest does it or the new completion preview in Emacs 30.
Also looked at fancy-dabbrev as I typically mainly use the simple dabbrev for completion.
I’m currently developing a very simple mode (I probably won’t release as a package as my idea is to make it small enough to insert directly into an Emacs config) - I’m currently in the process of coding up and simplifying all those use-packages I frequently use - which comes in useful for offline or airgapped Emacs installs.
This prototype allows inline autosuggestions in eshell, comint (inlining based on history, like capf-autosuggest) and also in-buffer using the mighty dabbrev (like fancy-dabbrev), here is what I have so far. (see below)
After evaluating, just run M-x simple-autosuggest-mode and there will be an inline autosuggestion appearing with acceptance using C-e
Oh, it’s also like the completion-preview coming to Emacs 30 but the in buffer inline is just using dabbrev as this is strangely the completion I find I use all the time.
(require 'dabbrev)
(defun simple-autosuggest--get-completion (input &optional bounds)
"Core function handling suggestion logic for INPUT with optional BOUNDS."
(let* ((bounds (or bounds
(cond ((derived-mode-p 'comint-mode)
(when-let ((proc-mark (process-mark (get-buffer-process (current-buffer)))))
(and (>= (point) proc-mark) (cons proc-mark (line-end-position)))))
((derived-mode-p 'eshell-mode)
(when (>= (point) eshell-last-output-end)
(cons (save-excursion (eshell-bol) (point)) (point-max))))
(t (bounds-of-thing-at-point 'symbol)))))
(input (or input (and bounds (buffer-substring-no-properties (car bounds) (cdr bounds)))))
(min-length (cond ((derived-mode-p 'comint-mode) 0)
((derived-mode-p 'eshell-mode) 0)
(t 2)))
(suggestion (and input (>= (length input) min-length)
(memq last-command '(org-self-insert-command self-insert-command yank))
(cond ((derived-mode-p 'comint-mode)
(when-let ((ring comint-input-ring))
(seq-find (lambda (h) (string-prefix-p input h t))
(ring-elements ring))))
((derived-mode-p 'eshell-mode)
(when-let ((ring eshell-history-ring))
(seq-find (lambda (h) (string-prefix-p input h t))
(ring-elements ring))))
(t (let ((dabbrev-case-fold-search t)
(dabbrev-case-replace nil))
(ignore-errors
(dabbrev--reset-global-variables)
(dabbrev--find-expansion input 0 t))))))))
(when (and suggestion (not (string= input suggestion)))
(let ((suffix (substring suggestion (length input))))
(put-text-property 0 1 'cursor 0 suffix)
(overlay-put simple-autosuggest--overlay 'after-string
(propertize suffix 'face '(:inherit shadow)))
(move-overlay simple-autosuggest--overlay (point) (point))
suggestion))))
(defun simple-autosuggest-end-of-line (arg)
"Move to end of line, accepting suggestion first if available.
Works with both standard `move-end-of-line` and `org-end-of-line`."
(interactive "^p")
(if-let ((overlay simple-autosuggest--overlay)
(suggestion (overlay-get overlay 'after-string)))
(progn
(insert (substring-no-properties suggestion))
(overlay-put overlay 'after-string nil))
;; Detect whether we're in org-mode and use the appropriate function
(if (and (eq major-mode 'org-mode)
(fboundp 'org-end-of-line))
(org-end-of-line arg)
(move-end-of-line arg))))
(defun simple-autosuggest-update ()
"Update the auto-suggestion overlay."
(when simple-autosuggest--overlay
(unless (simple-autosuggest--get-completion nil nil)
(overlay-put simple-autosuggest--overlay 'after-string nil))))
(define-minor-mode simple-autosuggest-mode
"Minor mode for showing auto-suggestions from history or dabbrev completion."
:lighter " SAM"
:keymap (let ((map (make-sparse-keymap)))
;; Use a unified function for both cases
(define-key map [remap move-end-of-line] #'simple-autosuggest-end-of-line)
(when (fboundp 'org-end-of-line)
;; If org-mode is loaded, also remap org-end-of-line
(define-key map [remap org-end-of-line] #'simple-autosuggest-end-of-line))
;; Explicitly bind C-e which is commonly used
(define-key map (kbd "C-e") #'simple-autosuggest-end-of-line)
map)
(if simple-autosuggest-mode
(progn
(setq-local simple-autosuggest--overlay (make-overlay (point) (point) nil t t))
(add-hook 'post-command-hook #'simple-autosuggest-update nil t))
(remove-hook 'post-command-hook #'simple-autosuggest-update t)
(when simple-autosuggest--overlay
(delete-overlay simple-autosuggest--overlay)
(setq simple-autosuggest--overlay nil))))
(provide 'simple-autosuggest)
(define-globalized-minor-mode global-simple-autosuggest-mode
simple-autosuggest-mode ;; The mode to be globalized
(lambda () ;; A function to enable the mode
(unless (minibufferp) ;; Avoid enabling the mode in the minibuffer
(simple-autosuggest-mode 1))))
(global-simple-autosuggest-mode 1)I never thought about this, but isn’t it nice to have dired shown in all its glory with a little more bling, well Emacs-solo came up with a nice solution and I have adapted it to focus more on a safe bunch of unicode characters so you don’t need to worry about installing the relevant font pack!
(defvar dired-icons-map
'(("el" . "λ") ("rb" . "◆") ("js" . "○") ("ts" . "●") ("json" . "◎") ("md" . "■")
("txt" . "□") ("html" . "▲") ("css" . "▼") ("png" . "◉") ("jpg" . "◉")
("pdf" . "▣") ("zip" . "▢") ("py" . "∆") ("c" . "◇") ("sql" . "▦")
("mp3" . "♪") ("mp4" . "▶") ("exe" . "▪")))
(defun dired-add-icons ()
(when (derived-mode-p 'dired-mode)
(let ((inhibit-read-only t))
(save-excursion
(goto-char (point-min))
(while (and (not (eobp)) (< (line-number-at-pos) 200))
(condition-case nil
(let ((line (buffer-substring-no-properties (line-beginning-position) (line-end-position))))
(when (and (> (length line) 10)
(string-match "\\([rwxd-]\\{10\\}\\)" line)
(dired-move-to-filename t)
(not (looking-at "[▶◦λ◆○●◎■□▲▼◉▣▢◇∆▦♪▪] ")))
(let* ((is-dir (eq (aref line (match-beginning 1)) ?d))
(filename (and (string-match "\\([^ ]+\\)$" line) (match-string 1 line)))
(icon (cond (is-dir "▶")
((and filename (string-match "\\.\\([^.]+\\)$" filename))
(or (cdr (assoc (downcase (match-string 1 filename)) dired-icons-map)) "◦"))
(t "◦"))))
(insert icon " "))))
(error nil))
(forward-line))))))
(add-hook 'dired-after-readin-hook 'dired-add-icons)As this configuration is mainly offline then naturally running some AI would take place locally and therefore using something like ollama, so lets replace the likes of gptel, chatgpt-shell and ellama can be replace by a scaled down version of my ollama-buddy package:
;;; ollama-buddy.el --- A mini version of ollama-buddy
;;; Commentary:
;;
(require 'json)
(require 'subr-x)
(require 'url)
(require 'cl-lib)
;;; Code:
(defgroup ollama-buddy nil "Customization group for Ollama Buddy." :group 'applications :prefix "ollama-buddy-")
(defcustom ollama-buddy-host "localhost" "Host where Ollama server is running." :type 'string :group 'ollama-buddy)
(defcustom ollama-buddy-port 11434 "Port where Ollama server is running." :type 'integer :group 'ollama-buddy)
(defcustom ollama-buddy-default-model nil "Default Ollama model to use." :type 'string :group 'ollama-buddy)
(defvar ollama-buddy--conversation-history nil "History of messages for the current conversation.")
(defvar ollama-buddy--current-model nil "Timer for checking Ollama connection status.")
(defvar ollama-buddy--chat-buffer "*Ollama Buddy Chat*" "Chat interaction buffer.")
(defvar ollama-buddy--active-process nil "Active Ollama process.")
(defvar ollama-buddy--prompt-history nil "History of prompts used in ollama-buddy.")
(defun ollama-buddy--add-to-history (role content)
"Add message with ROLE and CONTENT to conversation history."
(push `((role . ,role)(content . ,content)) ollama-buddy--conversation-history))
(defun ollama-buddy-clear-history ()
"Clear the current conversation history."
(interactive)
(setq ollama-buddy--conversation-history nil)
(ollama-buddy--update-status "History cleared"))
(defun ollama-buddy--update-status (status &optional model)
"Update the status with STATUS text and MODEL in the header-line."
(with-current-buffer (get-buffer-create ollama-buddy--chat-buffer)
(let* ((model (or ollama-buddy--current-model ollama-buddy-default-model "No Model")))
(setq header-line-format
(concat (propertize (format " %s : %s" model status) 'face `(:weight bold)))))))
(defun ollama-buddy--stream-filter (_proc output)
"Process stream OUTPUT while preserving cursor position."
(when-let* ((json-str (replace-regexp-in-string "^[^\{]*" "" output))
(json-data (and (> (length json-str) 0) (json-read-from-string json-str)))
(text (alist-get 'content (alist-get 'message json-data))))
(with-current-buffer ollama-buddy--chat-buffer
(let* ((inhibit-read-only t)
(window (get-buffer-window ollama-buddy--chat-buffer t))
(old-point (and window (window-point window)))
(at-end (and window (>= old-point (point-max))))
(old-window-start (and window (window-start window))))
(save-excursion
(ollama-buddy--update-status "Processing...")
(goto-char (point-max))
(insert text)
(when (boundp 'ollama-buddy--current-response)
(setq ollama-buddy--current-response
(concat (or ollama-buddy--current-response "") text)))
(unless (boundp 'ollama-buddy--current-response)
(setq ollama-buddy--current-response text))
(when (eq (alist-get 'done json-data) t)
(ollama-buddy--add-to-history "assistant" ollama-buddy--current-response)
(makunbound 'ollama-buddy--current-response)
(insert "\n\n" (propertize (concat "[" ollama-buddy--current-model ": FINISHED]")
'face '(:inherit bold)))
(ollama-buddy--show-prompt)
(ollama-buddy--update-status "Finished")))
(when window
(if at-end
(set-window-point window (point-max))
(set-window-point window old-point))
(set-window-start window old-window-start t))))))
(defun ollama-buddy--stream-sentinel (_proc event)
"Handle stream completion EVENT."
(when-let* ((status (cond ((string-match-p "finished" event) "Completed")
((string-match-p "\\(?:deleted\\|connection broken\\)" event)
"Interrupted"))))
(with-current-buffer ollama-buddy--chat-buffer
(let ((inhibit-read-only t))
(goto-char (point-max))
(insert (propertize (format "\n\n[Stream %s]" status) 'face '(:weight bold)))
(ollama-buddy--show-prompt)))
(ollama-buddy--update-status (concat "Stream " status))))
(defun ollama-buddy--swap-model ()
"Swap ollama model."
(interactive)
(let ((new-model (completing-read "Model: " (ollama-buddy--get-models) nil t)))
(setq ollama-buddy-default-model new-model ollama-buddy--current-model new-model)
(pop-to-buffer (get-buffer-create ollama-buddy--chat-buffer))
(ollama-buddy--show-prompt)
(goto-char (point-max))
(ollama-buddy--update-status "Idle")))
(defun ollama-buddy-menu ()
"Open chat buffer and initialize if needed."
(interactive)
(pop-to-buffer (get-buffer-create ollama-buddy--chat-buffer))
(with-current-buffer (get-buffer-create ollama-buddy--chat-buffer)
(when (= (buffer-size) 0)
(ollama-buddy-mode 1)
(insert "Send : C-c C-c\nCancel : C-c C-k\nModel : C-c m\n\n")
(insert (mapconcat 'identity (ollama-buddy--get-models) "\n"))
(ollama-buddy--show-prompt))
(ollama-buddy--update-status "Idle"))
(goto-char (point-max)))
(defun ollama-buddy--show-prompt ()
"Show the prompt with optionally a MODEL."
(interactive)
(when (not ollama-buddy-default-model)
;; just get the first model
(let ((model (car (ollama-buddy--get-models))))
(setq ollama-buddy--current-model model)
(setq ollama-buddy-default-model model)
(insert (format "\n\n* NO DEFAULT MODEL : Using best guess : %s" model))))
(let* ((model (or ollama-buddy--current-model ollama-buddy-default-model "Default:latest")))
(insert (format "\n\n%s\n\n%s %s"
(propertize "------------------" 'face '(:inherit bold))
(propertize model 'face `(:weight bold))
(propertize ">> PROMPT: " 'face '(:inherit bold))))))
(defun ollama-buddy--send (&optional prompt model)
"Send PROMPT with optional MODEL"
(unless (> (length prompt) 0)
(user-error "Ensure prompt is defined"))
(let* ((messages (reverse ollama-buddy--conversation-history))
(messages (append messages `(((role . "user")
(content . ,prompt)))))
(payload (json-encode
`((model . ,model)
(messages . ,(vconcat [] messages))
(stream . t)))))
(setq ollama-buddy--current-model model)
(ollama-buddy--add-to-history "user" prompt)
(with-current-buffer (get-buffer-create ollama-buddy--chat-buffer)
(pop-to-buffer (current-buffer))
(goto-char (point-max))
(insert (format "\n\n%s\n\n%s %s\n\n%s\n\n"
(propertize "------------------" 'face '(:inherit bold))
(propertize "[User: PROMPT]" 'face '(:inherit bold))
prompt
(propertize (concat "[" model ": RESPONSE]") 'face `(:inherit bold))))
(visual-line-mode 1))
(ollama-buddy--update-status "Sending request..." model)
(when (and ollama-buddy--active-process
(process-live-p ollama-buddy--active-process))
(set-process-sentinel ollama-buddy--active-process nil)
(delete-process ollama-buddy--active-process)
(setq ollama-buddy--active-process nil))
(condition-case err
(setq ollama-buddy--active-process
(make-network-process
:name "ollama-chat-stream"
:buffer nil
:host ollama-buddy-host
:service ollama-buddy-port
:coding 'utf-8
:filter #'ollama-buddy--stream-filter
:sentinel #'ollama-buddy--stream-sentinel))
(error
(ollama-buddy--update-status "OFFLINE - Connection failed")
(error "Failed to connect to Ollama: %s" (error-message-string err))))
(condition-case err
(process-send-string
ollama-buddy--active-process
(concat "POST /api/chat HTTP/1.1\r\n"
(format "Host: %s:%d\r\n" ollama-buddy-host ollama-buddy-port)
"Content-Type: application/json\r\n"
(format "Content-Length: %d\r\n\r\n" (string-bytes payload))
payload))
(error
(ollama-buddy--update-status "OFFLINE - Send failed")
(when (and ollama-buddy--active-process
(process-live-p ollama-buddy--active-process))
(delete-process ollama-buddy--active-process))
(error "Failed to send request to Ollama: %s" (error-message-string err))))))
(defun ollama-buddy--make-request (endpoint method &optional payload)
"Generic request function for ENDPOINT with METHOD and optional PAYLOAD."
(let* ((url (format "http://%s:%d%s" ollama-buddy-host ollama-buddy-port endpoint))
(url-request-method method)
(url-request-extra-headers '(("Content-Type" . "application/json")
("Connection" . "close")))
(url-request-data (when payload
(encode-coding-string payload 'utf-8))))
(with-temp-buffer
(url-insert-file-contents url)
(json-read-from-string (buffer-string)))))
(defun ollama-buddy--get-models ()
"Get available Ollama models."
(when-let ((response (ollama-buddy--make-request "/api/tags" "GET")))
(mapcar (lambda (m) (alist-get 'name m))(alist-get 'models response))))
(defun ollama-buddy--send-prompt ()
"Send the current prompt to a LLM.."
(interactive)
(let* ((bounds (save-excursion
(search-backward ">> PROMPT:")
(search-forward ":")
(point)))
(model (or ollama-buddy--current-model ollama-buddy-default-model "Default:latest"))
(query-text (string-trim (buffer-substring-no-properties bounds (point)))))
(when (and query-text (not (string-empty-p query-text)))
(add-to-history 'ollama-buddy--prompt-history query-text))
(ollama-buddy--send query-text model)))
(defun ollama-buddy--cancel-request ()
"Cancel the current request and clean up resources."
(interactive)
(when ollama-buddy--active-process
(delete-process ollama-buddy--active-process)
(setq ollama-buddy--active-process nil))
(ollama-buddy--update-status "Cancelled"))
(defvar ollama-buddy-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-c") #'ollama-buddy--send-prompt)
(define-key map (kbd "C-c C-k") #'ollama-buddy--cancel-request)
(define-key map (kbd "C-c m") #'ollama-buddy--swap-model)
map)
"Keymap for ollama-buddy mode.")
(define-minor-mode ollama-buddy-mode
"Minor mode for ollama-buddy keybindings."
:lighter " OB" :keymap ollama-buddy-mode-map)
(provide 'ollama-buddy)
;;; ollama-buddy.el ends hereAnd for another bonus, which I have just discovered from Emacs Solo, this:
(defun emacs-solo/ollama-run-model ()
"Run `ollama list`, let the user choose a model, and open it in `ansi-term`.
Asks for a prompt when run. If none is passed (RET), starts it interactive.
If a region is selected, prompt for additional input and pass it as a query."
(interactive)
(let* ((output (shell-command-to-string "ollama list"))
(models (let ((lines (split-string output "\n" t)))
(mapcar (lambda (line) (car (split-string line))) (cdr lines))))
(selected (completing-read "Select Ollama model: " models nil t))
(region-text (when (use-region-p)
(shell-quote-argument
(replace-regexp-in-string "\n" " "
(buffer-substring-no-properties
(region-beginning)
(region-end))))))
(prompt (read-string "Ollama Prompt (leave it blank for interactive): " nil nil nil)))
(when (and selected (not (string-empty-p selected)))
(ansi-term "/bin/sh")
(sit-for 1)
(let ((args (list (format "ollama run %s"
selected))))
(when (and prompt (not (string-empty-p prompt)))
(setq args (append args (list (format "\"%s\"" prompt)))))
(when region-text
(setq args (append args (list (format "\"%s\"" region-text)))))
(term-send-raw-string (string-join args " "))
(term-send-raw-string "\n")))))Just poodling around with some extreme yak shaving and I thought, why not shave down the mode-line, I now sometimes have many modes displayed for a file so lets cut back on them, turns out this is a very small defun replacement!
(defun tiny-diminish (mode &optional replacement)
"Hide or replace modeline display of minor MODE with REPLACEMENT."
(when-let ((entry (assq mode minor-mode-alist)))
(setcdr entry (list (or replacement "")))))
(tiny-diminish 'abbrev-mode)
(tiny-diminish 'visual-line-mode)
(tiny-diminish 'org-indent-mode)
Taken directly from Emacs Everywhere, even in Wayland | Thanos Apollo
(defun thanos/wtype-text (text)
"Process TEXT for wtype, handling newlines properly."
(let* ((has-final-newline (string-match-p "\n$" text))
(lines (split-string text "\n"))
(last-idx (1- (length lines))))
(string-join
(cl-loop for line in lines
for i from 0
collect (cond
;; Last line without final newline
((and (= i last-idx) (not has-final-newline))
(format "wtype -s 350 \"%s\""
(replace-regexp-in-string "\"" "\\\\\"" line)))
;; Any other line
(t
(format "wtype -s 350 \"%s\" && wtype -k Return"
(replace-regexp-in-string "\"" "\\\\\"" line)))))
" && ")))
(define-minor-mode thanos/type-mode
"Minor mode for inserting text via wtype."
:keymap `((,(kbd "C-c C-c") . ,(lambda () (interactive)
(call-process-shell-command
(thanos/wtype-text (buffer-string))
nil 0)
(delete-frame)))
(,(kbd "C-c C-k") . ,(lambda () (interactive)
(kill-buffer (current-buffer))))))
(defun thanos/type ()
"Launch a temporary frame with a clean buffer for typing."
(interactive)
(let ((frame (make-frame '((name . "emacs-float")
(fullscreen . 0)
(undecorated . t)
(width . 70)
(height . 20))))
(buf (get-buffer-create "emacs-float")))
(select-frame frame)
(switch-to-buffer buf)
(with-current-buffer buf
(erase-buffer)
(org-mode)
(flyspell-mode)
(thanos/type-mode)
(setq-local header-line-format
(format " %s to insert text or %s to cancel."
(propertize "C-c C-c" 'face 'help-key-binding)
(propertize "C-c C-k" 'face 'help-key-binding)))
;; Make the frame more temporary-like
(set-frame-parameter frame 'delete-before-kill-buffer t)
(set-window-dedicated-p (selected-window) t))))
(server-start)I’ve recently explored Emacs view-mode and found that, with a little customization, it serves as an excellent lightweight modal editing solution, potentially replacing Evil mode for many users.
Highlights:
- Single-key navigation (Vim or Emacs style, no modifiers)
- Easily toggled on/off (
C-<escape>,C-<tab>) - Cursor style changes for clear state indication
- Optional: enable read-only by default for “baby-proofing”
- Vanilla, extensible, and package-free
(add-hook 'find-file-hook
(lambda ()
(view-mode 1)))
(setq view-read-only t)
(with-eval-after-load 'view
(define-key view-mode-map (kbd "j") 'next-line)
(define-key view-mode-map (kbd "k") 'previous-line)
(define-key view-mode-map (kbd "h") 'backward-char)
(define-key view-mode-map (kbd "l") 'forward-char)
(define-key view-mode-map (kbd "i") 'View-exit)
;; Beginning/end of line (Vim style)
(define-key view-mode-map (kbd "0") 'beginning-of-line)
(define-key view-mode-map (kbd "$") 'end-of-line)
;; Beginning/end of buffers
(define-key view-mode-map (kbd "g") 'beginning-of-buffer)
(define-key view-mode-map (kbd "G") 'end-of-buffer)
;; Page movement
(define-key view-mode-map (kbd "u") '(lambda()
(interactive)
(View-scroll-page-backward 3)))
(define-key view-mode-map (kbd "d") '(lambda()
(interactive)
(View-scroll-page-forward 3))))
;; Optional: return to view-mode after saving
(add-hook 'after-save-hook
(lambda ()
(when (and buffer-file-name (not view-mode))
(view-mode 1))))
(add-hook 'view-mode-hook
(defun view-mode-hookee+ ()
(setq cursor-type (if view-mode 'box 'bar))))
(global-set-key (kbd "C-<escape>") 'view-mode)