Useful Emacs Lisp Scripts

A collection of my tiny but useful Emacs Lisp code.

Table of contents

A collection of my tiny but useful Emacs Lisp code.

If you need the complete code, the repository companion of this article is located at github.com/azzamsa/scripts.el.

I use modern libraries such f, s, dash, and ts heavily, feel free to port them to Emacs built-ins.

</svg><p class="callout__text">

I'll keep the post updated with any new scripts I have. Follow me on Twitter or or subscribe to the RSS Feed to stay in the loop!

Kill All buffers

I use crux-kill-other-buffers for some time. But crux didn't kill dired buffers. So I made my own. This will kill all buffers including dired, but special buffers.

(defunaza-kill-other-buffers()"Kill all buffers but current buffer and special buffers.(Buffer that start with '*' and white space ignored)"(interactive)(when(y-or-n-p"Really kill all other buffers? ")(let((killed-bufs0))(dolist(buffer (delq (current-buffer) (buffer-list)))(let((name (buffer-name buffer)))(when(andname (not(string-equalname""))(/=(arefname0) ?\s)(string-match"^[^\*]"name))(cl-incf killed-bufs)(funcall'kill-buffer buffer))))(message"Killed %d buffer(s)"killed-bufs))))

If you are afraid of losing unsaved buffers. You can add the function below and call it before (let ((killed-bufs 0)).

(defunsave-all-buffers-silently()(save-some-bufferst))

Before:

List of Dired and other buffers.
List of Dired and other buffers.

With aza-kill-other-buffers:

Dired buffer killed.
Dired buffer killed.

With crux-kill-other-buffers: (dired buffer didn't get killed)

Crux can't delete Dired buffer.
Crux can't delete Dired buffer.

Insert Today's Date

Insert today's date dynamically.
Insert today's date dynamically.

I work with huge org files that involve many plain timestamps. It's easy to get previous/next days using prefix arguments e.g +2 or -4. But it's painful if you need to get the previous/next timestamp based on a certain date.

This function will insert today's date if invoked without arguments and with no active region. Prefix arguments specify how many days to move, arguments can be negative, and negative means the previous day. If the region is selected, make fake today's date according to the date under the region.

You need to load ts.el for this to work. I decide to use ts.el rather than format-time-string. It's more readable, beautiful, and has less code. To duplicate line/region faster, I use crux-duplicate-current-line-or-region

(require'ts)(defunaza-today(&optionalarg)"Insert today's date.A prefix ARG specifies how many days to move;negative means the previous day.If region selected, parse region as today's date pivot."(interactive"P")(let((date (if(use-region-p)(ts-parse (buffer-substring-no-properties (region-beginning) (region-end)))(ts-now)))(arg (orarg0)))(if(use-region-p)(delete-region (region-beginning) (region-end)))(insert (ts-format"%A, %B %e, %Y"(ts-adjust'day arg date)))))

The 'beautiful' way to do region is using (interactive "r") but it always complain 'The mark is not set now, so there is no region' if you never invoke mark region before (e.g after Emacs start up).

Insert Current Filename

Insert current filename.
Insert current filename.

Most of the time my org first heading name is the same as the filename. So I made this function. My habit is to separate words in the filename by a dash. If you want a more robust function, take the second.

(defuninsert-filename-as-heading()"Take current filename (word separated by dash) as heading."(interactive)(insert(capitalize(replace-regexp-in-string"-" " "(file-name-sans-extension (buffer-name))))))
Handle multiple cases.
Handle multiple cases.

This is the more robust function. It can deal with almost separators in the filename. You have to load s.el to make this function works. Many packages already use s.el, probably it's installed in your Emacs.

(defuninsert-filename-as-heading()"Take current filename (word separated by dash) as a heading."(interactive)(insert(capitalize(s-join" "(s-split-words (file-name-sans-extension (buffer-name)))))))

Open External Terminal And Tmux From Dired

External terminal app opened.
External terminal app opened.

If no external terminal is opened, start one. Else attach to it and open a new window in current path

Change sterm to your favorite terminal.

(defunterm-here()(interactive)(start-process""nil"st""-e" "bash""-c" "tmux -q has-session && exec tmux new-window || exec tmux new-session -n$USER -s$USER@$HOSTNAME"))

If you prefer to attach to the current window (don't open a new window) and change exec tmux new-window in the command part to exec tmux attach-session -d directory path yourself. Use:

Remove Secrets From Region

Secrets removed.
Secrets removed.

Most of the time I have to attach the program output/log to the bug report. I don't my all my secrets words there.

Of course, you need to put (list-my-secrets) in your non-published files.

(defunlist-my-secrets()"The list of my secrets"'(("johndoe"."user")("johndoemachine"."machine")("johndoe@jdoe.com"."myemail")))(defunrm-mysecrets()"Remove all confidential information."(interactive)(dolist(pair (list-my-secrets))(save-excursion(replace-string (carpair) (cdrpair)))))

Smart Delete Line

Delete line, without killing.
Delete line, without killing.

Rather than having a separate key to delete a line, or having to invoke prefix-argument. You can use crux-smart-kill-line which will "kill to the end of the line and kill the whole line on the next call". But if you prefer delete instead of kill, you can use the code below.

For point-to-string operation (kill/delete) I recommend to use zop-to-char

(defunaza-delete-line()"Delete from the current position to end of the line without pushing to `kill-ring'."(interactive)(delete-region (point) (line-end-position)))(defunaza-delete-whole-line()"Delete whole line without pushing to kill-ring."(interactive)(delete-region (line-beginning-position) (line-end-position)))(defuncrux-smart-delete-line()"Kill to the end of the line and kill whole line on the next call."(interactive)(let((orig-point (point)))(move-end-of-line1)(if(=orig-point (point))(aza-delete-whole-line)(goto-char orig-point)(aza-delete-line))))

Change Selected Region To Snake Case

Dependency: s.el

Most of the time I use this as part of emacs macro to snake-case all attribute responses from an API.

(defunto-snake-case(start end)"Change selected text to snake case format"(interactive"r")(if(use-region-p)(let((camel-case-str (buffer-substring start end)))(delete-region start end)(insert (s-snake-case camel-case-str)))(message"No region selected")))

Remind Me After Certain Time

I use this function to remind me to switch off the water, check my cooking, other things while I work.

This function uses mpv to play sound. You replace it with your favorite music player.

(defunplay-reminder-sound()(start-process""nil"mpv"coin-work-medium-sound))(defunremind-me()"Notify with a sound after a certain time"(interactive)(let((time(read-string"Time (min|sec): " "10 min")))(message"I will remind you after %s"time)(run-at-timetimenil#'play-reminder-sound)))

Change Screen Brightness

This function uses light. You replace it with your favorite app.

(defunlight-set-value()"Set light value directly inside Emacs"(interactive)(let*((current-value (s-trim (shell-command-to-string"light -G")))(light-value (read-string"Set Value: "current-value)))(start-process""nil"light" "-S"light-value)))

Get A Day Name From A Date Time

I use this function to know what day some events happen.

(defunwhat-day()"Show day name from specific time"(interactive)(let((date (read-string"Date: "))(month (read-string"Month: "))(year (read-string"Year: "(number-to-string (ts-year (ts-now))))))(message (ts-day-name (ts-parse (s-join" "(listdate month year)))))))

Ask Github If I Have New Notification.

Often I need a fast reply to some issue in Github. Opening GitHub every time is costly (bandwidth and time).

Dependency: pass

I use pass to store my credential, so I use password-store-get-field.

(defunask-github()"GET Github notification API."(let*((github-pass (password-store-get-field"code/github" "token"))(archive-response (request"https://api.github.com/notifications?all":parser'json-read:headers`(("Authorization". ,(concat"token" " "github-pass))("Content-Type"."application/json")):synct))(data (request-response-data archive-response))(status (request-response-status-code archive-response)))(if(eqstatus200)data404)))(defungithub-show-notification()"Check if Github notification exist without opening browserReduce Distraction."(interactive)(let((result (ask-github)))(if(not(equalresult404))(if(equalresult'[])(message"No notification.")(message"Yey, You have notification!"))(message"Request failed"))))

If you don't use pass, you can put your secret under an encrypted .el file:

(require'keys keys.el.gpg); <+++ contains `(defvar github-pass 1234)`(defunask-github()"GET Github notification API."(let*((archive-response (request"https://api.github.com/notifications?all":parser'json-read:headers`(("Authorization". ,(concat"token" " "github-pass))("Content-Type"."application/json")):synct))....

Select Remote Machines and Connect

I work with several remote machines. So selecting them based on name and open dired there is very useful.

Previously I use the same code structure to select and copy my password to clipboard, but now I use pass which has nice emacs mode.

(defvarremote-machines`(("machine-api". ,(list:username"john":ip"10.10.10.10"))("machine-foo". ,(list:username"doe":ip"11.11.11.11"))))(defunconnect-remote()"Open dired buffer in the selected remote machine"(interactive)(let*((selected-machine (completing-read"connect to: "(mapcar'car remote-machines)))(machine-data (cdr(assocselected-machine remote-machines)))(username (plist-get machine-data :username))(ip-address (plist-get machine-data :ip)))(if(string=username"root")(dired (concat"/ssh:"username"@"ip-address":/"))(dired (concat"/ssh:"username"@"ip-address":/home/"username"/")))(message"Connected")))

This uses emacs completing-read instead of a third-party narrowing package. Some narrowing packages such as selectrum played well with emacs completing-read.

</svg><p class="callout__text">

I never use this function anymore. I put all SSH config in ~/.ssh/config and it will provide hostname completion directly using /ssh: in dired.

Playing Multimedia/Music Files

Since I browse using dired all the time. It's more convenient for me to invoke my external application within dired.

I always use mpv with --force-window even for songs, so that I have separate mpv window that accepts my mpv keybindings (increase volume, next playlist, etc).

Open dired buffer, place your cursor on the top of the media/playlist file, then invoke these interactive functions.

(defunstart-mpv(path&optionalplaylist-p)"Start mpv with specified arguments"(let*((default-cmd"mpv --force-window")(cmd (ifplaylist-p(s-append" --loop-playlist --playlist="default-cmd)(s-append" --loop "default-cmd))))(call-process-shell-command (s-concat cmd (shell-quote-argument path))nil0)))(defunmpv()"Play a file in the current line"(interactive)(start-mpv (dired-get-filename)))(defunmpv-dir()"Play all multimedia files in current directory"(interactive)(start-mpv default-directory))(defunmpv-playlist()"Play a playlist in current line"(interactive)(start-mpv (dired-get-filename)t))

If you liked this article, please support my work. It will definitely be rewarding and motivating. Thanks for the support!

</svg>

Changelog

Show Changelog
  • 2021-06-01: remove Helm dependency from Select Remote Machines and Connect

Comments