Commentary:
isamert's configuration
This is my Emacs configuration. My main focus is sanity. I'm a person who get frustrated pretty easily. So instead of optimizing the time spent on doing things, I try to find ways of doing things that does not make me frustrated. Most of the time you get speed boost as a byproduct.
Emacs installation
I use my distributions package manager to install Emacs. However, in MacOS (my work computer) I use homebrew and emacs-plus formula. Here is how you install it:
brew tap d12frosted/emacs-plus
brew install emacs-plus@30 \
--with-dragon-icon \
--with-dbus \
--with-imagemagick \
--with-poll \
--with-debug
There are also other nice Emacs distributions, like:
But I need the aforementioned switches and patches. Otherwise these are very good.
–with-poll
You can read about the options here, but the most important one is
--with-poll. Otherwise you'll get lot's of no file descriptors
left, too many files are open errors, especially if you are using
LSP etc..
–with-debug
It is nice for debugging crashes or freezes. Helps you get a better
output from the debugger. When Emacs freezes and pkill -USR2 Emacs
is not helping, what you can do to debug the issue is:
$ cd ~/Library/Caches/Homebrew/emacs-plus@30--git/src
$ lldb --local-lldbinit /opt/homebrew/Cellar/emacs-plus@30/<VERSION>/Emacs.app
(lldb) r # → Do this to start Emacs
# wait for the freeze
# C-c to quit the Emacs after freeze
(lldb) xbacktrace # or bt
Same story with crashes. Just run Emacs through lldb all the time
and be prepared I guess…
Usage notes
General notes and conventions
- This configuration is meant to be used with emacs daemon, so I don't really care about the startup time etc.
- I try to split package configurations into multiple org src blocks and unify them using
nowebreferences under a singleuse-packagedirective. - I try to put things in way that easily copyable from the configuration. An example would be using multiple
(setq ...)lines instead of having one(setq ... ...)call and setting multiple variables in one go. - I make use of
use-packagefeatures quite minimally. See for more information that. This is generally related with the item above and future-proofing. - I use
verbatim textandcode textcompletely randomly. - I try to prefer built-in packages or packages that enhances built-in ones where possible. I'm also trying to simplify my configuration, so another goal is to reduce the package number. Although I intend to keep packages that enhances the overall experience with no special configuration (just install and forget type of packages).
Keybinding conventions
- After leader
e- is reserved for independent programs, that is not related to editing/programming. For example, "ec" opens calendar, "ee" opens elfeed, "er…" controls the radio.
t- is reserved for toggling stuff. Toggle the terminal, toggle a frequently accessed buffer etc.
h- is reserved for any menu with fuzzy selection that does not fit anywhere else.
g- is for git related functionality.
p- is for project related functionality.
/- is for search/translate related functionality. (Generally external programs)
b- is for buffers.
w- is for windows. I also use
C-wfor this, which is default prefix for window-related functions in vim. o- is for org-mode/outline mode.
Updating packages
Just do M-x straight-pull-all. I do this quite infrequently. If
everything is working fine as it is, I tend to not update anything.
(defvar elpaca-installer-version 0.12)
(defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory))
(defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory))
(defvar elpaca-sources-directory (expand-file-name "sources/" elpaca-directory))
(defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git"
:ref nil :depth 1 :inherit ignore
:files (:defaults "elpaca-test.el" (:exclude "extensions"))
:build (:not elpaca-activate)))
(let* ((repo (expand-file-name "elpaca/" elpaca-sources-directory))
(build (expand-file-name "elpaca/" elpaca-builds-directory))
(order (cdr elpaca-order))
(default-directory repo))
(add-to-list 'load-path (if (file-exists-p build) build repo))
(unless (file-exists-p repo)
(make-directory repo t)
(when (<= emacs-major-version 28) (require 'subr-x))
(condition-case-unless-debug err
(if-let* ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*"))
((zerop (apply #'call-process `("git" nil ,buffer t "clone"
,@(when-let* ((depth (plist-get order :depth)))
(list (format "--depth=%d" depth) "--no-single-branch"))
,(plist-get order :repo) ,repo))))
((zerop (call-process "git" nil buffer t "checkout"
(or (plist-get order :ref) "--"))))
(emacs (concat invocation-directory invocation-name))
((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch"
"--eval" "(byte-recompile-directory \".\" 0 'force)")))
((require 'elpaca))
((elpaca-generate-autoloads "elpaca" repo)))
(progn (message "%s" (buffer-string)) (kill-buffer buffer))
(error "%s" (with-current-buffer buffer (buffer-string))))
((error) (warn "%s" err) (delete-directory repo 'recursive))))
(unless (require 'elpaca-autoloads nil t)
(require 'elpaca)
(elpaca-generate-autoloads "elpaca" repo)
(let ((load-source-file-function nil)) (load "./elpaca-autoloads"))))
(add-hook 'after-init-hook #'elpaca-process-queues)
(elpaca `(,@elpaca-order))
elpaca-use-package
Elpaca integrates itself with use-package via `elpaca-use-package' package. Setting always ensure t makes sure elpaca is used for package management automatically.
Some important stuff about elpaca:
- Always use :ensure (…) for elpaca related configurations.
- Add :wait t to :ensure if the package is immediately used within init.el and can't wait elpaca's async package installations.
- For my own package files from my dotfiles repo, use: :ensure `(:repo ,im-packages-path :files ("im-archive.el"))
(elpaca elpaca-use-package
(elpaca-use-package-mode))
(setq use-package-always-ensure t)
diminish.el provides a way to hide mode indicators from mode
line. Either pass :diminish t to use-package while installing or
just call (diminish 'x-mode).
(use-package diminish :ensure (:wait t))
(use-package gcmh :init (gcmh-mode 1) :diminish)
(defconst im-load-path (expand-file-name "~/.emacs.d/load/") "The path that I keep my extra stuff. Probably stuff that not committed to git.")
(defconst im-packages-path (expand-file-name "~/.emacs.d/packages/") "The path that I keep my experimental packages in. These packages are not published as a separate project but lives in my dotfiles repository.")
(add-to-list 'load-path im-load-path) (add-to-list 'load-path im-packages-path)
Also load im-secrets from load-path. I'll be utilizing some
variables defined here throughout my configuration. It contains
some api-keys, some tokens or some passwords etc. that I don't want
to leak into public. Instead of doing mutations on an external
hidden script, I define variables in this external hidden script
and reference them in the configuration. This way the logic stays
in the public configuration file so that everyone can take a look,
but only the variable itself will be hidden from the public.
(require 'im-secrets)
(use-package im
:ensure `(:repo ,im-packages-path :files ("im.el") :wait t))
(eval-when-compile (require 'im))
Essential packages
Following provides defmemoize macro. Use (memoize-restore 'fn-name) to restore the original function.
(use-package memoize)
JS-like async/await. Simply return a promise from a function with
(promise-new
(lambda (resolve reject)
(funcall resolve arg)))
and then
(async-defun some-function ()
(message "Result is %s" (await promise-returned-function)))
This is my wrapper with some extensions, real package is async-await.
(use-package im-async-await
:ensure `(:repo ,im-packages-path :files ("im-async-await.el")))
(use-package plz :defer t)
(use-package request :defer t :custom (request-log-level 'warn) (request-message-level -1))
(use-package llama :defer t :ensure (:host github :repo "tarsius/llama"))
(use-package posframe :defer t)
(use-package hide-mode-line :ensure (:host github :repo "hlissner/emacs-hide-mode-line") :defer t)
emacs-async
To be able execute elisp asynchronously. Of course this has lot's
of limitations, the lambda passed to async-start will be executed
in a freshly started Emacs instance and it will not have the any
context of the currently running Emacs instance. There are ways to
pass variables into the context of newly created Emacs instance
which helps a lot.
(use-package async :defer t)
midnight-mode
I run some functions periodically using midnight-mode. Runs once a
day at the specified time. Simply add your function via add-hook
to midnight-hook.
(use-package midnight :ensure nil :defer 60 :config ;; From: https://old.reddit.com/r/emacs/comments/y4nc0e/help_needed_with_midnightmode/ ;; To make it *not* work at each emacs startup, use numbers instead of "10:30am" ;; ;; Set it to 10:30am because some jobs require VPN to be open. (midnight-delay-set 'midnight-delay (truncate (* 10.5 60 60))) (midnight-mode +1))
Variables and functions
Some basic variable and function definitions that will be used in configuration.
(defun im-apply-patch-from-src-block (block-point-or-name target-folder)
"Apply the patch at src block BLOCK-POINT-OR-NAME to TARGET-FOLDER.
I keep some of my changes to packages as in patch format and this
simplifies applying those patches. I try to utilize advices most
of the time but this is for those times where advices either
complicates things or not sufficient."
(interactive
(list (if-let* ((x (org-babel-where-is-src-block-head)))
x
(save-excursion
(org-babel-goto-named-src-block (read-string "Block name: "))
(point)))))
(save-excursion
(if (stringp block-point-or-name)
(org-babel-goto-named-result block-point-or-name)
(goto-char block-point-or-name))
(let* ((block-content (org-element-property :value (org-element-at-point)))
(filename (make-temp-file "temp" nil ".patch" block-content))
(default-directory target-folder)
(patch-command (format "patch -p1 < %s" filename))
(patch-command-result (shell-command-to-string patch-command)))
(if (string-match-p "^patching file" patch-command-result)
(message "Patch applied successfully:\n%s" patch-command-result)
(error "Error applying patch:\n%s" patch-command-result)))))
(cl-defun im-tangle-file (&key _doc path contents)
"Tangle CONTENTS into PATH.
DOC is simply for documentation, have no specific use. If CONTENTS has
a shebang at the beginning, then the executable bit is set to file."
(make-directory (file-name-directory path) t)
(write-region (concat contents "\n") nil path)
;; Make it executable if needed
(when (s-matches? "^[ \t]*#!" contents)
(set-file-modes path (file-modes-symbolic-to-number "+x" (file-modes path)))))
(setq save-silently t)
(setenv "EDITOR" "emacsclient")
(setenv "VISUAL" "emacsclient")
(setenv "BROWSER" "emacs-browser")
(run-with-timer
10 nil
(lambda ()
(im-tangle-file
:doc "Simple `browse-url' wrapper. This enables external processes to call emacs with a url."
:path "~/.bin/emacs-browser"
:contents "#!/bin/bash
emacsclient --eval \"(browse-url \\\"$1\\\")\"")))
Echo area max 5 lines
(setq max-mini-window-height 5)
(defun delete-word (arg)
"Delete characters forward until encountering the end of a word.
With argument, do this that many times."
(interactive "p")
(if (use-region-p)
(delete-region (region-beginning) (region-end))
(delete-region (point) (progn (forward-word arg) (point)))))
(defun backward-delete-word (arg) "Delete characters backward until encountering the end of a word. With argument, do this that many times." (interactive "p") (delete-word (- arg)))
(global-set-key (read-kbd-macro "<M-DEL>") 'backward-delete-word)
Recent files
Save recent files. Also exclude package files that appears after installing a package or after an update from recent list.
(use-package recentf
:ensure nil
:hook (elpaca-after-init . recentf-mode)
:custom
(recentf-max-saved-items 500)
(recentf-exclude (list ".*\\.elc"
"/tmp/.*"
"/var/folders/.*")))
reveal-mode
Enable the reveal-mode. This is quite needed as some commands that does jumping does not reveal the text if it's hidden and reveal mode does that.
(global-reveal-mode) (setq reveal-auto-hide t)
(use-package savehist :ensure nil :hook (elpaca-after-init . savehist-mode) :config ;; Clipboard selections are copied into the kill-ring (setq save-interprogram-paste-before-kill t) (add-to-list 'savehist-additional-variables 'kill-ring) (add-to-list 'savehist-additional-variables 'search-ring) (add-to-list 'savehist-additional-variables 'regexp-search-ring) (setq savehist-file "~/.emacs.d/savehist"))
(setq mouse-wheel-scroll-amount '(1 ((shift) . 1)))
(setq mouse-wheel-progressive-speed nil)
(setq mouse-wheel-follow-mouse 't)
Mouse shortcuts for zooming
Ctrl-Scroll to zoom in and out
(global-set-key [C-mouse-4] 'text-scale-increase) (global-set-key [C-mouse-5] 'text-scale-decrease) (global-set-key (kbd "C-+") 'text-scale-increase) (global-set-key (kbd "C-=") 'text-scale-decrease)
Conservative scrolling
If the cursor is at the end of the file, when you scroll emacs does a strange jump. This fixes it.
(setq scroll-conservatively 101)
(setq scroll-margin 10) ;; Start scolling earlier
Backups
Instead of having a file that ends with ~ or '# files in same
directory, save all backup files in ~/.emacs.d/backups.
(defconst im-backup-dir (im-mkdir-if-not "~/.emacs.d/backups/"))
(setq backup-directory-alist `((".*" . ,im-backup-dir)))
(setq auto-save-file-name-transforms `((".*" ,im-backup-dir t)))
(setq backup-by-copying t)
^ Don't delink hardlinks
(setq version-control t)
^ Use version numbers on backups
(setq delete-old-versions t)
^ Automatically delete excess backups
(setq kept-new-versions 20)
^ How many of the newest versions to keep
(setq kept-old-versions 5)
^ How many of the old versions to keep
(add-hook 'before-save-hook 'delete-trailing-whitespace)
(add-hook 'after-save-hook #'executable-make-buffer-file-executable-if-script-p) (with-eval-after-load 'ob-tangle (add-hook 'org-babel-post-tangle-hook #'executable-make-buffer-file-executable-if-script-p))
Automatically run some commands after saving specific files
This is like autocmd BufWritePost of vim. When a particular file
is edited, I want to make sure a command runs after the save.
(defvar im-run-after-save-alist
'(("~/.\\(Xresources\\|Xdefaults\\)" . "xrdb %; notify-send 'xrdb updated'")
("~/.Xresources.d/.*" . "xrdb ~/.Xresources; notify-send 'xrdb updated'")
("~/.config/sxhkd/sxhkdrc" . "pkill -USR1 -x sxhkd; notify-send 'sxhkd updated'")
("~/.config/skhd/skhdrc" . "skhd --reload; osascript -e 'display notification \"skhd updated\"'")
("~/.config/kmonad-linux.kbd" . "systemctl --user restart kmonad"))
"File association list with their respective command.")
(add-hook 'after-save-hook #'im-post-save-run-command) (with-eval-after-load 'ob-tangle (add-hook 'org-babel-post-tangle-hook #'im-post-save-run-command))
(defun im-post-save-run-command ()
"Execute the specified command after saving specified file."
(when-let* ((fname (buffer-file-name))
(match (im-assoc-regexp fname im-run-after-save-alist #'expand-file-name)))
(mapcar
(pcase-lambda (`(,_ . ,command))
(cond
((stringp command)
(shell-command (s-replace "%" fname command)))
((functionp command)
(funcall command fname))
(t
(message ">> Wrong specification in `im-run-after-save-alist' for %s" fname))))
match)))
repeat-mode
Enables you to have repeatable keybindings.
(use-package repeat :ensure nil :hook (elpaca-after-init . repeat-mode))
(defmacro im-make-repeatable (name &rest pairs)
"Put given PAIRS in a keymap named NAME and mark them as repeatable."
(declare (indent 1))
(let ((pairs (cl-loop for (a b) on pairs by #'cddr collect (list a b)))
(map-name (intern (format "im-repeat-map-for-%s" name))))
`(progn
(defvar ,map-name
(let ((map (make-sparse-keymap)))
,@(mapcar (lambda (it) `(define-key map (kbd ,(car it)) ',(cadr it))) pairs)
map))
(-each ',(mapcar #'cadr pairs)
(lambda (it) (put it 'repeat-map ',map-name))))))
(menu-bar-mode 0)
Wrap long lines. (global-visual-line-mode t)
This sometimes causes performance problems. So instead of calling `global-visual-line-mode', I enable it per-mode basis. Here are some basic modes that it's enabled for:
(add-hook 'TeX-mode-hook #'visual-line-mode) (add-hook 'text-mode-hook #'visual-line-mode)
(diminish 'visual-line-mode)
Highlight current line
(use-package hl-line :ensure nil :if (not (eq system-type 'android)) :hook (elpaca-after-init . global-hl-line-mode))
(defun im-disable-hl-line-mode-for-buffer () "Disable `global-hl-line-mode' for current buffer only. I explicitly disable this mode for some modes in my configuration using this function." (setq-local global-hl-line-mode nil))
(defvar im-after-load-theme-hook nil "Hook to run after a theme is loaded using `load-theme'. This is good for fixing some themes after they are loaded. I quite frequently switch between themes, so a single `set-face-attribute' won't cut it. I need to run those statements on every theme change.")
(define-advice enable-theme (:after (theme &rest _) run-after-load-theme-hook) "Run `after-load-theme-hook'." (seq-each (lambda (it) (funcall it theme)) im-after-load-theme-hook))
(use-package acme-theme :ensure (:host github :repo "isamert/acme-emacs-theme"))
(use-package modus-themes :defer t)
(use-package ef-themes :defer t)
(use-package kaolin-themes :defer t)
(use-package doom-themes
:defer t
:config
;; Fix for doomemacs/themes#720
(custom-set-faces
`(tab-bar
((t (:background ,(or (doom-color 'bg-alt) 'unspecified) :foreground ,(or (doom-color 'fg-alt) 'unspecified)))))))
(use-package im-adaptive-theme
:ensure `(:repo ,im-packages-path :files ("im-adaptive-theme.el"))
:if (not (eq system-type 'android))
:hook (elpaca-after-init . im-adaptive-theme-mode)
:custom
(im-adaptive-theme-detect-geolocation-automatically t)
(im-adaptive-theme-day-themes
'(ef-summer
im-ef-acme
im-ef-solstice
im-ef-blossom))
(im-adaptive-theme-night-themes
'(doom-one
im-ef-acme-dark
im-ef-solstice-dark
im-ef-solstice-soft-dark
doom-bluloco-dark)))
Add my custom themes to load path
(add-to-list 'custom-theme-load-path (file-name-as-directory (expand-file-name "~/.emacs.d/packages/themes/")))
(defconst im-fonts
(seq-filter
#'im-font-exists-p
'("Cascadia Code NF"
"CaskaydiaCove Nerd Font"
"FiraCode Nerd Font"
"Iosevka Nerd Font"
"IBM Plex Mono"
"Blex Mono Nerd Font"
"Iosevka Comfy Motion"
"Iosevka Comfy"
"Noto Sans Mono"
"Liberation Mono"
"UbuntuMono Nerd Font"
"JetBrainsMono Nerd Font"
"Terminess Nerd Font"
"RobotoMono Nerd Font"
"SauceCodePro Nerd Font"
"Hack Nerd Font"
"Inconsolata Nerd Font"
"VictorMono Nerd Font"))
"Fonts that I use.")
(defvar im-current-font nil "Holds the currently used font name. One of `im-fonts'.")
(defconst im-font-height (im-when-on :linux 123 :darwin 160))
(defun im-set-font ()
"Set the first available font from the `im-fonts' list.
If NEXT is non-nil, then use the next font."
(interactive)
(and-let* ((font (if (called-interactively-p 'interactive)
(let* ((display-fonts (mapcar (lambda (f)
(propertize f 'face `(:family ,f)))
im-fonts)))
(completing-read "Font: " display-fonts nil t))
(car im-fonts))))
(set-face-attribute 'default nil
:font font
:weight 'normal
:width 'normal
:height im-font-height)
(setq im-current-font font)))
(add-hook 'after-init-hook #'im-set-font 99)
prettify-symbols-mode
I make use of this mode quite frequently throughout the configuration.
(setq prettify-symbols-unprettify-at-point t)
(defmacro im-prettify-mode (mode pairs)
"Prettify given PAIRS in given MODE.
Just a simple wrapper around `prettify-symbols-mode`"
(declare (indent 1))
(let ((hook (intern (concat (symbol-name mode) "-prettify-symbols-hook"))))
`(progn
(defun ,hook ()
(setq prettify-symbols-alist (append prettify-symbols-alist ,pairs))
(prettify-symbols-mode 1))
(add-hook ',mode #',hook))))
Frame title
Make window title contain buffer name so it's easier to identify
windows. I use rofi to switch between windows in my DE, so it
helps to have buffer name in window title.
(setq frame-title-format "emacs: %F") (set-frame-name "Main")
(use-package show-paren :ensure nil :hook (prog-mode . show-paren-local-mode) :config (setq show-paren-style 'parenthesis))
Rainbow
Colors parentheses depending on their dept.
(use-package rainbow-delimiters :hook (prog-mode . rainbow-delimiters-mode))
Highlight trailing spaces
Following highlights trailing spaces. Also see:
(use-package whitespace :ensure nil :diminish :hook (elpaca-after-init . global-whitespace-mode) :config (setq whitespace-style '(face empty tabs trailing)) (setq whitespace-global-modes '(not org-mode md-ts-mode markdown-mode vterm-mode magit-log-mode nov-mode eshell-mode dired-mode dirvish-mode w3m-mode eat-mode agent-shell-mode)))
(defun im-whitespace-mode-toggle ()
"Toggle between more and less agressive whitespace modes.
Toggles between showing every whitespace (tabs, spaces, newlines
etc.) and only showing trailing spaces and tabs. By default I use
the latter but sometimes I want to see everything and the
function helps me go between these modes easily."
(interactive)
(if (member 'spaces whitespace-style)
(setq whitespace-style '(face empty tabs trailing))
(setq whitespace-style '(face tabs spaces trailing lines space-before-tab newline indentation empty space-after-tab space-mark tab-mark newline-mark)))
(whitespace-mode 0)
(whitespace-mode 1))
Spaces instead of tabs
TODO: This breaks org-mode. (setq-default tab-width 2)
(setq-default indent-tabs-mode nil)
Shackle windows
Make some temproary windows appear at bottom. This makes buffer management so much easier. Buffers that will match given regex will appear at bottom while covering the given amount of screen.
SOURCE: https://www.reddit.com/r/emacs/comments/345vtl/make_helm_window_at_the_bottom_without_using_any/
(defun im-shackle-window (name size)
"Make the buffer NAME appear at bottom of the window, filling SIZE percent of the window."
(add-to-list 'display-buffer-alist
`(,name
(display-buffer-in-side-window)
(inhibit-same-window . t)
(window-height . ,size))))
(defun im-clear-side-windows ()
"Clear all side windows.
This is sometimes required to get around the error: `Cannot make
side window the only window'"
(interactive)
(when (window-with-parameter 'window-side)
(window-toggle-side-windows)))
Miscellaneous packages
Some small packages that enriches editing experience visually. I don't enable all of them by default, I enable most of them whenever I need the functionality. I utilize an appearance to quickly toggle the functionality I need.
Show column guidelines
(use-package fill-column-indicator :defer t)
By default Emacs scales fonts with text-scale-{increase,decrease} per buffer. This scales fonts with default-text-scale-{increase,decrease} globally.
(use-package default-text-scale :commands (default-text-scale-decrease default-text-scale-decrease))
This shows some indent guides and it's highly configurable.
(use-package highlight-indent-guides :defer t :config (set-face-background 'highlight-indent-guides-odd-face "darkgray") (set-face-background 'highlight-indent-guides-even-face "dimgray") (set-face-foreground 'highlight-indent-guides-character-face "dimgray") (setq highlight-indent-guides-method 'bitmap) (setq highlight-indent-guides-bitmap-function 'highlight-indent-guides--bitmap-line))
Spacious padding
Add some padding to Emacs frame to freshen things up.
Disabled for now.
(use-package spacious-padding :commands (spacious-padding-mode))
page-break-lines
I used to separate files using but then I discovered and it did the same job but better. I still use page-break-lines mode in some cases where I need to draw a horizontal line in the buffer to separate some stuff.
(use-package page-break-lines :autoload (page-break-lines-mode))
C-x [,] are bound to {backward,forward}-page, let's make them repeatable:
(im-make-repeatable page-movement "[" backward-page "]" forward-page)
(use-package evil
:demand t
:ensure (:wait t)
:init
;; Following two is required by evil-collection. It's probably wiser
;; to set evil-want-keybinding to t if you will not use
;; evil-collection
(setq evil-want-integration t)
(setq evil-want-keybinding nil)
;; This generally confuses me.
(setq evil-jumps-cross-buffers nil)
;; Disable evil bindings in insert mode. This needs to be called
;; before loading evil mode...
(setq evil-disable-insert-state-bindings t)
;; This seems to be the most reliable undo system. Also see `vundo'
;; package down below for undo-tree like functionality
(setq evil-undo-system 'undo-fu)
;; C-i interferes with TAB key, so disable it
(setq evil-want-C-i-jump nil)
:config
;; ...but I want some default evil bindings in insert mode, so just
;; remap them
(evil-define-key 'insert 'global
(kbd "C-d") #'evil-shift-left-line
(kbd "C-t") #'evil-shift-right-line
(kbd "C-n") #'evil-complete-next
(kbd "C-p") #'evil-complete-previous
(kbd "C-o") #'evil-execute-in-normal-state)
;; C-a is `completion-at-point' but I like to be standard Emacs
;; beginning-of-line. I already bind `M-o p' to
;; `completion-at-point'
(define-key evil-command-line-map (kbd "C-a") #'beginning-of-line)
;; C-i is bound to TAB, so I use C-l for `evil-jump-forward'
(evil-define-key 'normal 'global (kbd "C-l") #'evil-jump-forward)
(define-advice evil-jump-backward (:after (&rest _) reveal) (reveal-post-command))
(define-advice evil-jump-forward (:after (&rest _) reveal) (reveal-post-command))
;; When I paste something in visual mode, I don't want it to take
;; over the kill ring I also use evil-exchange, which eliminates the
;; need for this totally
(setq evil-kill-on-visual-paste nil)
;; Over the time I found myself utilizing emacs C-u more and more,
;; so disable this
(setq evil-want-C-u-scroll nil)
;; Make horizontal movement cross lines
(setq-default evil-cross-lines t)
;; Open ex-mode with `<`> text instead of '<'> by default while
;; visual mode is active. This way commands will work on selected
;; char range instead of selected line range.
(setq evil-ex-visual-char-range t)
;; This is needed for being able to use *-eval-last-sexp kind of
;; functions in normal mode. Elisp-related ones works out of the box
;; but other ones (like for Racket, Clojure etc.) are not patched by
;; default.
(setq evil-move-beyond-eol t)
;; Makes # and * search for symbols instead of words.
(setq evil-symbol-word-search t)
;; Move between visual lines instead of real lines
(evil-define-key 'normal 'global
(kbd "j") #'evil-next-visual-line
(kbd "k") #'evil-previous-visual-line)
(evil-define-key 'motion 'global
(kbd "j") #'evil-next-visual-line
(kbd "k") #'evil-previous-visual-line)
;; Use default yank-pop because it integrates itself with consult
;; The binding may seem a bit weird but it's how it's done.
(define-key evil-normal-state-map [remap yank-pop] 'yank-pop)
;; Change cursor colors based on current mode.
(defun im-set-evil-cursor-colors (&rest _)
(setq evil-normal-state-cursor '("green" box)
evil-visual-state-cursor '("orange" box)
evil-emacs-state-cursor '("purple" box)
evil-insert-state-cursor '("pink" bar)
evil-replace-state-cursor '("red" bar)
evil-operator-state-cursor '("red" hollow)))
(im-set-evil-cursor-colors)
(add-hook #'im-after-load-theme-hook #'im-set-evil-cursor-colors)
(evil-mode 1))
(use-package general :demand t :ensure (:wait t) :config (general-override-mode))
(general-create-definer im-leader :prefix "<SPC>" ;; This is important because if you don't set it some modes will ;; fail to pick up leader bindings. See: ;; https://github.com/noctuid/general.el/issues/190 :keymaps 'override :states 'normal)
(general-create-definer im-leader-lesser :prefix "<SPC>" :states 'normal)
(general-create-definer im-leader-v :prefix "<SPC>" :keymaps 'override :states '(normal visual))
(general-create-definer im-leader2 :prefix "<f16>" :keymaps 'override :states 'normal)
(general-create-definer im-leader2-v :prefix "<f16>" :keymaps 'override :states '(normal visual))
(use-package evil-collection
:after evil
:diminish (evil-collection-unimpaired-mode)
:custom
(evil-collection-want-unimpaired-p t)
(evil-collection-unimpaired-want-repeat-mode-integration t)
:config
(delq 'outline evil-collection-mode-list)
(evil-collection-init)
(add-hook 'evil-collection-unimpaired-mode-hook #'evil-normalize-keymaps)
;; ^ Otherwise it does not properly work and I am unable to understand why.
(define-advice evil-collection-unimpaired-previous-error (:after (&rest _) recenter) (recenter))
(define-advice evil-collection-unimpaired-next-error (:after (&rest _) recenter) (recenter))
(evil-collection-define-key 'normal 'evil-collection-unimpaired-mode-map
"[d" #'im-delete-line-above
"]d" #'im-delete-line-below))
evil-unimpaired
Apparently has a vim-unimpaired implementation already. It contains bindings like:
[<SPC>]<SPC>Insert newline above/below.[b]bGo to prev/next buffer.[p,]pPaste up/down.[e,]eMove line up/down.[d,]dDelete line above/below.[q]qGo to prev/next error.[Q]QGo to first/last error.[n]nGo to prev/next conflict marker.[t]tGo to prev/next TODO. (This is defined in )
These also support repeat-mode. You can do ]b and spam b to switch buffers.
Following are my extensions:
(defun im-delete-line-above ()
"Delete the line above."
(interactive)
(save-excursion
(forward-line -1)
(beginning-of-line)
(kill-line)
(when (s-blank? (s-trim (thing-at-point 'line t)))
(kill-line))))
(defun im-delete-line-below ()
"Delete the line below."
(interactive)
(save-excursion
(forward-line 1)
(beginning-of-line)
(kill-line)
(when (s-blank? (s-trim (thing-at-point 'line t)))
(kill-line))))
evim (multiple cursors)
- Select words with Ctrl-N (like Ctrl-d in Sublime Text/VS Code)
- Create cursors vertically with Ctrl-Down/Ctrl-Up
- Select one character at a time with Shift-Arrows
- Press n/N to get next/previous occurrence
- Press
[/]to select next/previous cursor - Press q to skip current and get next occurrence
- Press Q to remove current cursor/selection
- Start insert mode with i,a,I,A
Two main modes:
- in cursor mode commands work as they would in normal mode
- in extend mode commands work as they would in visual mode
- press Tab to switch between cursor and extend mode
(use-package evim :ensure (:host github :repo "Prgebish/evim") :after evil :config (evim-setup-global-keys))
evil-surround
- Change surroundings. Do
cs"'to turn"Hello world!"into'Hello world!'.'Hello world!'cs'<q><q>Hello world!</q>Hel|loysiw""Hello"(| is the cursor position.)Helloysw{{ Hello }({[(adds spaces)Helloysw}{Hello}(}])does not add spaces)
- Wrap selection with
<visual-state> S. - Wrap selection on new lines with
<visual-state> gS
(use-package evil-surround
:after evil
:init
(global-evil-surround-mode 1)
;; Invert some default pairs
(require 'map)
(map-put! evil-surround-pairs-alist ?\( '("(" . ")"))
(map-put! evil-surround-pairs-alist ?\) '("( " . " )"))
(map-put! evil-surround-pairs-alist ?\[ '("[" . "]"))
(map-put! evil-surround-pairs-alist ?\] '("[ " . " ]"))
(map-put! evil-surround-pairs-alist ?\{ '("{" . "}"))
(map-put! evil-surround-pairs-alist ?\} '("{ " . " }"))
;; Convert `` to `' in emacs-lisp mode
(add-hook 'emacs-lisp-mode-hook (lambda () (push '(?` . ("`" . "'")) evil-surround-pairs-alist))))
key-chord
Return back to normal mode using jk from anywhere. It does not
play well with multiple cursors, so use ESC to when using evil-mc
related stuff.
I used to use evil-escape but lately it started causing random freezes (as I understand from the backtraces), so I replaced it with key-chord. So far so good. It also feels less clunkier somehow.
(use-package key-chord :config (key-chord-mode 1) (key-chord-define evil-insert-state-map "jk" #'evil-normal-state) (key-chord-define evil-visual-state-map "jk" #'evil-normal-state))
evil-matchit
Jump between matching tags using %, like <div>...</div>,
{...} etc. ci%, da% etc. works as expected.
(use-package evil-matchit :after evil :config (global-evil-matchit-mode 1))
evil-goggles
evil-goggles gives nice visual feedbacks while editing with
evil-mode. When you do dd, yw, ciw or something similar, it
will give a visual feedback for the selection. Feels kinda natural
to have this.
(use-package evil-goggles
:after evil
:diminish
:config
(setq evil-goggles-duration 0.20
evil-goggles-pulse nil
evil-goggles-enable-change t
evil-goggles-enable-delete t
evil-goggles-enable-indent t
evil-goggles-enable-yank t
evil-goggles-enable-join t
evil-goggles-enable-fill-and-move t
evil-goggles-enable-paste t
evil-goggles-enable-shift t
evil-goggles-enable-surround t
evil-goggles-enable-commentary t
evil-goggles-enable-nerd-commenter t
evil-goggles-enable-replace-with-register t
evil-goggles-enable-set-marker t
evil-goggles-enable-undo t
evil-goggles-enable-redo t)
(evil-goggles-mode)
(evil-goggles-use-diff-faces))
evil-snipe
- Overall better
f/F/t/Tand . Nice visual feedbacks.
(use-package evil-snipe :after evil :hook (magit-mode . turn-off-evil-snipe-override-mode) :demand t :config ;; (evil-snipe-mode 1) ;; This enables s/S bindings. I use those keys with avy (evil-snipe-override-mode 1) ;; This overrides default f/F, t/T bindings (setq evil-snipe-scope 'visible) ;; See https://github.com/hlissner/evil-snipe/issues/72 (setq evil-snipe-skip-leading-whitespace nil))
evil-exchange
Change two parts of the text.
- Mark some text in visual mode and do
gx. - Mark some other text in visual mode and do
gxagain to exchange two parts. - You can use
gx<motion>instead of visual mode too.
(use-package evil-exchange :config (evil-exchange-install))
evil-visualstar
With this package, you can do a visual selection and *, # keys will work on them.
(use-package evil-visualstar
:after evil
:config
(setq evil-visualstar/persistent t)
(evil-define-key* 'visual 'global
"*" #'evil-visualstar/begin-search-forward
"#" #'evil-visualstar/begin-search-backward))
evil-numbers
Pretty useful for macros. Increment or decrement number at point
with +,- in normal mode.
(use-package evil-numbers :custom (evil-numbers-use-cursor-at-end-of-number t) (evil-numbers-pad-default t) :general (:states '(normal visual) "+" #'evil-numbers/inc-at-pt "-" #'evil-numbers/dec-at-pt))
goto-chg
g; goes to the last change. (repeatable)
There is also gv which selects the last selection. Not related to
this package, it's a default functionality but I wanted to mention.
(use-package goto-chg
:ensure nil
:commands (evil-goto-last-change)
:config
(define-advice evil-goto-last-change (:after (&rest _) recenter)
(reveal-post-command)
(recenter)))
(im-make-repeatable evil-goto-chg ";" evil-goto-last-change)
Custom text-objects
org blocks
There is `org-babel-mark-block' but it only works for source blocks but this one works for everything between #begin_<> … #end_<>. There is also "e" object defined by evil-org, which works for quite most things but it does not work, for example, org-ai blocks etc.
(evil-define-text-object im-evil-inner-org-block (count &optional _beg _end _type) "Select inner side of org source blocks." :extend-selection nil (im-find-block-range "^ *#\\+begin" "^ *#\\+end"))
(evil-define-text-object im-evil-outer-org-block (count &optional _beg _end _type) "Select outer side of org source blocks." :extend-selection nil (im-find-block-range "^ *#\\+begin" "^ *#\\+end" t))
(evil-define-text-object im-evil-inner-markdown-block (count &optional _beg _end _type) "Select inner side of org source blocks." :extend-selection nil (im-find-block-range "^ *```" "^ *```"))
(evil-define-text-object im-evil-outer-markdown-block (count &optional _beg _end _type) "Select outer side of org source blocks." :extend-selection nil (im-find-block-range "^ *```" "^ *```" t))
(define-key evil-inner-text-objects-map "#" 'im-evil-inner-org-block) (define-key evil-outer-text-objects-map "#" 'im-evil-outer-org-block)
Doing vi` already works for `this`, and now ~ works for blocks.
(define-key evil-inner-text-objects-map "~" 'im-evil-inner-markdown-block) (define-key evil-outer-text-objects-map "~" 'im-evil-outer-markdown-block)
(defun im-find-block-range (beginning end &optional include-hash-lines)
"Find the range around the cursor between BEGINNING and END."
(let ((up-pos nil)
(down-pos nil))
(save-excursion
(beginning-of-line)
(while (and (not up-pos) (not (bobp)))
(forward-line -1)
(beginning-of-line)
(when (looking-at beginning)
(unless include-hash-lines
(forward-line 1))
(setq up-pos (point)))))
(when up-pos
(save-excursion
(beginning-of-line)
(while (and (not down-pos) (not (eobp)))
(forward-line 1)
(beginning-of-line)
(when (looking-at end)
(when include-hash-lines
(forward-line 1))
(setq down-pos (point))))))
(when (and up-pos down-pos)
(list up-pos down-pos))))
(im-make-repeatable evil-section "[" evil-backward-section-begin "]" evil-forward-section-begin)
(im-make-repeatable evil "l" evil-scroll-column-right "h" evil-scroll-column-left)
(use-package text-mode :ensure nil :config (setopt text-mode-ispell-word-completion nil))
org-mode
Tips etc.
Some lesser known functions:
- org-copy-visible
- Useful for only sending the outline. Also works with selection.
- orgtbl-create-or-convert-from-region
- Useful for data that is separated by \t or spaces.
- (no term)
- After converting to table, I can use
org-table-sort-lines. Alternative is usingsort-fieldswithout converting the data into table but it's not that nice to use. - (no term)
- I generally use it in combination with
embark-collect. Any completing-read → embark-collect → orgtbl-create-or-convert-from-region.
(defconst inbox-org "~/Documents/notes/inbox.org") (defconst watchlist-org "~/Documents/notes/watchlist.org") (defconst readinglist-org "~/Documents/notes/readinglist.org") (defconst courses-org "~/Documents/notes/courses.org") (defconst bullet-org "~/Documents/notes/bullet.org") (defconst info-org "~/Documents/notes/info.org") (defconst directory-notes-org "~/Documents/notes/directory-notes.org") (defconst life-org "~/Documents/notes/life.org") (defconst reality-org "~/Documents/notes/reality.org") (defconst projects-org "~/Documents/notes/projects.org") (defconst gpt-org "~/Documents/notes/extra/gpt.org") (defconst people-org "~/Documents/notes/people.org") (defconst diary-org "~/Documents/notes/diary.org") (defconst snippets-org "~/Documents/notes/snippets.org") (defconst bookmarks-org "~/Documents/notes/bookmarks.org") (defconst work-org "~/Documents/notes/trendyol.org") (defconst temp-org "~/Documents/notes/temp.org") (defconst passwords-org "~/Documents/notes/passwords.org") (defconst engineering-org "~/Documents/notes/engineering.org") (defconst netherlands-org "~/Documents/notes/netherlands.org")
(use-package org
:mode ("\\.org\\'" . org-mode)
:custom
(org-return-follows-link t)
;; ^ Open links with RET
(org-src-fontify-natively t)
;; ^ Enable code highlighting in ~SRC~ blocks.
(org-src-window-setup 'current-window)
;; ^ Open indirect edits in current window and don't mess with window config at all
(org-hierarchical-todo-statistics t)
;; ^ Show all children in todo statistics [1/5]
(org-imenu-depth 7)
;; ^ include up to 7-depth headers in imenu search
(org-image-actual-width nil)
;; ^ Disable showing inline images in full width. Now you can add `#+ATTR_*: :width 300` to resize inline images
;; (setq org-ellipsis "⤵")
;; (setq org-ellipsis "…")
;; ^ Replace ... with … in collapsed sections
(org-hide-emphasis-markers t)
;; Hide *...* /.../ etc.
(org-pretty-entities t)
(org-log-into-drawer t)
;; ^ Log into LOGBOOK drawer instead of directly loging under the heading
(org-extend-today-until 3)
;; ^ Consider the current day to end at 3AM
(org-use-effective-time t)
;; ^ Make timestamp processing functions aware of the previous config
(org-element-use-cache nil)
;; ^ Cache causes bunch of random errors although disabling cache
;; reduces the agenda performance significantly
(org-tags-column 0)
;; Tags starts right after the heading.
(org-reverse-note-order t)
;; ^ I keep new notes at the beginning. This helps with that.
(org-confirm-babel-evaluate nil)
;; ^ Don't ask permissions for evaluating code blocks
(org-clock-clocked-in-display 'mode-line)
;; ^ Shows in the tab-bar, if tab-bar is enabled.
(org-habit-show-habits nil)
;; ^ Speeds up agenda a bit
(org-edit-src-content-indentation 0)
;; ^ No indentation for src blocks
(org-cycle-include-plain-lists 'integrate)
;; ^ Also toggle visibility of plain list with TAB etc. like they are subheadings
(org-fold-core-style 'overlays)
;; ^ Documentation says text-properties are more buggy but my
;; experience is the opposite. At least for now. overlay folding
;; messes things up with indirect buffers which I use quite a
;; lot. Haven't reported upstream tho.
;; UPDATE(2025-02-01): overlays seems to work better now.
;; Put archive files under an archive/ directory
;; I don't want them to pollute my directory
(org-archive-location "archive/%s_archive::")
(org-directory "~/Documents/notes")
(org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id)
;; ^ org-store-link creates an ID for header only if called interactively and if there is no custom id
(org-log-states-order-reversed nil)
;; ^ add log entries from top the bottom, in chronological order
(org-log-note-clock-out t)
;; ^ Add notes to LOGBOOK while clocking out from a task
(org-log-state-notes-insert-after-drawers t)
;; ^ Insert notes (added by `org-add-note') after the drawer
(org-log-into-drawer t)
;; ^ Insert notes (added by `org-add-note') inside the drawer, this overrides the one above but keeping both for reference
:config
;; Automatically invoke `org-indent-mode' which gives nice little
;; indentation under subsections. It makes reading easier. This does
;; not add any spaces/tabs to the text file, the indentation is only
;; visually apparent in Emacs.
;; (add-hook 'org-mode-hook #'org-indent-mode t)
(defvar im-org-calendar-directory (format "%s/calendars" org-directory))
(setq im-calendar-files
(when (file-exists-p im-org-calendar-directory)
(directory-files im-org-calendar-directory 'full (rx ".org" eos))))
;; Some files are not available in my work computer etc. Hence I
;; filter for existing ones
(setq org-agenda-files (-filter #'file-exists-p
`(,inbox-org
,bullet-org
,projects-org
,work-org
,people-org
,readinglist-org
,watchlist-org
,life-org
,netherlands-org
,@im-calendar-files)))
;; When I unfold an header if the contents are longer than remaining
;; window width then cursor recenters to the top of the window. I
;; disable with the following line.
(with-eval-after-load 'org-cycle
(setq org-cycle-hook (delq 'org-cycle-optimize-window-after-visibility-change org-cycle-hook)))
(add-to-list 'org-link-abbrev-alist '("imdb" . "https://www.imdb.com/title/%s"))
(add-to-list 'org-link-abbrev-alist '("yt" . "https://youtu.be/%s"))
;; ^ More info: https://orgmode.org/manual/Link-Abbreviations.html
;; With the following, I can call functions defined inside this file in other org files
(org-babel-lob-ingest (concat org-directory "/utils.org"))
(defun im--maybe-enable-dnd ()
(if-let* ((duration (im-org-heading-current-time-length)))
(im-notif-enable-dnd (* duration 60))
(when (and (not (bound-and-true-p im-notif-dnd))
(y-or-n-p "Enable Do Not Disturb? "))
(call-interactively #'im-notif-enable-dnd))))
(add-hook 'org-clock-in-hook #'im--maybe-enable-dnd 90)
(defun im--maybe-disable-dnd ()
(im-notif-disable-dnd))
(add-hook 'org-clock-out-hook #'im--maybe-disable-dnd 90))
(use-package org-contrib :after org)
http://www.foldl.me/2012/disabling-electric-indent-mode/
(defun im-disable-electric-indent ()
(set (make-local-variable 'electric-indent-functions)
(list (lambda (_arg) 'no-indent))))
(add-hook 'org-mode-hook #'im-disable-electric-indent)
(defun im-org-add-note-to-currently-clocked-in-header ()
"Add note to currently clocked in header.
If not clocked in, ask a header among one of the latest clocked in
headers."
(interactive)
(save-window-excursion
(save-excursion
(org-clock-goto (not org-clock-clocking-in))
(org-add-note))))
(use-package evil-org
:diminish evil-org-mode
:after (org org-agenda)
:general
(:keymaps 'org-mode-map :states 'normal
"<RET>" #'org-return
"S-<return>" #'im-org-link-to-indirect-buffer)
(:keymaps 'org-mode-map :states '(insert normal)
"M-Q" #'im-org-unfill-paragraph)
(im-leader
"oyy" #'im-org-store-link-dwim
"oa" #'org-agenda
"on" #'im-org-add-note-to-currently-clocked-in-header
;; org-capture
"og" #'org-capture
"oG" #'org-capture-goto-last-stored
;; org-clock
"occ" #'org-clock-in
"ocC" #'org-clock-cancel
"ocr" (λ-interactive (org-clock-in-last '(4)))
"ocl" (λ-interactive (org-clock-in-last '(4)))
"oco" #'org-clock-out
"ocg" #'org-clock-goto)
(im-leader :keymaps 'org-mode-map
;; general
"op" #'org-set-property
"oi" #'org-toggle-inline-images
"oI" #'im-org-insert-image-file-with-width
"or" #'org-refile
"oR" #'org-mode-restart
"oh" #'outline-show-only-headings
"os" #'org-schedule
"od" #'org-deadline
"of" #'org-set-effort
"oF" #'im-org-get-link-effort-at-point
"ov" #'org-babel-expand-src-block
"oq" #'im-org-tree-to-indirect-buffer
"oQ" #'im-org-link-to-indirect-buffer
"o1" #'im-show-outline-only
;; link stuff
"oyi" #'org-copy-id
;; src blocks etc
"d" #'org-babel-remove-result
"D" #'im-org-babel-remove-all-results
"oo" #'im-org-babel-result-to-buffer
"oe" #'org-edit-special
"ot" #'im-org-babel-tangle-current-block
"o-" #'org-babel-demarcate-block)
(im-leader :keymaps 'org-src-mode-map
"ot" #'im-org-babel-tangle-current-block)
(im-leader-v :keymaps 'org-mode-map
"o#" #'org-insert-structure-template)
(im-leader-v :keymaps 'org-src-mode-map
"oe" #'org-edit-src-exit
"oE" #'org-edit-src-abort)
:hook
(org-mode . evil-org-mode)
:config
(require 'evil-org-agenda)
(evil-org-agenda-set-keys)
(evil-org-set-key-theme '(textobjects insert navigation additional shift todo heading))
(add-hook 'org-src-mode-hook #'evil-normalize-keymaps))
(defun im-org-tree-to-indirect-buffer ()
"Like `org-tree-to-indirect-buffer' but let's you open multiple indirect buffers.
This also restricts the amount of indirect buffers per heading to 1 so
that you can have multiple indirect buffers for different headers but
only one indirect buffer per header to prevent unnecessary accumulation
of indirect buffers.."
(interactive)
(if-let* ((target-buffer-name (concat (buffer-name) "::" (org-get-heading 'no-tags)))
(existing-buffer (get-buffer target-buffer-name)))
(switch-to-buffer-other-window existing-buffer)
(let ((current-prefix-arg '(4)))
(call-interactively #'org-tree-to-indirect-buffer))))
(defmacro im-org-focused-tree-to-indirect-buffer (focus-buffer &rest forms)
"Open a tree in an indirect buffer.
First, switch to target buffer by executing FOCUS-BUFFER. Then execute
FORMS to focus into the header that you want to open in an
indirect-buffer. Then create an indirect buffer of that header. This
function does not change any restriction or position of any buffer, so
it saves everything done in FOCUS-BUFFER and FORMS. With this, you can
work simultaneously with different headers of single org buffer."
`(let ((current-prefix-arg '(4))
(source-buffer (current-buffer))
focused-buffer
existing-buffer
target-buffer-name)
;; Get the buffer that we want to focus. This is a separate step
;; because I want to save the restriction and excursion inside
;; that buffer.
(setq focused-buffer
(progn
(save-window-excursion
(save-restriction
,focus-buffer
(point)
(current-buffer)))))
(save-window-excursion
(with-current-buffer focused-buffer
(save-excursion
(save-restriction
(widen)
,@forms
(setq target-buffer-name (concat (buffer-name) "::" (org-get-heading 'no-tags)))
(setq existing-buffer (get-buffer target-buffer-name))
(unless existing-buffer
(call-interactively #'org-tree-to-indirect-buffer))))))
;; If the link points to another buffer, current window will start
;; showing that buffer. We don't want that, so we are restoring
;; the current buffer here:
(set-window-buffer nil source-buffer)
(switch-to-buffer target-buffer-name)
(im-show-outline-only)))
(defun im-org-link-to-indirect-buffer ()
"Open link at point on a new indirect buffer."
(interactive)
;; Force org to open the link in current window
(with-default-browser
(let ((org-link-frame-setup (cons (cons 'file 'find-file) org-link-frame-setup)))
(pcase (org-element-property :type (org-element-context))
((or "http" "https") (org-open-at-point))
(_
(switch-to-buffer-other-window
(save-window-excursion
(im-org-focused-tree-to-indirect-buffer
;; If we are in an indirect buffer, then find the real
;; buffer and widen it so that org-open-at-point
;; works. This does not restore narrowing but whatever.
(progn
(when-let* ((base (buffer-base-buffer)))
(with-current-buffer base
(switch-to-buffer (current-buffer)))))
;; org-open-at-point opens in other-window, prevent that
(switch-to-buffer
(save-window-excursion
(org-open-at-point)
(current-buffer))))
(current-buffer))))))))
(defun im-org-clocked-header-to-indirect-buffer () "Open currently clocked header in an indirect buffer. Normally you can go to clocked header with `org-clock-goto', this simply opens it in an indirect buffer narrowed to the header so that the original buffer is not affected and you can work on them simultaneously." (im-org-focused-tree-to-indirect-buffer (switch-to-buffer (marker-buffer org-clock-marker)) (goto-char (marker-position org-clock-marker))))
(defun im-show-outline-only ()
"Show all headers but hide all bodies."
(interactive)
(save-excursion
(goto-char (point-min))
(outline-next-heading)
(outline-show-branches)
(outline-hide-body)))
(with-eval-after-load 'org
(org-babel-do-load-languages
'org-babel-load-languages
'((emacs-lisp . t)
(shell . t)
(scheme . t)
(python . t)
(jupyter . t)
(julia . t)
(verb . t)
(http . t)
(R . t)
(haskell . t)
(js . t)
(sql . t)
(dot . t)
(gnuplot . t)
(plantuml . t))))
TODO: im-load-ob-lang-lazy (defmacro im-load-ob-lang-lazy (&rest langs) "Like `org-babel-do-load-languages' but load them lazily using `use-package'." `(progn ,@(mapcar (lambda (lang) `(use-package ,(intern (concat "ob-" (symbol-name lang))) :defer t :ensure nil ;; TODO add view :commands (,(intern (concat "org-babel-execute:" (symbol-name lang)))))) langs))) (im-load-ob-lang-lazy emacs-lisp shell scheme python verb http R haskell js sql dot gnuplot plantuml)
Helper functions
Some codeblocks produce image files as it's result (like dot language). Re-executing these blocks removes the image overlay. With this hook images are automatically updated after code-block execution and not removed from screen.
(add-hook 'org-babel-after-execute-hook #'im-org-redisplay-images-if-enabled)
(defalias 'im-org-babel-split-or-wrap-src-code-block #'org-babel-demarcate-block)
(defun im-org-babel-tangle-current-block ()
"Tangle the current source block and all other related blocks.
All blocks that tangles to same file are tangled.
This function also works inside `org-edit-special' buffers and
does not disrupt the current window configuration."
(interactive nil org-mode)
(let ((src-edit-p (org-src-edit-buffer-p))
buffer pos)
(save-window-excursion
(when src-edit-p
(org-edit-src-exit))
(setq buffer (current-buffer)
pos (point))
(org-babel-tangle '(16)))
(when src-edit-p
(let ((org-src-window-setup 'current-window))
(with-current-buffer buffer
(save-excursion
(goto-char pos)
(org-edit-special)))))))
https://emacs.stackexchange.com/questions/23870/org-babel-result-to-a-separate-buffer/27190#27190
(defun im-org-babel-result-to-buffer () "A function to efficiently feed babel code block result to a separate buffer." (interactive nil org-mode) (org-open-at-point) (org-babel-remove-result))
(defun im-org-redisplay-images-if-enabled ()
(when org-link-preview-overlays
(org-link-preview-refresh)))
ob-http & verb-mode
Http request in org-mode babel. You can get the generated curl command after executing the code block, from curl command history buffer or using `:print-curl' parameter to the block.
(use-package ob-http :ensure (:host github :repo "ag91/ob-http") :after org)
An alternative to ob-http, might be better
(use-package verb :ensure (:host github :repo "federicotdn/verb") :after org ;; :hook (org-mode . verb-mode) :config (im-leader "ur" #'verb-send-request-on-point))
(defun im-curl-to-org-http (curl-str)
"Convert CURL-STR into an ob-http block.
More concretely this function converts given curl command (that
is copied from Chrome/Firefox dev tools, using the `Copy as curl'
option) into an ob-http block to be able to use all the goodies
that is provided by ob-http."
(interactive
(list
(if (use-region-p)
(buffer-substring-no-properties (region-beginning) (region-end))
(read-string "Curl string: "))))
(let ((tokens (im-cmdargs-tokenize curl-str))
(headers '())
(request "GET")
url
(query-params '())
data)
(--each-indexed tokens
(pcase it
((or "-H" "--header")
(push (nth (1+ it-index) tokens) headers))
((or "-d" "--data-binary" "--data-raw")
(setq data (nth (1+ it-index) tokens)))
((or "-X" "--request" "--data-raw")
(setq request (nth (1+ it-index) tokens)))
;; Only supports --data-urlencode in the GET context. It will
;; append --data-urlencode arguments to url as query string.
("--data-urlencode"
(push (nth (1+ it-index) tokens) query-params))
((pred (lambda (x) (s-prefix? "http" x)))
(setq url it))))
(when-let* ((query-params)
(qs (url-build-query-string (--map (s-split-up-to "=" it 2) query-params))))
(if (s-contains? "?" url)
(setq url (concat url "&" qs))
(setq url (concat url "?" qs))))
(insert
(s-trim (im-s-interpolated "#{request} #{url}\n#{(s-join \"\\n\" headers)}\n\n#{(or data \"\")}\n")))))
(defun im-cmdargs-tokenize (input)
(with-temp-buffer
(insert input)
(goto-char (point-min))
(let ((tokens '()))
(while-let ((chr (get-byte))
((not (eobp))))
(pcase chr
((or ?\s ?\t ?\n) (forward-char))
(?\"
(forward-char)
(push
(replace-regexp-in-string
(regexp-quote "\\\"") "\""
(buffer-substring
(point)
(1- (re-search-forward "[^\\]\"" nil t))))
tokens))
(?\'
(forward-char)
(push (buffer-substring
(point)
(1- (re-search-forward "'" nil t)))
tokens))
(_ (push (buffer-substring
(point)
;; TODO Handle eol
(1- (re-search-forward "[ \t]" nil t)))
tokens))))
(nreverse tokens))))
(use-package htmlize :after org)
(with-eval-after-load 'org (setq org-icalendar-store-UID t) (setq org-icalendar-alarm-time 15) (setq org-icalendar-use-scheduled '(todo-start event-if-todo)) (setq org-icalendar-use-deadline '(todo-due event-if-todo)))
Agenda
Some general settings.
(use-package org-agenda
:ensure nil
:after org
:general
(:states 'normal :keymaps 'org-agenda-mode-map
(kbd "<RET>") #'org-agenda-switch-to
(kbd "\t") #'org-agenda-goto
"s" #'org-agenda-schedule
"w" #'org-agenda-week-view
"d" #'org-agenda-day-view
"t" #'org-agenda-todo
"L" #'org-agenda-log-mode
"q" #'org-agenda-quit
"R" #'org-agenda-clockreport-mode
"r" #'org-agenda-redo)
:custom
(org-agenda-remove-tags t)
(org-agenda-include-diary t)
(org-agenda-use-time-grid t)
(org-agenda-time-grid
'((daily today require-timed remove-match)
(800 900 1000 1100 1200 1300 1400 1500 1600 1700 1800 1900 2000 2100 2200 2300 2400)
" ┄┄┄┄┄ " "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄"))
;; Make it open faster
;; https://orgmode.org/manual/Speeding-Up-Your-Agendas.html
;; https://orgmode.org/worg/agenda-optimization.html
(org-agenda-dim-blocked-tasks nil)
(org-agenda-inhibit-startup t)
(org-agenda-use-tag-inheritance nil)
(org-agenda-ignore-drawer-properties '(effort appt category))
:config
(evil-set-initial-state 'org-agenda-mode 'normal)
(add-hook 'org-agenda-finalize-hook #'evil-force-normal-state))
org-caldav (calendar sync)
Sync calendars with: `org-caldav-sync'.
(use-package org-caldav
:after org
:defer t
:custom
(org-caldav-url (format "%s/remote.php/dav/calendars/%s" im-secrets-nextcloud-url im-secrets-nextcloud-user))
(org-caldav-calendars (--map
(list
:calendar-id it
:inbox (format "%s/%s.org" im-org-calendar-directory it))
im-nextcloud-calendars))
;; TODO: Fix the following, automatically get
(org-icalendar-timezone "Europe/Amsterdam")
;; Set it under the notes directory so that they will be synced
;; between devices
(org-caldav-save-directory "~/Documents/notes/data/")
;; Set the following to nil, I already have set `org-caldav-calendars' above
(org-caldav-inbox nil)
(org-caldav-calendar-id nil)
(org-caldav-files nil))
(with-eval-after-load 'org
;; Add this to org files if you need:
;; #+TODO: TODO PROG WAITING DONE
;; OR
(setq org-todo-keywords
'((sequence "TODO(t)" "PROG(p)" "WAIT(w)" "DONE(d)")))
;; Now you can do C-c C-t {t,p,w,d} to set the state directly
(setq org-todo-keyword-faces
'(("WAIT" . (:foreground "yellow" :weight bold))
("PROG" . (:foreground "magenta" :weight bold :underline t))))
(custom-set-faces
'(org-headline-done ((t (:strike-through t))))
'(org-agenda-done ((t (:strike-through t))))
'(org-column ((t (:background unspecified))))
'(org-scheduled-today ((t (:foreground "light green"))))
;; Make some stuff small
'(org-drawer ((t (:height 0.8))))
'(org-special-keyword ((t (:height 0.8))))
'(org-block-end-line ((t (:height 0.8))))))
org-capture
See this page for more detail on template elements.
Some functions that I utilize for capture templates:
(defun im-org-capture--find-first-top-level-todo-entry () "Find first top-level TODO entry in current buffer. This way you can insert new entry right after other non-TODO entries." (goto-char (point-min)) (re-search-forward "^\\* TODO" nil t) (beginning-of-line))
(defun im-org-capture--find-first-N-level-todo-entry (n)
"Find first N-level TODO entry in current buffer.
This way you can insert new entry right after other non-TODO
entries."
(goto-char (point-min))
(re-search-forward (format "^\\*\\{%s\\} TODO" n) nil t)
(beginning-of-line))
(defun im-org-capture--find-first-top-level-todo-entry-within-header (header)
"Find first top-level TODO entry in under HEADER in current buffer."
(lambda ()
(goto-char (point-min))
(let (n)
(if (re-search-forward (format "^\\(\\*+\\) %s" header) nil t)
(setq n (1+ (length (match-string 1))))
(user-error "Cannot find header %s" header))
(save-restriction
(org-narrow-to-subtree)
(im-org-capture--find-first-N-level-todo-entry n)))))
(defun im-org-capture--find-snippet ()
(let* ((mode-name (with-current-buffer (org-capture-get :original-buffer)
(symbol-name major-mode)))
(result (org-find-exact-headline-in-buffer mode-name)))
(if result
(goto-char result)
(goto-char (point-min))
(re-search-forward (concat "^\\(" org-outline-regexp "\\)") nil t)
(forward-line -1)
(insert (format "\n* %s" mode-name)))))
(defun im-org-capture--find-snippet-one-liner ()
(im-org-capture--find-snippet)
(unless (re-search-forward "\\*\\* One-liners" nil t)
(end-of-line)
(insert "\n** One-liners")))
(with-eval-after-load 'org
(setq
org-capture-templates
`(("i" "Inbox" item
(file+headline bullet-org "Inbox")
"- %U %?"
:prepend t)
("I" "Info" item
(file info-org)
"- %U %?"
:prepend t)
("n" "Meeting/clock note" item
(clock))
("w" "Work")
("wi" "Work Inbox" item
(file+headline work-org "Inbox")
"- %U %?"
:prepend t)
("m" "Watchlist item" plain
(file+function watchlist-org im-org-capture--find-first-top-level-todo-entry)
"* TODO %?\n:PROPERTIES:\n:CREATED_AT: %U\n:WHY: FILLME!\n:END:\n\n")
("s" "Snippets")
("ss" "Snippet" entry
(file+function snippets-org im-org-capture--find-snippet)
"** ")
("so" "One liner snippet" item
(file+function snippets-org im-org-capture--find-snippet-one-liner)
"- %? :: "))))
(with-eval-after-load 'org (setq org-refile-targets '((org-agenda-files . (:maxlevel . 3)))) ;; Show the full path of the header (setq org-refile-use-outline-path 'file) ;; Make the refile completion work with vertico (setq org-outline-path-complete-in-steps nil) ;; Add new headers by appending "/new header name" and refile creates it for you (setq org-refile-allow-creating-parent-nodes 'confirm))
(use-package org-modern
:after org
:custom
(org-modern-timestamp nil)
(org-use-sub-superscripts nil)
(org-modern-block-fringe nil)
(org-modern-table nil)
(org-modern-hide-stars " ")
(org-modern-list
'(;; (?- . "-")
(?* . "•")
(?+ . "‣")))
(org-modern-todo-faces
'(("TODO" :background "deep pink" :foreground "white")
("PROG" :background "purple" :foreground "white")
("WAIT" :background "goldenrod3" :foreground "white")
("NEXT" :background "dark cyan" :foreground "white")
("DEAD" :background "sienna" :foreground "white")))
(org-modern-priority-faces
'((?A :background "forest green" :foreground "beige")
(?B :background "goldenrod" :foreground "beige")
(?C :background "coral" :foreground "beige")
(?D :background "red3" :foreground "beige")))
:config
(add-hook 'org-agenda-finalize #'org-modern-agenda)
;; 99 is important or otherwise org-jupyter-client fails to
;; highlight ansi escape codes for some reason.
(add-hook 'org-mode-hook #'org-modern-mode 99)
(im-when-on
:android
(setq org-modern-checkbox '((88 . "☑") (45 . "□") (32 . "□")))))
(use-package org-rainbow-tags :custom (org-rainbow-tags-hash-start-index 6) (org-rainbow-tags-extra-face-attributes '(:inverse-video t :box nil :weight 'bold)) :hook (org-mode . org-rainbow-tags-mode))
(use-package org-ql
:defer t
:after org
;; Load org-ql-search prematurely to be able to use org-ql blocks in
;; org-mode
:init
(with-eval-after-load 'org
(require 'org-ql-search)))
Here are some utility functions that I use in org-ql dynamic blocks:
(defun sort-by-num-prop (prop x y)
(< (string-to-number (or (org-element-property prop y) "0"))
(string-to-number (or (org-element-property prop x) "0"))))
(defun sort-by-prop (prop x y)
(string< (or (org-element-property prop y) "")
(or (org-element-property prop x) "")))
(use-package im-org-grabbability
:ensure `(:repo ,im-packages-path :files ("im-org-grabbability.el"))
:after (org-ql))
You have to use :sort (lambda ...) syntax in org-ql dynamic
blocks if you want to supply a function for the :sort
parameter. You can't use a function that returns a lambda, hence
the functions defined above should be used like this:
Here are some predefined searches:
(defun im-org-ql-current-week-tasks ()
(interactive)
(let ((week-end (im-date "next monday - 1 day")))
(org-ql-search org-agenda-files
`(and (or (scheduled :to ,week-end)
(deadline :to ,week-end))
(not (done)))
:title (format "W%s" (format-time-string "%U"))
:super-groups '((:auto-category))
:sort '(date))))
Linking improvements
Org does not provide an easy way to copy link at point. Here is a fix for that:
(defun im-org-link-copy ()
"Extract URL from `org-mode' link and add it to kill ring."
(interactive)
(let* ((link (org-element-lineage (org-element-context) '(link) t))
(type (org-element-property :type link))
(url (org-element-property :path link))
(url (when (and type url (not (s-blank? url))) (concat type ":" url))))
(when (called-interactively-p 'any)
(message (concat "Copied URL: " (im-kill url))))
url))
(defun im-org-store-link-dwim ()
"Like `org-store-link' but if point is on an org-link, just copy it to clipboard.
Otherwise call `org-store-link'."
(interactive)
(if (org-in-regexp org-link-any-re 1)
(call-interactively #'im-org-link-copy)
(org-store-link nil t)))
Show expanded links at point
This following trick (got it from
this
comment) simply calls C-h . (display-local-help) when idle,
which shows the destination of links in the echo area (and maybe
displays other helpful stuff).
(define-minor-mode im-help-at-point-mode
"Show tooltips in the echo area automatically for current buffer."
:lighter " HelpPt"
(if im-help-at-point-mode
(progn
(setq-local help-at-pt-display-when-idle t)
(setq-local help-at-pt-timer-delay 0)
(help-at-pt-cancel-timer)
(help-at-pt-set-timer))
(setq-local help-at-pt-display-when-idle nil)
(help-at-pt-cancel-timer)))
(diminish 'im-help-at-point-mode)
(with-eval-after-load 'org (add-hook 'org-mode-hook #'im-help-at-point-mode))
Insert links/images more intelligently
- if region is selected and there is a url in the clipboard, convert it to a link directly.
- if nothing is selected and there is a link in clipboard, just insert it as a link with the link's own title.
- if clipboard has in image in it, save that into a file that you interactively select and then insert it into the buffer.
- otherwise call
org-insert-link
(im-leader-v "oP" #'im-org-insert-dwim) (general-def :keymaps 'minibuffer-mode-map "C-o" #'im-org-insert-dwim)
(defun im-org-insert-dwim ()
"Like `org-insert-link' but improved with dwim features.
Based on: https://xenodium.com/emacs-dwim-do-what-i-mean/"
(interactive)
(let* ((point-in-link (org-in-regexp org-link-any-re 1))
(clipboard-url (when (string-match-p "^http" (current-kill 0))
(current-kill 0)))
(region-content (when (region-active-p)
(buffer-substring-no-properties (region-beginning)
(region-end)))))
(cond
((im-clipboard-contains-image-p)
(call-interactively 'im-org-attach-image-from-clipboard))
((and region-content clipboard-url (not point-in-link))
(delete-region (region-beginning) (region-end))
(insert (org-link-make-string clipboard-url region-content)))
((and clipboard-url (not point-in-link))
(insert (im-org-make-link-string clipboard-url)))
(t
(call-interactively 'org-insert-link)))))
(defun im-org-attach-image-from-clipboard (&optional file-path)
"Attach the image in the clipboard into your org-buffer.
This function saves the image file into the FILE-PATH or if it's
not provided then it saves the image into ~/.cache."
(interactive
(list
(read-file-name
"Save file to (leave empty to create a temp file): "
nil nil nil (im-org-suggest-filename nil ".png"))))
(let ((file (if (and file-path (not (string-empty-p file-path)))
(file-relative-name file-path)
(expand-file-name (make-temp-file "~/.cache/org_temp_image_" nil ".png")))))
(if (im-save-clipboard-image-to-file file)
(insert (format "#+ATTR_ORG: :width 400\n[[file:%s]]" file))
(user-error "Saving file failed!"))))
(defun im-org-make-link-string (url) "Like `org-make-link-string' but fetch URL and extract the title automatically. Also adds author and duration info to YouTube links." (cond ((im-youtube-link-extract-id url) (im-org-format-youtube-link url)) (t (org-link-make-string url (im-url-get-title url)))))
YouTube related
(defun im-youtube-link-extract-id (link) (nth 4 (s-match "https?://\\(www\\.\\)?\\(youtu\\.be\\|youtube.com\\)/\\(watch\\?v=\\)?\\([a-zA-Z0-9_-]+\\)\\(\\?\\|&\\)?/?" link)))
(defun im-org-format-youtube-link (link)
"Format given YouTube link as `org-mode' link.
Length, author, title etc. are appended to the link."
(let ((id (im-youtube-link-extract-id link)))
(let-alist (empv--invidious-request (format "videos/%s" id))
(org-link-make-string
link
(format "%0.2f mins, by %s || %s" (/ .lengthSeconds 60.0) .author .title)))))
(defun im-org-link-find-backlinks (&optional id)
"Find backlinks for ID (or current heading)."
(interactive)
(let ((default-directory org-directory)
(id (or id (org-id-get))))
(unless id
(user-error "No ID found for current heading"))
(im-shell-command
:command "rg"
:args `(,(format "\\[\\[id:%s\\]" id) "--color=never" "--no-heading")
:on-finish
(lambda (out)
(let* ((infos (--map (-take 2 (s-split-up-to ":" it 2)) (s-split "\n" out t)))
(backlinks (->>
(--map
(with-current-buffer (find-file-noselect (f-join default-directory (car it)))
(save-excursion
(save-restriction
(widen)
(goto-char (point-min))
(forward-line (1- (string-to-number (nth 1 it))))
(format
"- [[file:%s][%s]] :: %s / %s"
(car it)
(car it)
(s-join " → " (--map (format "*%s*" it) (org-get-outline-path)))
(substring-no-properties (org-store-link nil))
(im-org-header-line-to-title (org-entry-get nil "ITEM"))))))
infos)
(-uniq))))
(im-peek
(lambda ()
(with-current-buffer (get-buffer-create "*im-org-link-backlinks*")
(erase-buffer)
(insert (s-join "\n" backlinks))
(unless (derived-mode-p 'org-mode)
(org-mode))
(current-buffer)))))))))
Jump to a header or link a header
Keybindings
(im-leader-v "ol" #'im-org-link-header ;; "oj" is bound to consult-org-agenda "oJ" #'im-org-jump-to-header)
Utility
(defun im-org-header-line-to-title (line)
"Remove TODO/*/unnecessary whitespace from given LINE.
Then return the title of given `org-mode` header. Just
like (org-entry-get nil \"ITEM\") but works on given string."
(->> line
(s-replace-regexp "\\(\\*\\|TODO\\|PROG\\|DONE\\|WAIT\\)" "") ;; Remove TODO states
(s-replace-regexp "\\(\\[#.\\{1\\}\\]\\\)" "") ;; Remove priorities
(s-replace-regexp ":\\(\\w+:\\)+$" "") ;; Remove tags
(replace-regexp-in-string "\\[\\[.*\\]\\[\\(.*\\)\\]\\]" "\\1") ;; Fix links
(replace-regexp-in-string "\\[\\[\\(.*\\)\\]\\]" "\\1") ;; Fix links
(s-trim)))
(defun im-org-file-get-header-id (file-path header-line)
"Return the id of given header at HEADER-LINE in FILE-PATH."
(interactive)
(save-excursion
(with-current-buffer (let ((enable-local-variables nil)) (find-file-noselect file-path))
(save-restriction
(save-excursion
(widen)
(goto-char 0)
(forward-line header-line)
(org-id-get nil 'create)
(save-buffer)
(org-id-get nil 'create))))))
(defun im-org-all-headers ()
"Return all headers in `org-directory'."
(->> (concat "cd " org-directory "; "
"rg"
" --no-heading" " --with-filename"
" --line-number" " -t org"
" " "\"^\\*+ \" ")
(shell-command-to-string)
(s-split "\n")
(--filter (not (s-blank? it)))
(--map
(-let* (((fname line . content) (split-string it ":"))
(header (im-org-header-line-to-title (string-join content ":"))))
`(,(format
"%s:%s %s %s"
(propertize fname 'face '(:foreground "plum" :slant italic))
(propertize line 'face '(:slant italic :weight thin))
(propertize "»" 'face '(:foreground "green"))
(propertize header 'face '(:foreground "sky blue" :weight bold)))
.
(:fname ,(concat org-directory "/" fname) :line ,(1- (string-to-number line)) :header ,header))))))
(defun im-org-link-header ()
"Interactively select a header and insert it as a link into the buffer.
Headers are gathered from all the org files found in `org-directory'."
(interactive)
(save-some-buffers)
(let* ((selected (im-alist-completing-read
"Select header: "
(im-org-all-headers)
(when (use-region-p)
(buffer-substring-no-properties (region-beginning) (region-end)))))
(header-id (im-org-file-get-header-id (plist-get selected :fname) (plist-get selected :line))))
(org-insert-link
nil
(concat "id:" header-id)
(unless (use-region-p)
(read-string "Enter link text: " (plist-get selected :header))))))
(defun im-org-jump-to-header ()
"Jump to selected header."
(interactive)
(let* ((selected (im-alist-completing-read "Select header:" (im-org-all-headers))))
(with-current-buffer (find-file (plist-get selected :fname))
(widen)
(goto-char 0)
(forward-line (plist-get selected :line))
(reveal-post-command))))
Insert image with width
This function is especially useful when used in combination with embark-act-all. The workflow is as follows:
M-xim-org-insert-image-file-with-width- Filter the files that you want to insert to your buffer.
- Hit enter if there is only one item.
- If there are multiple items that you want to insert, do
M-a(embark-act) and thenS(embark-collect-snapshot). - Then you can do
embark-act-allor just hit enter on the items that you want to insert to your buffer.
(defun im-org-insert-image-file-with-width ()
"Insert interactively selected image file with fixed width information."
(interactive)
(let ((fname (file-relative-name (read-file-name "Select file: "))))
(insert (format "#+ATTR_ORG: :width 400\n[[file:%s]]\n\n" fname))))
(defun im-org-rename-file-at-point ()
"Interactively rename the file under cursor and update the link."
(interactive)
(let* ((link (org-element-context))
(type (org-element-property :type link))
(path (org-element-property :path link))
(begin (org-element-property :begin link))
(end (org-element-property :end link))
(cbegin (org-element-property :contents-begin link))
(cend (org-element-property :contents-end link))
content)
(unless (equal type "file")
(user-error "Link is not a file"))
(when (and cbegin cend)
(setq content (format "[%s]" (buffer-substring-no-properties cbegin cend))))
(let ((use-relative? (not (file-name-absolute-p path)))
(fname (read-file-name "New name: "
(expand-file-name
(file-name-directory path))
nil nil
(im-org-suggest-filename path))))
(when use-relative?
(setq fname (concat "./" (file-relative-name fname))))
(save-excursion
(rename-file path fname)
(delete-region begin end)
(insert (format "[[file:%s]%s]"
fname
(or content "")))))))
Project management
I'm doing all of my project management in org-mode. Here you can find some supplementary functionality that makes project management within org-mode easy.
Do a regexp search in a project inside a org dynamic block
Here I create a dynamic block for org-mode, named
project-grep. You can create a block like the following:
When you invoke C-c C-c on that block, it will automatically run
given REGEXP in given ROOT and create a nicely formatted table
containing all the results. Results are formatted into org-links
you can easily jump into.
(defun org-dblock-write:project-grep (params)
"Do a regular expression search in given project.
PARAMS may contain `:root' or `:regexp'.
`:root' - Where to run the search. If it's skipped, it's
`default-directory'.
`:regexp' - Regexp to grep in given folder. If it's skipped it
searches for TODO/FIXME items in given folder."
(let* ((root (expand-file-name (or (plist-get params :root) default-directory)))
(regexp (or (plist-get params :regexp) "(//|#|--|;)+ ?(TODO|FIXME)"))
(default-directory root))
(--each '("" "ID" "File" "Content")
(insert (format "%s | " it)))
(insert "\n")
(insert "|-|\n")
(--each-indexed
(s-split
"\n"
(shell-command-to-string (format "rg --line-number '%s'" regexp))
'omit-nulls)
(let* ((data (s-split-up-to ":" it 2))
(file (s-join ":" (-take 2 data)))
(file-link (concat "[[file:" (f-join default-directory (s-replace ":" "::" file)) "][" file "]]"))
(content (s-replace "|" " \\vert " (-last-item data))))
(insert "| ")
(insert (format "%s" it-index))
(insert " | ")
(insert file-link)
(insert " | ")
(insert content)
(insert " |\n"))))
(delete-char 1)
(org-table-align))
(use-package im-archive
:ensure `(:repo ,im-packages-path :files ("im-archive.el"))
:defer t)
(defvar im-org-archive-url-path "~/Documents/notes/data/archvive/")
(defun im-org-archive-url ()
"Archive URL and generate an new org entry for it."
(interactive)
(require 'im-archive)
(let (url update?)
;; If we are on a heading and calling this function, we probably
;; just want to update/initialize the archive for current
;; heading. Otherwise we are creating a new archive.
(if-let* ((_ (org-at-heading-p))
(old-url (or (org-entry-get nil "URL")
;; There might not be an URL property but
;; the header itself might be a link, try to
;; extract it
(or (plist-get (cadr (org-element-context)) :raw-link)))))
(progn
(setq url old-url)
(setq update? t))
(progn
(setq url (read-string "URL: "))
(org-insert-heading)))
;; Precautionary call
(org-id-get-create)
(let* ((url-title (if update?
(im-org-header-line-to-title (org-entry-get nil "ITEM"))
(read-string "Title: " (im-url-get-title url))))
(archive-path (format
"%s/%s_%s_%s%s"
im-org-archive-url-path
(org-id-get-create)
(format-time-string "%Y%m%dT%H%M%S")
(im-string-url-case url-title)
(plist-get (cdar (im-assoc-regexp url im-archive-handlers)) :extension))))
(org-set-property
"ARCHIVED_AT"
(format "%s[[file:./%s][%s]]"
(if-let* ((older-archives (org-entry-get nil "ARCHIVED_AT")))
(format "%s, " older-archives)
"")
(f-relative archive-path)
(format-time-string "%Y-%m-%dT%H:%M")))
(unless update?
(insert (org-link-make-string url url-title)))
(org-set-property "URL" url)
(unless (org-entry-get nil "CREATED")
(org-set-property "CREATED" (format-time-string "[%Y-%m-%d %a %H:%M]")))
;; Create the archive
(f-mkdir im-org-archive-url-path)
(im-archive-url url archive-path))))
(with-eval-after-load 'org-capture
(add-to-list
'org-capture-templates
`("b" "Bookmark" entry
(file+headline ,bookmarks-org "Unsorted")
"** (call-interactively #'im-org-archive-url)\n- Don't forget to TAG!")))
(defun im-org-sort-entries-best ()
"Best sorting mechanism for your backlog.
Entries are sorted by priority, with most effort entries being
on top in their priority range. Also entries are sorted by their
TODO state, prioritizing in-progress items."
(interactive)
;; Sort by creation time
(org-sort-entries nil ?R nil nil "CREATED_AT")
;; Sort by Effort, the one with least effort is at top
(org-sort-entries nil ?f (lambda ()
(let* ((effort-str (org-entry-get nil "Effort"))
(effort-min (and effort-str (org-duration-to-minutes effort-str))))
(or effort-min most-positive-fixnum))))
;; Sort by priority
(org-sort-entries nil ?p)
;; Sort by TODO state, prioritizing in progress items.
(org-sort-entries nil ?f (lambda ()
(let ((todo-state (org-get-todo-state)))
(cond
((string= todo-state "PROG") 1)
((string= todo-state "WAIT") 2)
((string= todo-state "NEXT") 3)
((string= todo-state "TODO") 4)
((string= todo-state "DONE") 5)
((string= todo-state "DEAD") 6)
(t 0)))))
;; Move non-TODO headers to top
(org-sort-entries nil ?F (lambda ()
(if (or (org-entry-is-done-p) (org-entry-is-todo-p))
-1
(- 1000 (length (org-entry-get nil "ITEM")))))))
(defun im-org-unfill-paragraph (&optional justify region)
"Unfill the paragraph at point.
This function unfills the paragraph at point, removing line
breaks and joining the lines together. This function relies on
`org-fill-paragraph' to perform the actual unfilling."
(interactive (progn
(barf-if-buffer-read-only)
(list (when current-prefix-arg 'full) t)))
(let ((fill-column 9999))
(org-fill-paragraph justify region)))
im-org-new-todo
NOTE: With C-o in minibuffer, I can insert links to minibuffer.
(defun im-org-new-todo (&optional priority)
"Create new org TODO item with the fields I want them to be there."
(interactive)
(let ((content (concat
(format "TODO%s %s "
(if-let* ((priority
(im-non-blank-or-nil
(or priority
(completing-read
"Select priority:"
(mapcar #'char-to-string (number-sequence
org-priority-highest
(1+ org-priority-lowest))))))))
(format " [#%s]" priority)
"")
(s-trim (read-string "Header: ")))
(when-let* ((date (im-bullet-current-date)))
(format "\nSCHEDULED: <%s>" date))
(format
"\n:PROPERTIES:\n:CREATED_AT: [%s]\n:END:"
(format-time-string "%Y-%m-%d %a %H:%M")))))
(org-insert-heading '(4))
(insert content)
(call-interactively #'org-set-tags-command)
(org-set-effort)))
(defun im-org-new-heading ()
"Create a new heading with some automatically set properties."
(interactive)
(let ((content (concat
(format
"%s%s"
(s-trim (read-string "Header: "))
(format
"\n:PROPERTIES:\n:CREATED_AT: [%s]\n:END:"
(format-time-string "%Y-%m-%d %a %H:%M"))))))
(org-insert-heading '(4))
(insert content)
(call-interactively #'org-set-tags-command)))
(im-leader :keymaps 'org-mode-map "ok" #'im-org-new-todo "oK" #'im-org-new-heading)
(defun im-org-heading-current-time-length ()
"Get time length in minutes for the current/closest timestamp in heading."
(interactive)
(save-excursion
(org-back-to-heading t)
(let ((end (save-excursion (org-end-of-subtree t) (point)))
(time-range-re "<\\([0-9]+-[0-9]+-[0-9]+\\) [A-Za-z]+ \\([0-9]+:[0-9]+\\)-\\([0-9]+:[0-9]+\\)>")
(now (current-time))
slots)
;; Collect all time slots
(while (re-search-forward time-range-re end t)
(let* ((date-str (match-string 1))
(start-time (match-string 2))
(end-time (match-string 3))
(start-mins (+ (* 60 (string-to-number (car (split-string start-time ":"))))
(string-to-number (cadr (split-string start-time ":")))))
(end-mins (+ (* 60 (string-to-number (car (split-string end-time ":"))))
(string-to-number (cadr (split-string end-time ":")))))
(ts-start (org-read-date nil t (concat date-str " " start-time)))
(ts-end (org-read-date nil t (concat date-str " " end-time))))
(push (list :start ts-start
:end ts-end
:length (- end-mins start-mins))
slots)))
(when slots
(let ((current (--find (and (time-less-p (plist-get it :start) now)
(time-less-p now (plist-get it :end)))
slots)))
(if current
(plist-get current :length)
;; Find closest slot
(plist-get
(car (--sort (< (abs (float-time (time-subtract (plist-get it :start) now)))
(abs (float-time (time-subtract (plist-get other :start) now))))
slots))
:length)))))))
Capture changed headings on save
Sometimes, I want to capture which headings are changed on save and
do some actions on those headings. Following code accomplishes
that, but only for top and second level headings. To do so add a
function to im-org-header-changed-hook.
(defvar im-org-header-changed-hook-blacklist-files '("bullet.org")
"Do not run change hook on these files.
If file is too big (> 1 MB?, or too much headers), this hook
makes saving quite slow. I could simply put a file limit on the
hooks but that makes thing even more complicated.")
(defvar im-org-header-changed-hook '()
"Functions run after an entry is changed.
Functions are called with one argument which is the following plist:
(:begin ... :end ... :level ... :properties ... :header... :body ...)
Functions are called separately for each changed entry/header.")
(defvar-local im--org-heading-prev-state nil "Holds the previous state of the top-level headers.")
(defun im-org-store-heading-state ()
"Store the current buffer state of headers."
(when (and (derived-mode-p 'org-mode)
(buffer-file-name)
(not (-contains? im-org-header-changed-hook-blacklist-files
(f-filename (buffer-file-name)))))
(let* ((headers (im-org-get-some-headers))
(ht (make-hash-table :test 'equal :size (length headers))))
(--each headers
(puthash (plist-get it :accessor)
(cons (plist-get it :body-length) (plist-get it :body))
ht))
(setq im--org-heading-prev-state ht))))
(defun im-org-compare-heading-state ()
"Compare current headers with stored state to detect changes."
(when (and (derived-mode-p 'org-mode)
im--org-heading-prev-state
(not (-contains? im-org-header-changed-hook-blacklist-files
(f-filename (buffer-file-name)))))
(-each (im-org-get-some-headers)
(lambda (header-plist)
(let ((accessor (plist-get header-plist :accessor))
(body-length (plist-get header-plist :body-length))
(body (plist-get header-plist :body)))
(unless (and (eq body-length (car (gethash accessor im--org-heading-prev-state)))
(equal body (cdr (gethash accessor im--org-heading-prev-state))))
(run-hook-with-args 'im-org-header-changed-hook header-plist)))))
(im-org-store-heading-state)))
(defun im-org-get-some-headers ()
"Extract top-level headers and their content from the current Org buffer."
(let ((headers '()))
(save-restriction
(widen)
(org-map-entries
(lambda ()
(let* ((header (substring-no-properties (org-get-heading t t t t)))
(elem (org-element-at-point))
(body (when-let* ((start (org-element-property :contents-begin elem)))
(buffer-substring-no-properties
start
(org-element-property :contents-end elem))))
(properties (org-entry-properties)))
(push
(list
:accessor (or (alist-get "ID" properties nil nil #'equal)
(im-org-get-header-with-hierarchy))
:begin (org-element-property :begin elem)
:end (org-element-property :end elem)
:level (org-element-property :level elem)
:properties properties
:header header
:body body
:body-length (if body (length body) 0))
headers)))
"LEVEL<3"))
headers))
(defun im-org-get-header-with-hierarchy (&optional sep)
"Get the current Org header title along with all parent headings."
(when (derived-mode-p 'org-mode)
(org-with-wide-buffer
(let ((heading-hierarchy (list (substring-no-properties (org-get-heading t t t t)))))
(while (org-up-heading-safe)
(setq heading-hierarchy
(nconc (list (substring-no-properties (org-get-heading t t t t)))
heading-hierarchy)))
(mapconcat 'identity heading-hierarchy (or sep " ⟹ "))))))
Store initial state when file is opened
(add-hook 'org-mode-hook 'im-org-store-heading-state)
Compare and update state after save
(add-hook 'after-save-hook 'im-org-compare-heading-state)
(defun im-org-get-link-effort-at-point ()
"Return the effort of linked header.
Only works with id links. Useful while planning. When called
interactively, insert the effort at the end of the line.
I generally bind this to a key while using by
`im-embark-bind-leader-command'."
(interactive)
(let (effort)
(-let (((file . pos) (org-id-find (org-element-property :path (org-element-lineage (org-element-context) '(link) t)))))
(with-current-buffer (find-file-noselect file)
(save-excursion
(save-restriction
(widen)
(goto-char pos)
(setq effort (or (org-entry-get nil "Effort")
(progn
(org-set-effort)
(org-entry-get nil "Effort"))))))))
(when (called-interactively-p 'interactive)
(end-of-line)
(insert (format " → %s" effort)))))
(defun im-org-set-note-at-point ()
"Set a NOTE property on the heading at point or the target of a link at point."
(interactive)
(let* ((marker (or (im-org--link-target-at-point)
(save-excursion (org-back-to-heading t) (point-marker))))
(current-note (org-with-point-at marker
(org-entry-get nil "NOTE")))
(note (read-string "Note: " current-note)))
(org-with-point-at marker
;; Archive existing note to LOGBOOK if present and different
(when (and current-note
(not (string-empty-p current-note))
(not (string= current-note note)))
(im-org--add-log-note
(format "Previous note: %s" current-note)))
;; Set or delete the NOTE property
(if (string-empty-p note)
(org-entry-delete nil "NOTE")
(org-entry-put nil "NOTE" note)))))
(defun im-org--link-target-at-point ()
"Return a marker to the heading targeted by link at point, or nil."
(when-let* ((elem (org-element-context))
(_ (eq (org-element-type elem) 'link))
(type (org-element-property :type elem))
(path (org-element-property :path elem)))
(pcase type
((and "file" (guard (string-match "\\(.*\\)::\\*\\(.*\\)" path)))
(when-let* ((buf (find-file-noselect (match-string 1 path)))
(pos (org-find-exact-headline-in-buffer (match-string 2 path) buf)))
(set-marker (make-marker) pos buf)))
("fuzzy"
(when-let* ((pos (org-find-exact-headline-in-buffer path (current-buffer))))
(set-marker (make-marker) pos (current-buffer))))
("id"
(when-let* ((loc (org-id-find path))
(buf (find-file-noselect (car loc))))
(set-marker (make-marker) (cdr loc) buf))))))
(defun im-org--add-log-note (note)
"Insert NOTE into the LOGBOOK drawer at current heading."
(org-fold-core-ignore-modifications
(org-with-wide-buffer
(goto-char (org-log-beginning t))
(unless (bolp) (insert-and-inherit "\n"))
(when (looking-at "[ \t]*\\S-") (save-excursion (insert-and-inherit "\n")))
(let ((itemp (org-in-item-p)))
(indent-line-to
(if itemp
(let ((struct (save-excursion (goto-char itemp) (org-list-struct))))
(org-list-get-ind (org-list-get-top-point struct) struct))
(org-indent-line)
(current-indentation))))
(insert-and-inherit
(org-list-bullet-string "-")
note " \\\\\n")
(indent-line-to (org-list-item-body-column (line-beginning-position 0)))
(insert-and-inherit
(format-time-string (org-time-stamp-format 'long 'inactive))))))
corg.el — Org Mode block header completion
Completion at point for dynamic block headers, source block headers etc.
(use-package corg :ensure (:host github :repo "isamert/corg.el") :hook (org-mode . corg-setup))
im-svgcal & org-timeblock
Experimental SVG visual calendar for the current day.
At the time of writing this, I wasn't aware of (or maybe it didn't even exist?) org-timeblock[1]. im-svgcal is quite similar to that but only works for given day and it's independent from the agenda, it simply draws a calendar for current org file (or current restricted section of the org file).
(use-package im-svgcal :commands (im-svgcal-enable im-svgcal-run) :ensure nil)
(use-package org-timeblock :ensure (:host github :repo "ichernyshovvv/org-timeblock") :general (:keymaps 'org-timeblock-mode-map :states 'normal "h" #'org-timeblock-backward-column "j" #'org-timeblock-forward-block "k" #'org-timeblock-backward-block "l" #'org-timeblock-forward-column "t" #'org-timeblock-todo "c" #'org-timeblock-clock-in "n" #'org-timeblock-new-task "r" #'org-timeblock-redraw-buffers "T" #'org-timeblock-list "q" #'org-timeblock-quit "RET" #'org-timeblock-goto) :custom (org-timeblock-inbox-file inbox-org) (org-timeblock-span 5) (org-timeblock-scale-options nil))
(defvar im-open-thing-at-point-alist '() "List of (THING-DETECTOR . THING-OPENER) pairs. THING-DETECTOR should be a no-arg function that returns the thing, if found. Otherwise it should return nil. THING-OPENER should be a function with 1 arg, which should take the thing (which is the thing return by the THING-DETECTOR) and open.")
(im-leader-v "eo" #'im-open-thing-at-point "eO" #'im-open-thing)
(defun im-open-thing-at-point ()
"Detect and open the thing at point."
(interactive)
(--any
(when-let* ((result (funcall (car it))))
(funcall (cdr it) result))
im-open-thing-at-point-alist))
(defun im-open-thing (thing)
"Detect and open the thing at point."
(interactive "sThing: ")
(with-temp-buffer
(insert thing)
(--any
(when-let* ((result (funcall (car it))))
(funcall (cdr it) result))
im-open-thing-at-point-alist)))
(defun im-button-action-at-point ()
"Return button action at point.
Useful for checking if `push-button' can be used."
(and-let* ((btn (button-at (point)))
((button-get btn 'action)))
btn))
(add-to-list 'im-open-thing-at-point-alist `(,(apply-partially #'thing-at-point 'url) . browse-url)) (add-to-list 'im-open-thing-at-point-alist `(im-button-action-at-point . push-button))
(defun im-org-convert-item-to-todo-and-refile (&optional yank-only)
"Convert org item to todo header and refile.
I take notes in the following format during the day via
org-capture template:
- [2023-04-09 Sun 23:57] Some note
At the end of the day, I convert these notes into tasks if
applicable. This means rewriting them into a TODO header with
CREATED_AT property and refiling into the appropriate
header. This function automatizes that.
If YANK-ONLY is non-nil (with prefix arg while calling
interactively), only yank the result, do not refile.
>> (with-temp-buffer
(insert \"- [2023-06-28 Wed 10:13] Test\\n\")
(beginning-of-buffer)
(im-org-convert-item-to-todo-and-refile t))
=> \"** TODO [#B] Test
:PROPERTIES:
:CREATED_AT: [2023-06-28 Wed 10:13]
:END:
\"
>> (with-temp-buffer
(insert \"- [2023-06-28 Wed 10:13] Test :: The body.\\n\")
(beginning-of-buffer)
(im-org-convert-item-to-todo-and-refile t))
=> \"** TODO [#B] Test
:PROPERTIES:
:CREATED_AT: [2023-06-28 Wed 10:13]
:END:
The body.
\"
>> (with-temp-buffer
(insert \"- [2023-06-28 Wed 10:13] Test and a link [[here]] :: The body.\\n\")
(beginning-of-buffer)
(im-org-convert-item-to-todo-and-refile t))
=> \"** TODO [#B] Test and a link [[here]]
:PROPERTIES:
:CREATED_AT: [2023-06-28 Wed 10:13]
:END:
The body.
\"
Version: 2023-07-31
- Fixed not being able to parse headers with links.
Version: 2023-06-28
- Initial version."
(interactive "P")
(save-excursion
(beginning-of-line)
(let ((line (substring-no-properties (thing-at-point 'line)))
(timestamp nil)
(text nil))
(when-let* ((match (s-match "^- \\(\\[[ X-]] \\)?\\(\\[.*?]\\) \\(.*\\)" line)))
(setq timestamp (nth 2 match))
(setq text (nth 3 match))
(let* ((buffer (current-buffer))
(data (s-split " :: " text))
(header (nth 0 data))
(body (nth 1 data))
result)
(with-temp-buffer
(insert
(concat "** TODO [#B] " header "\n"
":PROPERTIES:" "\n"
":CREATED_AT: " timestamp "\n"
":END:" "\n"
(if body (concat body "\n") "")))
(setq result (buffer-string))
;; To be able to refile
(org-mode)
(if yank-only
(im-kill result)
(let ((org-refile-targets nil))
(org-refile nil buffer))
;; Remove the line after everything to prevent loss of data
(with-current-buffer buffer
(delete-region (line-beginning-position) (1+ (line-end-position))))
result)))))))
(use-package transient :demand t :ensure (:wait t))
Bind esc to quit
(with-eval-after-load 'transient (define-key transient-base-map (kbd "<escape>") 'transient-quit-one) (define-key transient-predicate-map (kbd "<escape>") 'transient-quit-one) (define-key transient-sticky-map (kbd "<escape>") 'transient-quit-one))
(use-package which-key :hook (elpaca-after-init . which-key-mode) :diminish :config ;; Disabling this because I use `embark-prefix-help-command' as the `prefix-help-command' (setq which-key-use-C-h-commands nil) (setq which-key-max-display-columns 4) (which-key-setup-minibuffer))
(with-eval-after-load 'doc-view (evil-collection-doc-view-setup))
world-clock
- Simply do
M-x world-clock.
(setq world-clock-list
'(("Europe/Amsterdam" "Amsterdam")
("Europe/Istanbul" "Istanbul")
("Asia/Tokyo" "Tokyo")
("America/Los_Angeles" "Seattle")))
(setq world-clock-time-format "%a %d %b %R %Z")
(use-package eldoc :ensure nil :diminish eldoc-mode :defer t :custom ;; I have a `im-peek-doc' function that displays the documentation ;; inline, which eleminates the need for multiline doc in minibuffer (eldoc-echo-area-use-multiline-p nil) (eldoc-documentation-strategy #'eldoc-documentation-compose-eagerly))
display-time-mode
I started using Emacs in full screen (Emacs maximalism goes on), so this is helpful.
UPDATE: Now I manually put this info into the modeline, in `im-update-global-mode-line' function.
(use-package display-time :ensure nil :hook (elpaca-after-init . display-time-mode) :init (setq display-time-format "%b %d, %H:%M %a") (setq display-time-default-load-average nil))
outline-mode
This is a built-in mode for providing basic outlining features. Here are a few configurations to make the navigation a bit more like what I use in org-mode:
(use-package outline-mode
:ensure nil
:defer t
:diminish outline-minor-mode
:general
(:keymaps 'outline-minor-mode-map :states 'normal
"gh" #'outline-up-heading
"gk" #'outline-backward-same-level
"gj" #'outline-forward-same-level
"gl" #'outline-next-visible-heading
;; Moving
"gJ" #'outline-move-subtree-down
"gK" #'outline-move-subtree-up)
:bind
(:repeat-map outline-mode-repeat-map
("[" . outline-previous-heading)
("]" . outline-next-heading)))
(with-eval-after-load 'outline (diminish 'outline-minor-mode))
(defun im-outline-narrow-to-subtree ()
"Narrow buffer to the current outline subtree."
(interactive)
(let ((beg) (end))
(save-excursion
;; Move to the heading if we're not already on one
(unless (outline-on-heading-p t)
(outline-previous-heading))
(setq beg (point))
;; Find the end of the subtree
(outline-end-of-subtree)
(setq end (point)))
(narrow-to-region beg end)))
(defun im-outline-end-of-subtree ()
"Move to the end of the current subtree."
(outline-back-to-heading)
(let ((level (funcall outline-level))
(found nil))
(forward-line 1)
(while (and (not (eobp)) (not found))
(if (and (outline-on-heading-p t)
(<= (funcall outline-level) level))
(setq found t)
(forward-line 1)))
(unless found
(goto-char (point-max)))))
(evil-set-initial-state 'debugger-mode 'normal)
(use-package hideshow :diminish hs-minor-mode :ensure nil :defer t :general (:states 'normal :keymaps 'hs-minor-mode-map ;; Hide all stuff under current level. ;; Useful in Java classes etc. "zf" #'hs-hide-level) :custom ;; Comment folding results in weird behavior combined with ;; outli-mode. And comments are not that long generally, so this is ;; good. (hs-hide-comments-when-hiding-all nil))
(use-package eshell
:ensure nil
:defer t
:hook
(eshell-mode . im-disable-hl-line-mode-for-buffer)
(eshell-mode . (lambda ()
(setq-local corfu-auto nil)
(corfu-mode)))
:general
(:states 'insert :keymaps '(eshell-prompt-mode-map override)
"C-l" (λ-interactive (eshell/clear t)))
:config
(evil-collection-eshell-setup)
;; This is nil on purpose. Because `im-eshell-append-history' does
;; duplication check before writing it to history and if this is
;; non-nil then it fails to do the duplication check because
;; duplicated items will not be inserted to history-ring at all.
(setq eshell-hist-ignoredups nil)
(setq eshell-history-size 10000)
;; I manage history with `im-eshell-append-history''
(setq eshell-save-history-on-exit nil)
;; You can jump to a directory by doing a regex match with:
;;
;; cd =some-regexp
;;
;; and you'll jump to the latest matching directory in in the last
;; dir ring.
;;
;; Also use
;;
;; cd -
;;
;; to jump to the latest directory before the current one.
(setq eshell-last-dir-ring-size 1000)
(defun im-eshell-prompt ()
(concat (abbreviate-file-name (eshell/pwd))
(if (= (user-uid) 0) " # " " \n$ ")))
(setq eshell-prompt-regexp "^$ ")
(setq eshell-prompt-function #'im-eshell-prompt))
(use-package eshell-syntax-highlighting :after eshell :config (eshell-syntax-highlighting-global-mode +1))
You may need to call `eat-compile-terminfo' after installing eat.
(use-package eat
:ensure (:type git
:host codeberg
:repo "akib/emacs-eat"
:files ("*.el" ("term" "term/*.el") "*.texi"
"*.ti" ("terminfo/e" "terminfo/e/*")
("terminfo/65" "terminfo/65/*")
("integration" "integration/*")
(:exclude ".dir-locals.el" "*-tests.el")))
:autoload (eat-make)
:defer t
:general
(im-leader-v
"tj" #'im-jump-to-visible-term
"tl" #'im-run-last-command-on-visible-term
"ty" #'im-send-selected-text-to-visible-term
"tr" #'im-run-command-on-visible-term-with-history)
:config
(setq eat-term-scrollback-size nil)
(setq eat-enable-shell-prompt-annotation nil)
(setq eat-shell (executable-find "fish"))
(setq eat-tramp-shells '(("docker" . "/bin/sh")
("ssh" . "/usr/bin/zsh")))
(add-hook 'eat-mode-hook #'im-disable-hl-line-mode-for-buffer)
;; While writing, eat buffers sometimes jump around for some unknown
;; reason. I realized that truncating lines helps with that.
(add-hook 'eat-mode-hook #'toggle-truncate-lines)
(with-eval-after-load 'eshell
(eat-eshell-mode)))
(use-package ghostel
:ensure (:host github :repo "dakra/ghostel"
:files (:defaults
"etc"
"src"
"vendor"
"build.zig"
"build.zig.zon"
"symbols.map"
"ghostel-pkg.el"
"lisp/*.el"
"extensions/*/*.el"
"extensions/*.el"))
:config
(defun im-ghostel-close-temp-frame ()
"Close the current frame when its name is `emacs-temp'."
(let ((frame (selected-frame)))
(when (equal (frame-parameter frame 'name) im-temp-frame-name)
(run-at-time
0 nil
(lambda (f)
(when (frame-live-p f)
(delete-frame f)))
frame))))
(defun im-ghostel-kill-hook-setup ()
(add-hook 'kill-buffer-hook
#'im-ghostel-close-temp-frame nil :local))
(add-hook 'ghostel-mode-hook #'im-ghostel-kill-hook-setup))
(use-package evil-ghostel :ensure nil :after (ghostel evil) :hook (ghostel-mode . evil-ghostel-mode))
(defvar im-terminal-buffer-name-regexp "\\*?\\$?\\(e?shell\\|v?term\\|eat\\|cursor-agent\\|ghostel\\).*")
(defun im-run-last-command-on-visible-term ()
(interactive)
(save-buffer)
(im-with-visible-buffer im-terminal-buffer-name-regexp
(pcase major-mode
('eshell-mode (eshell-previous-matching-input "" 0)
(eshell-send-input))
('ghostel-mode (ghostel-send-key "up")
(ghostel-send-key "return"))
('eat-mode (eat-self-input 1 (aref (kbd "<up>") 0))
(eat-self-input 1 (aref (kbd "RET") 0)))
('vterm-mode (vterm-send-up)
(vterm-send-return)
t))))
(defun im-run-command-on-visible-term (cmd)
(im-with-visible-buffer im-terminal-buffer-name-regexp
(pcase major-mode
('eshell-mode (insert cmd)
(eshell-send-input))
('ghostel-mode (ghostel-send-string cmd)
(ghostel-send-key "return"))
('eat-mode
(eat--send-string (eat-term-parameter eat-terminal 'eat--process) cmd)
(eat-self-input 1 (aref (kbd "RET") 0)))
('vterm-mode (vterm-send-string cmd)
(vterm-send-return))))
cmd)
(defun im-send-selected-text-to-visible-term (start end)
(interactive "r")
(if (use-region-p)
(im-run-command-on-visible-term (buffer-substring-no-properties start end))
(im-run-command-on-visible-term (s-trim (thing-at-point 'line t)))))
(defvar im-term-run-history '()) (defvar im-jump-to-term-last-window nil) (with-eval-after-load 'savehist (add-to-list 'savehist-additional-variables 'im-term-run-history))
(defun im-jump-to-visible-term ()
"Jump to the visible term window.
When invoked in a term window, return back to last window that
this command is invoked from."
(interactive)
(cond
((string-match im-terminal-buffer-name-regexp (buffer-name (window-buffer (selected-window))))
(select-window im-jump-to-term-last-window))
(t
(setq im-jump-to-term-last-window (selected-window))
(im-select-window-with-buffer im-terminal-buffer-name-regexp))))
(defun im-run-command-on-visible-term-with-history ()
(interactive)
(let ((cmd (im-run-command-on-visible-term
(completing-read "Run new command: " im-term-run-history))))
(when cmd
(setq im-term-run-history (cons cmd (delete cmd im-term-run-history))))))
Send a notification for long running commands
See `im-eshell-notify-duration' to control what a long running command means. This only sends if user is focused to another buffer or Emacs is out of focus.
I already have this feature in zsh, provided by starship prompt. This is just an enhanced eshell implementation.
(defvar-local im-eshell-notify-enabled t) (defvar im-eshell-notify-duration 1 "Minimum time in seconds that a command takes for a notification to fire.") (defvar-local im-eshell-notify--last-execution-time nil)
(with-eval-after-load 'eshell
(defun im-eshell-notify-toggle-for-buffer ()
(interactive nil eshell-mode)
(setq im-eshell-notify-enabled (not im-eshell-notify-enabled))
(message ">> eshell-notify :: %s" im-eshell-notify-enabled))
(defun im-eshell-notify--pre-command-hook ()
(setq im-eshell-notify--last-execution-time (float-time)))
(defun im-eshell-notify--post-command-hook ()
(when im-eshell-notify-enabled
(when im-eshell-notify--last-execution-time
(let ((execution-time (- (float-time) im-eshell-notify--last-execution-time)))
(when (> execution-time im-eshell-notify-duration)
;; Only send a notification if user focused to another buffer or
;; Emacs is out of focus.
(when (or
(not (frame-focus-state))
(not (eq (current-buffer) (window-buffer (selected-window)))))
(alert
(format "Execution took %s seconds." execution-time)
:title (format "Command finished: %s!" eshell-last-command-name))))))))
(add-to-list 'eshell-pre-command-hook 'im-eshell-notify--pre-command-hook)
(add-to-list 'eshell-post-command-hook 'im-eshell-notify--post-command-hook))
Proper command completion
This package provides command, subcommand and argument completion using fish shell. You need to have fish shell installed. There is also helm version that provides argument documentation but I haven't tried it yet and I need to make it work with corfu instead of helm.
- In `eshell', fish completion is only used when `pcomplete' fails.
- Don't forget that pcomplete works with TAB, not M-TAB.
Also do `fishupdatecompletions' time to time in a fish shell to parse and generate completions for fish shell.
(use-package fish-completion :after eshell :hook (eshell-mode . fish-completion-mode))
(defun im-eshell-load-my-aliases ()
"Load fish/zsh/bash aliases/abbreviations into eshell.
'$*' is appended after each alias so that they can take
positional parameters in eshell.
Aliases needs to be defined in a particular way:
alias alias-name='alias value'
- There should be no spaces around the equals sign.
- Single quote should be used instead of double quoutes.
For fish shel abbreviations, only the following form is accepted:
abbr -a -- alias-name 'alias value'
There is also a special syntax for defining eshell-specific aliases
that is read verbatim (meaning that no '$*' is appended):
#eshell test='ls'"
(interactive)
(setq
eshell-command-aliases-list
(-filter
#'identity
(--map
(-when-let ((_ eshell? name _2 imp)
(s-match "^\\(alias\\|#eshell\\|abbr -a --\\) \\([a-zA-Z0-9_-]+\\)\\(=\\| \\)'\\(.*\\)'\\( *#.*\\)*$" it))
(list name (if (s-prefix? "#eshell" eshell?)
imp
(concat imp " $*"))))
(--mapcat
(s-split "\n" (with-temp-buffer (insert-file-contents it) (buffer-string)))
(append
(when-let* ((path "~/.config/fish/conf.d/aliases.fish")
(_ (file-exists-p path)))
(list path))
(when (file-exists-p "~/.config/aliases/")
(directory-files "~/.config/aliases/" t directory-files-no-dot-files-regexp))))))))
(add-hook 'eshell-alias-load-hook #'im-eshell-load-my-aliases)
Eshell-specific aliases
Look at section in my config to see all bash/zsh aliases. These are eshell specific:
(with-eval-after-load 'eshell (im-tangle-file :path "~/.config/aliases/eshell" :contents "#eshell clear='clear 1'"))
Also here are some aliases/functions for eshell defined in elisp:
(defun eshell/mkcd (&rest args) (eshell/mkdir args) (eshell/cd args))
(defun eshell/project-root (&rest _args) (eshell/cd (im-current-project-root)))
Automatically print notes for given directory
I have the file im-autodir-file which contains notes for
directories, like in the following format:
~/a/directory
Here are my notes for the directory…
~/another/directory: This one contains some source code
Use the following to compile this: …
When I enter any of the listed directories, eshell automatically prints out my notes.
(defvar im-autodir-file directory-notes-org) (defvar im-autodir-last-doc nil)
(defun im-eshell-handle-dir-change ()
(let ((dir (file-name-as-directory (expand-file-name default-directory))))
(setq im-autodir-last-doc
(with-current-buffer (find-file-noselect im-autodir-file)
(save-restriction
(widen)
(catch 'found
(org-map-entries
(lambda ()
(when (->>
(org-entry-get nil "ITEM")
(s-split ":")
(car)
(expand-file-name)
(file-name-as-directory)
(equal dir))
(throw 'found (buffer-substring-no-properties
(org-element-property :contents-begin (org-element-at-point))
(org-element-property :contents-end (org-element-at-point))))))
"LEVEL=1")
nil))))
(when im-autodir-last-doc
(insert "im-autodir-show-doc")
(eshell-send-input))))
(defun im-autodir-show-doc () (->> im-autodir-last-doc s-lines (-take 20) (s-join "\n")))
(defun im-autodir-show-doc-force () (interactive nil eshell-mode) (im-eshell-handle-dir-change))
(add-hook 'eshell-directory-change-hook #'im-eshell-handle-dir-change)
eshell-directory-change-hook is not get triggered when eshell is opened, hence:
(add-hook 'eshell-mode-hook #'im-eshell-handle-dir-change)
(defalias 'im-show-directory-notes #'im-org-jump-to-project-documentation)
(defun im-org-jump-to-project-documentation ()
"Jump to *my* documentation for current project."
(interactive)
(let* ((projdir (abbreviate-file-name (or (im-current-project-root) default-directory)))
(buf (find-file-noselect directory-notes-org))
(pos (with-current-buffer buf
(widen)
(org-find-exact-headline-in-buffer projdir))))
(unless pos
(if (y-or-n-p "Note not found for this project. Want to create?")
(with-current-buffer buf
(widen)
(goto-char (point-max))
(org-insert-heading nil nil t)
(org-edit-headline projdir)
(org-set-property "CREATED_AT" (concat "[" (im-today) "]"))
(setq pos (point)))
(user-error "Doc not found directory: %s" projdir)))
(switch-to-buffer buf)
(goto-char pos)
(org-fold-show-entry)
(org-narrow-to-subtree)))
Consistent history
Normally, if you have more than one eshell instance open and quit
them consecutively, the last closed one will overwrite the
eshell-history-file. The following appends issued command to
history after each command.
(defvar im-eshell-history-blacklist "^\\(ls\\|pwd\\|cd\\|clear\\|exit\\|rm\\|mkdir\\|mkcd\\|im-autodir\\|lls\\|ll\\)")
Adapted from: https://emacs.stackexchange.com/questions/18564/merge-history-from-multiple-eshells Changes:
- Duplication check
- Blacklisted commands
(defvar-local im-eshell-append-history nil)
(defun im-eshell-append-history ()
"Call `eshell-write-history' with the `append' parameter set to t."
(if (not im-eshell-append-history)
;; Remove the eshell save hook on first run.
;; Also don't append on first run to prevent duplication.
(progn
(remove-hook 'eshell-exit-hook #'eshell--save-history t)
(setq im-eshell-append-history t))
(when (and eshell-history-ring
(ring-elements eshell-history-ring))
(let ((last-elem (s-trim (car (ring-elements eshell-history-ring))))
(prev-elem (or (ignore-errors (s-trim (nth 1 (ring-elements eshell-history-ring)))) "")))
(when (and (not (string= last-elem prev-elem))
(not (s-matches? im-eshell-history-blacklist last-elem)))
(with-temp-buffer
(insert last-elem "\n")
(eshell-with-private-file-modes
(write-region (point-min) (point-max)
(file-truename eshell-history-file-name)
'append 'no-message))))))))
(add-hook 'eshell-post-command-hook #'im-eshell-append-history)
(defun im-eshell-reset ()
"Reset current eshell."
(interactive nil eshell-mode)
(let ((name (buffer-name))
(dir default-directory))
(let ((kill-buffer-query-functions '()))
(kill-buffer))
(let ((default-directory dir))
(im-eshell name))))
bookmark.el
You can use list-bookmarks command to view/edit/delete them but
using consult-bookmark and calling embark-act on them to
view/delete/edit given bookmark might be easier.
(use-package bookmark
:ensure nil
:defer t
:general
(im-leader
"bs" #'bookmark-set
"bm" #'consult-bookmark)
:custom
;; Save bookmarks automatically
(bookmark-save-flag 1)
;; If it's a symlink, point to the real one so that bookmark.el can
;; detect changes
:config
(evil-collection-bookmark-setup)
(setq bookmark-default-file (file-truename bookmark-default-file)))
(general-def :keymaps 'process-menu-mode-map :states 'normal "r" #'revert-buffer "d" #'process-menu-delete-process "x" #'process-menu-delete-process)
(general-def :keymaps 'timer-list-mode-map :states 'normal "r" #'revert-buffer "d" #'timer-list-cancel "x" #'timer-list-cancel)
tabulated-list-mode
- It's a built-in mode that shows some kind of tabulated data.
- It is used by many major modes, like , , etc. I just add these common keybindings to have a consistent way of navigating in them.
- I also try to bind following keys in their respective mode maps:
akey to a function that lists all the actions that can be taken on current column.Enterto the default action (generally opening something etc.)
(with-eval-after-load 'tabulated-list
(evil-define-key 'normal tabulated-list-mode-map
(kbd "{") #'tabulated-list-narrow-current-column
(kbd "}") #'tabulated-list-widen-current-column
(kbd "H") #'tabulated-list-previous-column
(kbd "L") #'tabulated-list-next-column
(kbd "s") #'tabulated-list-sort
(kbd "r") #'tabulated-list-revert))
vtable-mode
Here are mostly used keys in vtable-map:
- S
- Sort by column.
- r
- Refresh the table
- {,}
- narrow/widen column
(with-eval-after-load 'vtable
;; Unbind g first
(general-def :keymaps 'vtable-map "g" nil)
;; Add evil bindings for vtable mode
(general-def
:keymaps 'vtable-map
:states 'normal
"r" #'vtable-revert-command
"H" #'vtable-previous-column
"L" #'vtable-next-column))
Alert & im-notif
Several packages are using this package to show system-level notifications. Here I set some defaults/fallback values.
(use-package alert
:defer t
:config
(setq alert-log-messages nil) ; im-notif already logs them
(alert-define-style
'im-notif
:title "im-notif"
:notifier
(lambda (info)
(im-notif
:title (plist-get info :title)
:message (plist-get info :message)
:margin t
:duration (unless (plist-get info :persistent) alert-fade-time)
:severity (plist-get info :severity)
:labels '("alert"))))
(setq alert-default-style 'im-notif)
(setq alert-fade-time 15)
(when (eq system-type 'darwin)
;; Re-define osx-notifier to handle strings better.
(defun alert-osx-notifier-notify (info)
(do-applescript (format "display notification %S with title %S"
(substring-no-properties (plist-get info :message))
(substring-no-properties (plist-get info :title))))
(alert-message-notify info)))
;; org-clock sends notification if the current tasks estimated effort is met.
(setq
org-show-notification-handler
(lambda (notification)
(im-notif :message notification
:title "*org-mode*"
:labels '("org"))))
(with-eval-after-load 'im-notif
(add-to-list 'im-notif-dnd-whitelist-labels "org")))
(use-package im-notif
:ensure `(:repo ,im-packages-path :files ("im-notif.el"))
:defer t
:general
(im-leader
"y" #'im-notif-menu)
:config
(setq im-notif-dnd-whitelist-regexp "check slack")
(defun im-notif--send-to-ntfy (data)
"Send notifications to ntfy after some idle time."
(when (and
(not im-notif-dnd)
(current-idle-time)
(>= (time-to-seconds (current-idle-time)) 30)
(not (seq-contains-p (plist-get data :labels) "ntfy")))
(im-ntfy-publish
(concat "emacs-" (system-name)) (plist-get data :message)
:title (or (plist-get data :title) "Emacs"))))
(add-hook 'im-notif-post-notify-hooks #'im-notif--send-to-ntfy))
Hydra
Hydra creates a menu for quickly calling/toggling functions/modes in a visually easy way. My main use case for it right now is grouping bunch of appearance related functions/modes that I use infrequently. I believe for hydra's are not very useful for commands that you use frequently, it makes things slower than a plain keybinding but it's quite useful for the stuff that you forget or use infrequently.
(use-package hydra
:defer t
:config
(setq hydra-hint-display-type (im-when-on
:android 'lv
:rest 'posframe))
(setq hydra-posframe-show-params `(:poshandler posframe-poshandler-frame-center
:internal-border-width 2
:internal-border-color "#61AFEF"
:left-fringe 16
:right-fringe 16)))
(use-package pretty-hydra
:after hydra
:general
(im-leader "a" (lambda () (interactive) (require 'pretty-hydra) (im-appearance/body)))
:config
(pretty-hydra-define im-appearance
(:foreign-keys warn :title "Appearance" :quit-key "q" :color amaranth)
("Writeroom"
(("W" writeroom-mode "Writeroom mode" :toggle t)
(">" writeroom-increase-width "Width +")
("<" writeroom-decrease-width "Width -")
("m" writeroom-toggle-mode-line "Toggle modeline" :toggle t)
("p" spacious-padding-mode "Spacious padding" :toggle t))
"Zoom"
(("0" text-scale-increase "Zoom In (Buffer)")
("9" text-scale-decrease "Zoom Out (Buffer)")
("+" default-text-scale-increase "Zoom In (All)")
("-" default-text-scale-decrease "Zoom Out (All)")
("=" default-text-scale-reset "Zoom Reset (All)"))
"Tabs"
(("tl" tab-line-mode "Tab Line mode (buffer)" :toggle t)
("tL" global-tab-line-mode "Tab Line mode (global)" :toggle t)
("tb" tab-bar-mode "Tab Bar mode (global)" :toggle t))
"Highlighting"
(("hg" diff-hl "Highlight git diff" :toggle t)
("hd" rainbow-delimiters-mode "Rainbow parens" :toggle t)
("hl" global-hl-line-mode "Highlight current line" :toggle t)
("hb" beacon-mode "Cursor trailer (baecon)" :toggle t)
("hw" im-whitespace-mode-toggle "Whitespaces" :toggle t)
("ht" highlight-thing-mode "Highlight current symbol" :toggle t))
"Miscellaneous"
(("n" display-line-numbers-mode "Line numbers" :toggle t)
("l" visual-line-mode "Wrap lines" :toggle t)
("T" toggle-truncate-lines "Truncate lines")
("v" visual-fill-column-mode "Wrap lines at 72th col" :toggle t)
("i" highlight-indent-guides-mode "Indent Guides" :toggle t)
("f" fci-mode "Fill column" :toggle t)
("<SPC>" nil "Quit" :color blue)))))
wgrep
With this package, you can make grep buffers editable and your
edits can be applied to the files itself. Also embark has a
feature where you can export the current completing-read results
into a grep buffer, the action is called embark-export and it
works on consult-ripgrep etc.
- Do
C-c C-p(ori, enabled by evil-collection) on agrepbuffer to make it editable. - Do
C-jorC-k(enabled by evil-collection, by default you need to usen) to peek at next/prev instance.
(use-package wgrep :defer t)
dired/dirvish
There is also wdired-mode which you can use to do bulk rename intuitively.
(use-package dired
:ensure nil
:defer t
:custom
(dired-dwim-target t)
(dired-listing-switches "-l -A -h -v --group-directories-first")
(ls-lisp-dirs-first t)
(ls-lisp-use-insert-directory-program nil)
(dired-kill-when-opening-new-dired-buffer t)
(dired-clean-confirm-killing-deleted-buffers nil)
(dired-recursive-copies 'always)
(dired-recursive-deletes 'always)
(dired-dwim-target t)
:init
(with-eval-after-load 'dired
(evil-collection-dired-setup)
(put 'dired-find-alternate-file 'disabled nil)))
(use-package dirvish
:ensure (:host github :repo "hlissner/dirvish")
:after dired
:general
(im-leader
"ed" #'im-dired-dwim
"eD" #'im-dired)
;; Add a way to open dirvish in selected directory using Embark
(:keymaps 'embark-file-map "J" #'im-dired)
(:keymaps 'dirvish-mode-map :states 'normal
"\\" #'im-dired-find-file-ace-window
"h" #'dired-up-directory
"l" #'dired-find-alternate-file
"T" #'dirvish-layout-toggle
"e" #'dirvish-dispatch
"q" #'dirvish-quit
"<tab>" #'dirvish-toggle-subtree
"f" #'dirvish-file-info-menu
"s" #'dirvish-setup-menu
"H" #'dirvish-history-go-backward
"L" #'dirvish-history-go-forward
"Q" #'im-quit)
:init
(with-eval-after-load 'dired
(dirvish-override-dired-mode))
:custom
(dirvish-subtree-always-show-state t)
(dirvish-attributes '(vc-state subtree-state all-the-icons collapse git-msg file-time file-size))
:config
;; Disable wrapping lines in some modes so that full-screen dirvish looks good
(add-hook 'dirvish-directory-view-mode-hook #'im-disable-line-wrapping)
(add-hook 'dired-mode-hook #'dired-omit-mode)
(add-hook 'dired-mode-hook #'im-disable-line-wrapping)
(add-hook 'dired-mode-hook #'im-disable-tab-line))
(defun im-dired (dir) "Start dirvish in selected DIR." (interactive "DOpen Dirvish in: ") (dired dir))
(defun im-dired-dwim () "Start dirvish in `default-directory'." (interactive) (dired default-directory))
(defun im-disable-line-wrapping ()
(let ((inhibit-message t))
(visual-line-mode -1)
(toggle-truncate-lines +1)))
(defun im-disable-tab-line () (tab-line-mode -1))
(defun im-dired-find-file-ace-window () "In Dired, visit this file or directory in another window." (interactive) (dired--find-file (if (one-window-p) #'find-file-other-window #'im-find-file-ace-window) (dired-get-file-for-visit)))
im-dired-rsync
Dired copies files sync. This function uses rsync to copy stuff async, especially useful while copying stuff over tramp/ssh.
(defun im-dired-rsync (dest)
"Copy selected files with rsync to DEST.
This works over TRAMP (but only ssh and remote to local is
supported).
NOTE: Use \"rsync --version\" > 3 or something like that."
(interactive
(let ((target (dired-dwim-target-directory)))
(list
(expand-file-name
(read-file-name
"Copy selected files to: "
(if (s-contains? "ssh:" target) "~/" target))))))
(setq dest (expand-file-name dest))
(let* ((files (dired-get-marked-files nil current-prefix-arg))
(ssh-regexp "/ssh:\\([a-zA-Z0-9_-\\.@]+\\):")
(resolve-dir (lambda (it) (if (f-dir? it) it (f-dirname it))))
(fix-remote-files
(lambda (it)
(if-let* ((host (nth 1 (s-match ssh-regexp it))))
(concat host ":" (string-trim-left it ssh-regexp))
(shell-quote-argument it))))
;; If `default-directory' points to ssh'ed directory, that
;; may cause some issues. This is just to keep things simple
;; and error-free.
(default-directory
(funcall resolve-dir (if (s-matches? ssh-regexp dest)
(car files)
dest))))
(im-shell-command
:command "rsync"
:eat t
:switch t
:args
`("-s" "--archive" "--recursive" "--verbose" "--human-readable" "--partial" "--xattrs" "--info=progress1"
,@(when (--any? (s-matches? ssh-regexp it) files)
'("-e" "ssh"))
,@(mapcar fix-remote-files files)
,@(when (s-matches? ssh-regexp dest)
'("-e" "ssh"))
,(funcall fix-remote-files dest))
:on-finish
(lambda (&rest _)
(message ">> Files copied successfully."))
:on-fail
(lambda (&rest _)
(message "!! Error while copying files.")))))
(evil-set-initial-state 'image-mode 'normal) (evil-define-key 'normal image-mode-map ;; "q" #'evil-delete-buffer "+" #'image-increase-size "-" #'image-decrease-size "H" #'image-scroll-right "L" #'image-scroll-left "J" #'image-scroll-up "K" #'image-scroll-down "gg" #'image-scroll-down "G" #'image-scroll-up "h" (λ-interactive (image-scroll-right 5)) "l" (λ-interactive (image-scroll-left 5)) "j" (λ-interactive (image-scroll-up 5)) "k" (λ-interactive (image-scroll-down 5)) "r" #'image-rotate "_" #'image-flip-horizontally "\\" #'image-flip-vertically)
calendar & diary & appt
Some tips:
.go to today0,$beginning/end of the week(,)beginning/end of the month{,}prev/next month[,]prev/next yeargsshow sunrise/sunset time of date under cursorgdgo to a date with a wizard.ato jump org-agenda for that day.dto show diary entries only. (agenda already shows these)iito insert entry for that date.iw/im/iy/ia/ibto insert weekly/monthly/anneversary/block (block being between the marked day and selected day) entry.- Set a mark by hitting
v, then go to another date and hitM-=which will show day count between those two dates.
(use-package im-holidays
:after calendar
:ensure `(:repo ,im-packages-path :files ("im-holidays.el"))
:config
;; Discard other holiday definitions and use these only
(setq calendar-holidays (append im-holidays-dutch-holidays im-holidays-turkish-holidays)))
(use-package diary-lib
:ensure nil
:defer t
:config
(setq diary-comment-start "{ ")
(setq diary-comment-end " }")
(add-hook 'diary-mode-hook #'outli-mode))
(use-package calendar
:ensure nil
:defer t
;; Enable including other diary entries using the #include "..." syntax
;; I use this to separate my work and normal diary
:hook ((diary-list-entries . diary-include-other-diary-files)
(diary-list-entries . diary-sort-entries)
;; Show sunrise/sunset when calendar opens
;; (Also use `gs' to manually show on current date)
(calendar-today-visible . calendar-sunrise-sunset)
(calendar-today-visible . calendar-mark-today))
:general
(im-leader "ec" #'calendar)
:commands calendar
:init
(evil-define-key 'normal diary-fancy-display-mode-map "q" #'evil-delete-buffer)
:config
(evil-collection-calendar-setup)
(defun im-setup-calendar-mode ()
(setq header-line-format
(substitute-command-keys
" \\[calendar-cursor-holidays] Holidays at point (or \\[calendar-list-holidays] to list holidays in buffer) │ \\[calendar-goto-today] Go to today │ \\[im-calendar-jump-org-agenda] Agenda at point │ \\[calendar-backward-month] Backward month │ \\[calendar-forward-month] Forward month ")))
(add-hook 'calendar-mode-hook #'im-setup-calendar-mode)
(calendar-set-date-style 'european)
;; Mark holidays by default
;; u → unmark them
;; x → mark them again
;; r → show holiday for current date
(setq calendar-mark-holidays-flag t)
;; Start the week from Monday
(setq calendar-week-start-day 1)
;; This looks cooler but breaks the alingment slightly:
;; (copy-face font-lock-constant-face 'calendar-iso-week-face)
;; (set-face-attribute 'calendar-iso-week-face nil :height 0.7 :line-height 1)
(setq
calendar-intermonth-text
'(propertize
(format "%2d "
(car
(calendar-iso-from-absolute
(calendar-absolute-from-gregorian (list month day year)))))
'font-lock-face 'font-lock-function-name-face))
;; lng and lat for my location, to get sunrise/sunset times on my
;; calendar (press `gs'). `calendar-sunrise-sunset' shows the
;; sunrise/sunset times for time under the cursor which means you
;; can look up sunrise/sunset times of past/future times.
;; See down below for im-adaptive-theme, it automatically sets
;; these variables. I keep this as fallback here:
(setq calendar-latitude im-amsterdam-lat)
(setq calendar-longitude im-amsterdam-long)
;; Use 24-hour format to display times
(setq calendar-time-display-form
'(24-hours ":" minutes (if time-zone " (") time-zone (if time-zone ")")))
(setq diary-display-function #'diary-fancy-display)
(setq diary-number-of-entries 7)
(general-def :keymaps 'calendar-mode-map :states '(normal)
"t" #'calendar-goto-today
"ii" #'diary-insert-entry
"id" #'diary-insert-entry
"iw" #'diary-insert-weekly-entry
"im" #'diary-insert-monthly-entry
"iy" #'diary-insert-yearly-entry
"ia" #'diary-insert-anniversary-entry
"ib" #'diary-insert-block-entry
"C-k" nil
"C-j" nil
"{" nil
"}" nil
"M-{" nil
"M-}" nil
"H" #'calendar-backward-month
"L" #'calendar-forward-month
;; Show agenda for selected date
"RET" #'im-calendar-jump-org-agenda)
;; Show suntimes automatically when cursor moves
(define-advice calendar-forward-day (:after (&rest _) show-suntimes)
(let ((message-log-max nil))
(calendar-sunrise-sunset)))
;; Show calendar at the bottom
(im-shackle-window "Calendar" 0.4))
(defun im-calendar-jump-org-agenda ()
"Open the currently selected calendar date on `org-agenda'."
(interactive nil calendar-mode)
(let ((org-agenda-window-setup 'other-window)
(org-agenda-span 1))
(org-calendar-goto-agenda)))
Automatically syncing with remote calendars
I'm experimenting with using default Emacs calendar as my work
calendar. To do so, I import the remote calendar into my diary
using the following function. Set im-calendar-remote-ics-file to
the url of ICS file beforehand.
You also need to import work diary file in your main diary file, like this:
#include "~/.emacs.d/.cache/work-diary"
(defconst im-work-diary-path (expand-file-name "~/.emacs.d/.cache/work-diary"))
(defun im-update-calendar ()
"Sync Emacs calendar/diary with my remote calendar."
(interactive)
(message ">> Updating the calender...")
(url-retrieve
im-calendar-remote-ics-file
(lambda (_status)
(delete-region (point-min) url-http-end-of-headers)
;; Following is required because some diary entries may contain
;; Turkish characters and url-retrieve does not set
;; buffer-multibyte to t automatically
(set-buffer-multibyte t)
;; Clean the work-diary first, so that items do not get
;; duplicated
(with-current-buffer (find-file-noselect im-work-diary-path)
(delete-region (point-min) (point-max)))
(icalendar-import-buffer im-work-diary-path t)
(message ">> Updating the calendar... DONE"))))
To sync it automatically:
Disabled it for now as our public calendar links are disabled. (run-with-timer 60 (* 30 60) #'im-update-calendar)
appt.el: notifications for org-agenda and diary items
appt.el shows notifications for upcoming events in your diary and
in your org-agenda.
- By default appt shows only the entries found in diary and the included files in
diary-file - If you want to add an appt but do not want to edit
diary-file, you can useappt-addwhich adds it to theappt-time-msg-list. - You can use
appt-deleteto remove appts from the list. appt-time-msg-listupdated when- this package is initialized, aka
(require 'appt) - at 00:01
- manually by
appt-check - manually by
org-agenda-to-appt - whenever
diary-file(and the files it includes) is edited - whenever one of
org-agenda-filesis edited. (see below)
appt-message-warning-time is the minutes before appt.el starts showing warnings and it shows warnings every appt-display-interval minutes until the event starts.
(use-package appt
:defer 30
:ensure nil
:config
;; Use my notification function for appt notifications
(setq appt-disp-window-function #'im-appt-notify)
(setq appt-message-warning-time 10)
(setq appt-display-interval 4)
(with-eval-after-load 'im-notif
(add-to-list 'im-notif-dnd-whitelist-labels "appt"))
;; Automatically update `appt-time-msg-list' after editing an
;; org-agenda file
(add-hook 'org-mode-hook #'im-org-agenda-to-appt-on-save)
(save-window-excursion
(appt-activate 1)))
(defun im-org-agenda-to-appt-on-save ()
(add-hook
'after-save-hook
(lambda ()
(when (-contains? (org-agenda-files 'unrestricted) (buffer-file-name))
(async-start
`(lambda ()
,(async-inject-variables "\\(org-agenda-files\\|load-path\\|org-todo-keywords\\)")
(require 'cl-lib)
(setq org-agenda-files
(cl-remove-if (lambda (it) (string-match-p ".*ssh:.*" it)) org-agenda-files))
(require 'org)
(require 'org-agenda)
(org-agenda-to-appt 'refresh)
;; Clear the text properties. Causes issues while passing
;; down the result
(mapcar
(lambda (it) (list (nth 0 it) (substring-no-properties (nth 1 it))
(nth 2 it)))
appt-time-msg-list))
(lambda (result)
(require 'appt)
(setq appt-time-msg-list result)
(message ">> appt updated")
(appt-check)))))
nil t))
(defun im-appt-notify (min-to-appt new-time appt-msg)
(if (listp min-to-appt)
(--each (-zip-pair min-to-appt appt-msg)
(im-appt-notify--helper (car it) new-time (cdr it)))
(im-appt-notify--helper min-to-appt new-time appt-msg)))
(defun im-appt-notify--helper (min-to-appt _new-time appt-msg)
(setq min-to-appt (string-to-number min-to-appt))
(message ">> appt :: %s, remaining %s mins" appt-msg min-to-appt)
(let ((important? (cond
((s-contains? "meeting" (s-downcase appt-msg)) t)
(t nil))))
(im-notif
:message (format "Reminder, %s mins left" min-to-appt)
:title (im-org-header-line-to-title appt-msg)
:severity (if important? 'high 'normal)
:duration (if important? nil 10)
:labels '("appt"))))
(defun im-diary-kill-entry-as-bullet-task (&optional include-description)
"Copy current diary entry as a bullet.org task."
(interactive "P")
(-as->
(thing-at-point 'line) it
(s-match "\\([0-9]+:[0-9]+-[0-9]+:[0-9]+\\) \\(.*\\)" it)
(format
"** TODO [#A] %s :work:\nSCHEDULED: <%s %s>\n%s"
(if-let* ((link (ignore-errors (im-diary-entry-meeting-link))))
(format "[[%s][%s]]" link (nth 2 it))
(nth 2 it))
(im-today)
(nth 1 it)
(if include-description (im-diary-entry-description) ""))
(im-kill it)))
(defun im-diary-entry-meeting-link ()
"Get meeting/zoom link of current diary entry."
(let ((entry-details (im-diary-entry-description)))
(with-temp-buffer
(insert entry-details)
(goto-char (point-min))
(re-search-forward "https://.*zoom")
(thing-at-point 'url))))
(defun im-diary-entry-description ()
"Get details/description of current diary entry."
(interactive)
(save-window-excursion
(push-button)
(let ((beg (save-excursion
(re-search-backward "^[^ \n].+")
(forward-line)
(point)))
(end (save-excursion
(re-search-forward "^[^ \n].+")
(beginning-of-line)
(point))))
(im-kill (buffer-substring beg end)))))
(setq tramp-default-method "ssh") (setq tramp-verbose 2)
^ Only show errors and warnings
(setq vc-handled-backends '(Git))
^ Only try to handle git, this speeds up things a little bit
(defconst im-reddit-comment-url-regexp ".*reddit.com/r/[a-zA-Z0-9_-]+/comments/[a-zA-Z0-9_-]+/\\([a-zA-Z0-9_-]+/?\\)\\(/[a-zA-Z0-9_-]+/?\\)?\\(&.*\\)?$")
(defun im-purified-url-handler (fn)
"Clear the redundant stuff from urls.
Some links are prefixed with google redirection url, this
removes (and other similar stuff) so that the url handler works
properly."
(lambda (url &rest _)
(funcall
fn
(s-chop-prefix "https://www.google.com/url?q=" url))))
(setq browse-url-secondary-browser-function #'browse-url-firefox)
(setq browse-url-handlers
`((".*jtracker.trendyol.*/browse/.*" . ,(im-purified-url-handler #'im-jira-view-ticket))
(".*slack.com/archives/.*" . ,(im-purified-url-handler #'im-slack-open-link))
(,im-reddit-comment-url-regexp . ,(im-purified-url-handler #'(lambda (url)
(let* ((post (replace-regexp-in-string "^https?://\\(www\\.\\)?reddit\\.com/" "" url)))
(reddigg-view-comments post)))))
(".*news.ycombinator.com/item\\?id=.*" . ,(im-purified-url-handler #'hnreader-comment))
(".*\\(stackoverflow.com\\|stackexchange.com\\).*" . ,(im-purified-url-handler #'im-open-stackexchange-link))
(".*\\(youtube.com/watch.*\\|youtu.be/.*\\)" . ,(im-purified-url-handler #'empv-play-or-enqueue))
(".*\\.mp3" . ,(im-purified-url-handler #'empv--play-or-enqueue))
(".*github.com/.*issues/.*" . ,(im-purified-url-handler #'lab-github-issue-view))
("https://github.com/\\([^/]+\\)/\\([^/]+\\)/blob/\\([^/]+\\)/\\(.+\\)" . ,(im-purified-url-handler #'lab-github-view-repo-file))
(".*github.com/[A-Za-z0-9\\. _-]+/[A-Za-z0-9\\. _-]+\\(\\?tab=readme-ov-file.*\\)?$" . ,(im-purified-url-handler #'lab-github-view-repo-readme))
(".*zoom.us/j/.*" . ,(im-purified-url-handler #'im-open-zoom-meeting-dwim))
(".*trendyol.*/merge_requests/.*" . ,(im-purified-url-handler #'lab-open-merge-request-diff))
(".*\\(trendyol\\|gitlab\\|slack\\|docs.google\\).*" . browse-url-firefox)
("." . (lambda (link &rest _) (im-eww link)))))
(use-package eww
:commands eww
:ensure nil
:defer t
:hook (eww-mode . tab-line-mode)
:general
(im-leader-v
"ew" #'im-eww-plain
"eW" #'im-eww)
(:keymaps 'embark-file-map "/" #'eww-open-file)
(:keymaps 'shr-map
"z" nil ;; So that regular evil bindings work
"v" nil ;; So that regular evil bindings work
"O" nil ;; So that my bindings work
"+" #'shr-zoom-image)
(:keymaps 'eww-mode-map :states 'normal
"Y" #'eww-copy-page-url
;; There is also u binding in shr-map but this is easier to remember
"c" #'shr-maybe-probe-and-copy-url
"d" #'im-eww-save-image
"O" (λ-interactive (eww (read-string "URL: " (eww-current-url))))
"t" #'im-eww
"T" (λ-interactive (funcall-interactively #'im-eww (read-string "URL: " (eww-current-url))))
"f" #'im-eww-avy-follow
"F" (λ-interactive (im-eww-avy-follow :new-session))
"r" #'eww-reload
"R" #'eww-readable
"<f2>" (λ-interactive (browse-url (eww-current-url)))
"<f3>" (λ-interactive (browse-url-default-browser (eww-current-url)))
"&" (λ-interactive (funcall browse-url-secondary-browser-function (eww-current-url))))
:custom
;; Gifs make my computer suffer, so I just disable them
;; (setq shr-image-animate nil)
(shr-max-image-proportion 0.6)
(shr-discard-aria-hidden t)
(shr-use-xwidgets-for-media t)
;; Use browse-url for each link opening. This way my
;; `browse-url-handlers' take precedence over eww.
(eww-use-browse-url ".*")
(eww-search-prefix (format "https://www.kagi.com/html/search?token=%s&q=" im-kagi-token))
(eww-auto-rename-buffer
(lambda () (format "*eww: %s*" (or (plist-get eww-data :title) "..."))))
:config
(evil-collection-eww-setup))
(defun im-eww (url) "`eww' wrapper. Like `eww' but open URL in a new eww buffer instead of reusing the same one if called interactively. If inside an eww buffer, then utilize `eww-browse-url' instead. See `eww-use-browse-url' for why." (interactive (list (im-web-autosuggest :history 'eww-prompt-history :initial (im-region-or "")))) (im-eww-plain url))
(defun im-eww-plain (url) (interactive (list (read-string "Search: " nil 'eww-prompt-history))) (cond ;; If called interactively, just use a new buffer ((called-interactively-p 'interactive) (eww url t)) ;; If we are inside an eww buffer and this function is called, simply navigate to the URL. ((equal major-mode 'eww-mode) (eww-browse-url url)) ;; Default to interactive behavior (t (eww url t))))
(defun im-eww-avy-follow (&optional follow-type)
(interactive)
(let ((wend (window-end))
(urls '()))
(save-excursion
(goto-char (window-start))
(while-let ((match (text-property-search-forward 'shr-tab-stop nil nil t))
((< (point) wend)))
(goto-char (prop-match-beginning match))
;; (push (get-text-property (point) 'shr-url) urls)
(push (point) urls)))
(avy-process (nreverse urls))
(pcase follow-type
(:new-session
(eww (get-text-property (point) 'shr-url) :new-session))
(:external (eww-browse-with-external-browser t))
(_ (eww-follow-link)))))
(defun im-eww-save-image ()
"Save the image at point."
(interactive)
(let ((image (get-text-property (point) 'display))
;; Disable all save hooks as they might damage the file
(before-save-hook '())
(after-save-hook '()))
(unless (and image
(eq (car image) 'image))
(user-error "No images at point!"))
(with-temp-buffer
(setq buffer-file-name
(read-file-name "Save to: " nil default-directory nil))
(insert (plist-get (cdr image) :data))
(save-buffer))))
(defvar im-web-autosuggest-history nil)
(cl-defun im-web-autosuggest
(&key
(history 'im-web-autosuggest-history)
(initial ""))
"Autosuggest as you type for web searches.
This uses StartPage's autosuggestion endpoint. It is not very good but
better than nothing.
HISTORY is by default `im-web-autosuggest-history', it should be a
symbol. Access history as usual, C-r or \\[consult-history].
INITIAL is the initial string shown in the prompt. By default it's
empty string."
(interactive)
(->>
(consult--read
(empv--consult-async-generator
(lambda (action on-result)
(when (not (string-empty-p (string-trim action)))
(im-request
;; "https://ac.ecosia.org/autocomplete"
"https://kagi.com/api/autosuggest"
:type "list"
:q action
:-on-success on-result
:-on-error (lambda (_x) (list action)))))
#'-flatten)
:prompt "Search for: "
:category 'url
:lookup (lambda (selected _candidates _cand &rest _) selected)
:initial initial
:sort nil
:history (or history 'im-web-autosuggest-history)
:async-wrap #'empv--consult-async-wrapper
:require-match nil)))
Fixing page layouts
Some pages I frequently visit needs some fixing to display the content better. The following piece does exactly that. For each given url, it runs the specified function in the eww buffer. This way I get a better view.
(defvar im-eww-page-fixers
'(("^https://\\(www.\\)?startpage.com/sp/search" . im-eww--fix-startpage)
("^https://\\(www.\\)?ecosia.org/search" . im-eww--fix-ecosia)
("^https://\\(www.\\)?kagi.com\\(:443\\)?/html" . im-eww--fix-kagi)
("^https://\\(www.\\)?eksisozluk.com" . im-eww--fix-eksisozluk)))
(with-eval-after-load 'eww
(add-hook 'eww-after-render-hook #'im-eww-after-render)
;; The changes disappear after doing eww-readable, this fixes that.
(define-advice eww-readable (:after (&rest _) run-after-render-hook)
(im-eww-after-render)))
(defun im-eww-after-render ()
(--each (im-assoc-regexp (eww-current-url) im-eww-page-fixers)
(let ((fn (cdr it)))
(let ((inhibit-read-only t))
(goto-char (point-min))
(condition-case err
(progn
(message "Cleaning page with: `%s'" fn)
(funcall fn))
(error
(message "im-eww-after-render :: Error while `%s': %s" fn err))))
(goto-char (point-min)))))
(defun im-eww--fix-startpage ()
(while (re-search-forward "Visit in Anonymous View" nil t)
(delete-region (line-beginning-position) (line-end-position))
(insert ""))
(goto-char (point-min))
(let ((pos (point)))
(while (and (not (eobp)) (not (looking-at "^Web results")))
(forward-char))
(delete-region pos (point)))
(re-search-forward "^Startpage isn’t")
(forward-line -1)
(delete-region (point) (point-max))
(page-break-lines-mode))
(defun im-eww--fix-ecosia ()
(when (re-search-forward "^Search" nil t)
(delete-region (point-min) (line-end-position)))
(while (re-search-forward "^Submit" nil t)
(delete-region (line-beginning-position) (line-end-position)))
(page-break-lines-mode))
(defun im-eww--fix-kagi ()
(flush-lines "^More results")
(replace-string "›" "»" nil (point-min) (point-max))
(goto-char (point-min))
(when (re-search-forward "^[0-9]+ relevant results" nil t)
(delete-region (point-min) (line-beginning-position)))
(when (re-search-forward "^Advanced Search" nil t)
(delete-region (line-beginning-position) (point-max))))
(defun im-eww--fix-eksisozluk ()
(save-excursion
(goto-char (point-min))
(while-let ((match (text-property-search-forward
'display nil (lambda (_ value) (imagep value)))))
(delete-region (1- (prop-match-beginning match)) (prop-match-end match))
(insert "\n")))
(page-break-lines-mode))
(use-package language-detection :defer t)
(with-eval-after-load 'shr
(setq shr-external-rendering-functions
'((pre . eww-tag-pre)))
(defun eww-tag-pre (dom)
(let ((shr-folding-mode 'none)
(shr-current-font 'default))
(shr-ensure-newline)
(insert (eww-fontify-pre dom))
(shr-ensure-newline)))
(defun eww-fontify-pre (dom)
(with-temp-buffer
(shr-generic dom)
(let ((mode (eww-buffer-auto-detect-mode)))
(when mode
(eww-fontify-buffer mode)))
(buffer-string)))
(defun eww-fontify-buffer (mode)
(delay-mode-hooks (funcall mode))
(font-lock-default-function mode)
(font-lock-default-fontify-region (point-min)
(point-max)
nil))
(defun eww-buffer-auto-detect-mode ()
(let* ((language (symbol-name
(language-detection-string
(buffer-substring-no-properties (point-min) (point-max)))))
(modes (list (concat language "-ts-mode")
(concat language "-mode")
()))
(alts '(("cpp" "c++-mode")
("emacslisp" "emacs-lisp-mode")
("fsharp" "fsharp-mode")
("shell" "shell-script-mode")))
(mode (cl-loop for name in `(,@modes ,@(cdr (assoc language alts)))
when name
for mode = (intern name)
when (fboundp mode)
return mode)))
mode)))
(use-package shell :ensure nil :defer t :init (evil-define-key 'insert 'shell-mode-map (kbd "C-l") #'comint-clear-buffer))
vc — version control
I used to use but because of it's slowness (mostly fault of macOs and my work mandatory security apps), I ditched it. vc is pretty nice most of the time.
I've also implemented a status/commit workflow, similar to magit's but it's very fast and much more streamlined for my usage. See `im-git-status' and `im-git-commit'.
(use-package vc
:ensure nil
:defer t
:general
(:keymaps 'vc-dir-mode-map :states 'normal
"r" #'vc-dir-refresh)
(:keymaps 'vc-git-log-view-mode-map :states 'normal
"<backtab>" #'im-vc-toggle-all-log-view-entries)
:config
(im-when-on :darwin
(setq vc-git-program "/opt/homebrew/bin/git"))
(setq vc-log-short-style '(directory file)))
(with-eval-after-load 'vc-git (evil-collection-vc-git-setup)) (with-eval-after-load 'log-view (evil-collection-log-view-setup)) (with-eval-after-load 'vc-dir (evil-collection-vc-dir-setup)) (with-eval-after-load 'vc-annotate (evil-collection-vc-annotate-setup))
(defun im-vc-toggle-all-log-view-entries ()
"Toggle the visibility of all log view entries in the current buffer."
(interactive nil vc-git-log-view-mode)
(message ">> Toggling all entries. This may take a while...")
(save-excursion
(goto-char (point-max))
(while (not (equal (point) (point-min)))
(ignore-errors (log-view-toggle-entry-display))
(forward-line -1))
(ignore-errors (log-view-toggle-entry-display)))
(message ">> Toggling all entries. This may take a while...Done"))
git-timemachine
- Toggle with
git-timemachine(SPC gt). - When in timemachine mode,
- use
gt<SOMETHING>to do timemachine specific operations. Some useful ones are: - t ->
git-timemachine-show-revision-fuzzy, selects a revision through completing-read interface. - y/Y -> Yank (abbreviated) revision hash.
C-j/kto go to prev/next revision of the file.
(use-package git-timemachine :defer t :commands (git-timemachine-toggle) :config (evil-collection-git-timemachine-setup))
(use-package diff
:defer t
:ensure nil
:config
;; Normally diff-mode uses reliable syntax highlighting by detecting
;; the full file with the support of vc backend but if there is no
;; context, then it does not apply the font lock. This option makes
;; it use reliable highlighting if available, if not then it uses
;; hunk based dumb highlighting without the context.
(setq diff-font-lock-syntax 'hunk-also)
(setq diff-font-lock-prettify t)
(setq diff-switches '("-u" "--color=never"))
(general-def
:keymaps 'diff-mode-map
:states '(normal motion)
;; This is for preventing strange jumps happening when hunks are
;; folded. evil-{next,previous}-visual-line causes it.
"j" #'evil-next-line
"k" #'evil-previous-line
"[[" #'diff-file-prev
"]]" #'diff-file-next
"gj" #'diff-hunk-next
"gk" #'diff-hunk-prev
"A" 'diff-add-change-log-entries-other-window
"-" #'diff-split-hunk
;; "u" #'evil-collection-diff-toggle-context-unified
;; "o" #'im-vc-diff-open-file-at-revision-dwim
"o" (λ-interactive (diff-goto-source :goto-old-file))
"RET" #'diff-goto-source
"D" #'diff-file-kill
"d" #'diff-hunk-kill
"TAB" #'outline-cycle
"S-TAB" #'outline-cycle-buffer
"1" (λ-interactive (outline-hide-sublevels 1))
"2" (λ-interactive (outline-hide-sublevels 2))
"3" #'outline-show-all)
(add-hook 'diff-mode-hook #'outline-minor-mode)
(defun im-diff-mode-setup ()
(set-face-attribute
'diff-file-header nil
:weight 'bold
:height 1.5
;; :background "unspecified"
;; :box nil
:underline nil)
(set-face-attribute
'diff-header nil
;; :background "unspecified"
:underline nil)
(set-face-attribute
'diff-hunk-header nil
:height 1.1
:underline t))
(add-hook 'diff-mode-hook #'im-diff-mode-setup)
(define-advice diff-file-next (:after (&rest _) go-to-next-beginning)
(forward-line 2)
(outline-show-entry)))
(use-package ediff
:ensure nil
:defer t
:custom
(ediff-keep-variants nil)
(ediff-make-buffers-readonly-at-startup nil)
(ediff-show-clashes-only t)
(ediff-split-window-function 'split-window-horizontally)
(ediff-window-setup-function 'ediff-setup-windows-plain)
:init
(with-eval-after-load 'ediff
(evil-collection-ediff-setup)))
diff-hl – git gutter alternative
Highlights changed lines in git. You can also stage hunks directly in the buffer.
I used to use `git-gutter' but it adds lots of advices and clashes with my configuration. diff-hl is a better maintained alternative and seemingly faster.
Doom Emacs customizations for diff-hl: https://github.com/doomemacs/doomemacs/blob/master/modules/ui/vc-gutter/config.el
Some observations on Doom Emacs configurations:
`evil-insert-state-exit-hook' causes performance issues, macros gets slower as the get in/out to insert state quickly.
(use-package diff-hl
:ensure (:host github :repo "dgutov/diff-hl")
:hook
((prog-mode . diff-hl-mode))
:custom
(diff-hl-disable-on-remote t)
;; Performance optiomization for diffs.
(vc-git-diff-switches '("--histogram"))
;; Does not work with flydiff mode in my setup for some reason
(diff-hl-update-async t)
(diff-hl-show-staged-changes nil)
(setq diff-hl-draw-borders nil)
:general
(:states 'normal :keymaps 'prog-mode-map
(kbd "[g") #'diff-hl-previous-hunk
(kbd "]g") #'diff-hl-next-hunk)
:config
;; https://github.com/doomemacs/doomemacs/issues/8554
(unless (eq system-type 'darwin)
(remove-hook 'diff-hl-mode-hook #'diff-hl-flydiff-mode))
(define-advice diff-hl-previous-hunk (:after (&rest _) reveal) (reveal-post-command) (recenter))
(define-advice diff-hl-next-hunk (:after (&rest _) reveal) (reveal-post-command) (recenter))
(define-advice diff-hl-show-hunk-previous (:after (&rest _) reveal) (reveal-post-command) (recenter))
(define-advice diff-hl-show-hunk-next (:after (&rest _) reveal) (reveal-post-command) (recenter))
(define-advice diff-hl-stage-dwim (:around (orig-fn &rest args) apply-even-if-buffer-is-narrowed)
"Original `diff-hl-stage-dwim' does not work if buffer is narrowed, this fixes it."
(save-restriction
(widen)
(apply orig-fn args))))
(use-package blame-reveal :ensure (:host github :repo "LuciusChen/blame-reveal") :defer t)
avy
avy is very similar to vim-easymotion. It simply jumps to a
visible text using a given char.
sfor jumping to beginning of a wordSfor jumping any part of the text
(use-package avy :defer t :commands (avy-goto-subword-1 avy-goto-word-1) :custom (avy-keys '(?q ?w ?e ?r ?t ?a ?s ?d ?f ?j ?k ?l ?u ?i ?o ?p)) (avy-case-fold-search nil) (avy-all-windows t) :general (:states 'normal (kbd "S") #'avy-goto-subword-1 (kbd "s") #'avy-goto-word-1))
vertico & marginalia & orderless
A nice, fast minibuffer narrowing framework. It works well with quite a lot of package.
marginalia.elbrings annotations to completing-read, ie. it adds current keybinding of a command, summary of command to M-x.miniframe.elshows all completing-read prompts in a nice mini popup frame.- Also see .
Keybindings:
SPC 0to repeat/open last vertico window you closed.
Some shortcuts you can use on any vertico window:
M-{n,p}goes {back,forward} in minibuffer history.M-{[,]}goes {previous,next} group.M-mcycles the marginalia detail level.M-abrings up embark-act menu. See .M-Aembark act all.M-wcopy the current candidate intelligently (im-vertico-save).M-qavy-like select and act.M-qavy-like select and act.M-rseparaeditM-dinsert directory (consult-dir)M-gvertico multigridM-cvertico-select (and act with M-A on selected)TABinserts the current candidate (into minibuffer).- Do &<searchterm> to search inside annotations (orderless feature). This is a bit slow.
(use-package vertico
:demand t
:hook ((elpaca-after-init-hook . vertico-mode))
:config
;; Grow and shrink the Vertico minibuffer
;; (setq vertico-resize t)
;; Show more candidates
(setq vertico-count 15)
;; Enable cycling for `vertico-next' and `vertico-previous'.
(setq vertico-cycle t)
;; Hide commands in M-x which do not work in the current mode.
(setq read-extended-command-predicate #'command-completion-default-include-p)
;; Add prompt indicator to `completing-read-multiple'.
(defun crm-indicator (args)
(cons (concat "[CRM] " (car args)) (cdr args)))
(advice-add #'completing-read-multiple :filter-args #'crm-indicator)
;; Do not allow the cursor in the minibuffer prompt
(setq minibuffer-prompt-properties '(read-only t cursor-intangible t face minibuffer-prompt))
(add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)
;; Enable recursive minibuffers
(setq enable-recursive-minibuffers t)
;; Bindings
(define-key vertico-map (kbd "M-w") #'im-vertico-save)
(define-key vertico-map (kbd "M-[") #'vertico-previous-group)
(define-key vertico-map (kbd "M-]") #'vertico-next-group)
(define-key vertico-map (kbd "M-j") #'next-line)
(define-key vertico-map (kbd "M-k") #'previous-line)
(define-key vertico-map (kbd "C-d") #'vertico-scroll-up)
(define-key vertico-map (kbd "C-b") #'vertico-scroll-down)
(define-key minibuffer-mode-map (kbd "C-w") #'evil-window-map)
;; Use `consult-completion-in-region' which works with vertico
(setq completion-in-region-function #'consult-completion-in-region))
(defun im-vertico-save ()
"Same as `vertico-save' but removes bogus char and closes minibuffer.
Also see: https://isamert.net/2021/03/27/killing-copying-currently-selected-candidates-content-text-in-selectrum.html"
(interactive)
(let ((candidate (vertico--candidate))
(prompt (minibuffer-prompt)))
(kill-new
(cond
((s-contains? "grep" prompt) (s-join ":" (-drop 2 (s-split ":" candidate))))
;; ^ Strip `filename:line-number:` from the text
((s-matches? ".*\\(Go to line\\|Switch to\\).*" prompt) (s-chop-right 1 candidate))
;; ^ `consult-line' and `consult-buffer' has an unrecognizable char at
;; the end of every candidate, so I just strip them here
(t candidate))))
(keyboard-escape-quit))
vertico extensions
TODO: Fix loading of these extensions Load vertico extensions (add-to-list 'load-path (expand-file-name (format "%sstraight/build/vertico/extensions" straight-base-dir)))
(defun im-vertico-preview-and-suspend ()
"Preview the candidate at point if possible and then suspend the session."
(interactive)
(when (and (minibufferp) consult--preview-function)
(execute-kbd-macro (kbd consult-preview-key)))
(vertico-suspend))
(use-package vertico-suspend :ensure nil :after vertico :general (:states '(insert normal) "C-s" #'vertico-suspend) (:keymaps 'vertico-map "C-s" #'im-vertico-preview-and-suspend "C-S-s" #'vertico-suspend) :config (define-key vertico-map "\M-q" #'vertico-quick-exit))
Enable avy style candidate selection
(use-package vertico-quick :ensure nil :after vertico :config (setq vertico-quick1 "asdfgqwe") (setq vertico-quick2 "hjklui") (define-key vertico-map "\M-q" #'vertico-quick-exit))
(use-package vertico-repeat :ensure nil :after vertico :config (add-hook 'minibuffer-setup-hook #'vertico-repeat-save) (im-leader "0" #'vertico-repeat))
Enable showing vertico in a buffer instead of minibuffer
(use-package vertico-buffer :ensure nil :after vertico)
Switch between different vertico display forms for different commands
(use-package vertico-multiform
:ensure nil
:after vertico
:config
;; Toggle grid mode by hitting M-g while in vertico
(define-key vertico-map "\M-g" #'vertico-multiform-grid)
;; '(command display-type... (buffer-local-variable . value))
;; vertico README explains this very well
(let ((show-at-top '(vertico-buffer-display-action . (display-buffer-in-side-window
(side . top)
(window-height . 0.6)))))
(setq vertico-multiform-commands
`((consult-org-heading buffer ,show-at-top)
(im-imenu buffer ,show-at-top))))
(vertico-multiform-mode))
Enable displaying candidates in a grid.
(use-package vertico-grid :ensure nil :after vertico)
Other complementary packages:
(use-package orderless
:config
;; See the following: https://github.com/minad/vertico?tab=readme-ov-file#tramp-hostname-and-username-completion
(setq completion-styles '(orderless partial-completion basic))
(setq completion-category-defaults nil)
;; This is the default `orderless-affix-dispatch-alist', only
;; changed the orderless-annotation binding to @ from &. Copied
;; everything here for documentation purposes.
(setq orderless-affix-dispatch-alist
`(
;; Match anything that resembles given string,
;; like %3a → "[3³₃③3𝟑𝟛𝟥𝟯𝟹🯳]\\(?:a[̀-̄̆-̨̣̥̊̌̏̑]\\|[aªà-åāăąǎǟǡǻȁȃȧᵃḁạảấầẩẫậắằẳẵặₐⓐa𝐚𝑎𝒂𝒶𝓪𝔞𝕒𝖆𝖺𝗮𝘢𝙖𝚊]\\)"
(?% . ,#'char-fold-to-regexp)
(?! . ,#'orderless-not)
;; Search inside marginalia annotations
(?@ . ,#'orderless-annotation)
;; Given character should appear at word beginnings
(?, . ,#'orderless-initialism)
(?= . ,#'orderless-literal)
;; Line begins with
(?^ . ,#'orderless-literal-prefix)
;; Match chars in given order
(?~ . ,#'orderless-flex)))
(setq completion-category-overrides '((file (styles basic partial-completion)))))
(use-package marginalia
:config
(define-key minibuffer-local-map (kbd "M-m") #'marginalia-cycle)
(setq marginalia-censor-variables '("pass\\|auth-source-netrc-cache\\|auth-source-.*-nonce\\|token\\|key\\|password\\|cookie\\|phone-number\\|app-id\\|feed-link"))
(marginalia-mode))
project.el and project management
(project-remember-projects-under im-projects-root t) does not
work as I expect, so I add all of my projects using a custom
function that returns all of my projects' paths.
(use-package project :ensure nil :defer t :autoload (project--find-in-directory))
(defalias 'im-import-projects #'im-load-projects-list)
(defun im-load-projects-list ()
(interactive)
(make-thread
(lambda ()
(message "Loading projects....")
;; Ensure all projects are known by project.el
;; The following does not recurse as I like it to be
;; (project-remember-projects-under im-projects-root t)
(setq project--list '())
(--each (--map (project--find-in-directory it) (im-all-project-roots))
(project-remember-project it t))
(message "Loading projects...Done."))))
(add-hook 'elpaca-after-init-hook #'im-load-projects-list)
(im-leader "p" #'im-project-menu "P" #'im-project-menu-for-dir) (setq project-switch-commands #'im-project-menu)
(defvar im-project-menu--current nil)
(defun im-project-menu ()
(interactive)
(let ((default-directory (or project-current-directory-override
(project-root (project-current t)))))
(setq im-project-menu--current default-directory)
(im-project-transient)))
(defun im-project-menu-for-dir () (interactive) (setq im-project-menu--current default-directory) (im-project-transient))
(defun im-project-transient--wrapper (children)
(cl-loop
for (type . data) in children
as command = (plist-get data :command)
as command-name = (symbol-name command)
as wrapped-name = (concat command-name "--within-project")
as already-wrapped? = (s-suffix? "--within-project" command-name)
do (unless already-wrapped?
(eval `(defun ,(intern wrapped-name) ()
(interactive)
(let ((default-directory im-project-menu--current))
(call-interactively ',command)))))
collect `(,type ,@(plist-put data :command (if already-wrapped? (intern command-name) (intern wrapped-name))))))
(with-eval-after-load 'project
(require 'transient)
(transient-define-prefix im-project-transient ()
"Project Commands"
[:description (lambda () (format
"%s: %s"
(propertize "Project" 'face '(:weight bold))
(propertize (f-abbrev im-project-menu--current) 'face '(:foreground "green"))))
("s" "Switch" project-switch-project)]
[["Core"
:setup-children im-project-transient--wrapper
("<return>" "Dired" im-dired-dwim)
("e" "Eshell" im-eshell-project)
("E" "Ghostel" ghostel-project)
("!" "Run shell command" im-shell-command)
("k" "Kill buffers" project-kill-buffers)
("n" "Project notes" im-org-jump-to-project-documentation)]
["Grep & Find"
:setup-children im-project-transient--wrapper
("f" "Files" im-find-file-in)
("g" "Grep" consult-ripgrep)
("d" "Deadgrep" deadgrep)
("r" "Replace regexp in project" project-query-replace-regexp)]
["Git (VC)"
:setup-children im-project-transient--wrapper
("vd" "Status" vc-dir)
("vs" "Git status" im-git-status)
("vl" "Show stash" im-git-list-stash)
("vc" "Clone URL" lab-git-clone)
("wl" "Worktrees (list)" im-git-worktree-switch)
("wc" "Worktree (create)" im-git-worktree-add)
("wd" "Worktree (delete)" im-git-worktree-delete)]
["GitLab"
:setup-children im-project-transient--wrapper
("M" "Merge requests" lab-list-project-merge-requests)
("P" "Pipelines" lab-list-project-pipelines)
("K" "Last failed pipeline job" lab-act-on-last-failed-pipeline-job)
("ms" "Search projects" lab-search-project)
("mc" "Create MR" lab-create-merge-request)]
["Links"
:setup-children im-project-transient--wrapper
("lh" "Homepage link" git-link-homepage)
("lc" "Copy homepage link" im-git-link-homepage)
("ll" "Link branch (region)" im-git-link-on-branch)]]))
All-projects commands
(defun im-select-any-project-file () (interactive) (im-find-file-in im-projects-root))
(defun im-consult-ripgrep-all-projects () (interactive) (im-consult-ripgrep im-projects-root))
Others
(defun im--suggest-shell-name (type)
(format "$%s: %s/%s" type (im-current-project-name)
(if (buffer-file-name (current-buffer))
(buffer-name)
(symbol-name major-mode))))
(defun im-eshell (name)
(interactive (list (read-string "Buffer name: " (generate-new-buffer-name (format "*$eshell: %s*" (im-current-project-name))))))
(with-current-buffer (eshell t)
(rename-buffer name t)
(current-buffer)))
(defun im-eshell-project ()
(interactive)
(let* ((default-directory (project-root (project-current t)))
(bname (format "*$eshell: %s*" (im-current-project-name))))
(if-let* ((existing (get-buffer bname)))
(switch-to-buffer existing)
(im-eshell bname))))
(defun im-term ()
"Select a term and open."
(interactive)
(empv--select-action "Term: "
"eshell project" → (im-eshell-project)
"eshell new" → (call-interactively #'im-eshell)
"ghostel project" → (ghostel-project)
"ghostel new" → (ghostel 'new)))
(im-leader "2" #'im-term "wf" (λ-interactive (im-split-window-below) (ghostel 'new)) "wg" (λ-interactive (im-split-window-right) (ghostel 'new)))
(use-package xref :ensure nil :defer t :config (define-advice xref-find-definitions (:after (&rest _) reveal) (reveal-post-command)))
consult
Some key points:
SPC RETbrings upconsult-buffer.- Typing
SPC {p,f,b,m}narrows the list into {project files, files, buffers, bookmarks}. - Also see:
M-ybrings upconsult-yank, where you can select from clipboard history and paste.C-fdoes fuzzy search on current file lines.- Do
M-,on a candidate to preview it. - Also don't forget to utilize
M-a(embark-act) in consult windows. - Use
M-n(future-history) to insert current symbol after running a consult command. Normally you would useM-{p,n}to cycle between history items but when you open minibuffer, typingM-ndirectly tries to guess what the user input would be.
(defun im-imenu ()
(interactive)
(condition-case nil
(consult-imenu)
(error (consult-outline))))
(use-package consult
:general
(im-leader
"oj" #'consult-org-agenda
"RET" #'consult-buffer)
(:keymaps 'minibuffer-mode-map
"C-r" #'consult-history)
(:states 'normal :keymaps 'org-mode-map
"M-i" #'consult-org-heading
"M-I" #'consult-org-agenda)
(:states 'insert :keymaps 'eshell-mode-map
"C-r" #'consult-history)
(:states 'normal
"M-i" 'im-imenu)
(:keymaps 'consult-narrow-map
;; Shows all narrowing possibilities when you hit "?"
;; If no narrowing is available, simply inserts "?"
"?" #'consult-narrow-help)
:config
(evil-collection-consult-setup)
(global-set-key (kbd "<f13>") #'consult-buffer)
(advice-add #'register-preview :override #'consult-register-window)
(setq consult-preview-key "M-;")
;; ^ When you do M-, on a candidate, it previews it
;; Also follow symlinks while grepping
(setq consult-ripgrep-args (concat consult-ripgrep-args " --follow"))
;; xref consult integration
(setq xref-show-xrefs-function #'consult-xref)
(setq xref-show-definitions-function #'consult-xref)
;; Hide some buffers from consult-buffer window. If you want to jump
;; on one of these buffers, start with a space after opening
;; `consult-buffer'.
(add-to-list
'consult-buffer-filter
"\\`\\*\\(Help\\|Backtrace\\|Messages\\|Buffer List\\|Flycheck.*\\|scratch.*\\)\\'")
(im-insert-at! consult-buffer-sources 1 'consult-source-bookmark)
(with-eval-after-load 'savehist
(add-to-list 'savehist-additional-variables 'consult--buffer-history))
;; This also supports previews. Use the `consult-preview-key'.
(defalias 'im-switch-theme #'consult-theme))
Project & file management
Some functionality for project management. I do some fine-tuning for find and ripgrep commands that consult uses.
(with-eval-after-load 'consult (setq consult-ripgrep-args "rg --hidden --null --line-buffered --color=never --max-columns=1000 --path-separator /\ --smart-case --no-heading --with-filename --line-number --search-zip") (setq consult-project-root-function #'im-current-project-root))
(transient-define-prefix im-find-and-grep-transient ()
"Finding and grepping."
[["Find"
("fd" "Find file in current directory" im-find-file-in)
("fD" "Find file in given directory" im-find-file-in-given-dir)
("F" "Files (all projects)" im-select-any-project-file)]
["Marks"
("M" "Marks (current dir)" im-marks)
("m" "Marks my docs" im-marks-my-docs)]
["Grep"
("G" "Ripgrep (all projects)" im-consult-ripgrep-all-projects)
("d" "Deadgrep" deadgrep)
("D" "Deadgrep" im-deadgrep-directory)
("gb" "Grep selected branch" im-consult-git-grep-branch)
("gd" "Ripgrep in current directory" im-consult-ripgrep-current-directory)
("gD" "Ripgrep in given directory" im-consult-ripgrep-in-given-directory)
("gg" "Git grep" consult-git-grep)]])
(im-leader "c" #'im-find-and-grep-transient)
(defun im-consult-ripgrep (&optional path) "`consult-ripgrep' in PATH (or in current project, if path is nil). `consult-ripgrep' with `consult-project-root-function' shows full path of the file in the results. I don't want that." (interactive) (consult-ripgrep (or path (im-current-project-root))))
(defun im-consult-ripgrep-current-directory () "Do ripgrep in `default-directory'." (interactive) (consult-ripgrep default-directory))
(defun im-consult-ripgrep-in-given-directory (dir) (interactive "DSelect directory: ") (consult-ripgrep dir))
(defun im-find-file-in (&optional dir)
"Find file in DIR.
`fd' is already fast enough, no need for `consult-find's async
approach."
(interactive)
(let ((default-directory (or dir default-directory)))
(im-output-select
:cmd "fd --exclude '.git' --exclude 'node_modules' --hidden ."
:prompt "Open file: "
:category 'file
:map (s-chop-prefix "./" it)
:do (find-file it))))
(defun im-find-file-in-given-dir (dir) (interactive "DSelect directory: ") (im-find-file-in dir))
(defun im-consult-git-grep-branch (branch)
"Run interactive grep inside given BRANCH and display the result at that revision."
(interactive
(list (im-output-select
:cmd "git branch -a --format='%(refname:lstrip=2)'"
:prompt (format "Select branch (current=%s): " (lab-git-current-branch))
:keep-order t)))
(pcase-let* ((default-directory (im-current-project-root))
(result (save-window-excursion
(prog1 (consult-git-grep (list branch))
(kill-buffer (current-buffer)))))
(`(,fname ,line-no) (s-split-up-to ":" (s-chop-prefix (concat branch ":") result) 2)))
(with-current-buffer (get-buffer-create (format "*%s at %s*" fname branch))
(erase-buffer)
(insert
(shell-command-to-string
(format "git show %s:%s" branch fname)))
(goto-char (point-min))
(forward-line (1- (string-to-number line-no)))
(delay-mode-hooks
(funcall (assoc-default fname auto-mode-alist 'string-match)))
(font-lock-ensure)
(switch-to-buffer (current-buffer))
(recenter))))
consult-buffer and some extensions
I use (consult-buffer) function for switching between
buffers/files/marks etc. Here I add a source for my frequently used
files. This is handy in a way that (consult-buffer) becomes my
go-to place for switching to anything.
(defvar im-consult-source-files (list :name "My files" :narrow ?f :category 'file :face 'consult-file :history 'file-name-history :state #'consult--file-state :default t :hidden nil :items #'im-my-files) "My frequently accessed files source for `consult-buffer'.")
https://github.com/minad/consult/issues/206#issuecomment-886380330
(defvar im-consult-source-dired-buffers
`(:name "Dired"
:narrow ?d
:hidden t
:category buffer
:face dired-header
:state ,#'consult--buffer-state
:items
,(lambda ()
(consult--buffer-query :mode 'dired-mode
:sort 'visibility
:as #'buffer-name)))
"Dired buffer candidate source for `consult-buffer'.")
(with-eval-after-load 'consult (im-append! consult-buffer-sources 'im-consult-source-dired-buffers))
(defun im-my-files ()
"Return list of all files I frequently use."
`(;; Org may be loaded lazily
,@(when (bound-and-true-p org-directory)
(directory-files org-directory t "^\\w+.*.org$"))
,@(directory-files im-load-path t "\\.el$")
,@(directory-files im-packages-path t "\\.el$")))
(with-eval-after-load 'consult (im-append! consult-buffer-sources 'im-consult-source-files))
I also add a source for listing all of my projects:
(defvar im-projects-history nil "History of projects. This helps persisting projects I switched to.")
(with-eval-after-load 'savehist (add-to-list 'savehist-additional-variables 'im-projects-history))
(defvar im-consult-source-projects (list :name "Projects" :narrow ?a :hidden nil :category 'file :history 'im-projects-history :action (lambda (it &rest _) (project-switch-project it)) :items (lambda () (mapcar #'car project--list))) "Projects source for `consult-buffer'.")
(with-eval-after-load 'consult (im-append! consult-buffer-sources 'im-consult-source-projects))
consult-dir: Jump to different places easily in minibuffer
Here is a nice description of the package from it's README:
> Avoid "navigating" long distances when picking a file or > directory in any Emacs command that requires one. Think of it > like the shell tools autojump, fasd or z but for Emacs.
(defvar consult-dir--my-dirs
(list
:name "My favorites"
:narrow ?m
:category 'file
:face 'consult-file
:items
(lambda ()
`("~/Downloads/"
"~/Documents/"
"~/Pictures/f3/Camera/"
"~/Music/"
"~/Music/mix/"
"~/Videos/Shows/"
"~/Videos/Movies/"
"~/Documents/notes/"
"~/Documents/notes/img/"
"~/Workspace/projects/"
"~/Workspace/temp/"
"~/Workspace/temp/git/"
"~/.emacs.d/"
"~/.local/share/"
"~/.local/bin/"
"~/.config/"
,(format "/run/media/%s/" (user-login-name))
,(format "/run/media/%s/BINGUS/" (user-login-name))))))
(use-package consult-dir
:defer
:bind (("C-x C-d" . consult-dir)
:map vertico-map
("M-d" . consult-dir))
:custom
(consult-dir-sources '(consult-dir--my-dirs
consult-dir--source-bookmark
consult-dir--source-recentf
consult-dir--source-tramp-local
consult-dir--source-default
consult-dir--source-project)))
embark
embark.el provides contextual command maps.
M-aactivates command mode. Next key should be command. DoC-hto list all commands with their keybindings.- Commands are context specific, ie. the commands is based on if currently selected item is a file, folder, buffer etc.
- It's mostly used within the minibuffer, some example functions:
M-a w(embark-save) saves the current candidate's text into kill-ring.M-a i(embark-insert) like the one above but instead of saving to the kill-ring, it directly inserts it to the buffer.M-a S(embark-collect-snapshot) creates a buffer containing all the candidates.- To apply an action to multiple items, filter the items first and then do
embark-collect-snapshot. A buffer will open and you can apply the main action or other actions on the items.
(use-package embark-consult :after (embark consult))
(use-package embark
:commands (embark embark-act-all embark-prefix-help-command)
:defer t
:custom
;; This directly shows the actions in a completing read window.
;; By default, it is set to `embark-keymap-prompter' and you need to
;; hit `C-h' to bring this menu up.
(embark-prompter #'embark-completing-read-prompter)
;; I removed embark-mixed-indicator from the list because I'm
;; using embark-completing-read-prompter by default which already
;; provides same functionality
(embark-indicators '(embark-highlight-indicator embark-isearch-highlight-indicator))
(embark-quit-after-action '((kill-buffer . nil)
(t . t)))
:init
(bind-key (kbd "M-a") #'embark-act)
(bind-key (kbd "M-A") #'embark-act-all)
;; If you want to clear selections, then do: `embark-act-all' → `embark-select'
(bind-key (kbd "M-c") #'embark-select)
;; Hitting C-h after any prefix command will open a completing-read
;; interface to select a command that prefix has. Complements
;; which-key.
(setq prefix-help-command #'embark-prefix-help-command)
(add-to-list 'vertico-multiform-categories '(embark-keybinding grid))
:config
;; Replace describe-symbol with helpful-symbol
(define-key embark-symbol-map "h" #'helpful-symbol))
(with-eval-after-load 'embark (define-key embark-region-map "c" nil) (define-key embark-region-map "cf" #'im-copy-region-as-markdown-fenced-block))
(defun im-copy-region-as-markdown-fenced-block (beg end)
"Copy the region between BEG and END wrapped in a markdown fenced code block."
(interactive "r")
(let ((text (buffer-substring-no-properties beg end))
(lang (or (and (derived-mode-p 'prog-mode)
(replace-regexp-in-string "-mode\\'" ""
(symbol-name major-mode)))
"")))
(im-kill (concat "```" lang "\n" (string-trim-right text) "\n```"))
(deactivate-mark)
(message "Copied region as markdown fenced code block.")))
sudo-file-edit action
Nice little embark action that let's you open files with sudo.
Source: https://karthinks.com/software/fifteen-ways-to-use-embark/
(defun im-sudo-find-file (file)
"Open FILE as root."
(interactive "FOpen file as root: ")
(when (file-writable-p file)
(user-error "File is user writeable, aborting sudo"))
(find-file (if (file-remote-p file)
(concat "/" (file-remote-p file 'method) ":"
(file-remote-p file 'user) "@" (file-remote-p file 'host)
"|sudo:root@"
(file-remote-p file 'host) ":" (file-remote-p file 'localname))
(concat "/sudo:root@localhost:" file))))
(with-eval-after-load 'embark (define-key embark-file-map (kbd "#") 'im-sudo-find-file))
(with-eval-after-load 'embark (define-key embark-general-map (kbd "G") #'im-google-this))
(defun im-embark-hexify/uri-encode (start end)
"Properly URI-encode the region between START and END in current buffer."
(interactive "r")
(let ((encoded (url-hexify-string (buffer-substring-no-properties start end))))
(delete-region start end)
(insert encoded)))
(with-eval-after-load 'embark (define-key embark-general-map (kbd "U") #'im-embark-hexify/uri-encode))
(with-eval-after-load 'embark (define-key embark-command-map (kbd "m") #'im-embark-bind-leader-key-command))
(defun im-embark-bind-leader-key-command (command key)
"Bind KEY to COMMAND in `im-leader' map.
KEY should not contain the leader key."
(interactive
(list
(read-command "Set key to command: ")
(read-string "Bind key (SPC<key>) (maybe use 3 4 5 6 7 8 9 etc.): ")))
(im-leader :states '(normal)
key command))
(define-advice embark-org-copy-as-markdown (:before (&rest _) load-required-libs) "Load required libs." (require 'ox-md nil t))
(with-eval-after-load 'embark (keymap-set embark-region-map "V" #'org-copy-visible))
(use-package flymake :ensure nil :custom (flymake-mode-line-lighter "") :hook ((prog-mode . flymake-mode)))
(use-package flymake-popon :ensure (:host codeberg :repo "akib/emacs-flymake-popon") :diminish flymake-popon-mode :hook (flymake-mode . flymake-popon-mode) ;; Disable showing diagnostics in eldoc (in echo area) as we already ;; show diagnostics in a posframe. (flymake-popon-mode . (lambda () (remove-hook 'eldoc-documentation-functions 'flymake-eldoc-function t))) :custom (flymake-popon-posframe-extra-arguments '(:poshandler posframe-poshandler-window-bottom-center)))
(defun im-flymake-yank-diagnostics-at-point () "Copy the flymake diagnostics at point." (interactive) (im-kill (mapconcat #'flymake-diagnostic-text (flymake-diagnostics (point)) "\n")))
(defun im-show-diagnostic-list (arg)
"Show all lsp errors or flycheck/flymake errors, depending on which is available.
When ARG is non-nil, query the whole workspace/project."
(interactive "P")
(cond
((bound-and-true-p lsp-mode)
(consult-lsp-diagnostics arg))
((bound-and-true-p flymake-mode)
(consult-flymake arg))
((bound-and-true-p flycheck-mode)
(consult-flycheck))
(t (user-error "No diagnostic provider found"))))
(general-def :states 'normal ;; Write ~n SPC~, ~w SPC~, ~e SPC~ to show notes, warnings, errors ;; only. "ge" #'im-show-diagnostic-list)
(use-package prescient :custom (prescient-history-length 1000) :config (prescient-persist-mode 1))
(use-package completion-preview :ensure nil :custom (completion-preview-minimum-symbol-length 3) (completion-preview-idle-delay 0.2) :config (setq completion-preview-sort-function #'prescient-completion-sort) (define-key completion-preview-active-mode-map (kbd "RET") #'completion-preview-insert) (define-key completion-preview-active-mode-map (kbd "<down>") #'completion-preview-next-candidate) (define-key completion-preview-active-mode-map (kbd "<up>") #'completion-preview-prev-candidate) (define-key completion-preview-active-mode-map (kbd "M-j") #'completion-preview-next-candidate) (define-key completion-preview-active-mode-map (kbd "M-k") #'completion-preview-prev-candidate) (global-completion-preview-mode +1))
- When corfu popup is open
M-SPCto insert a space to be able to filter with orderless.M-qto show an avy like quick selection keys.M-mto move completion items to mini-buffer (completing-read).
(use-package corfu
:ensure (:files (:defaults "extensions/*.el"))
:custom
(corfu-cycle t)
(corfu-auto nil)
(corfu-auto-delay 0.2)
(corfu-auto-prefix 3)
;; Enabled differently in eshell
(global-corfu-modes '((not eshell) t))
:config
(define-key corfu-map (kbd "M-j") #'corfu-next)
(define-key corfu-map (kbd "M-k") #'corfu-previous)
(define-key corfu-map (kbd "RET") #'corfu-complete)
(define-key corfu-map (kbd "<tab>") nil)
;; This is useful because sometimes I just want to collect list of
;; completions. With this I can use embark's collect functionality.
(defun corfu-move-to-minibuffer ()
(interactive)
(pcase completion-in-region--data
(`(,beg ,end ,table ,pred ,extras)
(let ((completion-extra-properties extras)
completion-cycle-threshold completion-cycling)
(consult-completion-in-region beg end table pred)))))
(keymap-set corfu-map "M-m" #'corfu-move-to-minibuffer)
(add-to-list 'corfu-continue-commands #'corfu-move-to-minibuffer)
(global-corfu-mode))
(use-package corfu-prescient :after (corfu prescient) :config (corfu-prescient-mode 1))
(use-package corfu-quick :ensure nil :after corfu :config (setq corfu-quick1 "asdfgqwe") (setq corfu-quick2 "hjklui") (define-key corfu-map "\M-q" #'corfu-quick-insert))
prescient is enough I guess? (use-package corfu-history :ensure nil :after corfu :config (corfu-history-mode) (with-eval-after-load 'savehist (add-to-list 'savehist-additional-variables 'corfu-history)))
(use-package kind-icon :after corfu :config (setq kind-icon-default-face 'corfu-default) (add-to-list 'corfu-margin-formatters #'kind-icon-margin-formatter))
(use-package corfu-popupinfo :ensure nil :after corfu :config (setq corfu-popupinfo-delay '(1.0 . 0.3)) (corfu-popupinfo-mode) ;; Toggle doc on/off while in corfu (define-key corfu-map (kbd "M-i") #'corfu-popupinfo-documentation) (define-key corfu-map (kbd "M-l") #'corfu-popupinfo-location) (define-key corfu-map (kbd "M-p") #'corfu-popupinfo-scroll-down) (define-key corfu-map (kbd "M-n") #'corfu-popupinfo-scroll-up))
(use-package cape
;; Bind dedicated completion commands
;; Alternative prefix keys: C-c p, M-p, M-+, ...
:bind (("M-o p" . completion-at-point) ;; capf
("M-o t" . complete-tag) ;; etags
("M-o d" . cape-dabbrev) ;; or dabbrev-completion
("M-o h" . cape-history)
("M-o f" . cape-file)
("M-o k" . cape-elisp-keyword)
("M-o s" . cape-elisp-symbol)
("M-o a" . cape-abbrev)
("M-o i" . cape-ispell)
("M-o l" . cape-line)
("M-o e" . cape-emoji)
("M-o w" . cape-dict)
("M-o \\" . cape-tex)
("M-o _" . cape-tex)
("M-o ^" . cape-tex)
("M-o &" . cape-sgml)
("M-o r" . cape-rfc1345))
:init
;; Add `completion-at-point-functions', used by `completion-at-point'.
;;(add-to-list 'completion-at-point-functions #'cape-history)
;;(add-to-list 'completion-at-point-functions #'cape-keyword)
;;(add-to-list 'completion-at-point-functions #'cape-tex)
;;(add-to-list 'completion-at-point-functions #'cape-sgml)
;;(add-to-list 'completion-at-point-functions #'cape-rfc1345)
;;(add-to-list 'completion-at-point-functions #'cape-abbrev)
;;(add-to-list 'completion-at-point-functions #'cape-ispell)
;;(add-to-list 'completion-at-point-functions #'cape-dict)
;;(add-to-list 'completion-at-point-functions #'cape-symbol)
(setq-default completion-at-point-functions nil)
(add-hook 'completion-at-point-functions #'cape-dabbrev t)
(add-hook 'completion-at-point-functions #'cape-line t)
(add-hook 'completion-at-point-functions #'cape-file t))
(cl-defmacro im-cape
(&key name completion extractor category
bound key annotate doc (exclusive 'no) (sort t) kind)
"Create a cape completion function with given parameters.
COMPLETION is a either a list that holds the completions or a
function that returns the completions. This can be an arbitrary
object as EXTRACTOR is used for extracting the real completions
from this object. EXTRACTOR is a function that is called with
the result of COMPLETION and real completion list is gathered
this way.
BOUND is boundaries of the thing that gets completed. May be
symbol, word, file etc.
KEY is key to bind this cape to.
ANNOTATE is a function that returns annotation for given
completion. It should return a string. The function is called
with two arguments, first being the object returned by COMPLETION
and second being current completion item.
DOC is a function that returns documentation for given
completion. Should return a string. Signature is same with
ANNOTATE.
KIND is a function that returns the kind for current completion
item. It should return a symbol like `file', `folder', `text',
`snippet', `keyword', `function', `variable', `module', `color'
etc. In turn, there is a icon displayed that is associated with
the kind symbol. Signature is same with ANNOTATE.
SORT should be nil to disable sorting."
(let ((cape-fn (intern (format "im-cape-%s" (symbol-name name)))))
`(progn
(defun ,cape-fn (&optional interactive)
(interactive (list t))
(if interactive
(cape-interactive #',cape-fn)
(pcase-let ((`(,beg . ,end) (cape--bounds ',bound))
(xs (if (functionp ,completion)
(funcall ,completion)
,completion)))
(append
(list beg end
(funcall ,extractor xs)
:exclusive ',exclusive)
(when ,annotate
(list :annotation-function (apply-partially ,annotate xs)))
(when ,kind
(list :company-kind (apply-partially ,kind xs)))
(when ,doc
(list :company-doc-buffer (lambda (x)
(with-current-buffer (get-buffer-create " *im-cape-doc*")
(erase-buffer)
(insert (funcall ,doc xs x))
(current-buffer)))))
(when ',category
(list :category ',category))))))
(when ,key
(bind-key ,key #',cape-fn)))))
(use-package eglot
:ensure nil
:general
(:states '(normal) :keymaps '(eglot-mode-map)
"ga" #'eglot-code-actions
"gi" #'eglot-find-implementation
"gt" #'eglot-find-typeDefinition
"gd" #'xref-find-definitions
"gr" #'xref-find-references)
:custom
(eglot-autoshutdown t)
(eglot-events-buffer-size 0)
(eglot-extend-to-xref nil)
(eglot-code-action-indicator "→")
(eglot-ignored-server-capabilities
'(:documentFormattingProvider
:documentRangeFormattingProvider
:documentOnTypeFormattingProvider
;; :colorProvider
:foldingRangeProvider))
:config
(add-to-list 'eglot-server-programs
`((python-ts-mode python-mode) . ("pyrefly" "lsp"))))
This is needed so that go-to definition works for library files.
(use-package eglot-java :after eglot :ensure (:host github :repo "yveszoundi/eglot-java") :hook (java-ts-mode . eglot-java-mode))
(setenv "LSP_USE_PLISTS" "true")
(use-package lsp-mode :defer t :commands (lsp lsp-deferred) :general (:keymaps'(lsp-mode-map) :states '(normal) "gr" #'lsp-ui-peek-find-references "gd" #'lsp-ui-peek-find-definitions "gi" #'lsp-ui-peek-find-implementation "ga" #'lsp-execute-code-action "K" #'im-peek-doc) :init (setq lsp-use-plists t) (setq lsp-keymap-prefix "M-l") :config (setq lsp-enable-xref t) (setq lsp-enable-links t) (setq lsp-enable-folding t) (setq lsp-enable-indentation nil) (setq lsp-enable-on-type-formatting nil) (setq lsp-before-save-edits nil) (setq lsp-eldoc-render-all nil) (setq lsp-headerline-breadcrumb-enable nil) (setq lsp-enable-snippet nil) (setq lsp-modeline-code-actions-enable nil) (setq lsp-modeline-diagnostics-enable nil) (setq lsp-modeline-workspace-status-enable nil) (setq lsp-diagnostics-provider :flymake) (setq lsp-completion-provider :none) ;; for corfu (defalias 'im-lsp-list-workspaces #'lsp-describe-session) (add-to-list 'lsp-file-watch-ignored-directories "[/\\\\]\\.devbox\\'"))
(use-package lsp-ui :ensure (:host github :repo "emacs-lsp/lsp-ui") :after lsp :custom (lsp-ui-doc-include-signature t) (lsp-ui-doc-position 'at-point) (lsp-ui-sideline-show-diagnostics nil))
(use-package consult-lsp :ensure (:host github :repo "gagbo/consult-lsp") :after (lsp consult) :general (:keymaps '(lsp-mode-map) :states '(normal) "M-i" #'consult-lsp-file-symbols "M-I" #'consult-lsp-symbols))
(use-package consult-eglot :ensure (:host github :repo "mohkale/consult-eglot") :after (eglot consult) :general (:keymaps '(eglot-mode-map) :states '(normal) ;; eglot already integrates with imenu ;; "M-i" #'consult-eglot-file-symbols "M-I" #'consult-eglot-symbols))
(defun im-lsp-java-find-lombok-jar () (car (sort (file-expand-wildcards "~/.m2/repository/org/projectlombok/lombok/*/lombok-*.jar") #'string>)))
(defun im-lsp-java-enable-lombok-support ()
(interactive)
(if-let* ((lombok (im-lsp-java-find-lombok-jar))
(lombok-arg (concat "-javaagent:" (expand-file-name lombok))))
(progn
(with-eval-after-load 'lsp-mode
(add-to-list 'lsp-java-vmargs lombok-arg))
(with-eval-after-load 'eglot-java
(add-to-list 'eglot-java-eclipse-jdt-args lombok-arg))
(when (bound-and-true-p lsp-mode)
(call-interactively #'lsp-workspace-restart))
(when (bound-and-true-p eglot-java-mode)
(call-interactively #'eglot-java-mode)))
(message ">> Lombok jar not found.")))
(use-package lsp-java :after lsp :config (im-lsp-java-enable-lombok-support))
Install metals with coursier:
cd ~/.local/bin/ coursier bootstrap org.scalameta:metals2.13:1.5.1 -o metals -f
(use-package lsp-metals
:ensure (:host github :repo "emacs-lsp/lsp-metals")
:after lsp
:custom
(lsp-metals-enable-semantic-highlighting t)
(lsp-metals-server-args '("-J-Dmetals.allow-multiline-string-formatting=off"
"-J-Dmetals.icons=unicode"))
:hook (scala-mode . lsp))
simple-modeline
Lightweight and nice modeline.
(use-package simple-modeline
:ensure (:host github :repo "gexplorer/simple-modeline")
:hook (elpaca-after-init-hook . simple-modeline-mode)
:custom (simple-modeline-segments
'((simple-modeline-segment-modified
simple-modeline-segment-buffer-name
simple-modeline-segment-position)
(simple-modeline-segment-vc
simple-modeline-segment-misc-info
simple-modeline-segment-process
simple-modeline-segment-minor-modes
simple-modeline-segment-major-mode))))
howdoyou
When you search for something, it opens the results in an org-mode buffer. Results are fetched from SX (stack-exchange, stackoverflow etc) sites.
SPC hsorhowdoyou-query- search function
C-M-Left- prev answer
C-M-Right- next answer
(use-package howdoyou
:defer t
:commands (howdoyou-query howdoyou--get-buffer)
:general
(:keymaps 'howdoyou-mode-map :states '(normal motion)
"gn" #'howdoyou-next-link
"gp" #'howdoyou-previous-link)
(im-leader
"is" #'howdoyou-query)
:config
(setq howdoyou-switch-to-answer-buffer t)
(setq howdoyou-number-of-answers 5)
(add-hook
'howdoyou-mode-hook
(lambda ()
(setq header-line-format "gn ➔ next, gp ➔ prev")
(evil-normalize-keymaps))))
(defun im-open-stackexchange-link (link)
"Open stackexchange LINK in a nicely formatted org buffer."
(interactive "sLink: ")
(switch-to-buffer (howdoyou--get-buffer))
(insert "Loading...")
(let ((buffer (current-buffer)))
(promise-chain (howdoyou-read-so-link link)
(then #'(lambda (_)
(im-url-get-title-async
link
(lambda (title)
(with-current-buffer buffer (rename-buffer (format "*se: %s*" title) :unique)))))))))
(with-eval-after-load 'howdoyou
;; Now that we cannot parse google reliably, I replaced this
;; function with my Kagi search.
(defun howdoyou-promise-answer (query)
"Process QUERY and print answers to *How Do You* buffer."
(howdoyou--print-message "Searching...")
(im-kagi-search
(concat
query
" site:stackoverflow.com OR site:stackexchange.com OR site:superuser.com OR site:serverfault.com OR site:askubuntu.com")
:success (lambda (results)
(setq
howdoyou--links
(--map (alist-get 'url it) results))
(setq howdoyou--current-link-index 0)
(if howdoyou--links
(howdoyou-n-link 0)
(howdoyou--print-message "No results: \"%s\"" query)))
:error (lambda (err)
(error "Failed to search: %s" err)))))
(use-package yasnippet :diminish 'yas-minor-mode :hook ((minibuffer-setup . yas-minor-mode) ;; ^ Enable expanding in mini-buffer. (elpaca-after-init . yas-global-mode)))
(use-package yankpad
:ensure (:host github :repo "Kungsgeten/yankpad")
:after (org yasnippet)
:autoload (yankpad--categories yankpad--snippets)
:general
(im-leader
"sr" #'yankpad-reload
"sc" #'yankpad-set-category
"se" #'yankpad-edit
"ss" #'yankpad-map
"sm" #'yankpad-map)
(:states 'normal
(kbd "M-s") #'yankpad-insert)
(:states 'insert
(kbd "M-s") #'yankpad-insert
(kbd "M-e") #'hippie-expand)
(:keymaps 'minibuffer-local-map
(kbd "M-s") #'yankpad-insert
(kbd "M-e") #'hippie-expand)
:custom
(yankpad-file snippets-org)
;; Categories returned by the following functions will be used to
;; expand snippets
(yankpad-auto-category-functions '(yankpad-major-mode-category im-current-project-name))
:init
(add-to-list 'hippie-expand-try-functions-list #'yankpad-expand))
I also like to use these snippets outside of Emacs. For this, I
defined im-select-any-snippet. It let's you select any snippet
from the yankpad file (no mode restriction, shows all snippets
prefixed with mode name they are defined for) through
completing-read. Combined with im-globally, you can use this
outside of Emacs.
(defvar im--all-snippets-cache nil)
(defun im-load-snippets-list ()
(interactive)
(message ">> Loading snippets...")
(async-start
`(lambda ()
,(async-inject-variables "^load-path$")
(require 'dash)
(require 'yankpad)
(setq yankpad-file ,snippets-org)
(-mapcat
(lambda (category)
(--map (cons (format "%s :: %s" category (car it)) it)
;; Clear some sections of the data that causes
;; issues with emacs-async. It prints out some data
;; like #3 #5 etc. which can't be `read'.
(--map
(if (alist-get 'src-block (nth 2 it))
`(,@(-take 2 it) nil ,@(-take-last 2 it))
it)
(ignore-errors
(yankpad--snippets category)))))
(yankpad--categories)))
(lambda (result)
(message ">> Loading snippets... Done")
(setq im--all-snippets-cache result))))
Load snippets for the first time, after the startup This may take a bit of time
(with-eval-after-load 'org (run-with-timer 5 nil #'im-load-snippets-list))
(defun im-select-any-snippet ()
"List all templates for all modes and return the applied template as string."
(interactive)
(-let* ((selected (completing-read "Select snippet: " im--all-snippets-cache))
(snippet (alist-get selected im--all-snippets-cache nil nil #'equal))
((_ category _) (s-match "\\(.*\\) :: \\(.*\\)" selected)))
(with-temp-buffer
(setq yankpad-category category)
;; Activate yas snippet on the temp buffer
(let ((yas-dont-activate-functions '()))
(yas-minor-mode-on))
(yankpad--run-snippet snippet)
(im-kill (buffer-string)))))
(use-package git-link :defer t :config (setq git-link-open-in-browser t))
(defun im-git-link-homepage ()
"Like `git-link-homepage' itself but it does not open in browser, simply returns the address as string."
(interactive)
(let ((git-link-open-in-browser nil))
(call-interactively 'git-link-homepage)
(car kill-ring)))
(defun im-git-link-on-branch (branch)
"Like `git-link' but let's you select the branch first when called interactively."
(interactive
(list
(completing-read
"Select a branch: "
(vc-git-branches)
nil nil (lab-git-current-branch))))
(let ((git-link-default-branch branch))
(call-interactively 'git-link)))
(defun im-git-link-commit ()
"Like `git-link' but use commit hash in url."
(interactive)
(let ((git-link-use-commit t))
(call-interactively 'git-link)))
(defun im-git-link-merge-requests ()
"Open MR page."
(interactive)
(browse-url
(let ((homepage (im-git-link-homepage)))
(cond
((s-contains? "gitlab" homepage) (concat homepage "/-/merge_requests"))
((s-contains? "github" homepage) (concat homepage "/pulls"))
(t (user-error "Forge undefined"))))))
tab-out
When you press tab, jump out from the current enclosing parens/quotes etc. When there is no enclosing stuff, TAB key automatically fallbacks to it's default behavior.
(use-package tab-jump-out :diminish :ensure (:host github :repo "isamert/tab-jump-out") :hook (elpaca-after-init . tab-jump-out-global-mode))
helpful and elisp-demos
- helpful
- Better help dialogs with syntax highlighting, references, source etc.
- elisp-demos
- Adds code examples into function help buffers.
- (no term)
- Code examples are maintained here, don't forget to contribute!
- (no term)
- Call
elisp-demos-add-demoto add a demo locally.
(use-package helpful
;; Override default help bindings
:bind (("C-h f" . helpful-callable)
("C-h v" . helpful-variable)
("C-h k" . helpful-key)
("C-h p" . helpful-at-point))
:config
(evil-define-key 'normal helpful-mode-map
"q" 'evil-delete-buffer))
(use-package elisp-demos :after helpful :config (advice-add 'helpful-update :after #'elisp-demos-advice-helpful-update))
Here, I also bind some keys for convenience. Taken from here.
(bind-key "C-h C-f" #'find-function) (bind-key "C-h C-v" #'find-variable) (bind-key "C-h C-l" #'find-library)
expand-region
Also see combobulate package. I use it instead (`combobulate-mark-node-dwim') whenever possible instead of expand-region.
(use-package expand-region :ensure (:host github :repo "karthink/expand-region.el") :general (:states '(insert normal) "M-w" #'er/expand-region))
aggressive-indent
It keeps your indentation working all the time. Seems like a good idea but I have some concerns about it, so I just use it with elisp for the time being.
(use-package aggressive-indent :hook (emacs-lisp-mode . aggressive-indent-mode) :config (add-to-list 'aggressive-indent-protected-commands 'evil-undo) (add-to-list 'aggressive-indent-protected-commands 'format-all-buffer))
(use-package slack
:ensure (:host github :repo "emacs-slack/emacs-slack")
:hook (elpaca-after-init . im-slack-initialize)
:if (workpc?)
:general
(im-leader-v
"es" #'im-slack-transient)
:config
(setq slack-log-level 'error)
;; ^ info level shows unnecessary stuff that distracts me
(setq slack-block-highlight-source t)
(setq slack-buffer-emojify nil)
(setq slack-render-image-p t)
(setq slack-image-max-height 150)
(setq slack-prefer-current-team t)
;; ^ Set current team with `slack-change-current-team'
(setq slack-buffer-function #'switch-to-buffer)
(setq slack-typing-visibility nil)
;; ^ Typing indicators causes some issues on my end. Gonna disable
;; it for now.
;; Disable filling as it fucks with copying stuff
(setq lui-fill-type nil)
(setq lui-fill-column 0)
(setq lui-time-stamp-format "[%a %b %d %H:%M]")
(setq slack-message-custom-notifier #'im-slack-notify)
(setq slack-message-custom-delete-notifier #'im-slack-notify)
(with-eval-after-load 'im-notif
(add-to-list 'im-notif-label-default-durations '("slack" . 5)))
(setq slack-visible-thread-sign "╚═> ")
(add-hook 'slack-message-buffer-mode-hook #'tab-line-mode)
(add-hook 'slack-all-threads-buffer-mode-hook #'tab-line-mode)
(add-hook 'slack-thread-message-buffer-mode-hook #'tab-line-mode)
(evil-set-initial-state 'slack-mode-map 'normal)
(evil-define-key 'normal slack-message-edit-buffer-mode-map
"@" #'slack-message-embed-mention
"mc" #'slack-message-embed-channel)
(evil-define-key 'normal slack-message-compose-buffer-mode-map
"@" #'slack-message-embed-mention
"mc" #'slack-message-embed-channel)
;; Fix for/from yuya373/emacs-slack#547-1542119271
(advice-add 'lui-buttonize-urls :before-until (lambda () (derived-mode-p 'slack-mode)))
(dolist (mode '(slack-buffer-mode-hook
slack-edit-message-mode-hook
slack-message-buffer-mode-hook
slack-all-threads-buffer-mode-hook
slack-message-edit-buffer-mode-hook
slack-search-result-buffer-mode-hook
slack-thread-message-buffer-mode-hook
slack-message-compose-buffer-mode-hook))
(add-hook mode #'im-slack--enable-completion-at-point)
(add-hook mode #'visual-line-mode)))
(use-package im-slack
:ensure `(:repo ,im-packages-path :files ("im-slack.el"))
:after slack)
(defun im-slack-initialize () (interactive) (ignore-errors (slack-ws-close)) (ignore-errors (slack-team-delete)) (setq im-slack--last-messages nil) (im-slack-kill-buffers) (slack-register-team :name ty-slack-name :token ty-slack-token :enterprise-token ty-slack-enterprise-token :cookie ty-slack-cookie :visible-threads nil :mark-as-read-immediately t) (slack-start) (setq slack-current-team (car (hash-table-values slack-teams-by-token))) (setq slack-extra-subscribed-channels ty-slack-channels))
(defun im-slack--enable-completion-at-point ()
(setq-local
completion-at-point-functions
`(,(cape-company-to-capf #'company-slack-backend)
cape-emoji)))
(defun im-slack-kill-buffers ()
(interactive)
(--each (--filter
(s-prefix? "*slack: " (buffer-name it)) (buffer-list))
(unless (ignore-errors (kill-buffer it))
(with-current-buffer it
(let ((kill-buffer-query-functions nil)
(kill-buffer-hook nil))
(kill-buffer))))))
(transient-define-prefix im-slack-transient ()
"Slack transient."
[["Overview"
("t" "All threads" slack-all-threads)
("a" "Activity feed" slack-activity-feed-show)]
["Rooms"
("s" "Select room" im-slack-select-room)
("S" "Select unread rooms" slack-select-unread-rooms)
("d" "DMs" im-slack-dms)]
["Message"
("m" "Send message" im-slack-send-message)
("y" "Yank last message" im-slack-yank-last-message)
("l" "Open last message" im-slack-open-last-message)
("r" "Last messages (per room)" im-slack-last-messages)
("R" "Last messages (all)" im-slack-last-messages-alternative)]
["Utils"
("uu" "User info" slack-user-select)]])
(defun im-slack--add-reaction-to-message (reaction)
(defalias (intern (concat "react-" reaction))
`(lambda ()
(interactive)
(slack-buffer-add-reaction-to-message
slack-current-buffer
,reaction
(slack-get-ts)))))
(general-def :keymaps '(slack-message-buffer-mode-map slack-thread-message-buffer-mode-map slack-all-threads-buffer-mode-map) :states '(normal motion) "q" #'im-quit "@@" 'slack-message-embed-mention "@c" 'slack-message-embed-channel "mf" 'slack-file-upload-quick "mc" 'im-slack-clipboard-image-upload "mm" 'slack-message-write-another-buffer "md" 'slack-message-delete "my" 'slack-message-copy-link "ml" 'slack-message-copy-link "ms" 'slack-message-share "me" 'slack-message-edit "mt" 'slack-thread-show-or-create "mq" 'im-slack-quote-message "mi" 'slack-display-user-profile-info "mj" 'slack-thread-message-buffer-jump-to-channel-buffer "mrr" 'slack-message-add-reaction "mR" 'slack-message-remove-reaction "mrs" (im-slack--add-reaction-to-message "seen") "mre" (im-slack--add-reaction-to-message "eyes") "mrd" (im-slack--add-reaction-to-message "white_check_mark") "mrc" (im-slack--add-reaction-to-message "cigara") "mr1" (im-slack--add-reaction-to-message "+1") "mr2" (im-slack--add-reaction-to-message "ok_hand") "mr3" (im-slack--add-reaction-to-message "eyes") "mr4" (im-slack--add-reaction-to-message "ultrafastparrot") "mr5" (im-slack--add-reaction-to-message "pepedance") "mrp" (im-slack--add-reaction-to-message "prayge") "mrh" (im-slack--add-reaction-to-message "handshake") "mrl" (im-slack--add-reaction-to-message "pepe-love") "[[" 'slack-buffer-goto-prev-message "]]" 'slack-buffer-goto-next-message)
(general-def :keymaps '(slack-thread-message-buffer-mode-map) :states '(normal motion) ;; Jump back to channel from a thread with the same keybinding that created it "mt" 'slack-thread-message-buffer-jump-to-channel-buffer)
(im-make-repeatable slack "[" slack-buffer-goto-prev-message "]" slack-buffer-goto-next-message)
(use-package prodigy
:ensure (:host github :repo "rejeep/prodigy.el")
:hook (elpaca-after-init . im-prodigy-autostart)
:demand t
:general
(im-leader
"et" #'prodigy)
:config
(evil-define-key 'normal prodigy-mode-map
"q" #'im-quit
"m" #'prodigy-mark
"u" #'prodigy-unmark
"x" #'prodigy-stop
"S" #'prodigy-start
"r" #'prodigy-restart
"R" #'prodigy-refresh
"f" #'prodigy-jump-file-manager
"M" #'prodigy-jump-magit
"t" #'prodigy-add-tag-filter
"T" #'prodigy-clear-filters
(kbd "RET") #'prodigy-display-process))
(defun im-prodigy-autostart ()
"Start all services with the `:auto-start' set to non-nil if they are not already started."
(interactive)
(prodigy-with-refresh
(--each
prodigy-services
(when (and (plist-get it :auto-start)
(not (prodigy-service-started-p it)))
(prodigy-start-service it)))))
separedit – edit comment blocks/multiline strings etc. indirectly
Pop current comment section OR a multiline string into a markdown buffer and edit it there. Pretty useful for writing long comments/strings.
It also supports editing code blocks inside comment blocks in their language, like ```c int main () { int x = 3; // Do M-x `separedit' here. } ```
If the code block does not have a language marker, then it's assumed to be in the same language of the buffer. ``` (progn (+ 1 2)) ```
(use-package separedit
;; C-u separedit → Let's you select the editing mode first.
:general
(:states '(normal visual)
"gm" #'separedit)
(:keymaps 'minibuffer-mode-map
"C-i" #'separedit
"M-r" #'separedit
"M-i" #'separedit)
:config
(setq separedit-default-mode 'md-ts-mode)
(setq separedit-continue-fill-column t)
(advice-add
'separedit--select-mode
:override
(lambda ()
(completing-read "Select major mode: " obarray
(lambda (x)
(and (fboundp x)
(commandp x)
(string-match "-mode$" (symbol-name x))))
t
nil 'separedit--mode-history (or (car separedit--mode-history)
(format "%s" major-mode))))))
all-the-icons
You should run all-the-icons-install-fonts command after
this. Also run fc-cache -f -v afterwards
(all-the-icons-install-fonts already does that but it may fail).
all-the-icons-completion gives you icons in completion UI, like
in completing-read etc.
(use-package all-the-icons) (use-package all-the-icons-completion :after (all-the-icons vertico) :config (all-the-icons-completion-mode +1))
reddigg and hnreader – org-mode interface for reddit and hacernews
These packages lets me display comments of given reddit/hn thread in an org buffer.
Also see `browse-url-handlers' configuration to see how I utilize these packages.
(use-package reddigg :defer t :ensure (:host github :repo "thanhvg/emacs-reddigg"))
(use-package hnreader :defer t)
(use-package im-ai
:ensure `(:repo ,im-packages-path :files ("im-ai.el"))
:custom
(im-ai-file "~/Documents/notes/extra/gpt.org")
:config
(add-hook 'gptel-mode-hook #'tab-line-mode))
(use-package im-ai-at-point
:ensure `(:repo ,im-packages-path :files ("im-ai-at-point.el"))
:general
(im-leader-v "sa" #'im-ai-at-point))
gptel seems more capable than org-ai. Probably going to replace org-ai with this in the long run or they might just simply co-exists where I use org-ai for simple cases and for more complex cases (like tool usage etc.) gptel.
- Supports images etc.
- Automatically converts markdown responses to org syntax on the fly,
- Very easy to add/delete context.
Use ... syntax to include media files in the prompt. Should be in a single line.
Commands
gptel→ Start a dedicated chat buffer.gptel-add→ Add to context DWIMgptel-send→ Send current region as prompt and insert the result.C-u gptel-send→ Edit stuff before sending prompt.
Inside a gptel buffer:
C-u C-x RETorgptel-menu→ Menu
By default, it uses authinfo to get api keys. See my netrc file. The ones with username "apikey" are for gtpel.
(use-package gptel
:defer t
:config
(setq gptel-default-mode #'org-mode)
(setq gptel-track-media t)
(setq gptel-prompt-prefix-alist '((org-mode . "-----\n[ME]: ")))
(setq gptel-response-prefix-alist '((org-mode . "-----\n[AI]: ")))
(setq gptel-expert-commands t)
;; As usual, my key is in netrc file.
(gptel-make-anthropic "Claude"
:stream t
:key gptel-api-key)
(gptel-make-deepseek "DeepSeek"
:stream t
:key gptel-api-key)
(defun im-gptel-on-response-finished (&optional start end)
"Send a notification when response is finished and buffer is not visible."
(unless (get-buffer-window (current-buffer) (selected-frame))
(let ((bname (buffer-name))
(wc (count-words start end))
(lc (count-lines start end)))
(im-run-deferred ; don't know why needed...
(im-notif :title (format "%s ⟹ FINISHED" bname)
:message (format "%s lines, %s words..." lc wc)
:source (get-buffer bname)
:labels '("ai"))))))
(define-advice gptel--display-tool-calls (:before (&rest _args) advice-name)
"Show notification"
(unless (get-buffer-window (current-buffer) (selected-frame))
(let ((bname (buffer-name)))
(im-run-deferred
(im-notif :title (format "%s ⟹ ACTION REQUIRED" bname)
:message "Tool called..."
:source (get-buffer bname)
:labels '("ai"))))))
(add-to-list 'gptel-post-response-functions #'im-gptel-on-response-finished))
(im-leader :keymaps '(gptel-mode-map org-ai-mode-map) :states '(normal motion) "[" #'im-ai-previous-block "]" #'im-ai-next-block)
(im-make-repeatable im-ai "[" im-ai-previous-block "]" im-ai-next-block)
agent-shell
cursor-cli and claude-code have external ACP implementations: cursor → npm install -g @blowmage/cursor-agent-acp claude-code → npm install -g @zed-industries/claude-code-acp
Inside the agent-shell buffers: agent-shell-set-session-mode → Set the mode (plan, ask, agent etc.) agent-shell-set-session-model → The model; opus-4-5, sonnet-4-5, gpt-5.2 etc.
(use-package agent-shell
:ensure (:host github :repo "xenodium/agent-shell")
:defer t
:custom
(agent-shell-show-welcome-message nil)
(agent-shell-highlight-blocks t)
(agent-shell-header-style 'text)
:bind (:map agent-shell-mode-map
("RET" . newline)
("C-c C-c" . shell-maker-submit)
("C-c C-k" . agent-shell-interrupt))
:general
(im-leader
"tS" 'im-toggle-side-agent-buffer))
(defun im-toggle-side-agent-buffer ()
(interactive)
(cond
((string-match-p (regexp-quote "Agent @") (buffer-name))
(agent-shell-help-menu))
((use-region-p)
(agent-shell-help-menu))
(t
(let* ((dir (im-current-project-root))
(bufs (seq-filter
(lambda (b)
(when-let* ((f (with-current-buffer b default-directory)))
(and (string-prefix-p dir (file-name-directory (expand-file-name f)))
(string-match-p (regexp-quote "Agent @")
(buffer-name b)))))
(buffer-list)))
(buf (cond
((null bufs) nil)
((= (length bufs) 1) (car bufs))
(t (get-buffer
(completing-read "Select agent buffer: "
(mapcar #'buffer-name bufs)
nil t))))))
(if buf
(im-toggle-side-buffer-with-name buf)
(agent-shell-new-shell))))))
(defvar im-agent-shell-notif--subscriptions nil "Alist mapping shell buffers to their subscription tokens.")
(defun im-agent-shell-notif--on-permission-request (event)
"Handle a permission-request EVENT with a notification."
(unless (im-buffer-visible-p (current-buffer))
(im-notif
:id (buffer-name (current-buffer))
:title (concat "agent-shell → " (capitalize (or (map-nested-elt event '(:data :tool-call :kind)) "")))
:message (im-agent-shell-notif--format-permission-message
(map-nested-elt event '(:data :tool-call)))
:duration 5
:severity 'normal
:labels '("agent" "agent-shell")
:source (current-buffer))))
(defun im-agent-shell-notif--on-turn-complete (event)
"Handle a turn-complete EVENT with a notification."
(unless (im-buffer-visible-p (current-buffer))
(im-notif
:id (buffer-name (current-buffer))
:title "agent-shell → finished"
:message (if (equal (map-nested-elt event '(:data :stop-reason))
"end_turn")
"Success"
"Failed")
:severity (if (equal (map-nested-elt event '(:data :stop-reason))
"end_turn")
'normal
'critical)
:duration 5
:severity 'normal
:labels '("agent" "agent-shell")
:source (current-buffer))))
(defun im-agent-shell-notif-subscribe (&optional shell-buffer)
"Subscribe to agent-shell events in SHELL-BUFFER.
If SHELL-BUFFER is nil, use the current buffer."
(let ((buf (or shell-buffer (current-buffer))))
(when (map-elt im-agent-shell-notif--subscriptions buf)
(im-agent-shell-notif-unsubscribe buf))
(setf (map-elt im-agent-shell-notif--subscriptions buf)
(list
(agent-shell-subscribe-to
:shell-buffer buf
:event 'permission-request
:on-event #'im-agent-shell-notif--on-permission-request)
(agent-shell-subscribe-to
:shell-buffer buf
:event 'turn-complete
:on-event #'im-agent-shell-notif--on-turn-complete)))))
(defun im-agent-shell-notif-unsubscribe (&optional shell-buffer)
"Unsubscribe from agent-shell events in SHELL-BUFFER.
If SHELL-BUFFER is nil, use the current buffer."
(let ((buf (or shell-buffer (current-buffer))))
(dolist (token (map-elt im-agent-shell-notif--subscriptions buf))
(agent-shell-unsubscribe :subscription token))
(setq im-agent-shell-notif--subscriptions
(map-delete im-agent-shell-notif--subscriptions buf))))
(define-minor-mode im-agent-shell-notif-mode
"Toggle notifications for the current agent-shell buffer."
:lighter " notif"
(if im-agent-shell-notif-mode
(im-agent-shell-notif-subscribe (current-buffer))
(im-agent-shell-notif-unsubscribe (current-buffer))))
(defun im-agent-shell-notif-mode-enable () "Enable `im-agent-shell-notif-mode'." (im-agent-shell-notif-mode 1))
(add-hook 'agent-shell-mode-hook #'im-agent-shell-notif-mode-enable)
(use-package im-cursor
:ensure `(:repo ,im-packages-path :files ("im-cursor.el"))
:defer t
:config
(with-eval-after-load 'im-notif
(add-to-list 'im-notif-dnd-whitelist-labels "agent")))
(use-package im-kagi-assistant
:ensure `(:repo ,im-packages-path :files ("im-kagi-assistant.el"))
:defer t
:custom
(im-kagi-assistant-cookie im-kagi-cookie))
tmr.el – timers, reminders etc.
Pretty timers. I forget everything, so it's quite important for me
to have a quick way to define timers. I was using appt-add for
this before but it does not let you view/manipulate upcoming timers
easily and tmr does this very well with tmr-tabulated-view.
- tmr
- To quickly define a timer, without any description.
- tmr-with-details
- Define a timer with description, also asks if
you need acknowledgment. Acknowledgment is useful in a sense that
you can re-schedule the timer if you need it. It will ask you to
write
ackwhen the time is up, if you are not ready, you can re-schedule the timer with the same notation you use while creating it which is super convenient.
(use-package tmr
:defer t
:autoload (tmr--parse-duration)
:init
(im-leader "T" #'tmr-with-details)
:config
;; Replace the notification function so that it works on my Mac
(remove-hook 'tmr-timer-finished-functions #'tmr-notification-notify)
(add-hook 'tmr-timer-finished-functions #'im-tmr-notify)
;; Show tmr notifications even if we are in DND
(with-eval-after-load 'im-notif
(add-to-list 'im-notif-dnd-whitelist-labels "tmr"))
(when (eq system-type 'darwin)
(setq tmr-sound-file "/System/Library/Sounds/Glass.aiff"))
;; evilify
(define-key tmr-tabulated-mode-map "j" #'next-line)
(define-key tmr-tabulated-mode-map "k" #'previous-line)
(define-key tmr-tabulated-mode-map "x" #'tmr-remove)
(define-key tmr-tabulated-mode-map "d" #'tmr-remove)
(evil-set-initial-state 'tmr-tabulated-mode 'emacs)
;; See `im-update-global-mode-line'
(add-hook 'tmr-timer-finished-functions #'im-update-global-mode-line)
(add-hook 'tmr-timer-cancelled-functions #'im-update-global-mode-line))
(defun im-tmr-notify (timer)
(im-notif
:title "TMR"
:message (tmr--long-description-for-finished-timer timer)
;; TMR events are high priority for me
:severity 'urgent
:labels '("tmr")))
(use-package lab
:ensure (:host github :repo "isamert/lab.el")
:autoload (lab--git lab--time-ago lab-git-clone lab-current-branch lab-git-origin-switch-to-ssh)
:init
;; Add advices for automatically starting to watch pipelines
(with-eval-after-load 'magit
(define-advice magit-push-current-to-pushremote (:after (&rest _) start-watching-pipeline)
(lab-watch-pipeline-for-last-commit)))
(with-eval-after-load 'vc
(define-advice vc-push (:after (&rest _) start-watching-pipeline)
(lab-watch-pipeline-for-last-commit)))
:config
;; Automatically refresh projects list after cloning a project
(add-hook 'lab-after-git-clone-functions #'im-load-projects-list)
;; Same thing but for bulk cloning
(add-hook 'lab-after-clone-bulk-functions #'im-load-projects-list)
;; And jump to that project
(add-hook 'lab-after-git-clone-functions (lambda () (dired default-directory)))
(setq lab-result-count 30)
(setq lab-projects-directory im-projects-root)
(setq lab-pipeline-watcher-initial-delay 30)
(setq lab-pipeline-watcher-debounce-time 15)
(setq lab-host ty-gitlab-url)
(setq lab-token ty-gitlab-token)
(setq lab-group ty-gitlab-group)
(setq lab-should-open-pipeline-on-manual-action? t)
(add-hook 'midnight-hook #'im-update-all-projects)
(add-hook
'lab-open-merge-request-diff-hook
(cl-function
(lambda (&key project &allow-other-keys)
(when (or (not (project-current))
(not (string-suffix-p (project-name (project-current))
(plist-get project :path)
'ignore-case)))
(cd (minibuffer-with-setup-hook
(lambda () (insert (plist-get project :path)))
(funcall project-prompter))))))))
(defun im-update-all-projects () (lab-pull-bulk (lab-all-project-roots im-projects-root)) (lab-clone-bulk lab-group (f-join im-projects-root im-company-name)))
I also have an experimental GitHub version, tailored for more open-source work needs instead of regular stuff.
(use-package im-github
:ensure `(:repo ,im-packages-path :files ("im-github.el"))
:custom
(lab-github-user "isamert")
(lab-github-token im-github-token)
(lab-github-open-issues-projects-blacklist '("isamert.github.io" "addalias" "gedi" "gracer" "scli"))
:config
(add-to-list 'im-open-thing-at-point-alist '(lab-github-issue-at-point . lab-github-open-issue)))
(use-package swagg
:ensure (:host github :repo "isamert/swagg.el")
:general
(im-leader
"us" #'swagg-request-with-rest-block
"uS" #'swagg-request)
:config
(setq swagg-rest-block-org-header-tags "verb")
(setq swagg-fetch-lang "deno :allow net"))
(use-package whisper
:ensure (:host github :repo "natrys/whisper.el")
:general
(im-leader
"er" #'whisper-run)
:config
;; Override `whisper-command' as instructed, to be able to use my
;; whisper server installation on my home lab.
(setq whisper-install-whispercpp nil) ; Opt-out from local whisper installation
(defun whisper-command (input-file)
;; No support for `whisper-translate',
;; `whisper-show-progress-in-mode-line' and `whisper-language'.
`("curl"
,(concat im-server "/whisper/inference")
"-H" ,(concat "isamert-token: " im-token)
"-H" "Content-Type: multipart/form-data"
"-F" ,(concat "file=@" (expand-file-name input-file))
"-F" "response_format=text")))
(define-advice whisper-run (:before (&rest _) set-microphone)
(unless whisper--ffmpeg-input-device
(im-whisper-select-microphone)))
(defun im-whisper-select-microphone ()
(interactive)
(setq
whisper--ffmpeg-input-device
(->>
(im-output-select
:prompt "Select mic: "
:cmd "/opt/homebrew/bin/ffmpeg -f avfoundation -list_devices true -i ''"
:keep-order t
:filter (s-prefix? "[AVFound" it))
(s-match "\\[\\([0-9]+\\)\\]")
(nth 1)
(s-prepend ":"))))
osm – OpenStreetMaps in Emacs
Very cool and the nice thing is it integrates itself with the built-in bookmarking system. So you can bookmark places (or store them as org links) and jump to them whenever needed.
(use-package osm :defer t :general ;; TODO: Maybe add this to evil-collection (:keymaps 'osm-mode-map :states 'normal ;; Navigation "h" #'osm-left "l" #'osm-right "j" #'osm-down "k" #'osm-up "H" #'osm-left-left "L" #'osm-right-right "J" #'osm-down-down "K" #'osm-up-up ;; Positioning "+" #'osm-zoom-in "-" #'osm-zoom-out "c" #'osm-center "g" #'osm-home "r" #'revert-buffer ;; Url "u" #'osm-save-url "y" #'org-store-link ;; Other "s" #'osm-search "X" #'osm-gpx-hide "q" #'quit-window) :custom (osm-server 'default) (osm-copyright nil) :init (add-hook 'osm-mode-hook #'im-disable-line-wrapping))
reveal-in-finder
Well, sometimes you need to open that pesky program.
(use-package reveal-in-osx-finder :defer t :if (eq system-type 'darwin))
(setq undo-limit 67108864)
(setq undo-strong-limit 100663296)
(setq undo-outer-limit 1006632960)
(use-package vundo :defer t :general (:states 'normal "U" #'vundo) (:states 'normal :keymaps 'vundo-mode-map "q" #'vundo-quit "RET" #'vundo-confirm "d" #'vundo-diff "h" #'vundo-backward "l" #'vundo-forward "k" #'vundo-previous "j" #'vundo-next) :custom (vundo-compact-display t) (vundo-window-max-height 20) (vundo-glyph-alist vundo-unicode-symbols) :config (evil-set-initial-state 'vundo-mode 'normal))
(use-package undo-fu :demand t)
(use-package undo-fu-session :after undo-fu :demand t :custom (undo-fu-session-compression 'zst) :config (undo-fu-session-global-mode 1))
deadgrep
consult-ripgrep is nice but having a dedicated search buffer
where I can tinker with options is sometimes better
(use-package deadgrep
:defer t
:custom
(deadgrep-display-buffer-function #'switch-to-buffer)
(deadgrep-project-root-function #'im-current-project-root)
;; Normally =--multiline= has memory and performance penalty and not
;; recommended for every search but I normally use =consult-ripgrep=
;; and if I'm using deadgrep than this means shit got serious and I
;; need the advanced stuff.
(deadgrep-extra-arguments '("--no-config" "--multiline"))
(deadgrep-project-root-function (lambda ()
(or (im-current-project-root)
default-directory)))
:general
(:keymaps 'deadgrep-mode-map :states 'normal
"RET" #'deadgrep-visit-result
"o" #'deadgrep-visit-result-other-window
"TAB" #'deadgrep-toggle-file-results
"S" #'deadgrep-search-term
"T" #'deadgrep-cycle-search-type
"C" #'deadgrep-cycle-search-case
"F" #'deadgrep-cycle-files
"D" #'deadgrep-directory
"^" #'deadgrep-parent-directory
"r" #'deadgrep-restart
"I" #'deadgrep-incremental
"i" #'deadgrep-edit-mode))
(defun im-deadgrep-directory (dir term)
(interactive (list
(read-directory-name "Dir: ")
(read-string "Search term")))
(deadgrep term dir))
(use-package consult-gh
:ensure (:host github :repo "armindarvish/consult-gh")
:after consult
:defer t
:custom
;; Previews are triggered with `consult-preview-key'
(consult-gh-show-preview t)
(consult-gh-issues-state-to-show "all")
(consult-gh-issue-maxnum 150)
(consult-gh-repo-maxnum 150)
(consult-gh-pr-maxnum 150)
(consult-gh-code-maxnum 50)
(consult-gh-default-orgs-list '("isamert"))
(consult-gh-default-clone-directory "~/Workspace/temp")
(consult-gh-file-action #'consult-gh--files-view-action)
(consult-gh-issue-action
(lambda (it)
(browse-url (format "https://github.com/%s/issues/%s" (plist-get (cdr it) :repo) (plist-get (cdr it) :issue)))))
:config
(setq
consult-gh-repo-action
(lambda (it)
(let ((repo (car (s-split " " (s-trim (substring-no-properties it))))))
(empv--select-action "Action: "
"View README" => (browse-url (format "https://github.com/%s.git" repo))
"Files" => (consult-gh--repo-browse-files-action it)
"Issues" => (consult-gh-issue-list repo)
"Clone" => (lab-git-clone
(format "https://github.com/%s.git" repo)
(read-directory-name "Directory to clone in: " lab-projects-directory))))))
;; Some aliases so that all GitHub functionality is under same prefix
(defalias 'lab-github-search-repo #'consult-gh-search-repos)
(defalias 'lab-github-search-code #'consult-gh-search-code))
artist-mode
Here I simply add some bindings form artist-mode and a cheatsheet in the header line.
(add-hook 'artist-mode-hook #'im-artist-mode-hook)
(defun im-artist-mode-hook () (local-set-key (kbd "<f2>") 'artist-select-op-pen-line) ; f2 = pen mode (local-set-key (kbd "<f3>") 'artist-select-op-line) ; f3 = line (local-set-key (kbd "<f4>") 'artist-select-op-square) ; f4 = rectangle (local-set-key (kbd "<f5>") 'artist-select-op-ellipse) ; f5 = ellipse (setq-local header-line-format "f2 → pen, f3 → line, f4 → square, f5 → ellipse"))
(use-package biome
:ensure (:host github :repo "SqrtMinusOne/biome")
:defer t
;; These commands are generated in the :config section.
:commands (im-biome-weather-daily-ankara
im-biome-weather-daily-amsterdam
im-biome-weather-daily-istanbul
im-biome-weather-hourly-ankara
im-biome-weather-hourly-amsterdam
im-biome-weather-hourly-istanbul)
:custom
;; See README, this fixes request--callback: peculiar error issue.
(biome-api-try-parse-error-as-response t)
(biome-query-coords
`(("Amsterdam, Netherlands" ,im-amsterdam-lat ,im-amsterdam-long)
("Ankara, Turkey" ,im-ankara-lat ,im-ankara-long)
("Istanbul, Turkey" ,im-istanbul-lat ,im-istanbul-long)))
(biome-grid-highlight-current t)
(biome-query-override-column-names
'(("apparent_temperature_max" . "Max feel")
("apparent_temperature_min" . "Min feel")
("temperature_2m_min" . "Min temp")
("temperature_2m_max" . "Max temp")
("precipitation_probability_max" . "🌧️ %%")
("precipitation_probability" . "🌧️ %%")
("precipitation_hours" . "🌧️ hours")
("precipitation_sum" . "🌧️ sum")
("precipitation" . "🌧️ ")
("relativehumidity_2m" . "Humid")
("weathercode" . " Code")
("is_day" . "Zone ")
("shortwave_radiation_sum" . "Radiation")
("time" . "📅 ")))
:config
;; Generate a preset for each city in biome-query-coords
(--each (map-keys biome-query-coords)
(eval
`(biome-def-preset ,(intern (concat "im-biome-weather-daily-" (downcase (car (s-split ", " it)))))
((:name . "Weather Forecast")
(:group . "daily")
(:params
("daily" "shortwave_radiation_sum" "uv_index_clear_sky_max" "uv_index_max" "precipitation_probability_max"
"precipitation_hours" "precipitation_sum" "sunset" "sunrise" "apparent_temperature_min" "apparent_temperature_max"
"temperature_2m_min" "temperature_2m_max" "weathercode")
("latitude" . ,(car (map-elt biome-query-coords it)))
("longitude" . ,(cadr (map-elt biome-query-coords it))))))))
(--each (map-keys biome-query-coords)
(eval
`(biome-def-preset ,(intern (concat "im-biome-weather-hourly-" (downcase (car (s-split ", " it)))))
((:name . "Weather Forecast")
(:group . "hourly")
(:params
("hourly" "uv_index_clear_sky" "uv_index" "relativehumidity_2m" "precipitation"
"precipitation_probability" "apparent_temperature" "temperature_2m" "is_day" "weathercode")
("latitude" . ,(car (map-elt biome-query-coords it)))
("longitude" . ,(cadr (map-elt biome-query-coords it)))))))))
outli – outlier for code files
I migrated my .org config into a single elisp file, thanks to outli. This way it is easier to maintain and I get the use some of the org features in my configuration.
(use-package outli
:ensure (:host github :repo "jdtsmith/outli")
:hook (prog-mode . outli-mode)
:diminish outli-mode
:defer t
:general
;; You can jump between /pages/ by using ~C-x [~ and ~C-x ]~. See
;; [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Pages.html][this]]
;; for more information. Using the same convention for outli:
(:keymaps 'outli-mode-map
"C-x [" #'outline-previous-heading
"C-x ]" #'outline-next-heading)
(im-leader :keymaps 'outli-mode-map
"<left>" #'outline-previous-heading
"<right>" #'outline-next-heading)
(im-leader-lesser
"<left>" #'backward-page
"<right>" #'forward-page)
;; Here are some org-mode like bindings for outli:
(:keymaps 'outli-mode-map :states 'normal
"gh" #'outline-up-heading
"gk" #'outline-backward-same-level
"gj" #'outline-forward-same-level)
:config
;; Add h as narrow prefix for headings in consult-imenu
(with-eval-after-load 'consult-imenu
(push '(?h "Headings") (plist-get (cdr (assoc 'emacs-lisp-mode consult-imenu-config)) :types))))
(use-package upver :commands (upver) :defer t :ensure (:host github :repo "isamert/upver.el"))
(use-package ol-notmuch :after notmuch)
Easy to use, fast email client. Tagging feature is something I am familiar from elfeed, and notmuch executes this concept very well. A pleasure to use.
Summary of what is going on:
- `mbsync' (or `isync') is used for fetching mail from remote mail
server to local.
- The configuration is kept under
~/.mbsyncrc - This needs to be run periodically to sync mail. See my `im-sync-mail' function.
- The configuration is kept under
- `notmuch' is the mail indexer which essentially lets you search
your mail.
- The configuration is kept under
~/.notmuch-config - It is an external program, that works on your local maildir (which is created by the `mbsync')
- It needs to be run after each update to index your mail. See my `im-sync-mail' function.
- It is also an Emacs package which let's you view your email and interact with them.
- The configuration is kept under
- `msmtp' is the SMTP client that let's you send your mails.
- The configuration is kept under
~/.msmtprc. - See `sendmail' and `message' configuration down below for Emacs integration.
- The configuration is kept under
See ../index.html for relevant configuration files.
Great video, explaining notmuch and the workflow by prot: https://youtube.com/watch?v=g7iF11qamh8
For gmail configuration, following is helpful: https://frostyx.cz/posts/synchronize-your-2fa-gmail-with-mbsync#gmail-with-2fa
Another helpful and easy to follow guide: https://jonathanchu.is/posts/emacs-notmuch-isync-msmtp-setup/
Most of the customizations down below are inspired by prot's configuration: https://protesilaos.com/emacs/dotemacs
Good time to set these values:
(setq user-full-name "Isa Mert Gurbuz") (setq user-mail-address im-primary-mail)
(use-package notmuch
:custom
(notmuch-show-logo nil)
(notmuch-hello-auto-refresh t)
(notmuch-show-all-tags-list t)
(notmuch-show-empty-saved-searches t)
(notmuch-show-relative-dates t)
(notmuch-show-part-button-default-action 'notmuch-show-view-part)
(notmuch-saved-searches
'((:name "📥 inbox" :query "tag:inbox and not tag:deleted and not tag:spam" :key "i")
(:name "💬 unread (inbox)" :query "tag:unread and tag:inbox and not tag:deleted and not tag:spam" :key "u")
(:name "🏳️ flagged" :query "tag:flagged" :key "f")
(:name "✍🏻 sent" :query "tag:sent" :key "t")
(:name "📰 drafts" :query "tag:draft" :key "d")
(:name "💭 all mail" :query "*" :key "a")))
(notmuch-mua-hidden-headers nil)
(notmuch-always-prompt-for-sender t)
;; Automatically move sent mails to Sent folder of corresponding
;; account and add/remove relevant tags
(notmuch-fcc-dirs `((,im-primary-mail . "gmail/Sent +sent -new -inbox -unread")
(,im-secondary-mail . "proton/Sent +sent -new -inbox -unread")))
:hook
;; Check if attachments are really attached before sending
;; Also see `notmuch-mua-attachment-regexp'
(notmuch-mua-send . notmuch-mua-attachment-check)
:general
(im-leader
"eN" #'im-notmuch-inbox
"en" #'notmuch-hello)
:config
(setq-default notmuch-search-oldest-first nil)
(evil-collection-notmuch-setup)
(general-def :keymaps 'notmuch-search-mode-map :states 'normal
;; gr → Refresh view
;; C → compose mail
"u" #'evil-collection-notmuch-search-toggle-unread
"gs" #'notmuch-search
"gS" #'notmuch-tree
"o" #'notmuch-search-show-thread
"d" #'im-notmuch-toggle-delete)
(general-def :keymaps 'notmuch-show-mode-map :states 'normal
"RET" #'im-open-thing-at-point
"o" #'notmuch-show-view-part
"O" #'notmuch-show-save-part)
;; I use the s key for another thing
(evil-collection-define-key 'normal 'notmuch-common-keymap
"s" nil
"S" nil)
;; Fix RET on notmuch-show-mode
(evil-collection-bind 'notmuch-show-mode-map
'action 'im-open-thing-at-point))
(use-package message :ensure nil :defer t :hook (message-setup . message-sort-headers) :custom (mail-user-agent 'message-user-agent) (message-mail-user-agent t) (message-elide-ellipsis "\n> [... %l lines elided]\n") (compose-mail-user-agent-warnings nil) (mail-signature "Isa Mert Gurbuz\nhttps://isamert.net\n") (message-citation-line-function #'message-insert-formatted-citation-line) (message-confirm-send nil) (message-kill-buffer-on-exit t))
(use-package sendmail :ensure nil :after message :config (setq send-mail-function 'sendmail-send-it) (setq sendmail-program (executable-find "msmtp")) (setq message-sendmail-envelope-from 'header))
(defun im-notmuch-inbox ()
"Open Inbox directly."
(interactive)
(require 'notmuch)
(let ((unread-query (plist-get (nth 1 notmuch-saved-searches) :query)))
(notmuch-search unread-query)))
(defun im-notmuch-toggle-delete ()
"Toggle the deleted tag on the current notmuch message.
If adding deleted, also add read."
(interactive)
(let ((tags (notmuch-search-get-tags)))
(if (member "deleted" tags)
(notmuch-search-tag (list "-deleted"))
(notmuch-search-tag (list "+deleted" "-unread")))
(notmuch-search-next-thread)))
(defvar im-unread-mail-count 0) (defvar im-mbsync-config-file (expand-file-name "~/.mbsyncrc") "Path to the mbsync configuration file.")
(defun im-mbsync-accounts ()
"Return channel names declared in `im-mbsync-config-file'.
mbsync synchronizes Channels, while Groups are named collections of
Channels. This configuration uses one Channel per account, with names
such as \"gmail\" and \"proton\", so Channel names can be treated as
account names and synchronized independently in parallel.
If the configuration later has multiple Channels for one account,
create one Group per account and parse the Group declarations instead.
Running each Channel independently could otherwise create concurrent
connections to the same account and local mail store."
(unless (file-readable-p im-mbsync-config-file)
(error "Cannot read mbsync config: %s" im-mbsync-config-file))
(with-temp-buffer
(insert-file-contents im-mbsync-config-file)
(let (accounts)
(goto-char (point-min))
(while (re-search-forward
"^[ \t]*Channel[ \t]+\\([^# \t\n]+\\)"
nil t)
(push (match-string-no-properties 1) accounts))
(unless accounts
(error "No mbsync channels found in %s"
im-mbsync-config-file))
(nreverse (delete-dups accounts)))))
(defun im-mail-notmuch-batch-rules (accounts)
"Return notmuch batch-tagging rules for ACCOUNTS.
Each account is tagged according to its local maildir path. Additional
folder-specific rules are added for known Proton and Gmail folders."
(append
(mapcar
(lambda (account)
(format "+%s -- not tag:%s and path:%s/**"
account account account))
accounts)
(when (member "proton" accounts)
'("+sent -inbox -- not tag:sent and folder:proton/Sent"
"+spam -- not tag:spam and folder:proton/Spam"))
(when (member "gmail" accounts)
'("+spam -- not tag:spam and folder:gmail/[Gmail]/Spam"))))
(async-defun im-sync-mail (&optional interactive?)
"Sync mail, update the notmuch index, and notify about unread mail.
Account names are discovered from the Channel declarations in
`im-mbsync-config-file'. All discovered Channels are synchronized
in parallel.
When called interactively, use --pull-new and restrict synchronization
to INBOX, so that only newly arrived inbox messages are downloaded.
Non-interactive calls perform the normal synchronization configured
for each Channel."
(interactive (list t))
(when (im-check-internet-connection)
(let ((accounts (im-mbsync-accounts)))
(condition-case reason
(let (count mails)
(when interactive?
(message ">> Checking mail..."))
;; Construct all promises before awaiting them so that the
;; account synchronizations run concurrently.
(await
(promise-all
(mapcar
(lambda (account)
(im-shell-command
:command "mbsync"
:args
(if interactive?
;; Only download newly arrived INBOX messages.
(list "--pull-new"
(format "%s:INBOX" account))
;; Perform the channel's normal full synchronization.
(list account))
:buffer-name (format "*mbsync-%s*" account)
:async t))
accounts)))
;; Index mail, discarding all output from `notmuch new'.
(ignore
(await
(im-shell-command
:command "notmuch"
:args '("new")
:buffer-name "*notmuch*"
:async t)))
;; Apply account and folder tags, discarding all tagging output.
(ignore
(await
(im-shell-command
:command
(concat
"printf '%s\\n' "
(mapconcat
#'shell-quote-argument
(im-mail-notmuch-batch-rules accounts)
" ")
" | notmuch tag --batch")
:buffer-name "*notmuch-tag*"
:async t)))
(setq mails
(->> (im-shell-command
:command "notmuch"
:args
'("search"
"tag:inbox and tag:unread and not tag:spam")
:buffer-name "*notmuch-search*"
:async t)
(await)
(s-trim)
(funcall
(lambda (output)
(s-split "\n" output t)))
(--map
(nth 1 (s-split-up-to " " it 1)))))
(setq count (length mails))
(when (or interactive?
(and (> count 0)
(not (equal im-unread-mail-count count))))
(when interactive?
(im-notmuch-inbox)
(message ">> Checking mail...Done"))
(setq im-unread-mail-count count)
(unless interactive?
(im-notif
:title (format "New mail (%s)!" count)
:message (s-join "\n" mails)
:source #'im-notmuch-inbox
:labels '("mail")))))
(error
;; Avoid notifications caused by transient failures while the
;; computer is asleep or has only just awakened.
(when (or (not (current-idle-time))
(<= (time-to-seconds (current-idle-time)) 60))
(alert
(format
"Exit code: %s. See buffers *notmuch*, *notmuch-tag*, %s."
reason
(mapconcat
(lambda (account)
(format "*mbsync-%s*" account))
accounts
", "))
:title "Checking for mail failed!")))))))
(run-with-timer 60 (* 15 60) #'im-sync-mail)
Mail auto completion for my contacts
With the following, "M-o m" completes email addresses of my contacts.
(run-with-timer
10 nil
(lambda ()
(im-cape
:name people-emails
:completion
(lambda ()
(->>
(im-deserialize-from-file "~/.emacs.d/sync/people")
(-map
(lambda (entry)
(let ((mails (--filter (s-prefix? "EMAIL" (car it)) entry)))
(--map
(let ((type (s-titleize (s-chop-prefix "_" (s-chop-prefix "EMAIL" (car it))))))
(list
(cdr it)
;; annotation
(concat (if (s-blank? type) "" (concat type " of " )) (map-elt entry "ITEM"))
;; doc
entry))
mails))))
(-flatten-n 1)
(-non-nil)))
:extractor (lambda (xs) (mapcar #'car xs))
:bound symbol
:category symbol
:key "M-o m"
:doc (lambda (xs x) (pp-to-string (nth 1 (map-elt xs x))))
:annotate (lambda (xs x) (concat " -> " (nth 0 (map-elt xs x)))))
(im-cape
:name buffer-names
:completion
(lambda ()
(mapcar
(lambda (b)
(let* ((name (buffer-name b))
(file (or (buffer-file-name b) ""))
(mode (with-current-buffer b (format "%s" major-mode ))))
(cons
name
(list
:ann (string-trim (concat (or mode "?") " "))
:doc (format "Buffer: %s\nFile: %s\nMode: %s\n\n%s"
(buffer-name b)
(or file "N/A")
(or mode "N/A")
(with-current-buffer b
(buffer-substring-no-properties
(point-min) (save-excursion (goto-char (point-min)) (forward-line 10) (point)))))))))
(seq-filter
(lambda (b) (not (string-prefix-p " " (buffer-name b))))
(buffer-list))))
:extractor (lambda (xs) (mapcar #'car xs))
:bound symbol
:category file
:key "M-o b"
:doc (lambda (xs x) (plist-get (map-elt xs x) :doc))
:annotate (lambda (xs x) (concat " → " (plist-get (map-elt xs x) :ann))))))
easysession.el – session manager
Simple package to save/retrieve Emacs sessions. It works well with tab-bar. Currently my workflow is:
`easysession-save-as' :: Save a named session. `easysession-switch-to' :: Load a session. Usually called after starting Emacs.
By default, all file visiting buffers, dired buffers, and indirect buffers are persisted and restored.
(use-package easysession :ensure (:host github :repo "jamescherti/easysession.el") :hook (elpaca-after-init . easysession-save-mode) :diminish easysession-save-mode :custom (easysession-mode-line-misc-info t) (easysession-buffer-list-function #'im-tab-bar-visible-buffers) :config (setq easysession-mode-line-misc-info-prefix " S:["))
(use-package im-git
:ensure `(:repo ,im-packages-path :files ("im-git.el"))
:defer t
:init
(add-hook 'im-git-commit-finished-hook #'im-update-git-state)
(add-hook 'im-git-commit-pre-hook #'im-git-check-commit)
:general
(im-leader-v "g" #'im-git-transient)
:config
(im-when-on
:darwin (setopt im-git-path "/opt/homebrew/bin/git"))
(transient-define-prefix im-git-transient ()
[["Status"
("s" "Show status" im-git-status)
("B" "Blame reveal mode" blame-reveal-mode)
("R" "Refresh state" im-update-git-state)]
["Commit/Branch"
("c" "Commit changes" im-git-commit)
("al" "Amend last commit" im-git-amend-last-commit)
("aa" "Amend to any commit" im-git-amend-commit)
("ar" "Reword any commit" im-git-reword-commit)
("bc" "New branch" vc-create-branch)
("bs" "Switch branch" vc-switch-branch)
("P" "Push" vc-push)
("p" "Pull" vc-pull)]
["Log/History"
("t" "Open timemachine" git-timemachine-toggle)
("hr" "Repo log" vc-print-root-log)
("hf" "File log" vc-print-log)]
["Stash/tags/..."
("E" "Stash changes" vc-git-stash)
("e" "Show stashes" im-git-list-stash)
("T" "Show tags" im-git-list-tags)]
["Worktrees"
("wa" "Add worktree (existing)" im-git-worktree-add-existing)
("wA" "Add worktree (new)" im-git-worktree-add-new)
("ws" "Switch worktree" im-git-worktree-switch)
("wd" "Remove worktree" im-git-worktree-delete)]
["Diff"
("d" "Diff hunk" diff-hl-show-hunk)
("[" "Prev hunk" diff-hl-previous-hunk)
("]" "Next hunk" diff-hl-next-hunk)
("{" "Show prev hunk" diff-hl-show-hunk-previous)
("}" "Show next hunk" diff-hl-show-hunk-next)
("S" "Stage hunk" diff-hl-stage-dwim)
("r" "Revert hunk" diff-hl-revert-hunk)]
["GitLab"
("ms" "Search projects" lab-search-project)
("mm" "MRs assigned to me" lab-list-my-merge-requests)
("ma" "Group MRs" lab-list-group-merge-requests)
("mp" "Project MRs" lab-list-project-merge-requests)
("mc" "Create MR" lab-create-merge-request)]
["Git Link"
("lm" "Merge Requests" im-git-link-merge-requests)
("ll" "On Branch" im-git-link-on-branch)
("lL" "Commit hash" im-git-link-commit)
("lc" "Commit (hash at point)" git-link-commit)
("lh" "Homepage" git-link-homepage)]]))
(async-defun im-git-check-commit (diff)
"Create an automated TODO list for my commit.
This checks for some mistakes I repeadetly do and tries to warn me in
the commit buffer."
(goto-char (point-max))
(let (todos
(default-directory (im-current-project-root)))
(condition-case reason
(progn
(let* ((new-files
(s-split "\n" (await (lab--git "ls-files" "--others" "--exclude-standard")) t))
(has-tags?
(not (s-blank? (s-trim (await (lab--git "tag" "--list"))))))
(last-tag
(when has-tags?
(await (lab--git "describe" "--tags" "--abbrev=0"))))
(last-tag-date
(when has-tags?
(await (lab--git "log" "-1" "--format=%ai" last-tag))))
(commit-count-since-last-tag
(when has-tags?
(string-to-number (or (await (lab--git "rev-list" (format "%s..HEAD" last-tag) "--count")) 0)))))
(when (and diff (s-matches? "^-;; Version: " diff))
(setq todos (concat todos "\n- [ ] Version updated: Add tag with new version")))
(when (and (not (f-parent-of?
im-git-work-root
default-directory))
(save-excursion (search-forward im-work-email nil t)))
(setq todos (concat todos "\n- [ ] Wrong email: using work, should be personal")))
(when (length> new-files 0)
(setq todo t)
(setq todos (concat todos (format "\n- [ ] %s new file, stage or delete them" (length new-files)))))
(when (and commit-count-since-last-tag (> commit-count-since-last-tag 0))
(setq todos (concat todos (format "\n- [ ] Check: commits since last tag is **%s**" commit-count-since-last-tag)))
(setq todos (concat todos (format "\n- [ ] Check: last tag date is *%s*" last-tag-date))))))
(error (message "im-git :: warn: Failed to run commit checkers: %s" reason)))
;; TODO: Maybe add an after-message hook that checks if these checks are ticked?
(when todos
(let ((inhibit-read-only t))
(goto-char (point-min))
(search-forward "\n")
(insert "\n# Checks" todos)))))
(defun im-update-git-state (&rest _)
"Refresh VC and diff-hl state for live programming buffers in this Git worktree."
(interactive)
(when-let* ((root (ignore-errors
(file-name-as-directory
(file-truename (or (vc-git-root default-directory) default-directory))))))
;; Do not use `project-buffers': its default implementation matches
;; `default-directory' lexically and consequently misses symlinked paths.
(dolist (buffer (buffer-list))
(with-current-buffer buffer
(when (and buffer-file-name
(derived-mode-p 'prog-mode))
(let* ((file buffer-file-name)
(buffer-root (ignore-errors
(when-let* ((git-root
(or (vc-git-root file)
(vc-git-root (file-truename file)))))
(file-name-as-directory
(file-truename git-root))))))
(when (equal root buffer-root)
(ignore-errors
(vc-refresh-state) ;; Refresh VC first, since diff-hl obtains its state from VC
(when diff-hl-mode
(diff-hl-update))))))))))
(use-package im-readeck
:ensure `(:repo ,im-packages-path :files ("im-readeck.el"))
:custom
(im-readeck-url im-homeserver-readeck-url)
(im-readeck-token im-homeserver-readeck-token)
(im-readeck-elfeed-tags '(later bookmark readeck))
:defer t
:init
(with-eval-after-load 'elfeed
(require 'im-readeck)
(im-readeck-enable-elfeed-support)))
(use-package tabku
:ensure (:host github :repo "isamert/tabku.el")
:defer t
:general
(im-leader
;; Save current tab window configuration to a key
"ww2" #'tabku-configuration-save-current
"ww3" #'tabku-configuration-save-current
"ww4" #'tabku-configuration-save-current
"ww5" #'tabku-configuration-save-current
;; Restore a saved configuration with a key
"w2" #'tabku-configuration-restore-current
"w3" #'tabku-configuration-restore-current
"w4" #'tabku-configuration-restore-current
"w5" #'tabku-configuration-restore-current
;; Restore a saved configuration but keep the current buffer
"W2" #'tabku-configuration-restore-current-sticky
"W3" #'tabku-configuration-restore-current-sticky
"W4" #'tabku-configuration-restore-current-sticky
"W5" #'tabku-configuration-restore-current-sticky))
(use-package im-filebrowser
:ensure `(:repo ,im-packages-path :files ("im-filebrowser.el"))
:defer t
:custom
(im-filebrowser-url im-homeserver-filebrowser-url)
(im-filebrowser-username im-homeserver-username)
(im-filebrowser-password im-homeserver-password)
(im-filebrowser-base-path "/ssh:files:"))
(use-package im-kube
:ensure `(:repo ,im-packages-path :files ("im-kube.el"))
:defer t)
(use-package im-nextcloud
:ensure `(:repo ,im-packages-path :files ("im-nextcloud.el"))
;; For automatic Maps & Contacts integration
:after (org)
:custom
(im-nextcloud-url im-secrets-nextcloud-url)
(im-nextcloud-user im-secrets-nextcloud-user)
(im-nextcloud-auth im-secrets-nextcloud-auth))
(use-package im-char-picker
:ensure `(:repo ,im-packages-path :files ("im-char-picker.el"))
:general
(im-leader "ic" #'im-char-picker)
(:states '(insert)
"M-u" #'im-char-picker))
im-arr — arr-stack functionality (radarr, sonarr etc.)
Also see: orgmdb.el
(use-package im-arr
:ensure `(:repo ,im-packages-path :files ("im-arr.el"))
:defer t
:custom
(im-radarr-url im-homeserver-radarr-url)
(im-radarr-api-key im-homeserver-radarr-api-key)
(im-sonarr-url im-homeserver-sonarr-url)
(im-sonarr-tmdb-bearer-token im-tmdb-token)
(im-sonarr-api-key im-homeserver-sonarr-api-key))
(use-package im-ntfy
:ensure `(:repo ,im-packages-path :files ("im-ntfy.el"))
:defer 60
:custom
(im-ntfy-server im-homeserver-ntfy-server)
(im-ntfy-topics im-homeserver-ntfy-topics)
(im-ntfy-username im-homeserver-ntfy-username)
(im-ntfy-password im-homeserver-ntfy-password)
:config
(im-ntfy-subscribe-all))
(use-package im-wpm :ensure nil :hook (elpaca-after-init . im-wpm-mode))
(use-package ereader
:ensure (:host github :repo "bddean/emacs-ereader")
:after (org)
:config
;; For org links integration
(require 'org-ebook)
(defun im-ereader-setup ()
(interactive)
;; Otherwise it gets weird due to `variable-pitch-mode'
(setq-local page-break-lines-max-width 80)
(page-break-lines-mode)
(variable-pitch-mode))
(add-hook 'ereader-mode-hook 'im-ereader-setup))
(use-package pdf-tools
:defer t
:general
;; P → Fit to page
;; W → Fit to window
(:states '(normal visual) :keymaps 'pdf-view-mode-map
"J" #'pdf-view-next-page-command
"K" #'pdf-view-previous-page-command
"s" 'pdf-occur)
:magic ("%PDF" . pdf-view-mode)
:config
(pdf-tools-install :no-query)
(defun im-pdf-view-setup ()
(hl-line-mode -1))
(evil-collection-pdf-setup)
(evil-collection-define-key 'visual 'pdf-view-mode-map
"y" 'evil-collection-pdf-yank)
(add-hook 'pdf-view-mode #'im-pdf-view-setup))
(use-package im-macos
:ensure `(:repo ,im-packages-path :files ("im-macos.el"))
:if (eq system-type 'darwin)
:demand t)
(use-package file-info :ensure (:host github :repo "artawower/file-info.el") :defer t)
(use-package im-occur
:ensure `(:repo ,im-packages-path :files ("im-occur.el"))
:hook (occur-mode-hook . im-occur-mode)
:config
(add-hook 'occur-mode-hook 'im-occur-mode))
(use-package im-chawan
:ensure `(:repo ,im-packages-path :files ("im-chawan.el"))
:defer t)
Editing
Breaking long texts/comments into multiple lines
I use M-q (fill-paragraph) to break long texts into multiple
lines. It also works well within comment sections. 80 col length is
quite readable. See how this item is formatted, it's done
automatically by the usage of M-q.
(setq fill-column 80)
synosaurus & wordnut
Both synosaurus and wordnut uses wordnet to work. Use
synosaurus-choose-and-replace to replace the current word with
one of it's synonyms.
(use-package synosaurus :defer t :general (im-leader "mr" #'synosaurus-choose-and-replace) :custom (synosaurus-choose-method nil))
(use-package wordnut :defer t)
(use-package sozluk :ensure (:host github :repo "isamert/sozluk.el") :defer t :custom ;; (sozluk-include-etymology-on-sozluk t) (sozluk-deasciify-if-not-found t))
This is an optional dependency for sozluk.
(use-package turkish :defer t :general (im-leader-v "tc" #'turkish-correct-region))
jinx – spellchecker, flyspell alternative
I was using flyspell and flyspell-correct before but Jinx works
much better & faster out-of-the-box. It automatically works nicely
in code buffers too!
You need to install enchant to make Jinx work (and compile).
Recently I have been having random crashes, and it turns out (after a very long session of debugging) the reason is enchant. enchant SEGFAULTs when using Apple Spell, so what I did is:
rm /opt/homebrew/Cellar/enchant/2.8.2/share/enchant-2/AppleSpell.config
rm /opt/homebrew/Cellar/enchant/2.8.2/lib/enchant-2/enchant_applespell.so
rm /opt/homebrew/Cellar/enchant/2.8.2/lib/enchant-2/enchant_applespell.a
Seems like other people also had this issue and resolved differently, like building enchant from source etc:
(use-package jinx :hook (emacs-startup . global-jinx-mode) :custom (jinx-languages "en_US tr_TR") :diminish jinx-mode :general (:keymaps 'evil-normal-state-map "z=" #'jinx-correct-word ;; Override flyspell bindings from evil-commands with jinx ones "[s" #'jinx-previous "]s" #'jinx-next) (:keymaps 'evil-visual-state-map "z=" #'jinx-correct-word) :config (add-to-list 'global-jinx-modes 'slack-message-compose-buffer-mode))
puni & combobulate & electric-pair-mode (structrual editing stuff)
Using `electric-pair-mode' to automatically pair parenthesis.
(add-hook 'after-init-hook #'electric-pair-mode) (diminish 'electric-pair-mode)
`puni' provides structral editing for a lot of languages using some Emacs built-ins. It's pretty good most of the time and I really use a very small subset of it's feature set.
(use-package puni
:ensure (:host github :repo "AmaiKinono/puni")
:general
(:keymaps '(prog-mode-map inferior-emacs-lisp-mode-map) :states '(insert)
"M-[" #'puni-barf-forward
"M-]" #'puni-slurp-forward
"M-d" #'puni-splice
"M-t" #'puni-transpose
"M-\\" #'puni-split
"M-(" #'puni-wrap-round
"M-{" #'puni-wrap-curly)
(:keymaps 'prog-mode-map :states '(normal)
"(" #'puni-backward-sexp-or-up-list
")" #'puni-forward-sexp-or-up-list))
`combobulate' provides structural editing for some languages using treesit.el, here are the bindings that I find useful (which are enabled by `combobulate-mode'):
- M-P (also M-k)
- combobulate-drag-up
- M-N (also M-j)
- combobulate-drag-down
I also replace `expand-region' with combobulate equivalent and also re-bind some of the bindings from `puni' with their combobulate equivalent:
- M-w
- combobulate-mark-node-dwim
- M-t
- combobulate-transpose-sexps
The reason I use a very little subset of these packages is that I already use evil-mode and it's editing capabilities cover most of the feature set provided by puni and combobulate.
writeroom-mode
Gives you a nice, uncluttered editing experience by removing all unneeded visual clutter and by justifying the text in the middle.
(use-package writeroom-mode :defer t :commands writeroom-mode :config (setq writeroom-fullscreen-effect 'maximized) (setq writeroom-global-effects nil) (setq writeroom-mode-line-toggle-position 'header-line-format) (setq writeroom-width 81))
Dummy IDE mode
I try to use lsp-mode and other language-specific packages for
the languages I use (see ), but sometimes
either they are too slow or the computer I'm currently working on
requires some extra setup or I just don't want to use them for some
reason. For those cases, I use a collection of packages that gives
you the power of IDEs but in some dummy/restricted way.
- Automatically highlights the all instances of the symbol under the cursor in the buffer. Simply use evils
*and#to jump between them. - Jumps to definition by using predefined-regexps, generally works fine. Use
gd. - (no term)
- To debug why it's not working: M-x
set-variable dumb-jump-debug t, then go to Messages buffer. - This is a generic parser for bunch of languages. You can also inspect the syntax tree on the fly and do whatever you want to do with it. Best feature so far is just better (like, miles ahead better) syntax highlighting for some languages. Especially for JS/TS and Rust.
- Highlight TODO/FIXME etc.
- (no term)
- Use
]tand[tto go next/prev TODO/FIXME item.
(use-package dumb-jump :defer t :init (add-hook 'xref-backend-functions #'dumb-jump-xref-activate 80) :config ;; ag is supported by nearly every rule but rg is not. ;; also see: https://github.com/jacktasia/dumb-jump/issues/376 (setq dumb-jump-force-searcher 'ag) (setq dumb-jump-ignore-context nil) (setq dumb-jump-fallback-search t) (remove-hook 'xref-backend-functions #'etags--xref-backend))
(use-package hl-todo
:hook (prog-mode . hl-todo-mode)
:bind ( :map evil-normal-state-map
("[t" . hl-todo-previous)
("]t" . hl-todo-next))
:config
(setq hl-todo-keyword-faces '(("TODO" . "#FF0000")
("FIXME" . "#FF0000")
("DEBUG" . "#A020F0")
("GOTCHA" . "#FF4500")
("STUB" . "#1E90FF")))
(define-advice hl-todo-next (:after (&rest _) reveal) (reveal-post-command))
(define-advice hl-todo-previous (:after (&rest _) reveal) (reveal-post-command)))
(setq
hi-lock-face-defaults
'("hi-salmon" "hi-aquamarine" "hi-blue" "hi-yellow" "hi-pink" "hi-green" "hi-black-b" "hi-blue-b" "hi-red-b" "hi-green-b" "hi-black-hb"))
(defun im-highlight-thing-at-point-dwim ()
"Hightlight or unhighlight current symbol or selection."
(interactive)
(cond
((--any?
(-contains? hi-lock-face-defaults
(symbol-name it))
(-flatten (list (plist-get (text-properties-at (point)) 'face))))
(unhighlight-regexp
(if (use-region-p)
(regexp-quote (buffer-substring-no-properties (region-beginning) (region-end)))
(hi-lock-regexp-okay (find-tag-default-as-symbol-regexp)))))
((use-region-p)
(highlight-regexp
(regexp-quote (buffer-substring-no-properties (region-beginning) (region-end))))
(deactivate-mark))
((thing-at-point 'symbol)
(hi-lock-face-symbol-at-point))
(t (unhighlight-regexp t))))
(evil-define-key 'normal 'global (kbd "M-h") #'im-highlight-thing-at-point-dwim (kbd "M-H") (λ-interactive (unhighlight-regexp t)))
Media/feed/IRC
I try to maximize my Emacs usage which brings it's own benefits and downsides which I will not go over here. Here are some packages and configurations that are not related to programming/editing.
elfeed (RSS feeds)
Feed reader.
- Filter examples (after hitting
s) - +tag OR -tag (unread is also a tag)
- #number-of-entries-limit (like #20)
- !inverse-regex (!x?emacs will filter out titles containing x?emacs regex)
- =regex (entries that contains the regex will be shown)
- +unread +youtube =emacs #10 @5-months-ago
(use-package elfeed
:defer t
:commands (elfeed im-elfeed-reload-and-open)
:general
(im-leader
"ee" #'elfeed)
(:keymaps 'elfeed-search-mode-map :states 'normal
"o" #'elfeed-search-browse-url
"O" #'im-elfeed-search-browse-url-in-default-browser
"q" #'im-elfeed-quit)
:config
(evil-collection-elfeed-setup)
(evil-collection-define-key 'normal 'elfeed-search-mode-map
"s" nil
"S" nil
"gs" 'elfeed-search-live-filter
"gS" 'elfeed-search-set-filter)
(evil-collection-define-key 'normal 'elfeed-show-mode-map
"s" nil
"S" nil)
;; When adding tags, don't add any hierarchical tags like (blog
;; blog-software), or (metal metal-black) Just use something like:
;; (blog software) and (metal black)
(require 'im-feeds)
(setq elfeed-search-title-max-width 100)
(setq elfeed-curl-extra-arguments '("--netrc"))
(setq elfeed-feeds (mapcar #'im-elfeed--expand im-feeds))
;; To apply hooks to all existing entries, use: elfeed-apply-hooks-now
(im-elfeed-auto-tag-url '(("youtube\\.com" youtube)))
(im-elfeed-auto-tag-title '(("youtube\\.com" youtube)
("c\\+\\+" (programming c++))
("python" (programming python))
("haskell" (programming haskell)))))
(defun im-elfeed-search-browse-url-in-default-browser () "Open URL in the default browser." (interactive) (with-default-browser (elfeed-search-browse-url)))
(defun im-elfeed-auto-tag-url (pairs)
"Takes a list of url-regex and tag-list pairs and adds a new entry hook for each of them."
(--map
(add-hook 'elfeed-new-entry-hook
(elfeed-make-tagger :feed-url (car it)
:add (cdr it)))
pairs))
(defun im-elfeed-auto-tag-title (pairs)
"Takes a list of title-regex and tag-list pairs and adds a new entry hook for each of them."
(--map
(add-hook 'elfeed-new-entry-hook
(elfeed-make-tagger :entry-title (car it)
:add (cdr it)))
pairs))
https://github.com/skeeto/.emacs.d/blob/master/etc/feed-setup.el
(defvar youtube-feed-format
'(("^UC" . "https://www.youtube.com/feeds/videos.xml?channel_id=%s")
("^PL" . "https://www.youtube.com/feeds/videos.xml?playlist_id=%s")
("" . "https://www.youtube.com/feeds/videos.xml?user=%s")))
(defun im-elfeed--expand (listing)
"Expand feed URLs depending on their tags."
(cl-destructuring-bind (url . tags) listing
(cond
((member 'youtube tags)
(let* ((case-fold-search nil)
(test (lambda (s r) (string-match-p r s)))
(format (cl-assoc url youtube-feed-format :test test)))
(cons (format (cdr format) url) tags)))
((member 'reddit tags)
(cons (format "https://www.reddit.com/r/%s/.rss" url) tags))
((member 'gh-release tags)
(cons (format "https://github.com/%s/releases.atom" url) tags))
(listing))))
(defun im-elfeed-quit () (interactive nil elfeed-search-mode) (kill-buffer "*elfeed-search*") ; This saves db automatically (quit-window) (elfeed-db-unload))
empv (music/media/radio/youtube management)
Manage media and streams through completing-read.
(use-package empv
:ensure (:host github :repo "isamert/empv.el")
:defer t
:autoload (empv--select-action)
:general
(im-leader
"r" empv-map
"R" #'empv-hydra/body
"r4" #'empv-subsonic-search)
:config
(require 'im-radio-channels)
(setq empv-radio-channels im-radio-channels)
(setq empv-radio-log-file "~/Documents/notes/songs.org")
(setq empv-base-directory "~/Music/")
(setq empv-video-dir `("~/Videos" "~/Movies" ,(format "/run/media/%s/BINGUS/Videos" (user-login-name)) ,(format "/run/media/%s/FLOPPA/Videos" (user-login-name))))
(setq empv-audio-dir `("~/Music" ,(format "/run/media/%s/BINGUS/Music" (user-login-name)) ,(format "/run/media/%s/FLOPPA/Music" (user-login-name))))
(setq empv-allow-insecure-connections t)
(setq empv-invidious-instance im-empv-invidious-instance)
(setq empv-invidious-request-headers `(("isamert-token" . ,im-token)))
;; ^ see https://api.invidious.io/
(setq empv-youtube-use-tabulated-results t)
(add-to-list 'empv-mpv-args "--ytdl-format=bestvideo+bestaudio/best[ext=mp4]/best")
;; (add-to-list 'empv-mpv-args "--ytdl-raw-options=cookies-from-browser=firefox")
(add-to-list 'empv-mpv-args "--save-position-on-quit")
(setq empv-reset-playback-speed-on-quit t)
(add-hook 'empv-init-hook #'empv-override-quit-key)
(add-hook 'empv-youtube-results-mode-hook #'im-disable-line-wrapping)
(add-hook 'empv-media-title-changed-hook #'im-update-global-mode-line)
(add-hook 'empv-player-state-changed-hook #'im-update-global-mode-line)
(setq empv-subsonic-url im-navidrome-server)
(setq empv-subsonic-username im-navidrome-username)
(setq empv-subsonic-password im-navidrome-password)
(evil-make-overriding-map empv-youtube-results-mode-map 'normal)
(with-eval-after-load 'embark (empv-embark-initialize-extra-actions))
(with-eval-after-load 'org
(add-to-list 'org-file-apps '("\\.\\(mp3\\|ogg\\)\\'" . (lambda (path _str) (empv-play-file path))))))
(defun im-export-radio-channels-as-m3u (file)
"Export radio list into an M3U FILE."
(interactive
(list
(read-file-name
"Where to save the .m3u file?"
"~/Documents/sync/"
"radiolist.m3u")))
(with-temp-file file
(->>
im-radio-channels
(--map
(format
"#EXTINF:0, %s\n%s"
(car it)
;; Replace http:// with icyx://, because VLC on Android can't
;; retrieve song name if the stream is on http://
(if (s-contains? "radcap.ru" (car it))
(s-replace "http://" "icyx://" (cdr it))
(cdr it))))
(--reduce (format "%s\n%s" acc it))
(s-prepend "#EXTM3U\n")
(insert))))
orgmdb (movies & shows)
I have a file called watchlist.org where I keep list of movies and shows that I watched and going to watch. Here are some packages and functions to deal with them.
(use-package orgmdb
:defer t
:ensure (:host github :repo "isamert/orgmdb.el")
:bind (:map evil-normal-state-map ("go" . orgmdb-act))
:config
(setq orgmdb-omdb-apikey im-orgmdb-omdb-apikey)
(setq orgmdb-poster-folder "~/Documents/notes/data/posters")
(setq orgmdb-fill-property-list '(genre runtime director country imdb-id imdb-link imdb-rating metascore tomatometer actors poster plot)))
(use-package rcirc
:ensure nil
:defer t
:custom
(rcirc-server-alist `(("irc.libera.chat"
:port 6697
:nick "isamert"
:password ,im-libera-chat-password
:encryption tls
:channels ("#rcirc" "#emacs"))))
(rcirc-default-nick "isamert")
(rcirc-default-full-name "Isa Mert Gurbuz")
(rcirc-reconnect-delay 15)
(rcirc-reconnect-attempts 5)
:config
(evil-set-initial-state 'rcirc-mode 'normal)
(add-hook 'rcirc-mode-hook #'im-rcirc-setup-buffer)
(with-eval-after-load 'all-the-icons
(setq rcirc-prompt (concat
(let ((all-the-icons-default-adjust 0))
(all-the-icons-faicon "angle-double-right")) "_ "))))
(defun im-rcirc-setup-buffer () (setq-local scroll-conservatively 8192) (whitespace-mode -1) (rcirc-omit-mode +1) (rcirc-track-minor-mode +1) (turn-on-visual-line-mode))
Keybindings
Keybindings are generally set in-place, following have no context, so they are here.
(global-set-key (kbd "<escape>") 'keyboard-escape-quit) (global-set-key (kbd "C-x <escape>") 'keyboard-escape-quit) (global-set-key (kbd "C-c <escape>") 'keyboard-escape-quit)
(evil-define-key 'normal prog-mode-map "gd" 'xref-find-definitions "gr" 'xref-find-references)
(evil-define-key 'normal prog-mode-map (kbd "M-;") 'comment-line) (evil-define-key 'visual prog-mode-map (kbd "M-;") 'comment-dwim)
(evil-define-key 'normal 'global (kbd "M-d") #'im-kill-this-buffer)
(im-leader
"1" (λ-interactive
(call-interactively (local-key-binding (kbd "C-c C-c")))))
(defun im-execute-extended-command-alt ()
(interactive)
(let ((current-prefix-arg '(4)))
(call-interactively #'execute-extended-command)))
(im-leader-v "x" #'execute-extended-command "X" #'im-execute-extended-command-alt "/k" #'helpful-key "/f" #'helpful-callable "/v" #'helpful-variable "/l" #'find-library "/." #'display-local-help "/F" #'describe-face)
Programming languages
General/language-agnostic functionality
Jump to beginning/end of a statement
Using [w and ]w, I can jump between statements. For
TODO: should be able to descend down the tree
(defun im-treesit-end-of-statement ()
(interactive)
(treesit-end-of-thing
(rx (or "local_variable" "return" "if" "for" "with" "try" "method" "lexical" "call")
(or "_statement" "_declaration" "_definition" "_expression"))))
(defun im-treesit-beginning-of-statement ()
(interactive)
(treesit-beginning-of-thing
(rx (or "local_variable" "return" "if" "for" "with" "try" "method" "lexical" "call")
(or "_statement" "_declaration" "_definition" "_expression"))))
Only tried in java-ts-mode so far
(evil-define-key 'normal java-ts-mode-map "[w" 'im-treesit-beginning-of-statement) (evil-define-key 'normal java-ts-mode-map "]w" 'im-treesit-end-of-statement) (evil-define-key 'normal tsx-ts-mode-map "[w" 'im-treesit-beginning-of-statement) (evil-define-key 'normal tsx-ts-mode-map "]w" 'im-treesit-end-of-statement) (evil-define-key 'normal typescript-ts-mode-map "[w" 'im-treesit-beginning-of-statement) (evil-define-key 'normal typescript-ts-mode-map "]w" 'im-treesit-end-of-statement)
(im-make-repeatable im-treesit-statement "[" im-treesit-beginning-of-statement "]" im-treesit-end-of-statement)
format-all, apheleia – Format buffers, automatically
Use format-all-buffer function to format current buffer. Works for any language.
(use-package format-all :defer t :commands (format-all-buffer format-all-region))
Apheleia is like format-all but works async and automatically
formats on buffer save. I keep both of them because sometimes I
need format-all-buffer and it works with more formatters.
(use-package apheleia :hook (elpaca-after-init . apheleia-global-mode) :diminish 'apheleia-mode :init (defalias 'im-toggle-auto-code-formatter #'apheleia-mode) ;; Disable it inside `emacs-lisp-mode' as I already use ;; `aggressive-indent-mode' for that. (add-hook 'emacs-lisp-mode-hook (lambda () (apheleia-mode -1))))
(use-package which-function :ensure nil :hook (elpaca-after-init . which-function-mode))
(defun im-disable-which-func () (which-function-mode -1))
Generally unnecessary and sometimes causes problems
(add-hook 'org-mode-hook #'im-disable-which-func)
outline-indent-mode – Code folding based on indendation
I generally use the built-in hs-minor-mode but it does not work with all languages. outline-indent is a more generic solution that works fairly well.
I used to use origami-mode for the use cases I mentioned but outline-indent is much better and makes more sense while folding. origami was a bit confusing (as the thing itself).
(use-package outline-indent :ensure (:host github :repo "jamescherti/outline-indent.el") :hook (python . outline-indent-minor-mode) (python-ts . outline-indent-minor-mode) (yaml . outline-indent-minor-mode) (yaml-ts . outline-indent-minor-mode) (docker-compose-mode . outline-indent-minor-mode) (sql-mode . outline-indent-minor-mode) :custom (outline-indent-ellipsis " ▼ "))
(use-package im-repl
:ensure `(:repo ,im-packages-path :files ("im-repl.el"))
:defer t
:general
(im-leader-v
";" (general-predicate-dispatch (im-eval-dwim #'eros-eval-last-sexp #'eval-region #'eros-eval-defun)
(eq major-mode 'java-ts-mode) #'im-repl-eval
(eq major-mode 'js-ts-mode) #'im-repl-eval
(eq major-mode 'typescript-ts-mode) #'im-repl-eval
(eq major-mode 'clojure-mode) (im-eval-dwim #'cider-eval-last-sexp #'cider-eval-region #'cider-eval-defun-at-point)
(eq major-mode 'lisp-mode) (im-eval-dwim #'slime-eval-last-expression #'slime-eval-region #'slime-eval-region)
(eq major-mode 'racket-mode) (im-eval-dwim #'racket-eval-last-sexp #'racket-send-region #'racket-send-definition)
(eq major-mode 'scheme-mode) (im-eval-dwim #'geiser-eval-last-sexp #'geiser-eval-region #'geiser-eval-definition)
(eq major-mode 'kotlin-mode) (im-eval-dwim #'kotlin-send-line #'kotlin-send-region #'kotlin-send-line)
;; I only use lua for hammerspoon
(eq major-mode 'lua-mode) (im-eval-dwim #'im-eval-hammerspoon #'im-eval-hammerspoon #'im-eval-hammerspoon))
"'" (general-predicate-dispatch #'eros-inspect-last-result
(eq major-mode 'java-ts-mode) #'im-repl-inspect-last-result
(eq major-mode 'js-ts-mode) #'im-repl-inspect-last-result
(eq major-mode 'typescript-ts-mode) #'im-repl-inspect-last-result
(eq major-mode 'clojure-mode) #'cider-inspect-last-result)))
(defun im-eval-dwim (lastf regionf defunf)
"Generate an interactive function that you can bind to a key.
It calls LASTF, REGIONF or DEFUNF depending on the context. Only
works for Lispy languages."
(lambda ()
(interactive)
(cond
((use-region-p)
(call-interactively regionf))
((or (-contains? '(?\) ?\") (char-before))
(-contains? '(?\ ?\)) (char-after)))
(call-interactively lastf))
(t
(call-interactively defunf)))))
(use-package md-ts-mode :ensure (:host github :repo "dnouri/md-ts-mode") :general (:states 'normal :keymaps 'md-ts-mode-map "TAB" #'outline-cycle "<backtab>" #'outline-cycle-buffer) :config (md-ts-mode-enable-global) (add-hook 'md-ts-mode-hook #'outline-minor-mode))
(use-package edit-indirect :after md-ts-mode)
(use-package haskell-mode :mode "\\.hs\\'")
(use-package lsp-haskell :after (lsp haskell) :config (setq lsp-haskell-process-path-hie "ghcide" lsp-haskell-process-args-hie '()))
(use-package rust-mode
:mode "\\.rs\\'"
:config
(setq lsp-rust-server 'rust-analyzer
lsp-rust-clippy-preference 'on))
web-mode
This helps me edit html files with css and javascipt inside them.
(use-package web-mode :mode "\\.html\\'")
java
See lsp-java configuration above.
(add-hook 'java-ts-mode-hook #'hs-minor-mode)
(use-package jsdoc :ensure (:host github :repo "isamert/jsdoc.el") :defer t)
(defun im-add-node-modules-to-path ()
"Add node_modules/.bin to variable `exec-path'."
(interactive)
(-some--> (locate-dominating-file "." "node_modules")
(expand-file-name it)
(f-join it "node_modules/.bin")
(setq-local exec-path `(,it ,@exec-path))))
(add-hook 'js-ts-mode-hook #'im-add-node-modules-to-path) (add-hook 'eshell-mode-hook #'im-add-node-modules-to-path)
Helper functions
TODO make this idempotent, it breaks absolute imports TODO make it work on a line instead of between quotes
(defun im-js-relative-import-to-abs ()
"Convert a relative import to an absolute import.
For example, if you are on the line:
import Test from '../../test'
This function transforms the line into:
import Test from 'src/a/b/test'"
(interactive)
(let ((fname (substring-no-properties (thing-at-point 'filename)))
(bounds (bounds-of-thing-at-point 'filename)))
(delete-region (car bounds) (cdr bounds))
(insert (s-chop-prefix (im-current-project-root) (expand-file-name fname)))))
(defun im-convert-js-object-to-json ()
"Convert selected JS object into JSON object.
Example:
{a: 3, b: 5}
is converted into
{
\"a\": 3,
\"b\": 5
}"
(interactive)
(if (use-region-p)
(shell-command-on-region
(region-beginning)
(region-end)
"node -e 'console.log(JSON.stringify(eval(\"(\" + require(\"fs\").readFileSync(0, \"utf-8\") + \")\"), null, 2))'"
(current-buffer)
t)
(user-error "Select something first")))
(defun im-convert-json-to-js-object ()
"Convert selected JS object into JSON object.
Example:
{
\"a\": 3,
\"b\": 5
}
is converted into
{
a: 3,
b: 5,
}"
(interactive)
(if (use-region-p)
(shell-command-on-region
(region-beginning)
(region-end)
"node -e 'console.log(require(\"util\").inspect(JSON.parse(require(\"fs\").readFileSync(0, \"utf-8\")), false, null))'"
(current-buffer)
t)
(user-error "Select something first")))
(defun im-refactor-debug-log-text () "Return a string in the following format: 'BufferName:FunctionName:LineNumber'. This is used in my snippets." (format "%s:%s:%s" (buffer-name) (which-function) (line-number-at-pos)))
(defun im-js-insert-debug-log-for-current-variable ()
"Insert a `console.log' line for currently focused variable."
(interactive)
(let* ((curr-indent)
(node (treesit-node-child-by-field-name
(car (jsdoc--tsc-find-descendants-with-type
(treesit-node-parent (treesit-node-at (point)))
"variable_declarator"))
"name"))
(text (treesit-node-text node)))
(goto-char (treesit-node-end (treesit-node-parent node)))
(end-of-line)
(setq curr-indent (current-indentation))
(insert "\n")
(insert (make-string curr-indent ? ))
(insert
(format
"console.log(\"%s\", %s) // FIXME remove log"
(im-refactor-debug-log-text)
(->>
text
(s-split "\n")
(-map #'s-trim)
(s-join " "))))))
(setq-default typescript-ts-mode-indent-offset 2)
(add-to-list 'auto-mode-alist '("\\.ts\\'" . typescript-ts-mode))
(add-hook 'typescript-ts-mode-hook #'im-add-node-modules-to-path)
(add-hook 'tsx-ts-mode-hook #'im-add-node-modules-to-path)
(add-hook 'typescript-ts-mode-hook #'hs-minor-mode)
(add-hook 'tsx-ts-mode-hook #'hs-minor-mode)
(use-package im-deno
:ensure `(:repo ,im-packages-path :files ("im-deno.el"))
:demand t)
(defun im-treesit-find-parent-with-type (node wanted-type)
"Get first parent of NODE where parents type is WANTED-TYPE."
(let ((curr-node node)
(curr-type nil))
(while (and curr-node (not (equal curr-type wanted-type)))
(setq curr-node (treesit-node-parent curr-node))
(setq curr-type (when curr-node (treesit-node-type curr-node))))
curr-node))
(defun im-ts-current-expression ()
"Get smallest meaningful expression (as something that can be sent to REPL)."
(let* ((current-node (treesit-node-at (point)))
(getter (lambda (it)
(when-let* ((node (im-treesit-find-parent-with-type current-node (car it))))
(if (numberp (cdr it))
(treesit-node-child node (cdr it))
node)))))
(->>
'(("lexical_declaration")
("expression_statement" . 0)
("function_declaration")
("interface_declaration")
("enum_declaration")
("import_statement")
("class_declaration"))
(-find getter)
(funcall getter))))
ob-deno
To allow net access (–allow-net), use ":allow net" in the src block header.
Use "return …" to get the object.
(use-package ob-deno :after org :ensure (:host github :repo "isamert/ob-deno"))
(with-eval-after-load 'org
(add-to-list 'org-src-lang-modes '("deno" . typescript-ts)))
json
hs-minor-mode works great with JSON.
(add-hook 'json-mode-hook #'hs-minor-mode) (add-hook 'json-ts-mode-hook #'hs-minor-mode)
Running JQ or Javascript on given JSON
I work with JSON a lot. Here I have some functions that let's you run given expression on selected JSON.
(defvar im-nodejs-runner-preface
(concat "const R = require(\"ramda\");\n"
(with-temp-buffer
(insert-file-contents (f-join im-packages-path "functions.js"))
(buffer-string))))
(defvar im-deno-runner-preface
(concat "const R = await import(\"https://esm.sh/v122/ramda@0.29.0\");\n"
(with-temp-buffer
(insert-file-contents (f-join im-packages-path "functions.js"))
(buffer-string))))
(defun im-run-jq-on-json (json expression &optional replace)
"Run given EXPRESSION with jq on JSON and return the result.
If REPLACE is non-nil, then clear the buffer or current
selection and insert the result.
When called with prefix argument, REPLACE becomes non-nil."
(interactive (im--read-json-and-expression))
(im--run-x-on-json
(with-temp-buffer
(insert json)
(shell-command-on-region (point-min) (point-max) (format "jq -r \"%s\"" expression) nil 't)
(buffer-string))
(called-interactively-p 'interactive)
replace))
(defun im-run-nodejs-on-json (json expression &optional replace)
"Run given EXPRESSION with nodejs on JSON and return the result.
If REPLACE is non-nil, then clear the buffer or current
selection and insert the result.
When called with prefix argument, REPLACE becomes non-nil."
(interactive (im--read-json-and-expression))
(im--run-x-on-json
(with-temp-buffer
(insert im-nodejs-runner-preface)
(insert "\n")
(insert (format "const it = %s;" json))
(insert (format "JSON.stringify(%s, null, 2)" expression))
(shell-command-on-region (point-min) (point-max) "node -p" nil 't)
(buffer-string))
(called-interactively-p 'interactive)
replace))
(defun im-run-deno-on-json (json expression &optional replace)
"Run given EXPRESSION with deno on JSON and return the result.
If REPLACE is non-nil, then clear the buffer or current
selection and insert the result.
When called with prefix argument, REPLACE becomes non-nil."
(interactive (im--read-json-and-expression))
(im--run-x-on-json
(with-temp-buffer
(insert im-deno-runner-preface)
(insert "\n")
(insert (format "const it = %s;" json))
(insert (format "console.log(JSON.stringify(%s, null, 2))" expression))
(shell-command-on-region (point-min) (point-max) "deno run --allow-all -" nil 't)
(buffer-string))
(called-interactively-p 'interactive)
replace))
(defun im--run-x-on-json (result interactive? &optional replace)
"Run given EXPRESSION with nodejs on JSON and return the result.
If REPLACE is non-nil, then clear the buffer or current
selection and insert the result.
When called with prefix argument, REPLACE becomes non-nil."
(when (and interactive?
(not replace))
(if (>= (max-mini-window-lines) (length (s-lines result)))
(message ">> %s" (s-trim result))
(with-current-buffer (get-buffer-create "*nodejs-result*")
(switch-to-buffer-other-window (current-buffer))
(insert result))))
(cond
((and replace (use-region-p))
(delete-region (region-beginning) (region-end))
(insert result))
(replace
(erase-buffer)
(insert result)))
result)
(defun im--read-json-and-expression ()
(list
(if (use-region-p)
(buffer-substring-no-properties (region-beginning) (region-end))
(let ((json (read-string "JSON (leave empty to use whole buffer): ")))
(if (s-blank? json)
(buffer-substring-no-properties (point-min) (point-max)))))
(read-string "Expression: ")
current-prefix-arg))
(im-leader-v "qj" #'im-run-deno-on-json)
(defun im-jsons-print-path-python ()
(interactive)
(let ((jsons-path-printer 'jsons-print-path-python))
(jsons-print-path)))
(defun im-jsons-print-path-javascript-js-jq ()
(interactive)
(let ((jsons-path-printer 'jsons-print-path-jq))
(jsons-print-path)))
Provides jsons-print-path function, it simply kills the path to the key under point
(use-package json-snatcher :defer t :config ;; Copies paths like:.definition.summary.pastGroup.trackingResults[0].trackingItemReferenceId ;; I've created two functions above for the variations (setq jsons-path-printer 'jsons-print-path-jq))
Add execution ability to json blocks inside org-mode. Either add `:jq some-jq-query' or `:node it.accessor' to code block's header to filter the json
(defun org-babel-execute:json (body params)
(let ((jq (cdr (assoc :jq params)))
(node (cdr (assoc :node params))))
(cond
(jq (im-run-jq-on-json body jq))
(node (im-run-nodejs-on-json body node)))))
(use-package scala-mode
:mode ("\\.\\(scala\\|sbt\\|worksheet\\.sc\\)\\'" . scala-mode)
:interpreter
("scala" . scala-mode))
Enable sbt mode for executing sbt commands
(use-package sbt-mode
:commands sbt-start sbt-command
:config
;; WORKAROUND: https://github.com/ensime/emacs-sbt-mode/issues/31
;; allows using SPACE when in the minibuffer
(substitute-key-definition
'minibuffer-complete-word
'self-insert-command
minibuffer-local-completion-map)
(setq sbt:program-options '("-Dsbt.supershell=false")))
(defun im-jshell ()
"Open JShell with runtime dependencies of the current project loaded.
Please note that tab-completion for runtime dependencies *do not*
work. You need to enter full path while importing by yourself."
(interactive)
(require 'eat)
(let* ((default-directory (im-current-project-root))
(eat-shell "mvn -q com.github.johnpoth:jshell-maven-plugin:1.3:run -DtestClasspath")
;; ^ This plugin makes it possible to import project's maven dependencies
;; another option is to use jshell directly.
(eat-buffer-name "*$eat: jshell*"))
(eat)))
clojure
Here is the current workflow I use:
lein new app project-namecider-jack-in
(use-package cider
:defer t
:general
(general-def :keymaps 'cider-inspector-mode-map :states 'normal
"RET" #'cider-inspector-operate-on-point
"DEL" #'cider-inspector-pop)
(general-def :keymaps 'cider-mode-map :states 'normal
"K" #'im-peek-doc)
:config
(setq cider-inspector-page-size 50)
(setq cider-show-error-buffer nil)
(add-hook 'clojure-mode-hook 'hs-minor-mode)
(add-hook 'clojure-mode-hook 'aggressive-indent-mode)
(add-to-list 'outli-heading-config '(clojure-mode ";;" ?\; t))
;; `compojure' indent
(define-clojure-indent
(defroutes 'defun)
(execute! 1)
(execute-one! 1)
(GET 2)
(POST 2)
(PUT 2)
(DELETE 2)
(HEAD 2)
(ANY 2)
(OPTIONS 2)
(PATCH 2)
(rfn 2)
(let-routes 1)
(context 2)))
(use-package slime :hook (lisp-mode . slime-mode) :general (:keymaps 'lisp-mode-map-map :states 'normal "K" #'slime-documentation) :custom (inferior-lisp-program "sbcl"))
emmet-mode
Hit <C-j> after these and get:
a<a href="|">|</a>.x<div class="x"></div>br/<br />p.x.y.z<p className="x y z"></p>(Works well with JSX)input[type=text]<input type="text" name="" value=""/>a>b>c<a href=""><b><c></c></b></a>b*3<b></b><b></b><b></b>
(use-package emmet-mode :hook (js-mode css-mode sgml-mode web-mode tsx-mode) :custom (emmet-expand-jsx-className? t) (emmet-self-closing-tag-style " /"))
(use-package kotlin-mode :mode "\\.kt\\'" :custom ;; ki is a better REPL for Kotlin. You can save and reload your session. (kotlin-command "ki"))
(use-package yaml-ts-mode
:ensure nil
:mode ("\\.yaml\\'" "\\.yml\\'")
:config
(add-hook 'yaml-ts-mode-hook #'outline-indent-minor-mode)
(add-hook 'yaml-ts-mode-hook #'highlight-indent-guides-mode))
elisp
Inspector/debugging/pretty-printing
There is a built-in function (data-debug-eval-expression) to
inspect objects (not good as CIDERs inspector but it works). I made
a helper function to evaluate last expression and open data-debug
window of it.
(defun im-eval-and-inspect-last-sexp () (interactive) (require 'data-debug) (data-debug-show-stuff (eval-last-sexp nil) "last sexp"))
(use-package data-debug-mode :ensure nil :general (:states 'normal :keymaps 'data-debug-mode-map "RET" #'data-debug-expand-or-contract "TAB" #'data-debug-expand-or-contract))
There is also pp-eval-last-sexp which evaluates and pretty-prints
the result of last expression in a separate buffer, which can be
better for inspection sometimes.
eros
Like CIDER, it shows the results of eval-last-sexp etc. in an
overlay, right next to the expression itself. There is also
eros-inspect-last-result which essentially shows the result of
last evaluation in a pretty printed format in a different buffer.
(defface im-eval-defun-pulse-highlight-face
'((((class color) (min-colors 88) (background dark))
:background "#00CED1" :extend t :foreground "black") ; bright turquoise
(((class color) (min-colors 88) (background light))
:background "#32CD32" :extend t :foreground "black") ; vibrant lime green
(t :inherit primary-selection))
"Face for highlighting evaluated regions.")
(use-package eros
:ensure (:host github :repo "isamert/eros")
:hook (elpaca-after-init . eros-mode)
:config
(add-hook 'eros-inspect-hooks (lambda () (flycheck-mode -1)))
(defun im-set-eros-last-result (orig-val)
(setq eros--last-result orig-val)
orig-val)
(advice-add 'eval-expression :filter-return #'im-set-eros-last-result)
(define-advice eros-eval-defun (:after (&rest _) highlight-expr)
"Highlight expression."
(pcase-let ((`(,start . ,end) (bounds-of-thing-at-point 'defun)))
(when (and start end)
(im-pulse-highlight-region start end 'im-eval-defun-pulse-highlight-face 0.3))))
(define-advice eros-eval-last-sexp (:after (&rest _) highlight-expr)
"Highlight expression."
(pcase-let ((`(,start . ,end) (bounds-of-thing-at-point 'sexp)))
(when (and start end)
(im-pulse-highlight-region start end 'im-eval-defun-pulse-highlight-face 0.3)))))
Pretty stuff
Highlights quotes. Surprisingly useful
(use-package highlight-quoted :hook (emacs-lisp-mode . highlight-quoted-mode))
(use-package package-lint :defer t :commands package-lint-current-buffer)
(use-package flycheck-package :after flycheck :config (flycheck-package-setup))
(add-hook 'emacs-lisp-mode-hook #'hs-minor-mode) (add-hook 'lisp-interaction-mode-hook #'hs-minor-mode)
Indentation fix
Taken from: https://github.com/Fuco1/.emacs.d/blob/2c302dcbedf2722c5c412b6a6d3e3258f6ac1ccf/site-lisp/my-redef.el#LL18C1-L100C62 Redefines the silly indent of keyword lists before (:foo bar :baz qux) after (:foo bar :baz qux)
(eval-after-load "lisp-mode"
'(defun lisp-indent-function (indent-point state)
"This function is the normal value of the variable `lisp-indent-function'.
The function `calculate-lisp-indent' calls this to determine
if the arguments of a Lisp function call should be indented specially.
INDENT-POINT is the position at which the line being indented begins.
Point is located at the point to indent under (for default indentation);
STATE is the `parse-partial-sexp' state for that position.
If the current line is in a call to a Lisp function that has a non-nil
property `lisp-indent-function' (or the deprecated `lisp-indent-hook'),
it specifies how to indent. The property value can be:
* `defun', meaning indent `defun'-style
\(this is also the case if there is no property and the function
has a name that begins with \"def\", and three or more arguments);
* an integer N, meaning indent the first N arguments specially
(like ordinary function arguments), and then indent any further
arguments like a body;
* a function to call that returns the indentation (or nil).
`lisp-indent-function' calls this function with the same two arguments
that it itself received.
This function returns either the indentation to use, or nil if the
Lisp function does not specify a special indentation."
(let ((normal-indent (current-column))
(orig-point (point)))
(goto-char (1+ (elt state 1)))
(parse-partial-sexp (point) calculate-lisp-indent-last-sexp 0 t)
(cond
;; car of form doesn't seem to be a symbol, or is a keyword
((and (elt state 2)
(or (not (looking-at "\\sw\\|\\s_"))
(looking-at ":")))
(if (not (> (save-excursion (forward-line 1) (point))
calculate-lisp-indent-last-sexp))
(progn (goto-char calculate-lisp-indent-last-sexp)
(beginning-of-line)
(parse-partial-sexp (point)
calculate-lisp-indent-last-sexp 0 t)))
;; Indent under the list or under the first sexp on the same
;; line as calculate-lisp-indent-last-sexp. Note that first
;; thing on that line has to be complete sexp since we are
;; inside the innermost containing sexp.
(backward-prefix-chars)
(current-column))
((and (save-excursion
(goto-char indent-point)
(skip-syntax-forward " ")
(not (looking-at ":")))
(save-excursion
(goto-char orig-point)
(looking-at ":")))
(save-excursion
(goto-char (+ 2 (elt state 1)))
(current-column)))
(t
(let ((function (buffer-substring (point)
(progn (forward-sexp 1) (point))))
method)
(setq method (or (function-get (intern-soft function)
'lisp-indent-function)
(get (intern-soft function) 'lisp-indent-hook)))
(cond ((or (eq method 'defun)
(and (null method)
(> (length function) 3)
(string-match "\\`def" function)))
(lisp-indent-defform state indent-point))
((integerp method)
(lisp-indent-specform method state
indent-point normal-indent))
(method
(funcall method indent-point state)))))))))
(use-package doctest :ensure (:host github :repo "ag91/doctest") :commands (doctest doctest-here doctest-defun))
suggest.el – Function suggestions
Do M-x suggest, enter the inputs and the expected output and
it'll suggest you some functions. Quite nice and useful, I used it
plenty of time.
(use-package suggest :defer t :commands suggest)
(use-package redshank
;; All interactive redshank commands
:ensure (:host github :repo "isamert/redshank")
:commands (redshank-maybe-splice-progn
redshank-point-at-enclosing-let-form
redshank-ignore-event
redshank-asdf-insert-module-components
redshank-highlight-binder
redshank-unhighlight-binder
redshank-slime-uninstall
redshank-letify-form
redshank-backward-down-list
redshank-letify-form-up
redshank-extract-to-defun
redshank-enclose-form-with-lambda
redshank-condify-form
redshank-eval-whenify-form
redshank-rewrite-negated-predicate
redshank-elisp-generate-form
redshank-lisp-generate-form
redshank-generate-thing-at-point
redshank-defclass-skeleton
redshank-define-condition-skeleton)
:defer t)
(use-package emr :ensure (:host github :repo "isamert/emacs-refactor") :defer t)
Racket
- Open a racket buffer.
- Do C-c C-c (racket-run)
- It'll drop you on a REPL within the scope of the file.
(use-package racket-mode :mode "\\.rkt\\'")
(use-package swift-mode :defer t)
(use-package scheme
:ensure nil
:defer t
:config
(with-eval-after-load 'outli
(add-to-list 'outli-heading-config '(scheme-mode ";;" ?\; t))))
(use-package geiser
:defer t
:commands geiser
:custom
(geiser-debug-jump-to-debug-p nil)
(geiser-default-implementation 'guile)
:init
(with-eval-after-load 'geiser
(evil-collection-geiser-setup)))
(use-package geiser-guile :after geiser :defer t)
Docker stuff
Some major modes for editing files.
(use-package dockerfile-mode :mode "Dockerfile\\'") (use-package docker-compose-mode :mode "docker-compose\\'" :hook (docker-compose-mode . highlight-indent-guides-mode))
A package for managing docker.
(use-package docker :defer t :config (evil-define-key 'normal docker-container-mode-map (kbd "a") #'docker-container-help) (evil-define-key 'normal docker-image-mode-map (kbd "a") #'docker-image-help) (evil-define-key 'normal docker-machine-mode-map (kbd "a") #'docker-machine-help) (evil-define-key 'normal docker-network-mode-map (kbd "a") #'docker-network-help) (evil-define-key 'normal docker-volume-mode-map (kbd "a") #'docker-volume-help))
(use-package graphviz-dot-mode
:mode ("\\.dot\\'" "\\.gv\\'")
:init
(with-eval-after-load 'org-src
(add-to-list 'org-src-lang-modes '("dot" . graphviz-dot))))
(use-package plantuml-mode
:mode "\\.\\(plantuml\\|pum\\|plu\\)\\'"
:init
(with-eval-after-load 'org
(add-to-list 'org-src-lang-modes '("plantuml" . plantuml)))
:config
(setq plantuml-default-exec-mode 'jar)
(setq plantuml-jar-path "/nix/store/q0v5nv70zc23fx4hjgghnqf7lvydr6fq-plantuml-1.2021.3/lib/plantuml.jar")
(setq org-plantuml-jar-path plantuml-jar-path))
(use-package im-couchbase
:ensure `(:repo ,im-packages-path :files ("im-couchbase.el"))
:defer t)
(use-package im-bq
:ensure `(:repo ,im-packages-path :files ("im-bq.el"))
:mode "\\.bqsql\\'"
:commands (sql-bigquery
org-babel-execute:bqsql
im-bq-run-query-at-point
im-bq-switch-project
im-bq-job-status
im-bq-cancel-job
im-bq-table-info)
:init
(with-eval-after-load 'org-src
(add-to-list 'org-src-lang-modes '("bqsql" . bqsql))))
(use-package jsonnet-mode
:mode ("\\.jsonnet\\'" "\\.libsonnet\\'")
:config
;; Apheleia config, so that it automatically formats on save
(setf (alist-get 'jsonnet apheleia-formatters) '("jsonnetfmt" "-"))
(setf (alist-get 'jsonnet-mode apheleia-mode-alist) 'jsonnet))
sql
I generally do not edit .sql files but instead I do SQL through
code blocks in org-mode. lsp-mode offers lsp-org command to
help using LSP features inside org-mode code blocks. It synergzes
well with lsp-sqls. One important thing to remember is that you
need to have a :tangle <file> in the code blocks so that LSP mode
can actually have the tangled file to send to the corresponding
LSP.
lsp-org- Start LSP mode. Must be executed with cursor being source block.
lsp-virtual-buffer-disconnect- Turn off lsp-mode.
sql-connect also works fairly well, except for code
completion. But you can always list the tables and columns with SQL
and afterwards dabbrev helps you like you have code completion.
I initialize sql-connection-alist in another file that is not
checked out to this repository and afterwards I call the
im-sql-setup-lsp function.
(defun im-sql-setup-lsp ()
"Set `lsp-sqls-connections' using `sql-connection-alist'.
Only works for PostgreSQL connections right now."
(setq
lsp-sqls-connections
(--map
`((driver . "postgresql")
(dataSourceName . ,(format "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable"
(car (alist-get 'sql-server it))
(car (alist-get 'sql-port it))
(car (alist-get 'sql-user it))
(car (alist-get 'sql-password it))
(car (alist-get 'sql-database it)))))
sql-connection-alist)))
(defun im-sql-run-query ()
(interactive)
"Open a new scratch buffer with code completion to run a query, using org-mode."
(let ((buff (format-time-string "sql_%Y-%m-%d.org")))
(unless (get-buffer buff)
(with-current-buffer (get-buffer-create buff)
(insert (im-s-interpolated
"#+TITLE: #{(format-time-string \"%Y-%m-%d\")}-sql
#+PROPERTY: header-args:sql :noweb yes :tangle .#{(format-time-string \"%Y%m%d\")}.sql
#+PROPERTY: header-args:bqsql :noweb yes\n
You can always use ~sql-connect~ to get a simple REPL. It is
faster for longer outputs.
Use ~lsp-sql-switch-connection~ to switch between different
connections, to make code completion work for your current
context.
Don't forget about snippets too."))
(org-mode)
(im-disable-line-wrapping)
(write-file (format "~/Documents/notes/temp/%s" buff))))
(with-current-buffer buff
(goto-char (point-max))
(insert
(im-s-interpolated
"\n\n* #{(format-time-string \"%Y-%m-%d %a %H:%M\")}
#+begin_src sql :engine postgresql :dbconnection #{(sql-read-connection \"Select DB: \")}
SELECT * FROM _ LIMIT 1;
#+end_src\n\n"))
(forward-line -3)
(unless lsp-mode
(lsp-org))
;; I wasn't able to find a way to retrieve current
;; connection. If I was able to, then I would compare it with
;; the selected dbconnection from above and show the following
;; selection only if needed
(lsp-sql-switch-connection))
(switch-to-buffer buff)))
(use-package go-mode :mode "\\.go\\'" :defer t)
(use-package nginx-mode
:mode (("nginx\\.conf\\'" . nginx-mode)
("/nginx/.+\\.conf\\'" . nginx-mode))
:defer t)
(use-package conf-mode :ensure nil :defer t :config (add-hook 'conf-mode-hook #'outli-mode))
(use-package fish-mode
:mode ("\\.fish\\'"))
python
Jupyter & org-mode
jupyterpackage enables you to use jupyter notebooks in Emacs.- It provides two ways, one is through org-mode and it's the one I prefer.
- You need to convert
ipynbfiles into org-mode files by running:M-x im-convert-ipynb-to-org-buffer jupyter-org-interaction-modeis enabled in all org-mode buffers.- Yan simply use
jupyter-pythonsource blocks to write your code and run them exactly as you do with normal org code blocks. - There are also these functions which you can utilize:
- jupyter-eval-defun
- jupyter-inspect-at-point → K
- jupyter-eval-line-or-region → C-x C-e
- jupyter-repl-interrupt-kernel → C-c C-i
- jupyter-repl-restart-kernel → C-c C-r
See org-babel-default-header-args:jupyter-python down below for
more details on how these blocks are getting executed.
WARNING: org-modern breaks ANSI code escaping of jupyter-org-client somehow if it starts before jupyter-org-client. Hence I increased it's depth. See org-modern configuration for more info.
Virtual Envs and Jupyter
If you are using virtualenvironments, then simply do
pyvenv-activate and select the virtenv folder.
python3 -m venv .venvto create a virtenvpip install -r requirements.txtto install requirementssource .venv/bin/activate.{,.fish,.zsh}to active the venv in terminal- or
pyvenv-activeto active inside Emacs.
After activating the venv, you may need to do
jupyter-repl-restart-kernel.
(use-package pyvenv :defer t)
(use-package jupyter :ensure (:host github :repo "isamert/jupyter") :defer t :custom (jupyter-org-want-keybinding . nil) (jupyter-org-want-integration . nil))
(with-eval-after-load 'jupyter-org-client
(setq org-babel-default-header-args:jupyter-python
'((:async . "yes") ;; Make outputs async
(:session . "py") ;; A named session
(:kernel . "python3") ;; Don't know but works
(:pandoc . t) ;; Convert rich outputs to org-mode format
;; Prefer plain-text outputs from jupyter instead of
;; direct html outputs
(:display "text/plain text/html"))))
(defun im-convert-ipynb-to-org-buffer ()
"Convert the current IPython Notebook file to Org format and open it in a new buffer."
(interactive)
(let* ((fname (file-name-sans-extension (buffer-name)))
(org-buffer-name (concat fname ".org"))
(pandoc-output (shell-command-to-string
(format "pandoc --from ipynb --to org '%s'" (buffer-file-name)))))
(with-current-buffer (get-buffer-create org-buffer-name)
(erase-buffer)
(insert pandoc-output)
(switch-to-buffer (current-buffer))
;; Add :session to code blocks so that each file has a separate session
(save-excursion
(goto-char (point-min))
(while (re-search-forward " jupyter-python" nil t)
(replace-match (concat " jupyter-python :session " fname) nil nil)))
(goto-char (point-min))
(org-mode)
(jupyter-org-interaction-mode))))
Window and buffer management
With the following. previous-buffer or next-buffer will only cycle through the given window's buffer history. Normally if the history is exhausted previous/next-buffer determines some buffer as the next/prev, quite randomly in effect (I mean there are heuristics, yes, but it is impossible to predict what is going to happen as a user), so this disables that behavior. I would like prev/next-buffer to stop when the history is exhausted but that is seemingly impossible and this is the best we can get in terms of having some determinism.
(setq
switch-to-prev-buffer-skip
(lambda (window tobuffer _)
(not (seq-contains-p
(mapcar #'car (window-prev-buffers window))
tobuffer))))
tab-bar-mode
It's a great workspace manager that comes bundled with Emacs. I was
using an abomination where persp.el and eyebrowse was glued
together but I guess this is a simpler and more sane alternative to
them.
(use-package tab-bar
:ensure nil
:custom
(tab-bar-new-tab-to 'rightmost)
(tab-bar-new-tab-choice im-init-file)
;; Experimenting with not showing tab-bar at all.
;; TODO: need a way to display on which tab am I, possibly inside
;; modeline?
(tab-bar-show t)
(tab-bar-history-limit 20)
;; Show numbers before tab names
(tab-bar-tab-hints t)
(tab-bar-auto-width nil)
(tab-bar-auto-width-max t)
(tab-bar-tab-hints nil)
(tab-bar-separator " ❯ ")
(tab-bar-close-button nil)
(tab-bar-tab-name-format-function #'tab-bar-tab-name-format-default)
(tab-bar-tab-name-function #'tab-bar-tab-name-current)
;; FIXME: For some reason, my function gets slower over time. Dunno
;; why, will investigate later.
;; (setq tab-bar-tab-name-function #'im-current-project-name)
(tab-bar-format
'(tab-bar-format-tabs
tab-bar-separator
tab-bar-format-add-tab
tab-bar-format-align-right
tab-bar-format-global))
;; Don't show global-mode-string in mode-line because we already show
;; it on right side of the tab-bar
(mode-line-misc-info (assq-delete-all 'global-mode-string mode-line-misc-info))
:hook (elpaca-after-init . tab-bar-mode)
:config
;; Enable history so that I can use `tab-bar-history-back'
;; `tab-bar-history-forward'. `winner-mode' also have the same
;; functionality but `tab-bar-history-mode' works per tab so that it
;; does not mangle things up between different tabs which is just
;; great.
(tab-bar-history-mode)
(defun im-fix-tab-bar-faces (&rest _)
"Most of the themes I use does not support tab-bar, this mostly fixes it."
(set-face-attribute 'tab-bar-tab nil :background 'unspecified :box nil :bold t :underline nil :foreground (face-foreground 'font-lock-type-face nil t))
(set-face-attribute 'tab-bar-tab-inactive nil :background 'unspecified :box nil :bold nil :underline nil :italic t))
(add-hook 'im-after-load-theme-hook #'im-fix-tab-bar-faces))
I want to show consult-buffer when I open a new tab to be able to
quickly jump a file. I also want to show it in a buffer, not in
mini-buffer.
(defun im-tab-bar-new-tab ()
"Open a new tab and display `consult-buffer'."
(interactive)
(tab-bar-new-tab)
(let ((vertico-multiform-commands '((consult-buffer buffer (vertico-buffer-display-action . (display-buffer-same-window)))))
(this-command 'consult-buffer))
(consult-buffer)))
(defun im-tab-bar-new-tab-with-current-buffer ()
"Open a new tab and display the current buffer."
(interactive)
(let ((tab-bar-new-tab-choice t))
(tab-bar-new-tab)))
(defun im-tab-bar-visible-buffers ()
"Return live buffers visible in all tab-bar tabs."
(let (buffers)
(cl-labels
((walk (tree)
(cond
((and (consp tree) (eq (car tree) 'buffer))
(when-let ((buffer (get-buffer (cadr tree))))
(push buffer buffers)))
((proper-list-p tree)
(mapc #'walk tree)))))
;; Current tab.
(dolist (window (window-list nil 'no-minibuf))
(push (window-buffer window) buffers))
;; non-current tab
(dolist (tab (tab-bar-tabs))
(unless (eq (car tab) 'current-tab)
(walk (alist-get 'ws tab)))))
(delete-dups (nreverse buffers))))
(defvar im-tab-line-hidden-buffer-name-regexp (concat "\\*\\(Calc.*\\|Calendar\\|elfeed-log\\|helpful.*\\|Compile-Log\\|Help\\|lsp-log\\)\\*" "\\|magit.*" "\\|slack-curl-downloader" "\\|slack-log" "\\|slack-event-log" "\\|dir-data" "\\|.*-ls\\*" "\\|.*stderr\\*" "\\|.*Dirvish-.*\\*" "\\| \\*" "") "Regexp to filter out buffer names on tab line.")
(use-package tab-line
:ensure nil
;; :hook (elpaca-after-init . global-tab-line-mode)
:general
(:keymaps 'tab-line-mode-map :states 'normal
"M-[" #'tab-line-hscroll-left
"M-]" #'tab-line-hscroll-right)
:custom
(tab-line-close-button-show 'selected)
(tab-line-tabs-function #'im-tab-line-buffers)
(tab-line-tab-name-truncated-max 32)
(tab-line-tab-name-function #'im-tab-line-buffer-name)
:config
(setq tab-line-separator " • ")
(setq tab-line-close-button nil)
(defun im-fix-tab-line-faces (&rest _)
"Most of the themes I use does not support tab-bar, this mostly fixes it."
(set-face-attribute 'tab-line-tab-current nil
:background 'unspecified :box 'unspecified
:foreground (face-foreground 'font-lock-keyword-face nil t)
:bold t :underline nil :italic 'unspecified)
(set-face-attribute 'tab-line-tab nil
:background 'unspecified :box 'unspecified
:foreground (face-foreground 'shadow nil t)
:bold t :underline nil :italic 'unspecified)
(set-face-attribute 'tab-line-highlight nil
:background 'unspecified :box 'unspecified
:bold t :underline nil)
(set-face-attribute 'tab-line-tab-inactive nil
:background 'unspecified :box 'unspecified
:bold nil :underline nil :italic t))
(add-hook 'im-after-load-theme-hook #'im-fix-tab-line-faces))
(defun im-tab-line-buffer-name (buffer &optional _buffers)
(let ((name (tab-line-tab-name-truncated-buffer buffer)))
;; im-tab-line-buffers groups special with the same prefix
;; together. I mostly prefix my buffers like "*XXX: ..." and here
;; I delete that prefix to gain a little bit more space while
;; displaying.
(if-let* ((str (s-match " ?\\*[a-zA-Z0-9_$-]+: \\([^\\*]*\\)" name)))
(cadr str)
name)))
(defvar-local im-buffer-project-root nil
"Cached value for project root of a buffer.
Looping all buffers and re-calculating their roots are quite expensive,
hence the cache.")
(defun im-tab-line-buffers ()
"Return buffers grouped for tab-line display.
A group consists of buffers that:
- Share the same project root directory (as determined by
`im-current-project-root').
- Have buffer names starting with the same prefix (typically for buffers
such as '*eshell:', '*gptel:', and other duplicable special modes).
This function is intended for use with `tab-line-mode' to conveniently
group related buffers (e.g., multiple shells or AI chat sessions in the
same project) together on the tab line, improving navigation in projects
where these special buffers may be duplicated."
(let ((orig-name (buffer-name (current-buffer))))
(if (string-prefix-p "*" orig-name) ; Handle special buffers
(let* ((cur-root (or (im-current-project-root) default-directory))
(prefix (if (> (length orig-name) 3)
(substring orig-name 0 4)
orig-name)))
(seq-sort-by
#'buffer-name #'string<
(cl-loop for buf in (buffer-list)
as buf-name = (buffer-name buf)
unless (string-match-p im-tab-line-hidden-buffer-name-regexp buf-name)
when (when-let* ((proj-root
(or (buffer-local-value 'im-buffer-project-root buf)
(with-current-buffer buf
(setq-local im-buffer-project-root
(or (im-current-project-root)
default-directory))))))
(and (equal proj-root cur-root)
(string-prefix-p prefix buf-name)))
collect buf)))
;; Normal buffers uses this.
(tab-line-tabs-fixed-window-buffers))))
(use-package tabgo
:ensure (:host github :repo "isamert/tabgo.el")
:demand t
:general
(im-leader
"<SPC>" #'tabgo))
(use-package ace-window
:defer t
:custom
(aw-background t)
(aw-ignore-current t)
(aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l))
:config
(set-face-attribute 'aw-leading-char-face nil :height 4.5 :weight 'bold :foreground "deep sky blue")
(when (display-graphic-p)
(ace-window-posframe-mode)))
(defun im-find-file-ace-window (filename &optional wildcards)
"Edit file FILENAME, in selected window.
Like \\[find-file] (which see), but uses the selected window by `ace-select-window'."
(interactive
(find-file-read-args "Find file in other window: "
(confirm-nonexistent-file-or-buffer)))
(let ((aw-ignore-current nil))
(select-window (ace-select-window)))
(switch-to-buffer (find-file-noselect filename nil nil wildcards)))
bufler
Good for buffer management, especially for groupped killings.
(use-package bufler
:defer t
:commands (bufler)
:config
;; HACK: Fix for alphapapa/bufler.el#104
(add-hook 'bufler-list-mode-hook
(lambda ()
(setq-local font-lock-unfontify-region-function #'ignore)))
(evil-define-key 'normal bufler-list-mode-map
(kbd "q") #'quit-window
(kbd "x") #'bufler-list-buffer-kill
(kbd "<return>") #'bufler-list-buffer-switch
(kbd "<tab>") #'magit-section-cycle
(kbd "M-,") #'bufler-list-buffer-peek
(kbd "p") #'bufler-list-buffer-peek))
(evil-define-key 'normal 'global "Q" #'im-quit (kbd "M-\\") #'im-split-window-right (kbd "M--") #'im-split-window-below (kbd "[2") #'tab-line-switch-to-prev-tab (kbd "]2") #'tab-line-switch-to-next-tab (kbd "[1") #'tab-bar-switch-to-prev-tab (kbd "]1") #'tab-bar-switch-to-next-tab)
(im-leader "l" #'evil-window-right "h" #'evil-window-left "j" #'evil-window-down "k" #'evil-window-up)
(im-leader ;; misc window operations "wo" #'other-window "w1" #'delete-other-windows "w=" #'balance-windows ;; window-move "wL" #'evil-window-move-far-right "wH" #'evil-window-move-far-left "wJ" #'evil-window-move-very-bottom "wK" #'evil-window-move-very-top ;; window-size ;; "w<right>" #'shrink-window ;; "w<left>" #'enlarge-window "w+" #'enlarge-window "w-" #'shrink-window "w>" #'enlarge-window-horizontally "w<" #'shrink-window-horizontally "w," #'enlarge-window-horizontally "w." #'shrink-window-horizontally ;; workspace (tab-bar-mode) "wu" #'tab-bar-history-back ;; undo "wr" #'tab-bar-history-forward ;; redo "wt" #'im-tab-bar-new-tab ;; tab "wT" #'im-tab-bar-new-tab-with-current-buffer ;; tab "wn" #'tab-bar-rename-tab ;; name "wm" #'tab-move ;; move "wl" #'tab-bar-switch-to-recent-tab ;; last "we" #'tab-switch "wx" #'tab-close ;; kill ;; buffer "wb" #'bufler ;; ace-window "wa" #'ace-window "ws" #'ace-swap-window "wh" #'im-split-window-right "wj" #'im-split-window-below)
Buffer related bindings
(im-leader-v "b0" #'erase-buffer "br" #'rename-buffer "bg" #'revert-buffer "bo" #'im-open-region-in-temp-buffer "bO" #'im-temp-buffer)
(evil-define-key '(normal insert motion) 'global (kbd "M-1") (λ-interactive (tab-bar-select-tab 1)) (kbd "M-2") (λ-interactive (tab-bar-select-tab 2)) (kbd "M-3") (λ-interactive (tab-bar-select-tab 3)) (kbd "M-4") (λ-interactive (tab-bar-select-tab 4)) (kbd "M-5") (λ-interactive (tab-bar-select-tab 5)) (kbd "M-6") (λ-interactive (tab-bar-select-tab 6)) (kbd "M-7") (λ-interactive (tab-bar-select-tab 7)))
(im-make-repeatable winner "u" tab-bar-history-back "r" tab-bar-history-forward)
(im-make-repeatable window-resize "+" enlarge-window "-" shrink-window ">" enlarge-window-horizontally "<" shrink-window-horizontally)
Misc functions
marks integration
marks is a grep-like tool for
searching org-mode and markdown files. The following snippet
provides a consult support for marks.
(defun im-consult--marks-builder (paths)
(lambda (input)
(pcase-let ((`(,arg . ,opts) (consult--command-split input)))
(cons
(list "marks" "--no-color" "--null" "--limit=25" (s-trim arg) (expand-file-name default-directory))
(apply-partially #'consult--highlight-regexps (list (regexp-quote arg)) t)))))
(defun im-marks (&optional directory)
"Run `marks'."
(interactive)
(let ((default-directory (or directory default-directory)))
(consult--grep "marks" #'im-consult--marks-builder default-directory "")))
(defun im-marks-my-docs () "Run marks inside `~/Documents/notes'." (interactive) (im-marks (expand-file-name "~/Documents/notes")))
(im-leader "eji" #'im-jira-list-issues "ejq" #'im-jira-jql-interactive "ejc" #'im-jira-create-ticket)
(use-package jiralib2 :defer t :custom (jiralib2-url ty-jira-url) (jiralib2-auth 'token) (jiralib2-user-login-name ty-jira-login) (jiralib2-token ty-jira-secret))
(use-package im-jira
:ensure `(:repo ,im-packages-path :files ("im-jira.el"))
:custom
(im-git-main-branch "master")
(im-jira-projects '("PRA" "STA"))
(im-jira-base-branch "origin/master")
(im-jira-feature-branch-prefix "feature/")
(im-jira-my-issues-query
"Pair-programmer = currentUser() OR assignee = currentUser() OR creator = currentUser() ORDER BY createdDate DESC")
(im-jira-board-ids '(("STA" . "2307")
("PRA" . "2306")))
:config
(add-to-list 'im-open-thing-at-point-alist '(im-jira-issue-at-point . im-jira-open-issue)))
people.org - Contact management
Please see this post for further information.
(defun im-contacts--build-contact-item (template-string contact-property)
(cond
((s-contains? "$1" contact-property)
(->>
(org-entry-properties)
(--filter (s-matches? (s-replace "$1" ".*" contact-property) (car it)))
(--map (let ((subprop (nth 1 (s-match (s-replace "$1" "\\([a-zA-Z0-9_-]+\\)" contact-property) (car it)))))
(concat (format (s-replace "$1" subprop template-string) (cdr it)) "\n")))))
(t (if-let* ((stuff (org-entry-get nil contact-property)))
(concat (format template-string stuff) "\n")
""))))
(defun im-contacts--contact-entry? ()
(thread-last
(org-entry-properties)
(-map #'car)
(--any (s-prefix? "PHONE" it))))
(defun im-contacts-build-vcard-for-heading ()
"Build a VCARD for the current heading and return it as string."
(when (im-contacts--contact-entry?)
(string-join
`("BEGIN:VCARD\nVERSION:4.0\nPRODID:ez-vcard 0.11.3\n"
,(format "UID:urn:uuid:%s\n" (org-id-get nil t))
,(im-contacts--build-contact-item "FN:%s" "ITEM")
,(if (org-entry-get nil "DISPLAY_NAME")
(im-contacts--build-contact-item "N:;%s;;;" "DISPLAY_NAME")
(im-contacts--build-contact-item "N:;%s;;;" "ITEM"))
,@(im-contacts--build-contact-item "TEL;TYPE=$1:%s" "PHONE_$1")
,@(im-contacts--build-contact-item "EMAIL;TYPE=$1:%s" "EMAIL_$1")
,@(im-contacts--build-contact-item "ADR;TYPE=$1:;;%s;;;;" "ADDRESS_$1")
,(im-contacts--build-contact-item "ORG:%s" "GROUP")
,(im-contacts--build-contact-item "CATEGORIES:%s" "GROUP")
,(thread-last
(im-contacts--build-contact-item "BDAY:%s" "SCHEDULED")
(s-replace-regexp "BDAY:<[0-9]\\{4\\}\\(.*\\) \\(\\w+\\)>" "BDAY:1900\\1")
(s-replace "-" ""))
;; vcard supports multiple anniversaries but there is no
;; way to name them as far as I understand. So I just
;; drop anniversary names and add all of them.
,@(thread-last
(im-contacts--build-contact-item "ANNIVERSARY:%s" "ANNIVERSARY_$1")
(--map (->>
it
(s-replace-all '((" " . "") ("-" . "") ("[" . "") ("]" . "") ("<" . "") (">" . "")))
(s-trim)
(s-replace-regexp "[a-zA-Z]+$" "\n"))))
;;,(format "REV:%s\n" (format-time-string "%Y-%m-%dT%T"))
;; TODO: add notes
;; Be careful while exporting and sending contact to other people
,(when-let* ((image-base64
(-some->>
(org-agenda-get-some-entry-text (point-marker) most-positive-fixnum)
(substring-no-properties)
(s-match "^\\[\\[file:\\(.+\\(\\.jpg\\|jpeg\\|png\\)\\)]]")
(nth 1)
(im-encode-image-base64))))
(format "PHOTO:%s\n" image-base64))
"TITLE:\nEND:VCARD")
"")))
(defun im-contacts-export-as-vcard (&optional file-name)
"Create a .vcf FILE-NAME containing all contact information.
I use \"Simple Contacts Pro\" to import contacts to my phone."
(interactive
(list
(read-file-name
"Where to save the .vcf file?"
"~/Documents/sync/"
"contacts.vcf")))
(write-region
(thread-last
(org-map-entries #'im-contacts-build-vcard-for-heading "LEVEL=1")
(-non-nil)
(s-join "\n"))
nil
file-name))
Also see im-nextcloud.el/Contacts
(defun im-people ()
"Select and kill people info.
people.org should contain the following snippet on it's `after-save-hook':
(im-serialize-into-file \"~/.emacs.d/sync/people\"
(org-map-entries #'org-entry-properties \"LEVEL=1\"))"
(interactive)
(let* ((selected
(->>
(im-completing-read
"Select info"
(-mapcat
(lambda (info)
(--map
(list
:format
(format
"%s >> %s → %s"
(propertize (alist-get "ITEM" info nil nil #'equal) 'face '(:weight bold))
(propertize (car it) 'face '(:foreground "yellow"))
(propertize (cdr it) 'face '(:weight bold :foreground "green")))
:item it :info info)
(--filter
(and
(not (-contains? '("PRIORITY" "CATEGORY") (car it)))
(not (s-blank? (cdr it))))
info)))
(im-deserialize-from-file "~/.emacs.d/sync/people"))
:formatter (lambda (it) (plist-get it :format))
:initial (im-region-or nil))))
(prop (car (plist-get selected :item)))
(val (cdr (plist-get selected :item)))
(info (cdr (plist-get selected :info))))
(im-kill val)
(cond
;; Insert link to person if we are in org-mode
((and (frame-focus-state)
(derived-mode-p 'org-mode)
(equal prop "ID"))
(insert (format "[[id:%s][%s]]"
val
(if (use-region-p)
(let* ((beg (region-beginning))
(end (region-end))
(str (buffer-substring-no-properties beg end)))
(delete-region beg end)
str)
(s-trim (read-string "Name: " (alist-get "ITEM" info nil nil #'equal)))))))
;; Strip out links from addresses
((s-prefix? "ADDRESS" prop)
(if-let* ((x (s-match "\\[\\[\\(.*\\)\\]\\[\\(.*\\)\\]\\]" val)))
(progn
;; Put them both in clipboard
(im-kill (car x))
(im-kill (nth 2 x)))
(im-kill val)))
(t (im-kill val)))))
(im-leader-v "ou" #'im-people)
(defun im-google-this (input)
"Search INPUT in Google.
Interactively, INPUT is either region or current word. Let's you edit
the query beforehand."
(interactive
(list
(read-string
"Googling: "
(im-region-or 'word))))
(browse-url
(concat
eww-search-prefix
input)))
(im-leader-v "mg" #'im-google-this)
bullet.org
I have a bullet.org file that I keep my daily journal and here
are some utility functions that use with it. I may document this
later (or even publish it as a package?)
Keybindings
(im-leader "ob" 'im-bullet-transient)
(transient-define-prefix im-bullet-transient ()
"IM Bullet Focus Commands"
[["Focus"
("n" "Non-day header" im-bullet-focus-non-day-header)
("y" "Yesterday" im-bullet-focus-yesterday)
("Y" "Yesterday indirect" im-bullet-focus-yesterday-indirect)
("t" "Today" im-bullet-focus-today)
("T" "Tomorrow" im-bullet-focus-tomorrow)
("f" "Given day" im-bullet-focus-given-day)
("r" "Recurring" im-bullet-focus-recurring)
("R" "Recurring indirect" im-bullet-focus-recurring-indirect)
("w" "Work" im-bullet-focus-work)
("l" "Life" im-bullet-focus-life)
("L" "Life indirect" im-bullet-focus-life-indirect)
("c" "Computer" im-bullet-focus-computer)
("C" "Computer indirect" im-bullet-focus-computer-indirect)
("i" "Inbox" im-bullet-focus-inbox)
("I" "Inbox indirect" im-bullet-focus-inbox-indirect)]
["Linking"
("b" "Backlinks" im-org-link-find-backlinks)]])
Date utils
(defun im-today () "Get todays date in format YYYY-MM-DD Day." (format-time-string "%Y-%m-%d %a"))
(defun im-u () "Get day of week as number." (string-to-number (format-time-string "%u")))
(defun im-d () "Get day of month as number." (string-to-number (format-time-string "%d")))
(defun im-V () "Get week number." (string-to-number (format-time-string "%V")))
(defun im-Y () "Get year." (string-to-number (format-time-string "%Y")))
(defun im-date (date &optional str)
"Display the time described by DATE.
DATE can be 'now', 'yesterday', 'two days ago' etc."
(s-trim-right
(shell-command-to-string
(concat (locate-file "date" exec-path) " --date='" date "' +'" (or str "%Y-%m-%d %a") "'"))))
Create utils
(defun im-bullet-create-a-day (date)
"Create given DATE heading in bullet.org in the appropriate place..
DATE should be in the form of YYYY-MM-DD."
(when-let* ((point (im-bullet-find-a-day date)))
(goto-char point)
(user-error "The day already exists"))
(widen)
(goto-char (point-max))
(cl-loop
(when (not (re-search-backward "^* \\[\\([0-9]+-[0-9]+-[0-9]+\\)\\( \\w+\\)?\\].*" nil t))
(cl-return))
(when (time-less-p
(date-to-time (concat (match-string 1) "T000"))
(date-to-time (concat date "T000")))
(org-insert-heading-after-current)
(insert (format "[%s%s] [%%]"
date
(if (s-matches? "[a-zA-Z]+$" date)
""
(format " %s" (format-time-string "%a" (date-to-time (concat date "T000")))))))
(org-narrow-to-subtree)
(cl-return))))
Focus a day
(defun im-bullet-find-a-day (day)
(save-excursion
(widen)
(goto-char (point-max))
(when (re-search-backward (concat "^* \\[" day "\\( \\w+\\)?\\]") nil t)
(point-marker))))
(defun im-bullet-focus-a-day (day)
"Focus to given DAY."
(im-bullet-org-ensure)
(widen)
(when-let* ((day-entry (im-bullet-find-a-day day)))
(goto-char day-entry)
(beginning-of-line)
(org-narrow-to-subtree)
(im-show-outline-only)
t))
(defun im-bullet-focus-given-day (date)
"Focus given DATE's header.
If it does not exists, create it."
(interactive
(list (org-read-date)))
(when (not (im-bullet-focus-a-day date))
(im-bullet-create-a-day date)))
(defun im-bullet-focus-today ()
"Focus todays header.
If it does not exists, create it."
(interactive)
(let ((today (if (<= (string-to-number (format-time-string "%H")) 3)
(im-date "yesterday")
(format-time-string "%Y-%m-%d"))))
(when (not (im-bullet-focus-a-day today))
(im-bullet-create-a-day today))))
(defun im-bullet-focus-tomorrow ()
"Focus yesterdays header."
(interactive)
(let ((tomorrow (im-date "tomorrow")))
(when (not (im-bullet-focus-a-day tomorrow))
(im-bullet-create-a-day tomorrow))))
(defun im-bullet-focus-tomorrow-indirect () "Like `im-bullet-focus-tomorrow' but in an indirect buffer." (interactive) (im-org-focused-tree-to-indirect-buffer (im-bullet-org-ensure) (im-bullet-focus-tomorrow)))
(defun im-bullet-focus-yesterday () "Focus yesterdays header." (interactive) (im-bullet-focus-a-day (im-date "yesterday")))
(defun im-bullet-focus-yesterday-indirect () "Like `im-bullet-focus-yesterday' but in an indirect buffer." (interactive) (im-org-focused-tree-to-indirect-buffer (im-bullet-org-ensure) (im-bullet-focus-yesterday)))
(defun im-bullet-focus-heading (heading)
(interactive)
(im-bullet-org-ensure)
(widen)
(goto-char (point-min))
(when (re-search-forward (format "^*+ %s" heading))
(beginning-of-line)
(org-narrow-to-subtree)
(im-show-outline-only)
t))
(defun im-bullet-focus-recurring () (interactive) (im-bullet-focus-heading "Recurring"))
(defun im-bullet-focus-recurring-indirect () "Like `im-bullet-focus-recurring' but in an indirect buffer." (interactive) (im-org-focused-tree-to-indirect-buffer (im-bullet-org-ensure) (im-bullet-focus-recurring)))
(defun im-bullet-focus-work () (interactive) (find-file work-org))
(defun im-bullet-focus-life () (interactive) (im-bullet-focus-heading "Life backlog"))
(defun im-bullet-focus-life-indirect () "Like `im-bullet-focus-life' but in an indirect buffer." (interactive) (im-org-focused-tree-to-indirect-buffer (im-bullet-org-ensure) (im-bullet-focus-life)))
(defun im-bullet-focus-computer () (interactive) (im-bullet-focus-heading "Computer backlog"))
(defun im-bullet-focus-computer-indirect () "Like `im-bullet-focus-computer' but in an indirect buffer." (interactive) (im-org-focused-tree-to-indirect-buffer (im-bullet-org-ensure) (im-bullet-focus-computer)))
(defun im-bullet-focus-inbox () (interactive) (im-bullet-focus-heading "Inbox"))
(defun im-bullet-focus-inbox-indirect () "Like `im-bullet-focus-inbox' but in an indirect buffer." (interactive) (im-org-focused-tree-to-indirect-buffer (im-bullet-org-ensure) (im-bullet-focus-inbox)))
Focus non-day
(defun im-bullet-focus-non-day-header ()
"Interactively select and focus a non-day header."
(interactive)
(save-match-data
(widen)
(goto-char 0)
(re-search-forward "^\\*+ \\[[0-9]+-" nil t)
(narrow-to-region (point-min) (pos-bol))
(consult-org-heading)
(org-narrow-to-subtree)))
Template utils
(defun im-bullet-current-date ()
"Return current date."
(save-restriction
(save-excursion
(widen)
(when (re-search-backward "^* \\[202" nil t)
(-some->> (org-get-heading t t t t)
(s-match "\\[.*?\\]")
(car)
(substring-no-properties)
(s-chop-prefix "[")
(s-chop-suffix "]"))))))
(defmacro im-when-weekday (template &rest template-args)
`(when (<= (im-u) 5)
(s-trim (im-s-interpolated ,template ,@template-args))))
(defmacro im-when-weekday-n (n template &rest template-args)
`(when (= (im-u) ,n)
(s-trim (im-s-interpolated ,template ,@template-args))))
(defmacro im-when-monthday-n (n template &rest template-args)
`(when (= (im-d) ,n)
(s-trim (im-s-interpolated ,template ,@template-args))))
(defmacro im-when-weekend (template &rest template-args)
`(when (> (im-u) 5)
(s-trim (im-s-interpolated ,template ,@template-args))))
(defmacro im-when-saturday (template &rest template-args)
`(when (= (im-u) 6)
(s-trim (im-s-interpolated ,template ,@template-args))))
(defmacro im-when-sunday (template &rest template-args)
`(when (= (im-u) 7)
(s-trim (im-s-interpolated ,template ,@template-args))))
(defmacro im-when-monday (template &rest template-args)
`(when (= (im-u) 1)
(s-trim (im-s-interpolated ,template ,@template-args))))
(defmacro im-when-friday (template &rest template-args)
`(when (= (im-u) 5)
(s-trim (im-s-interpolated ,template ,@template-args))))
Daily summary
Implementation is a bit cumbersome but it's easy to adopt for my ad-hoc requests
(defun org-dblock-write:daily-summary (params)
"Create a daily summary for my bullet.org."
(let* ((items (org-map-entries
(lambda nil
(list
:name
(org-entry-get nil "ITEM")
:clock
(org-clock-sum-current-item)
:tags
(or (org-get-tags) '(empty))
:level
(org-current-level)
:parent
(save-excursion
(when (org-up-heading-safe)
(org-entry-get nil "ITEM")))))
"LEVEL>1"))
(total-time (->>
items
(--filter (= (plist-get it :level) 2))
(--map (plist-get it :clock))
(-sum)
(org-duration-from-minutes)))
(routines (--filter (s-equals? (plist-get it :parent) "Routines") items))
(work (--filter (-contains? (plist-get it :tags) "work") items))
(others (--filter (not (or
(-contains? (plist-get it :tags) "work")
(s-equals? (plist-get it :parent) "Routines")
(s-equals? (plist-get it :name) "Routines"))) items)))
(insert "| [Event] | [Duration] ||\n")
(insert "|-|\n")
(->>
items
(--find (s-equals? "Routines" (plist-get it :name)))
(funcall (-flip #'plist-get) :clock)
(org-duration-from-minutes)
(format "|Routines|%s||\n")
(insert))
(->>
routines
(--filter (-contains? '("Breakfast" "Dinner") (plist-get it :name)))
(--map (plist-get it :clock))
(-sum)
(org-duration-from-minutes)
(format "|\\-- Eating||%s|\n")
(insert))
(->>
routines
(--filter (not (-contains? '("Breakfast" "Dinner") (plist-get it :name))))
(--map (plist-get it :clock))
(-sum)
(org-duration-from-minutes)
(format "|\\-- Other||%s|\n")
(insert))
(insert "|-|\n")
(->>
work
(--map (plist-get it :clock))
(-sum)
(org-duration-from-minutes)
(format "|Work|%s||\n")
(insert))
(->>
work
(--filter (-contains? (plist-get it :tags) "meeting"))
(--map (plist-get it :clock))
(-sum)
(org-duration-from-minutes)
(format "|\\-- Meetings||%s|\n")
(insert))
(->>
work
(--filter (not (-contains? (plist-get it :tags) "meeting")))
(--map (plist-get it :clock))
(-sum)
(org-duration-from-minutes)
(format "|\\-- Other||%s|\n")
(insert))
(insert "|-|\n")
(->>
others
(--map (plist-get it :clock))
(-sum)
(org-duration-from-minutes)
(format "|Other|%s||\n")
(insert))
(->>
others
(--filter (-contains? (plist-get it :tags) "side"))
(--map (plist-get it :clock))
(-sum)
(org-duration-from-minutes)
(format "|\\-- Side projects||%s|\n")
(insert))
(->>
others
(--filter (not (-contains? (plist-get it :tags) "side")))
(--map (plist-get it :clock))
(-sum)
(org-duration-from-minutes)
(format "|\\-- Other||%s|\n")
(insert))
(insert "|-|\n")
(insert (format "|Total|%s||\n" total-time))
(delete-char 1)
(org-table-align)))
Misc utils
(defun im-bullet-schedule-all-today ()
"Schedule all level-2 org mode tasks today's date.
This does not change tasks time. If the task is not scheduled,
schedules them to today's date."
(interactive)
(unless (buffer-narrowed-p)
(user-error ">> Buffer needs to be narrowed for this!"))
(let* ((today (save-excursion
(goto-char (point-min))
(let ((line (thing-at-point 'line)))
(if (string-match "\\[\\([^]]+\\)\\]" line)
(match-string 1 line)
(user-error ">> Not on a bullet heading!")))))
(today-time (date-to-time today)))
(org-map-entries
(lambda ()
(let ((entry-date (org-entry-get nil "SCHEDULED")))
(when (and
(org-entry-get nil "TODO")
(if entry-date
(time-less-p
(date-to-time entry-date)
today-time)
t))
(org-schedule nil
(if-let* ((entry-date)
(hour-min (nth 2 (s-split " " entry-date))))
(concat today " " hour-min)
today)))))
"LEVEL=2")))
(defun im-bullet-open-weekly-plan ()
"Open weekly planning entry in an indirect buffer."
(interactive)
(with-current-buffer (find-buffer-visiting life-org)
(save-restriction
(save-excursion
(when (or
(ignore-errors
(org-link-open
`(link
(:type "fuzzy"
:path ,(format "W%s" (im-V))
:application nil :search-option nil :begin 0 :end 0))))
;; Try one week earlier
(ignore-errors
(org-link-open
`(link
(:type "fuzzy"
:path ,(format "W%s" (1- (im-V)))
:application nil :search-option nil :begin 0 :end 0)))))
(im-org-tree-to-indirect-buffer))))))
(defun im-bullet-open-monthly-plan ()
"Open monthly planning entry in an indirect buffer."
(interactive)
(with-current-buffer (find-buffer-visiting bullet-org)
(save-restriction
(save-excursion
(when (or
(ignore-errors
(org-link-open
`(link
(:type "fuzzy"
:path ,(format "%s" (format-time-string "%B"))
:application nil :search-option nil :begin 0 :end 0))))
;; Try one month earlier
(ignore-errors
(org-link-open
`(link
(:type "fuzzy"
:path ,(format "%s" (format-time-string "%B" (time-subtract (current-time) (days-to-time 30))))
:application nil :search-option nil :begin 0 :end 0)))))
(im-org-tree-to-indirect-buffer))))))
(defun im-listen-voice-recordings () (interactive) (mapcar #'empv-enqueue (file-expand-wildcards "~/Audio/watch-recordings/*/*.m4a")))
(defun im-bullet-org-ensure ()
(if-let* ((buffer (find-buffer-visiting bullet-org)))
(switch-to-buffer buffer)
(find-file bullet-org)))
(defun im-org-suggest-filename (&optional path ext)
"Intelligently suggest a filename for the current context."
(let ((fname (if path (f-filename path) "")))
(if (and (equal
(expand-file-name (or (buffer-file-name (buffer-base-buffer (current-buffer))) "NOT_A_FILE"))
(expand-file-name bullet-org))
(not (s-prefix? "bullet_" fname)))
(f-join
(format "bullet_%s_%s%s"
(ignore-errors
(car (s-split
" "
(im-bullet-current-date))))
(if ext
(s-chop-suffix (f-ext fname) fname)
fname)
(if ext
ext
"")))
fname)))
Scratch/temporary buffers in side windows
Here I define two important functions and their helpers. The following functions are pretty useful for quick note taking or evaluating elisp. Having these buffers in a side window makes them immune to some window commands which is what I want.
- im-display-side-scratch-buffer
- This one opens (or closes if it's open) the scratch buffer in a side window, on the right. This is nice for quickly evaluating elisp, taking elisp related notes.
- im-display-side-temp-org-buffer
- This one opens (or closes if it's open) the
temp.orgfile in a in a side window, on the right. This is nice for taking some quick notes, writing some temporary todos etc.
(defun im-buffer-visible-p (buffer) "Check if given BUFFER is visible or not." (get-buffer-window buffer 'visible))
(defun im-display-buffer-in-side-window (buffer &optional width)
"Just like `display-buffer-in-side-window` but only takes a BUFFER and rest of the parameters are for my taste."
(when-let* ((existing-window (window-with-parameter 'window-side 'right)))
(delete-window existing-window))
(set-window-dedicated-p
;; ^ Setting this to nil so that `pop-to-buffer-same-window' calls
;; works in this window otherwise it'll set `window-dedicated-p' to
;; `side' and this will cause `pop-to-buffer-same-window' to open
;; stuff in another window.
(select-window
(display-buffer-in-side-window
buffer
`((side . right)
(slot . 0)
(window-width . ,(or width 84))
(window-parameters
(no-delete-other-windows . t)
(no-other-window . nil)))))
nil))
(defun im-remove-window-with-buffer (buffer-or-name)
"Remove window containing given BUFFER-OR-NAME."
(let ((buf (get-buffer buffer-or-name)))
(when buf
(mapc (lambda (window)
(when (eq (window-buffer window) buf)
(delete-window window)))
(window-list (selected-frame))))))
(defun im-toggle-side-buffer-with-file (file-path)
"Toggle FILE-PATH in a side buffer.
The buffer is opened in side window so it can't be accidentaly removed."
(interactive)
(let ((fname (file-name-nondirectory file-path)))
(if (im-buffer-visible-p fname)
(im-remove-window-with-buffer fname)
(im-display-buffer-in-side-window
(save-window-excursion
(find-file file-path)
(current-buffer))))))
(defun im-toggle-side-buffer-with-name (buffer-name)
"Hide/show given BUFFER-NAME in a side window."
(interactive)
(if (im-buffer-visible-p buffer-name)
(im-remove-window-with-buffer buffer-name)
(im-display-buffer-in-side-window (get-buffer buffer-name))))
(defun im-toggle-side-scratch-buffer () "Toggle the scratch buffer in side window. The buffer is opened in side window so it can't be accidentaly removed." (interactive) (im-toggle-side-buffer-with-file "~/.emacs.d/scratch.el"))
(defun im-toggle-side-temp-org-buffer () "Toggle `temp.org` in a side buffer for quick note taking. The buffer is opened in side window so it can't be accidentaly removed." (interactive) (im-toggle-side-buffer-with-file temp-org))
(defun im-toggle-side-bullet-org-buffer () "Toggle `bullet.org` in a side buffer for quick note taking. The buffer is opened in side window so it can't be accidentaly removed." (interactive) (im-toggle-side-buffer-with-file bullet-org))
(defun im-toggle-side-projects-buffer () "Toggle `projects.org` in a side buffer for quick note taking. The buffer is opened in side window so it can't be accidentaly removed." (interactive) (im-toggle-side-buffer-with-file projects-org))
(defun im-toggle-side-messages-buffer () "Toggle `projects.org` in a side buffer for quick note taking. The buffer is opened in side window so it can't be accidentaly removed." (interactive) (im-toggle-side-buffer-with-name "*Messages*"))
(defun im-toggle-side-tmr-buffer () (interactive) (im-toggle-side-buffer-with-name (get-buffer-create "*tmr-tabulated-view*")) (tmr-tabulated-mode))
(defun im-toggle-side-gpt-buffer () (interactive) (im-toggle-side-buffer-with-file gpt-org))
(defun im-toggle-side-clocked-header ()
"Toggle currently clocked header in a narrowed side buffer.
The created buffer is an indirect buffer. So the narrowing of the
original buffer is not changed. This let's you simultaneously work on
more than one header of a single org buffer."
(interactive)
(if (org-clock-is-active)
(progn
(im-toggle-side-buffer-with-name
(save-window-excursion
(im-org-clocked-header-to-indirect-buffer)
(buffer-name)))
(org-fold-show-all))
(user-error "No active clock")))
Toggle temproary buffers
(im-leader "ts" 'im-toggle-side-scratch-buffer "to" 'im-toggle-side-temp-org-buffer "th" 'im-toggle-side-bullet-org-buffer "tg" 'im-toggle-side-gpt-buffer "tH" 'im-toggle-side-clocked-header "tp" 'im-toggle-side-projects-buffer "tt" 'im-toggle-side-tmr-buffer "tm" 'im-toggle-side-messages-buffer)
(defvar im-scratch-project-path "~/workspace/projects/personal/scratch")
(defun im-new-scratch-file ()
"Create a new scratch file in `im-scratch-project-path' with selected major mode."
(interactive)
(let* ((mode (completing-read
"Select major mode: " obarray
(lambda (x)
(and (fboundp x)
(commandp x)
(string-match "-mode$" (symbol-name x))))
t
nil nil
(format "%s" major-mode)))
(extensions '((python-ts-mode . ".py")
(js-ts-mode . ".js")
(typescript-ts-mode . ".ts")
(java-ts-mode . ".java")
(emacs-lisp-mode . ".el")
(org-mode . ".org")
(md-ts-mode . ".md")
(go-ts-mode . ".go")
(bash-ts-mode . ".sh")))
(mode-symbol (intern mode))
(date (format-time-string "%Y-%m-%d")))
(find-file (make-temp-file
(concat im-scratch-project-path "/adhoc/" date "_")
nil
(alist-get mode-symbol extensions ".txt")))
(funcall mode-symbol)))
(defun im-toggle-side-scratch-buffer-typescript () "Toggle the scratch buffer in side window." (interactive) (im-toggle-side-buffer-with-file (concat im-scratch-project-path "/" "scratch.ts")))
(defun im-org-babel-remove-all-results nil
(interactive)
(goto-char 1)
(let ((total-removed 0))
(while (org-babel-next-src-block)
(when (org-babel-remove-result)
(setq total-removed (+ total-removed 1))))
(message (format "%d result blocks are removed." total-removed))))
(defun im-setup-indent-local (n) "Set indent for current buffer." (interactive "nHow many spaces do you want? ") (setq-local tab-width n) (setq-local c-basic-offset n) (setq-local sh-basic-offset n) (setq-local coffee-tab-width n) ; coffeescript (setq-local java-ts-mode-indent-offset n) (setq-local javascript-indent-level n) ; javascript-mode (setq-local js-indent-level n) ; js-mode (setq-local web-mode-markup-indent-offset n) ; web-mode, html tag in html file (setq-local web-mode-css-indent-offset n) ; web-mode, css in html file (setq-local web-mode-code-indent-offset n) ; web-mode, js code in html file (setq-local css-indent-offset n) ; css-mode (setq-local typescript-indent-level n) ; typescript-mode (setq-local typescript-ts-mode-indent-offset n) (setq-local java-ts-mode-indent-offset n) (setq-local go-ts-mode-indent-offset n) (message "OK!"))
(defun im-setup-indent-global (n) "Set indent globally." (interactive "nHow many spaces do you want? ") (setq tab-width n) (setq c-basic-offset n) (setq sh-basic-offset n) (setq coffee-tab-width n) ; coffeescript (setq java-ts-mode-indent-offset n) (setq javascript-indent-level n) ; javascript-mode (setq js-indent-level n) ; js-mode (setq web-mode-markup-indent-offset n) ; web-mode, html tag in html file (setq web-mode-css-indent-offset n) ; web-mode, css in html file (setq web-mode-code-indent-offset n) ; web-mode, js code in html file (setq css-indent-offset n) ; css-mode (setq typescript-indent-level n) ; typescript-mode (setq typescript-ts-mode-indent-offset n) (setq java-ts-mode-indent-offset n) (setq go-ts-mode-indent-offset n) (message "OK!"))
(im-leader-v "f" #'im-file-transient)
(general-def :states 'normal "M-f" #'dirvish-side)
(general-def :keymaps 'embark-file-map "O" #'im-display-buffer-other-frame)
(transient-define-prefix im-file-transient ()
[["File Operations"
("s" "Save buffer" save-buffer)
("r" "Rename file & buffer" im-rename-current-file-name-and-buffer)
("D" "Delete file" im-delete-current-file)
("i" "Show file info" file-info-show)
("I" "Show file info" im-print-buffer-file-info)
("g" "Revert to disk" revert-buffer)
("F" "Open in another frame" im-display-buffer-other-frame)]
["Copy Path"
("cc" "Copy path (pretty)" im-copy-current-filename)
("cC" "Copy path (raw)" (lambda () (interactive) (im-copy-current-filename :pretty nil)))
("cl" "Copy path (with context)" (lambda () (interactive) (im-copy-current-filename :pretty t :line t)))
("cd" "Copy directory" (lambda () (interactive) (im-copy-current-filename :fname default-directory :pretty t)))]
["Navigate"
("o" "Open file" find-file)
("d" "Open directory" consult-dir)
("h" "Dirvish sidebar" dirvish-side)
("b" "Switch buffer" switch-to-buffer)]
["Search/Index"
("l" "Search lines in file" consult-line)
("L" "Search lines in project" consult-line-multi)
(";" "Search lines in all open buffers" (lambda () (interactive) (consult-line-multi t)))
("m" "Jump to symbol in file" consult-imenu)
("M" "Jump to symbol in project" consult-imenu-multi)]])
Sometimes I just want to delete/rename/move etc. the current file without resorting to dired or any other file manager. Here are some interactive functions to do that.
Slightly modified from: http://steve.yegge.googlepages.com/my-dot-emacs-file
(defalias 'im-rename-this-file-name-and-buffer #'im-rename-current-file-name-and-buffer)
(defun im-rename-current-file-name-and-buffer ()
"Renames both current buffer and file it's visiting to NEW-NAME."
(interactive)
(cond
;; Rename file at point if we have one
((and (derived-mode-p 'org-mode)
(equal "file" (org-element-property :type (org-element-context))))
(im-org-rename-file-at-point))
;; Otherwise rename current buffer
(t (let* ((new-name (read-file-name "New name: " nil nil nil (f-filename (buffer-file-name))))
(name (buffer-name))
(filename (buffer-file-name)))
(unless filename
(user-error "Buffer '%s' is not visiting a file!" name))
(when (get-buffer new-name)
(user-error "A buffer named '%s' already exists!" new-name))
(rename-file filename new-name 1)
(rename-buffer new-name)
(set-visited-file-name new-name)
(set-buffer-modified-p nil)))))
(defalias 'im-delete/remove-this-file #'im-delete-current-file)
Slightly modified version of: http://www.ergoemacs.org/emacs/elisp_delete-current-file.html
(defun im-delete-current-file ()
"Delete the current file and copy it's content to `kill-ring'."
(interactive)
(when (y-or-n-p (format "Do you really want to remove this: \"%s\"?" (buffer-file-name)))
(kill-new (buffer-string))
(message "Buffer content copied to kill-ring.")
(when (and (buffer-file-name) (file-exists-p (buffer-file-name)))
(delete-file (buffer-file-name))
(message "Deleted file: 「%s」." (buffer-file-name)))
(let ((buffer-offer-save nil))
(set-buffer-modified-p nil)
(kill-buffer (current-buffer)))))
(defalias 'im-copy-current-file-path 'im-copy-current-filename)
(cl-defun im-copy-current-filename (&key fname uri pretty line)
"Copy the current buffer file name to the clipboard.
If the URI is non-nil, then add file:// in front of the file-path.
If LINE is non-nil, append the current line number in grep format."
(interactive
(list :pretty t))
(let* ((fname (if (equal major-mode 'dired-mode)
default-directory
(or fname (buffer-file-name))))
(filename (cond
((and fname uri)
(concat "file://" fname))
((and fname pretty)
(string-replace (expand-file-name "~") "~" fname))
(fname fname)
(t (buffer-name))))
(filename (if (and line fname (not (equal major-mode 'dired-mode)))
(format "%s:%d" filename (line-number-at-pos))
filename)))
(message ">> Copied: '%s'" (im-kill filename))))
(defun im-print-buffer-file-info (&optional kill-file-path)
(interactive "P")
(cl-flet ((prop (x) (propertize x 'face '(:foreground "systemPurpleColor" :slant italic)))
(plum (x) (propertize x 'face '(:foreground "systemPurpleColor")))
(bold (x) (propertize x 'face 'bold))
(yellow (x) (propertize (if (numberp x) (number-to-string x) x) 'face '(:foreground "systemOrangeColor")))
(italic (x) (propertize x 'face '(:slant italic))))
(let* ((region? (use-region-p))
(point-min (if region? (region-beginning) (point-min)))
(point-max (if region? (region-end) (point-max)))
(proj-name (im-current-project-name))
(proj-path (or (im-current-project-root) (expand-file-name "~/")))
(fpath (or (buffer-file-name) (buffer-name)))
(fpath-pretty (f-abbrev fpath))
(fpath-true buffer-file-truename)
(in-git? (file-exists-p (expand-file-name ".git" (im-current-project-root))))
(text
(format
"[%s] %s\n%s\n%s%s\n%s\n%s%s\n%s\n%s%s%s%s"
(bold proj-name)
(plum (string-remove-prefix proj-path fpath))
(if region? "*region*" "")
(format "%s%s" (if kill-file-path (bold "Copied: ") "") (italic fpath-pretty))
(if (equal fpath-pretty fpath-true)
""
(concat " → " fpath-true))
(format "%s: %s" (prop "- Size") (yellow (im-human-readable-size (- point-max point-min))))
(format
"%s: %s, %s: %s, %s: %s\n"
(prop "- Lines")
(yellow (count-lines point-min point-max))
(prop "Words")
(yellow (length (split-string (buffer-substring-no-properties point-min point-max) "\\W+" 'omit-nulls)))
(prop "Chars")
(yellow (- point-max point-min)))
(format "%s: %s" (prop "- Major Mode") major-mode)
(if in-git? (format "%s: %s" (prop "- Current branch") (bold (lab-git-current-branch))) "")
(concat "\n" (im-read-time) "\n")
(if in-git?
(concat (bold "\nGit Status: \n") (ansi-color-apply (shell-command-to-string (format "git -c color.ui=always status %s" fpath))))
(italic "\n Not in a git repository."))
"\n"
(if in-git?
(let* ((diff (ansi-color-apply (shell-command-to-string (format "git diff --color=always %s" fpath))))
(len (or (-some->> (s-split "\n" diff) (car) (length)) 0)))
(concat
"\n" (s-repeat len "─") "\n\n"
diff))
""))))
(im-peek
(lambda ()
(with-current-buffer (get-buffer-create "*im-buffer-info*")
(page-break-lines-mode)
(erase-buffer)
(insert (s-trim text))
(current-buffer))))
(when kill-file-path
(im-kill fpath-pretty)))))
xah-open-file-at-cursor
This is better than find-file-at-point because it takes line
numbers etc. into account.
(defun xah-open-file-at-cursor ()
"Open the file path under cursor.
If there is text selection, uses the text selection for path. If
the path starts with “http://”, open the URL in browser. Input
path can be {relative, full path, URL}.
Path may have a trailing “:‹n›” that indicates line number, or
“:‹n›:‹m›” with line and column number. If so, jump to that line
number. If path does not have a file extension, automatically
try with “.el” for elisp files. This command is similar to
`find-file-at-point' but without prompting for confirmation.
URL `http://ergoemacs.org/emacs/emacs_open_file_path_fast.html'
Version 2020-10-17"
(interactive)
(let* (
($inputStr
(if (use-region-p)
(buffer-substring-no-properties (region-beginning) (region-end))
(let ($p0 $p1 $p2
;; chars that are likely to be delimiters of
;; file path or url, e.g. whitespace, comma. The
;; colon is a problem. cuz it's in url, but not
;; in file name. Don't want to use just space as
;; delimiter because path or url are often in
;; brackets or quotes as in markdown or html
($pathStops "^ \t\n\"`'‘’“”|[]{}「」<>〔〕〈〉《》【】〖〗«»‹›❮❯❬❭〘〙·。\\"))
(setq $p0 (point))
(skip-chars-backward $pathStops)
(setq $p1 (point))
(goto-char $p0)
(skip-chars-forward $pathStops)
(setq $p2 (point))
(goto-char $p0)
(buffer-substring-no-properties $p1 $p2))))
($path
(replace-regexp-in-string
"^file:///" "/"
(replace-regexp-in-string
":\\'" "" $inputStr))))
(if (string-match-p "\\`https?://" $path)
(if (fboundp 'xahsite-url-to-filepath)
(let (($x (xahsite-url-to-filepath $path)))
(if (string-match "^http" $x )
(browse-url $x)
(find-file $x)))
(progn (browse-url $path)))
(progn ; not starting “http://”
(if (string-match "#" $path )
(let (
( $fpath (substring $path 0 (match-beginning 0)))
( $fractPart (substring $path (1+ (match-beginning 0)))))
(if (file-exists-p $fpath)
(progn
(find-file $fpath)
(goto-char 1)
(search-forward $fractPart ))
(when (y-or-n-p (format "File no exist: 「%s」. Create?" $fpath))
(find-file $fpath))))
(if (string-match "^\\`\\(.+?\\):\\([0-9]+\\)\\(:[0-9]+\\)?\\'" $path)
(let (
($fpath (match-string 1 $path))
($line-num (string-to-number (match-string 2 $path))))
(if (file-exists-p $fpath)
(progn
(find-file $fpath)
(goto-char 1)
(forward-line (1- $line-num)))
(when (y-or-n-p (format "File no exist: 「%s」. Create?" $fpath))
(find-file $fpath))))
(if (file-exists-p $path)
(progn ; open f.ts instead of f.js
(let (($ext (file-name-extension $path))
($fnamecore (file-name-sans-extension $path)))
(if (and (string-equal $ext "js")
(file-exists-p (concat $fnamecore ".ts")))
(find-file (concat $fnamecore ".ts"))
(find-file $path))))
(if (file-exists-p (concat $path ".el"))
(find-file (concat $path ".el"))
(when (y-or-n-p (format "File no exist: 「%s」. Create?" $path))
(find-file $path ))))))))))
(define-key evil-normal-state-map (kbd "gf") 'xah-open-file-at-cursor)
(defun xah-escape-quotes (@begin @end)
"Replace 「\"」 by 「\\\"」 in current line or text selection.
See also: `xah-unescape-quotes'
URL `http://ergoemacs.org/emacs/elisp_escape_quotes.html'
Version 2017-01-11"
(interactive
(if (use-region-p)
(list (region-beginning) (region-end))
(list (line-beginning-position) (line-end-position))))
(save-excursion
(save-restriction
(narrow-to-region @begin @end)
(goto-char (point-min))
(while (search-forward "\"" nil t)
(replace-match "\\\"" "FIXEDCASE" "LITERAL")))))
(defun xah-unescape-quotes (@begin @end)
"Replace 「\\\"」 by 「\"」 in current line or text selection.
See also: `xah-escape-quotes'
URL `http://ergoemacs.org/emacs/elisp_escape_quotes.html'
Version 2017-01-11"
(interactive
(if (use-region-p)
(list (region-beginning) (region-end))
(list (line-beginning-position) (line-end-position))))
(save-excursion
(save-restriction
(narrow-to-region @begin @end)
(goto-char (point-min))
(while (search-forward "\\\"" nil t)
(replace-match "\"" "FIXEDCASE" "LITERAL")))))
(general-def :states '(visual) "ze" #'xah-escape-quotes "zE" #'xah-unescape-quotes)
(defun im-eksi-gundem-sirali ()
"Eksi gundemini entry sayisina gore sirala ve `completing-read' yap."
(interactive)
(let ((results (->>
(with-temp-buffer
(insert
(shell-command-to-string "curl --silent --header \"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0\" https://eksisozluk.com/basliklar/gundem | grep '?a=popular' | sed -E 's/[ ]*href=\"(.*)\">(.*) <small>(.*)<\\/small>(.*)/(\\3) \\2|||\\1/' | sort -V -r | uniq"))
(goto-char (point-min))
(xml-parse-string))
(s-trim)
(s-split "\n")
(--map (s-split "|||" it))
(--map `(,(car it) . ,(cadr it))))))
(->>
(im-completing-read
"Baslik: " results
:sort? nil
:formatter #'car)
(cdr)
(format "https://eksisozluk.com/%s")
(browse-url))))
(defun im-uuid () "Generate a UUID." (s-trim (shell-command-to-string "uuidgen")))
(defun im-insert-uuid () "Insert UUID." (interactive) (insert (im-uuid)))
(defvar-local im-shell-command-mode-command nil "Current shell command that belongs to the buffer.")
(define-minor-mode im-shell-command-mode "Shell command mode." :lighter "iscm" :keymap (make-sparse-keymap))
(evil-define-key 'normal im-shell-command-mode-map (kbd "gr") #'im-shell-command-mode-rerun-command)
(defun im-shell-command-mode-rerun-command () "Re-run the shell command." (interactive) (apply #'im-shell-command im-shell-command-mode-command))
In normal mode, hitting ! will display im-shell-command but in visual mode hitting ! will open evil's default evil-shell-command on selected region.
(evil-define-key 'normal 'global (kbd "!") #'im-shell-command)
(defun im-shell-smart-history ()
(->>
(with-temp-buffer
(insert-file-contents eshell-history-file-name)
(buffer-string))
s-trim
(s-split "\n")
(--map (if (s-prefix? ":" it)
(s-replace-regexp "^: [0-9:]+;" "" it)
it))
(--filter (and (not (s-blank? it))
(> (length it) 5)
(not (s-matches? "^\\(ls\\|pwd\\|exit\\|cd\\|echo\\)" it))
(not (s-suffix? "\\" it))))
(reverse)))
(cl-defun im-shell-command
(&key
command
args
eat
env
async
switch
(legend t)
(buffer-name (concat "*" command "*"))
(on-start (lambda (&rest _)))
(on-finish (lambda (&rest _)))
(on-fail (lambda (&rest _))))
"Run given shell COMMAND and redirect output to given BUFFER-NAME.
This is a wrapper around `start-process-shell-command' that adds
support for ANSI term colors and some syntactic convenience.
If ARGS is non-nil list, then use `start-process' with command as
COMMAND and args as ARGS.
ENV is a list of environment variables and values in the
following form:
\\='(\"DISPLAY=\" \"X=5\" \"Y=6\")
If EAT is non nil, use eat shell to display output which handles
terminal outputs way better. ARGS must be a list.
When called interactively, asks for a command to run (with eshell
completion).
In BUFFER-NAME, `im-shell-command-mode' is activated and you can
use `im-shell-command-mode-rerun-command' or \"gr\" to replay the
command.
If EAT is non-nil and BUFFER-NAME does not contain \"*...*\", the
asterisks will be added automatically by eat.
Returns process buffer."
(interactive
(let ((command (im-completing-read "Command: " (im-shell-smart-history) :sort? nil)))
(list
:command command
:switch t
:on-finish
(lambda (&rest _)
(let ((msg (format ">> \"%s\" finished successfully." command)))
(message msg)
(when (not (frame-focus-state))
(alert msg))))
:on-fail
(lambda (&rest _)
(let ((msg (format ">> \"%s\" FAILED." command)))
(message msg)
(when (not (frame-focus-state))
(alert msg)))))))
(let* ((process-environment (append env process-environment))
(start-time (current-time))
(proc
(if eat
(progn
(let ((buffer (apply #'eat-make (->>
buffer-name
(s-chop-prefix "*")
(s-chop-suffix "*"))
command nil args)))
(setq buffer-name (buffer-name buffer))
(get-buffer-process buffer)))
(if args
(apply #'start-process command buffer-name command args)
(start-process-shell-command command buffer-name command))))
(proc-out ""))
(let ((fn (lambda (resolve reject)
(set-process-sentinel
proc
(lambda (p _e)
(with-current-buffer (get-buffer-create buffer-name)
(read-only-mode -1)
(when legend
(let ((exit-code (process-exit-status p)))
(im-shell-command--update-header-line
(if (= 0 exit-code) 'finished 'failed)
start-time
p))))
(let ((exit-code (process-exit-status p)))
(if (= 0 exit-code)
(funcall resolve proc-out)
(funcall reject exit-code))))))))
(prog1 (if async
(promise-new fn)
(funcall fn on-finish on-fail))
(unless eat
(set-process-filter
proc
(lambda (proc str)
(with-current-buffer buffer-name
(im-shell-command--update-header-line 'running start-time proc)
(setq proc-out (concat proc-out str))
(let ((inhibit-read-only t))
(save-excursion
(goto-char (point-max))
(insert (ansi-color-apply (s-replace "
" "\n" str)))))))))
(with-current-buffer buffer-name
(unless eat
(prog-mode))
(im-shell-command-mode 1)
(evil-normal-state)
(setq-local
im-shell-command-mode-command
(list
:command command
:args args
:eat eat
:env env
:async async
:switch switch
:legend legend
:buffer-name buffer-name
:on-start on-start
:on-finish on-finish
:on-fail on-fail))
(when legend
(im-shell-command--update-header-line 'running start-time proc))
(funcall on-start))
(when switch
(switch-to-buffer buffer-name))
(get-buffer buffer-name)))))
(defun im-shell-command--update-header-line (status &optional start-time proc)
"Update header line to show STATUS and keybindings.
STATUS should be one of `running', `finished', or `failed'.
START-TIME is the time when the command started.
PROC is the process object."
(setq header-line-format
(list
;; Status indicator
(propertize
(pcase status
('running " ● Running ")
('finished " ✓ Finished ")
('failed " ✗ Failed "))
'face (pcase status
('running '(:foreground "orange" :weight bold))
('finished '(:foreground "PaleGreen4" :weight bold))
('failed '(:foreground "red" :weight bold))))
;; Elapsed time
(when start-time
(let ((elapsed (float-time (time-subtract (current-time) start-time))))
(propertize
(format " [%s] " (im-shell-command--format-duration elapsed))
'face 'font-lock-comment-face)))
;; Exit code (when finished/failed)
(when (and proc (memq status '(finished failed)))
(propertize
(format "exit: %d " (process-exit-status proc))
'face (if (eq status 'failed)
'(:foreground "red")
'font-lock-comment-face)))
;; PID (when running)
(when (and proc (eq status 'running))
(propertize
(format "pid: %d " (process-id proc))
'face 'font-lock-comment-face))
;; Working directory
(propertize
(format "in: %s " (abbreviate-file-name default-directory))
'face 'font-lock-doc-face)
" | "
;; Keybindings
(propertize "gr" 'face 'help-key-binding) ": rerun "
;; (propertize "q" 'face 'help-key-binding) ": quit "
;; (when (eq status 'running)
;; (concat (propertize "C-c C-c" 'face 'help-key-binding) ":kill "))
)))
(defun im-shell-command--format-duration (seconds)
"Format SECONDS as human-readable duration."
(cond
((< seconds 60) (format "%.1fs" seconds))
((< seconds 3600) (format "%dm%ds" (/ (floor seconds) 60) (mod (floor seconds) 60)))
(t (format "%dh%dm%ds"
(/ (floor seconds) 3600)
(mod (/ (floor seconds) 60) 60)
(mod (floor seconds) 60)))))
Run functions globally
Simply use im-dmenu function instead of the default
completing-read function when the code is called inside this
macro. This way, you can use your completing-read based functions
within your system, without needing to focus Emacs first.
(defmacro im-globally (&rest body)
`(let ((completing-read-function #'im-dmenu))
,@body))
(use-package narrow-indirect
:defer t
:ensure (:host github :repo "emacsmirror/narrow-indirect")
:general
(im-leader-v
"n" #'im-narrow-dwim
"N" #'im-narrow-indirect-dwim
"ow" #'widen)
:config
(defun im-narrow-dwim ()
"Smart narrowing."
(interactive)
(cond
((use-region-p) (narrow-to-region (region-beginning) (region-end)))
((eq major-mode 'org-mode) (org-narrow-to-subtree))
((derived-mode-p 'diff-mode) (diff-restrict-view))
((and (derived-mode-p 'prog-mode) (which-function)) (narrow-to-defun))
(outli-mode (outli-toggle-narrow-to-subtree))
(outline-minor-mode (im-outline-narrow-to-subtree))))
(defun im-narrow-indirect-dwim ()
"Smart indirect narrowing.
Does the same thing as `im-narrow-dwim' but opens the narrowed part in
an indirect buffer."
(interactive)
(pcase-let ((`(,min ,max ,title) (save-excursion
(save-restriction
(widen)
(im-narrow-dwim)
(list (point-min)
(point-max)
(progn
(goto-char (point-min))
(s-trim (thing-at-point 'line))))))))
(ni-narrow-to-region-indirect-other-window
min max (point) (format "*narrow-indirect: %s:%s*" (buffer-name) title)))))
(defun im-meme-downloader (url &optional file-title)
(interactive
(list
(read-string "URL: ")
(read-string "Title: ")))
(let* ((title
(or file-title
(s-trim
(thread-last
url
im-url-get-title
im-string-url-case
s-downcase))))
(dir (expand-file-name "~/Documents/memes"))
(default-directory dir))
(message "im-meme-downloader :: Downloading...")
(when-let* ((buf (get-buffer "*im-meme-downloader*")))
(kill-buffer buf))
(im-shell-command
:buffer-name "*im-meme-downloader*"
:command "yt-dlp"
:args `( "--output" ,(format "%s.%%(ext)s" title) ,url)
:switch nil
:on-finish
(lambda (output &rest _)
(if-let* ((matches (or
(s-match "\\[download\\] \\(.*\\) has already been downloaded" output)
(s-match "^\\[Merger\\] Merging formats into \"\\(.*\\)\"" output)
(s-match "^\\[download\\] Destination: \\(.*\\)" output)))
(fname (abbreviate-file-name (nth 1 matches))))
(progn
(alert (format "Downloaded: %s!" fname))
(message "im-meme-downloader :: Downloading...Done. Copied to clipboard")
(kill-new (format "%s/%s" dir fname)))
(alert "Can't parse yt-dlp output!")))
:on-fail
(lambda (&rest _)
(message "im-meme-downloader :: Downloading...Failed")
(alert (format "Error while downloading: %s!" url))))))
(defun im-open-zoom-meeting-dwim (&optional link)
(interactive
(list
(or (thing-at-point 'url) (read-string "Link: "))))
(when-let* ((zoom (s-match
"https://.*zoom\\.us/j/\\(\\w+\\)\\?pwd=\\(\\w+\\)"
link))
(cmd (format
"open 'zoommtg://zoom.us/join?confno=%s&pwd=%s'"
(nth 1 zoom)
(nth 2 zoom))))
(message ">> Running %s" cmd)
(shell-command cmd)))
(defun im-join-current-zoom-meeting ()
"Join the most appropriate meeting via Zoom link."
(interactive)
(with-current-buffer (find-buffer-visiting bullet-org)
(save-window-excursion
(save-excursion
(save-restriction
(im-bullet-focus-today)
(let* ((now (current-time))
(meetings
(->> (org-map-entries
(lambda ()
(when-let* ((sched (org-entry-get nil "SCHEDULED"))
(_ (string-match "<\\([0-9-]+\\) [A-Za-z]+ \\([0-9:]+\\)-\\([0-9:]+\\)>" sched))
(date (match-string 1 sched))
(start (date-to-time (format "%s %s" date (match-string 2 sched))))
(end (date-to-time (format "%s %s" date (match-string 3 sched))))
(until-start (float-time (time-subtract start now))))
(list :marker (point-marker)
:heading (org-get-heading t t t t)
:zoom (org-entry-get nil "ZOOM")
:until-start until-start
:in-progress (and (time-less-p start now) (time-less-p now end))
:starting-soon (and (> until-start 0) (<= until-start 300))
:well-past (> (float-time (time-subtract now start)) 600))))
"meeting" 'agenda)
(-non-nil)))
(best (->> meetings
(--filter (or (plist-get it :in-progress) (plist-get it :starting-soon)))
(--sort (cond
((and (plist-get it :starting-soon)
(plist-get other :in-progress)
(plist-get other :well-past)) t)
((and (plist-get other :starting-soon)
(plist-get it :in-progress)
(plist-get it :well-past)) nil)
(t (< (plist-get it :until-start) (plist-get other :until-start)))))))
(best (if (length= best 1)
(car best)
(im-completing-read
"Which? " best
:formatter (lambda (x) (plist-get x :heading))))))
(cond
(best
(when (org-clock-is-active)
(let ((org-log-note-clock-out nil))
(org-clock-out)))
(goto-char (plist-get best :marker))
(let ((zoom (or (plist-get best :zoom)
(when-let* ((choice (completing-read
(format "No ZOOM for '%s'. Select link: "
(plist-get best :heading))
(cons "Cancel" (-map #'car im-zoom-links))
nil t))
(_ (not (equal choice "Cancel"))))
(alist-get choice im-zoom-links nil nil #'equal)))))
(when zoom
(message "Joining: %s" (plist-get best :heading))
(browse-url zoom))))
(t (message "No meeting in progress or starting within 5 minutes")))))))))
macOS calendar functions
My work computer is a Mac. I synchronize my work calendar into my local by using macOS' Calendar app and I utilize icalBuddy to interact with that calendar from Emacs.
(defun im-calendar-now ()
"Show the current calendar item details in a buffer and open the zoom link if it has one."
(interactive)
(im-shell-command
:command "icalBuddy -f -ea eventsNow"
:switch t
:buffer-name "*calendar-now*"
:on-start (lambda (&rest _) (erase-buffer))
:on-finish (lambda (it)
(when-let* ((zoom (nth 1 (s-match "\\(https://.*zoom.us/j/.*\\)\\(\b\\|\n\\)" it))))
(im-open-zoom-meeting-dwim zoom)))))
(define-derived-mode im-calendar-mode outline-mode "Calendar" "Calendar...")
(general-def :keymaps 'im-calendar-mode-map :states 'motion "TAB" #'outline-cycle)
(defun im-calendar-today ()
"Show today's calendar in a buffer.
The resulting buffer has `outline-mode' enabled, so you can use
`outline-mode' specific bindings to jump, hide/show stuff."
(interactive)
(im-shell-command
:command "icalBuddy -f eventsToday"
:switch t
:on-start (lambda (&rest _) (erase-buffer))
:on-finish (lambda (&rest _)
(im-calendar-mode)
(setq-local outline-regexp "• ")
(setq-local outline-level #'outline-level)
(goto-char (point-min))
(outline-cycle-buffer))
:buffer-name "*calendar-today*"))
(defun im-calendar-for-date (date)
"Show given DATEs calendar in a buffer."
(interactive (list (format-time-string "%Y-%m-%d" (org-read-date nil 'to-time nil "Date: "))))
(im-shell-command
:command (format "icalBuddy -f eventsFrom:%s to:%s" date date)
:switch t
:on-start (lambda (&rest _) (erase-buffer))
:on-finish (lambda (&rest _)
(im-calendar-mode)
(setq-local outline-regexp "• ")
(setq-local outline-level #'outline-level)
(goto-char (point-min))
(outline-cycle-buffer))
:buffer-name (format "*calendar-%s*" date)))
(defalias 'im-clipboard-tesseract-ocr-text #'im-clipboard-image-to-text)
(defun im-clipboard-image-to-text ()
(interactive)
(let ((temp-image (make-temp-file "tesseract-image" nil ".png"))
(temp-text (make-temp-file "tesseract-text"))
(buffer (get-buffer-create "*ocr-result*")))
(im-save-clipboard-image-to-file temp-image)
(set-process-sentinel
(start-process "*tesseract-ocr*" nil "tesseract" temp-image temp-text)
(lambda (proc out)
(if (and
(eq 'exit (process-status proc))
(eq 0 (process-exit-status proc)))
(progn
(with-current-buffer buffer
(erase-buffer)
(insert (format "[[%s]]\n\n\n" temp-image))
(insert-file-contents (concat temp-text ".txt"))
(iimage-mode))
(switch-to-buffer buffer))
(message "*tesseract-ocr*: %s" (string-trim out)))))))
(defun im-image-edit-tags (tags image &optional clear-all)
"Add TAGS to IMAGE.
If CLEAR-ALL is non-nil, clear all tags before setting TAGS as image
tags. Otherwise TAGS are appended. If cursor is on an org link, use
that as the IMAGE (when called interactively)."
(interactive
(let* ((org-link? (org-in-regexp org-link-any-re 1))
(file
(if org-link?
(progn
(goto-char (car org-link?))
(plist-get (cadr (org-element-link-parser)) :path))
(read-file-name "Image file: ")))
(file-tags (s-join ", " (im-image-tags file)))
(tags (completing-read-multiple
"Tags: "
(im-directory-image-tags (f-dirname file))
nil nil
(if (s-blank? file-tags) nil (concat file-tags ",")))))
(list
tags
file
t)))
(setq image (expand-file-name image))
(when clear-all
(shell-command (format "exiftool -overwrite_original -keywords='' '%s'" image)))
(shell-command
(format "exiftool -overwrite_original %s '%s'"
(s-join " " (--map (format "-keywords+='%s'" (s-trim it)) tags))
image))
;; Also add these tags as file extended attribute which helps KDE to
;; easily index these files. I'm using this in conjunction with exif
;; attributes because it's quite easy to loose extended attributes
;; and exif attributes serves as a backup.
(if clear-all
(im-set-file-attribute image "user.xdg.tags" (s-join "," tags))
(im-set-file-attribute
image "user.xdg.tags"
(s-join "," (cons (im-get-file-attribute image "user.xdg.tags") tags)))))
(defun im-image-tags (image)
"Return tags of IMAGE."
(let ((result
(plist-get
(seq-first
(json-parse-string
(shell-command-to-string (format "exiftool -quiet -json -keywords '%s'" (expand-file-name image)))
:array-type 'list
:object-type 'plist))
:Keywords)))
(if (listp result) result (list result))))
(defun im-directory-image-tags (&optional directory)
"Return all unique image tags in DIRECTORY.
If DIRECTORY is null, `default-directory' is used."
(-uniq
(--mapcat
(plist-get it :Keywords)
(json-parse-string
(shell-command-to-string
(im-ensure-binary
(format "exiftool -m -quiet -json -keywords '%s'" (expand-file-name (or directory default-directory)))))
:object-type 'plist :array-type 'list))))
(defun im-get-file-attribute (file name) "Get extended attribute NAME for FILE." (shell-command-to-string (format "getfattr --absolute-names --only-values --name='%s' '%s'" name (expand-file-name file))))
(defun im-set-file-attribute (file name value)
"Set extended attribute NAME as VALUE for FILE.
When called interactively, set given NAME to VALUE as extended
attribute for current buffers file or selected file."
(interactive
(let ((attr (completing-read "Attribute: " '("user.xdg.tags"))))
(list
(or (buffer-file-name) (read-file-name "File: "))
attr
(read-string (format "Value for '%s': " attr)))))
(shell-command-to-string (format "setfattr --name='%s' --value='%s' '%s'" name value (expand-file-name file))))
(defun im-read-time ()
"Show approximate read time of the selected area or whole buffer."
(interactive)
(funcall
(if (called-interactively-p 'interactive) #'message #'format)
"%s min read"
(ceiling
(/ (apply #'count-words
(if (region-active-p)
(list (region-beginning) (region-end))
(list (point-min) (point-max))))
;; I am using a value lower than average wpm to account for other
;; things (distractions etc.)
230.0))))
im-peek – Inline/popup documentation/translate/dictionary etc.
quick-peek
I based im-peek on quick-peek but will drop the dependency eventually as I am not able to properly configure some aspects of the "peek window".
(use-package quick-peek :ensure (:host github :repo "isamert/quick-peek") :defer t)
(im-leader-v "mt" #'im-peek-translate "ms" #'im-peek-sozluk "mS" #'im-peek-dictionary "me" #'im-peek-etimoloji "mE" #'im-peek-etymology "md" #'im-peek-doc "mD" #'im-popup-doc "mc" #'im-peek-source)
(general-def :states 'normal :keymaps '(prog-mode-map) "K" #'im-peek-doc "M-RET" #'im-popup-doc)
(with-eval-after-load 'evil (setq evil-lookup-func #'im-peek-doc))
(general-def :states 'normal :keymaps 'im-peek-mode-map (kbd "K") #'im-peek-remove)
(defvar im-peek--buffer nil) (defvar im-peek--string nil) (defvar im-peek--pos 0) (defconst im-peek--line-count 30)
(define-minor-mode im-peek-mode
"Elisp popup documentation mode."
:lighter " ElPopDoc"
:global t
:keymap (let ((map (make-sparse-keymap)))
(define-key map (kbd "M-j") #'im-peek-mode-scroll-down)
(define-key map (kbd "M-k") #'im-peek-mode-scroll-up)
(define-key map (kbd "C-g") #'im-peek-remove)
(define-key map (kbd "M-RET") #'im-peek-jump)
map)
(setf (alist-get #'im-peek-mode minor-mode-overriding-map-alist)
im-peek-mode-map)
(evil-normalize-keymaps))
(defun im-peek-jump () (interactive nil im-peek-mode) (im-peek-remove) (im-display-buffer-other-frame im-peek--buffer))
(defun im-peek-mode-scroll-down (&optional lines)
(interactive nil im-peek-mode)
(quick-peek-hide)
(quick-peek-show
(->>
(s-lines im-peek--string)
(-drop (setq im-peek--pos (+ (or lines 1) im-peek--pos)))
(s-join "\n"))
nil nil im-peek--line-count))
(defun im-peek-mode-scroll-up (&optional lines)
(interactive nil im-peek-mode)
(quick-peek-hide)
(quick-peek-show
(->>
(s-lines im-peek--string)
(-drop (setq im-peek--pos (max 0 (+ (- (or lines 1)) im-peek--pos))))
(s-join "\n"))
nil nil im-peek--line-count))
(defun im-peek-remove ()
(interactive nil im-peek-mode)
(setq im-peek--pos 0)
(im-peek-mode -1)
(quick-peek-hide)
(setq minor-mode-overriding-map-alist
(assq-delete-all #'im-peek-mode
minor-mode-overriding-map-alist)))
(defun im-unfold-if-folded (&optional pt)
(setq pt (or pt (point)))
(when (listp buffer-invisibility-spec)
(when (and
;; Check if org-fold is enabled or not first
(alist-get 'org-fold-outline buffer-invisibility-spec)
(org-fold-folded-p pt))
(save-excursion
(goto-char pt)
(org-fold-show-entry)))
(when (and
(alist-get 'outline buffer-invisibility-spec)
(outline-invisible-p pt))
(save-excursion
(goto-char pt)
(outline-show-entry)))
(when (and
(alist-get 'hs buffer-invisibility-spec)
(save-excursion
(goto-char pt)
(hs-already-hidden-p)))
(save-excursion
(goto-char pt)
(hs-toggle-hiding)))))
(defvar im-peek-use-popup nil "Instead of showing peek in `quick-peek' overlay, show it in a popup frame.")
TODO support fn returning a string instead of a buffer. Create a temp buffer when jump is called
(defun im-peek (fn)
"Display a nice little small window below current line.
FN should take no args and return a buffer with the intended
contents."
(let ((buffer (save-window-excursion (funcall fn)))
(popup? im-peek-use-popup))
(setq im-peek--buffer buffer)
;; `run-at-time' function is useful for forcing the following code
;; to run in main event loop. This helps when `im-peek' is called
;; in an async context.
(run-at-time
0 nil
(lambda ()
(let ((str (setq im-peek--string
(with-current-buffer buffer
(font-lock-ensure)
(buffer-string)))))
(if popup?
(im-peek-jump)
;; Center the current point if the remaining line count is low
(when (and
(< (im-line-count-below-cursor) (length (s-lines str)))
(< (im-line-count-below-cursor) (- (window-height) 10)))
(recenter 10))
(quick-peek-show str nil nil im-peek--line-count)
(im-unfold-if-folded (save-excursion (end-of-line) (1+ (point))))
(im-peek-mode +1)))))))
(defun im-peek-open? () "Return t if >=1 peek window is open." (car quick-peek--overlays))
(use-package google-translate
:defines (google-translate-pop-up-buffer-set-focus)
:defer t
:custom
(google-translate-listen-program "mpv")
;; Try 'curl or 'wget if getting any errors
(google-translate-backend-method 'emacs)
:init
(setq google-translate-translation-directions-alist
;; Ordered based on my usage:
'(("auto" . "en")
("en" . "tr")
("nl" . "en")
("tr" . "en")
("en" . "nl"))))
(defun im-peek-translate ()
"Simple `google-translate-smooth-translate' wrapper.
Use C-n C-p to switch between translation directions."
(interactive)
(require 'google-translate)
(im-peek
(lambda ()
(let ((google-translate-pop-up-buffer-set-focus t))
(google-translate-smooth-translate)
(current-buffer)))))
(defun im-peek-sozluk ()
"Show definition of the word at point (tr)."
(interactive)
(im-peek
(lambda ()
(call-interactively #'sozluk)
(current-buffer))))
(defun im-peek-dictionary ()
"Show definition of the word at point (en)."
(interactive)
(im-peek
(lambda ()
(call-interactively #'wordnut-lookup-current-word)
(current-buffer))))
(defun im-peek-etimoloji ()
"Show etymology of the word at point (tr)."
(interactive)
(im-peek
(lambda ()
(call-interactively #'sozluk-etymology)
(current-buffer))))
(defun im-peek-etymology ()
"Show etymology of the word at point (en)."
(interactive)
(im-peek
(lambda ()
(call-interactively #'im-etymology-en)
(current-buffer))))
This also works in org mode as "K" keybinding because `evil-lookup-func' is set to this
(defun im-peek-doc ()
"Show documentation at point."
(interactive)
(im-peek
(cond
((ignore-errors (symbol-value 'lsp-mode)) #'im-peek-doc--lsp)
((-contains? '(emacs-lisp-mode lisp-interaction-mode) major-mode) #'im-peek-doc--elisp)
((equal major-mode 'clojure-mode) #'im-peek-doc--clojure)
((equal major-mode 'org-mode)
(cond
(jupyter-org-interaction-mode
#'im-peek-doc--jupyter)
((string= "id" (org-element-property :type (org-element-context)))
#'im-peek-doc--org-id-at-point)
(t
(lambda ()
(call-interactively #'wordnut-lookup-current-word)
(current-buffer)))))
(eldoc-mode (lambda ()
(let ((doc-buf (eldoc-doc-buffer))
(new-buf (get-buffer-create "*eldoc-copy*")))
(with-current-buffer new-buf
(erase-buffer)
(insert (with-current-buffer doc-buf (buffer-string))))
new-buf))))))
(defun im-popup-doc ()
"Same as `im-peek-doc' but open in a popuup frame."
(interactive)
(let ((im-peek-use-popup t))
(im-peek-doc)))
TODO: Add lsp mode etc.
(defun im-peek-source ()
"Show source at point."
(interactive)
(let (pos)
(im-peek
(lambda ()
(call-interactively #'xref-find-definitions)
(setq pos (line-number-at-pos))
(current-buffer)))
(im-peek-mode-scroll-down (- pos 2))))
(defun im-peek-doc--elisp ()
(let ((help-xref-following t))
(helpful-symbol (symbol-at-point))
(current-buffer)))
(defun im-peek-doc--lsp ()
(let ((result
;; Taken from lsp-describe-thing-at-point
(-some->> (lsp--text-document-position-params)
(lsp--make-request "textDocument/hover")
(lsp--send-request)
(lsp:hover-contents)
(funcall (-flip #'lsp--render-on-hover-content) t)
(string-trim-right))))
(unless result
(user-error "No doc at point"))
(with-current-buffer (get-buffer-create "*im-lsp-md-doc*")
(erase-buffer)
(insert result)
(current-buffer))))
(defun im-peek-doc--clojure ()
(save-window-excursion
(cider-doc)
(current-buffer)))
(defun im-peek-doc--jupyter ()
(save-window-excursion
(jupyter-inspect-at-point)
(get-buffer "*Help*")))
(defun im-peek-doc--org-id-at-point ()
(interactive)
(if-let* ((marker (im-org--link-target-at-point)))
(org-with-point-at marker
(let ((text (buffer-substring
(point)
(save-excursion (org-end-of-subtree t t) (point)))))
(with-current-buffer (get-buffer-create " *im-org-ref*")
(erase-buffer)
(insert text)
(current-buffer))))
(message "Not a valid org link at point.")))
(general-def :states 'normal "<f3>" #'im-popup-ghostel-project "<f4>" #'im-popup-ielm)
(defun im-popup-ielm ()
"Open IELM in another frame with the current buffer as the working buffer.
This allows evaluating expressions in the context of the current buffer
while viewing results in a separate frame."
(interactive)
(let ((buf (current-buffer)))
(save-window-excursion
(ielm))
(with-current-buffer "*ielm*"
(setq ielm-working-buffer buf))
(im-display-buffer-other-frame "*ielm*")))
(defun im-popup-ghostel-project ()
(interactive)
(im-display-buffer-other-frame
(save-window-excursion
(if (project-current)
(ghostel-project)
(let ((dir default-directory))
(or (--find (with-current-buffer it (and (eq major-mode 'ghostel-mode) (f-same? default-directory dir))) (buffer-list))
(ghostel 'new)))))))
(cl-defun im-extract (files)
"Extract each FILES into a folder.
This is a simple wrapper around `aunpack' binary. It simply
finds the best way to extract and does not overwrite anything."
(interactive (list (dired-get-marked-files)))
(let ((buff (current-buffer)))
(im-shell-command
:command (im-ensure-binary "aunpack" :package "atool")
:args (append '("--each") files)
:on-finish
(lambda (out)
(let ((extracted (--keep
(s-match "\\(.*\\): extracted to `\\(.*\\)'\\( (.*)\\)?" it)
(s-split "\n" out t))))
(with-current-buffer buff
;; Dirvish does not kick in after doing the following, I
;; don't know why. Doing anything on the buffer fixes the
;; buffer tho.
(when (derived-mode-p 'dired-mode)
(revert-buffer)
(dired-goto-file (expand-file-name (nth 2 (car extracted))))))
(message (s-join "\n" (-map #'car extracted)))))
:on-fail
(lambda (&rest _)
(error ">> Extraction failed! See *im-extract* buffer"))
:buffer-name "*im-extract*")))
(defun im-etymology-en (input)
"Find and retrieve the given word etymology from etymonline.
Adapted from: https://babbagefiles.xyz/emacs_etymologies/"
(interactive
(list (read-string "Word: " (im-region-or 'word))))
(let* ((buffer (generate-new-buffer (format "*etymology: %s*" input))))
(switch-to-buffer buffer)
(insert (shell-command-to-string (concat "links -dump http://www.etymonline.com/word/" input)))
(goto-char (point-min))
(if (re-search-forward "Error 404 (Not Found)" nil t)
(progn
(kill-buffer)
(user-error "Word not found: %s" input))
(progn
(delete-region (point-min) (progn
(goto-char (point-min))
(search-forward "[\s\s]")))
(ignore-errors
(delete-region
(progn
(goto-char (point-max))
(search-backward-regexp "See all related words"))
(point-max)))))
(goto-char (point-min))))
(defun im-git-use/apply-my/personal-config ()
"Configure current repo to use my personal (non-work) git info."
(interactive)
(ignore-errors
(lab-git-origin-switch-to-ssh))
(shell-command-to-string "git config user.name \"Isa Mert Gurbuz\"")
(shell-command-to-string "git config user.email \"isamertgurbuz@gmail.com\"")
(message "Done."))
(defun im-yaml-to-json (yaml &optional replace?)
"Convert YAML to JSON."
(interactive (list (im-region-or 'string)))
(let ((json (im-kill (json-encode (yaml-parse-string yaml)))))
(when (and replace? (use-region-p))
(replace-region-contents
(region-beginning)
(region-end)
(lambda () json)))))
(defun im-json-to-yaml (json &optional replace?)
"Convert JSON to YAML."
(interactive (list (im-region-or 'string)))
(let ((yaml (im-kill (yaml-encode (json-parse-string json)))))
(when (and replace? (use-region-p))
(replace-region-contents
(region-beginning)
(region-end)
(lambda () yaml)))))
(defun im-test-run-code-coverage-report () "Run all tests and then open coverage." (interactive) (im-open-test-code-coverage-report t))
(defun im-test-run-current-test-only () "Run currently focused test only." (interactive) (im-test-run-current-test-file t))
(defun im-test-open-code-coverage-report (run-test-before)
"Open code coverage report for current project.
With RUN-TEST-BEFORE is non-nil, then run tests before and then
open coverage. You can simply call \"gf\" or
`xah-open-file-at-cursor' to navigate to a file shown on the
output."
(interactive "P")
(let ((default-directory (im-current-project-root))
(cov-file (format "file://%s/coverage/lcov-report/index.html" (im-current-project-root))))
(if run-test-before
(im-shell-command
:command (pcase (im-test-get-nodejs-test-program)
('jest "yarn jest --coverage --colors")
('vitest "FORCE_COLOR=1 yarn vitest --coverage --run"))
:buffer-name (format "%s-test-cov" (im-current-project-name))
:switch t
:on-finish (lambda (&rest _) (browse-url-default-browser cov-file))
:on-fail (lambda (&rest _) (user-error ">> Tests are failed!")))
(browse-url-default-browser cov-file))))
(defun im-test-run-current-test-file (run-only-focused-test) "Run the current test file." (interactive "P") (cond ((locate-dominating-file "." "yarn.lock") (im-test-run-current-file--yarn run-only-focused-test)) ((locate-dominating-file "." "pom.xml") (im-test-run-current-file--mvn run-only-focused-test)) (t (user-error "This project type is not recognized"))))
(defun im-test-get-nodejs-test-program ()
"Return either `vitest' or `yarn'.
Throw error otherwise."
(let-alist (json-read-file (f-join (im-current-project-root) "package.json"))
(or (and .devDependencies.vitest 'vitest)
(and .devDependencies.jest 'jest)
(user-error "Not a jest or vitest file project!"))))
(defun im-test-run-current-file--yarn (run-only-focused-test)
(let* ((test-file (file-relative-name buffer-file-name (im-current-project-root)))
(default-directory (im-current-project-root))
(program (im-test-get-nodejs-test-program))
(current-test-name
(ignore-errors
(save-excursion
(end-of-line)
(re-search-backward "it(['\"]" nil t)
(nth 1 (s-match "it(['\"]\\(.*\\)['\"]," (substring-no-properties (thing-at-point 'line))))))))
(pcase program
('vitest
(switch-to-buffer-other-window
(apply
#'eat-make
(format
"yarn test: %s%s" test-file
(if run-only-focused-test (concat "::" current-test-name) ""))
"yarn"
nil
"vitest"
test-file
(when (and run-only-focused-test current-test-name)
(list "--testNamePattern" current-test-name)))))
('jest
(switch-to-buffer-other-window
(apply
#'eat-make
(format
"yarn test: %s%s" test-file
(if run-only-focused-test (concat "::" current-test-name) ""))
"yarn"
nil
"jest"
"--colors"
"--runTestsByPath"
test-file
(when (and run-only-focused-test current-test-name)
(list current-test-name))))))))
(defun im-test-run-current-file--mvn (run-only-focused-test)
(let* ((default-directory (im-current-project-root))
(current-test-class
(save-excursion
(goto-char (point-min))
(search-forward-regexp "^\\(public\\|internal\\)? ?class \\([A-Za-z0-9_]+\\)\\( \\|$\\)" nil t)
(beginning-of-line)
(nth 1 (s-match "class \\([A-Za-z0-9_]+\\)\\( \\|$\\)" (substring-no-properties (thing-at-point 'line))))))
(current-test-name
(ignore-errors
(save-excursion
(end-of-line)
(re-search-backward "@Test" nil t)
(forward-line)
(nth 1 (s-match " \\([A-Za-z0-9_]+\\)(" (substring-no-properties (thing-at-point 'line))))))))
(im-shell-command
:command (format "./mvnw test --offline -Dstyle.color=always -Djansi.force=true -Dtest='%s%s'"
current-test-class
(if (and run-only-focused-test current-test-name)
(format "#%s" current-test-name)
""))
:switch t)))
Shopping Mode
I have a simple shopping list that I sync with my phone. This is a simple mode for easier editing on that file.
(define-derived-mode alisveris-mode md-ts-mode "AlisverisMode" "Mode for my shopping list." (setq-local header-line-format (substitute-command-keys "Bindings :: \\[im-alisveris-add-to-market] → Market")))
(general-def :keymaps 'alisveris-mode-map :states 'normal "\\a" #'im-alisveris-add-to-market)
(defun im-alisveris-add-to-market ()
"Add current item to market alisverisi."
(interactive nil im-alisveris-mode)
(let ((thing (s-trim (thing-at-point 'line t))))
(save-excursion
(goto-char (point-min))
(search-forward-regexp "^## Market")
(insert (format "\n%s" thing))
(message "Done."))))
Global Mode Line formatting
Automatically format global mode-line just like I wanted. There is no easy way of editing the order of items inside `global-mode-string' and no really way to inject adjacent items to already existing items. So I simply format it just like what I want periodically.
(when tab-bar-show (add-hook #'elpaca-after-init-hook (apply-partially #'run-at-time 10 45 #'im-update-global-mode-line)))
(defun im-update-global-mode-line (&rest _)
(interactive)
(let ((all-the-icons-default-adjust 0))
(setq
global-mode-string
`(""
,@(when (and (bound-and-true-p empv-media-title)
(not (eq empv-player-state 'stopped)))
(list
(pcase empv-player-state
('playing (all-the-icons-faicon "music"))
('paused (all-the-icons-faicon "pause")))
" "
(s-truncate 25 empv-media-title "…")
" · "))
,@(when im-is-mic-muted?
(list
(all-the-icons-faicon "microphone-slash")
" · "))
,@(when (bound-and-true-p im-notif-dnd)
(let ((all-the-icons-default-adjust -0.15))
(list
(all-the-icons-material "do_not_disturb_on")
" · ")))
,@(when-let* ((_ (bound-and-true-p tmr--timers))
(upcoming (--filter (and (not (tmr--timer-finishedp it))
(let ((remaining (- (float-time (tmr--timer-end-date it))
(float-time))))
(<= remaining 300)))
tmr--timers)))
(list
(all-the-icons-octicon "flame")
(-some->> upcoming
(--map (format "%s (%s)"
(propertize (tmr--timer-description it) 'face 'underline)
(propertize (tmr--format-remaining it) 'face 'italic)))
(s-join ", ")
(s-prepend " «")
(s-append "»"))
" · "))
,@(when (bound-and-true-p appt-mode-string)
(list
(all-the-icons-faicon "calendar-check-o")
" "
(propertize appt-mode-string 'face '(:underline t))
"· "))
,@(when (and (functionp #'org-clock-is-active) (org-clock-is-active))
(list
(all-the-icons-fileicon "org")
" "
(s-replace-regexp
"[()]" ""
(s-truncate 25 org-mode-line-string "…"))
"· "))
,@(when t
(list
(all-the-icons-faicon
"calendar")
" "
(format-time-string "%b %d, %a")
"· "))
,@(when t
(list
(all-the-icons-wicon
(format "time-%s"
;; Convert to number first so that leading 0s are stripped away
(string-to-number (format-time-string "%I"))))
" "
(format-time-string "%H:%M "))))))
(force-mode-line-update :force))
(with-eval-after-load 'org
(add-hook 'org-clock-in-hook #'im-update-global-mode-line)
(add-hook 'org-clock-out-hook #'im-update-global-mode-line)
(add-hook 'im-after-load-theme-hook
#'(lambda (&rest _)
(set-face-attribute 'org-mode-line-clock nil :inherit nil :italic t))))
(defun im-osx-select-audio-output ()
(im-output-select
:cmd (im-ensure-binary "SwitchAudioSource -t output -a" :package "switchaudio-osx")
:prompt (let ((current (im-shell-command-to-string "SwitchAudioSource -t output -c")))
(format "Select audio output (%s): " current))
:do (progn
(shell-command-to-string (format "SwitchAudioSource -t output -s '%s'" it))
(when-let* ((out (-find (lambda (out) (equal it out))
(s-split "\n" (shell-command-to-string "SwitchAudioSource -t input -a")))))
(when (y-or-n-p (format "Found an output with the same name (%s), switch to it?" out))
(shell-command-to-string (format "SwitchAudioSource -t input -s '%s'" out)))))))
(defun im-osx-select-audio-input ()
(im-output-select
:cmd (im-ensure-binary "SwitchAudioSource -t input -a" :package "switchaudio-osx")
:prompt (let ((current (im-shell-command-to-string "SwitchAudioSource -t input -c")))
(format "Select audio input (%s): " current))
:do (shell-command-to-string (format "SwitchAudioSource -t input -s '%s'" it))))
(defun im-linux-select-audio-output ()
(let ((sink
(->>
(shell-command-to-string "pactl list sinks")
(s-trim)
(s-split "\n")
(mapcar #'s-trim)
(--filter (s-matches? "Name:\\|device.description = " it))
(-partition 2)
(mapcar #'nreverse)
(--map (-let (((description name) it))
(list (s-trim (nth 1 (s-split-up-to "=" description 1)))
(s-trim (nth 1 (s-split-up-to ":" name 1))))))
(im-alist-completing-read "Select sink: ")
car)))
;; Set default sink
(shell-command-to-string (format "pactl set-default-sink %s" sink))
;; Move inputs to the new sink
(->>
(shell-command-to-string "pactl list short sink-inputs | cut -d'\t' -f1")
(s-trim)
(s-split "\n")
(--map (shell-command-to-string (format "pactl move-sink-input %s %s" it sink))))))
(defun im-select-audio-output () (interactive) (im-when-on :linux (im-linux-select-audio-output) :darwin (im-osx-select-audio-output)))
(defun im-select-audio-input () (interactive) (im-when-on :linux (im-linux-select-audio-input) :darwin (im-osx-select-audio-input)))
(defvar im-is-mic-muted? nil)
(defun im-set-mic-status (status)
"Set mic status to STATUS.
STATUS is either mute, unmute or toggle.
Asks for STATUS if called interactively."
(interactive
(list
(s-chop-suffix
"d" (cadr (read-multiple-choice "Your mic should be " '((?m "muted") (?u "unmuted") (?t "toggled")))))))
(im-when-on
:linux
(ignore status)
(user-error "Not implemented for gnu/linux")
:darwin
(set-process-filter
(start-process "*SwitchAudioSourceMic*" "*SwitchAudioSourceMic*"
"SwitchAudioSource" "-t" "input" "-m" status)
(lambda (_proc out)
(let* ((status (car (s-match "\\(\\(un\\)?muted\\)" (s-trim out))))
(muted? (equal status "muted"))
(icon (if muted? "🔇" "🔊")))
(unless (frame-focus-state)
(let ((alert-fade-time 5)
(alert-default-style 'osx-notifier))
(alert icon
:title (format "Mic is %s" status)
:persistent nil
:never-persist t)))
(setq im-is-mic-muted? muted?)
(im-update-global-mode-line)
(message
"Your mic is %s %s"
(propertize status 'face 'bold)
icon))))))
(defun im-toggle-mic () "Toggle the mute status of your microphone." (interactive) (im-set-mic-status "toggle"))
(im-leader "em" #'im-toggle-mic "eM" #'im-set-mic-status "ea" #'im-select-audio-output "eA" #'im-select-audio-input)
(defun im-osx-connect-paired-bluetooth-device ()
(interactive)
(im-output-select
:prompt "Select Bluetooth device: "
:cmd (im-ensure-binary "blueutil --paired")
:formatter (->> it
(s-split ", ")
(--map (s-split ":" it))
(--find (equal (car it) "name"))
cadr
(s-replace "\"" "")
s-trim)
:filter (s-contains? "not connected" it)
:keep-order nil
:do (let ((id (->> (s-split "," it) car (s-split ":") cadr s-trim)))
(message ">> Connecting to %s..." id)
(shell-command-to-string (format "blueutil --connect %s" id))
(message ">> Connecting to %s...Connected." id))))
(defun im-linux-connect-paired-bluetooth-device ()
(interactive)
(let* ((paired-devices (->>
(im-ensure-binary "bluetoothctl devices Paired" :package "bluez-utils")
(shell-command-to-string)
(s-trim)
(s-split "\n")))
(connected-devices (->>
(shell-command-to-string "bluetoothctl devices Connected")
(s-trim)
(s-split "\n")))
(device
(->>
(-difference paired-devices connected-devices)
(--map (->>
(s-split-up-to " " it 2)
(-drop 1)
(nreverse)))
(im-alist-completing-read "Select bluetooth device to connect: ")
(car))))
(set-process-filter
(start-process "im-connect-bluetooth" nil "bluetoothctl" "connect" device)
(lambda (_proc out)
(cond
((s-matches? "Failed to connect" out) (message "%s" out))
((s-matches? "Connection successful" out) (message "%s" out)))))))
(defun im-connect-paired-bluetooth-device () (interactive) (im-when-on :darwin (im-osx-connect-paired-bluetooth-device) :linux (im-linux-connect-paired-bluetooth-device)))
(im-leader "eb" #'im-connect-paired-bluetooth-device)
(defun im-ddcutil-toggle/switch-monitor-input (monitor)
"Switch to other MONITOR, USBC, DisplayPort or HDMI."
(interactive (list (intern (completing-read "Monitor: " '(usbc hdmi dp)))))
(let* ((target (alist-get monitor '((hdmi . "17")
(dp . "15")
(usbc . "27"))))
(cmd (im-when-on
:linux
(format
"%s setvcp 0x60 %s"
(im-ensure-binary "ddcutil" :package "ddcutil" :installer "See index.org")
target)
:darwin
(format
"%s set input %s"
(im-ensure-binary "m1ddc" :package "m1ddc" :installer "brew install")
target))))
(shell-command-to-string cmd)))
(dolist (file (directory-files im-load-path t (rx ".el" eos))) (load file nil 'no-message))
(setq custom-file (concat user-emacs-directory "custom.el")) (when (file-exists-p custom-file) (load custom-file nil 'no-message))
(require 'server) (when (and (not (daemonp)) (not (server-running-p))) ;; This is good for the cases where emacsclient may be called inside ;; emacs (from vterm etc.). Otherwise Emacs acts weird about the ;; window placement. (setq server-window #'pop-to-buffer) (server-start))
(add-hook
'elpaca-after-init-hook
(lambda ()
(message
">> Started in %.2f seconds"
(float-time (time-subtract (current-time) before-init-time)))))
(provide 'init)
Local Variables: byte-compile-warnings: (not unresolved free-vars docstrings) separedit-default-mode: org-mode eval: (setq elisp-flymake-byte-compile-load-path load-path) End:
*