;; -*- lexical-binding: t -*-
Packages and utilities needed for this config.
(require 'dash)
(require 's)
(require 'f)
(require 'noflet)
(template "2 = <<(+ 1 1)>>")
;;;###autoload
(defmacro template (text)
"Template literals"
(let ((pattern "<<\\(.*?\\)>>"))
;; The regexp matches anything between delimiters, non-greedily
(with-temp-buffer
(save-excursion (insert text))
(let ((matches '()))
(while (re-search-forward pattern nil t)
(push (match-string 1) matches)
(replace-match "%s" t t))
`(format ,(buffer-string) ,@(reverse (mapcar 'read matches)))))))
;;;###autoload
(defalias 't! 'template)
;;;###autoload
(defmacro ignore-args (fnc)
"Return function that ignores its arguments and invokes FNC."
`(lambda (&rest _rest)
(funcall ,fnc)))
;;;###autoload
(defalias 'η 'ignore-args)
Prevent async-shell-command
displaying a popup or a buffer.
(defun async-shell-command-no-window (command)
"Execute async command without showing the result buffer."
(interactive)
(let ((display-buffer-alist (list (cons "\\*Async Shell Command\\*.*" (cons #'display-buffer-no-window nil)))))
(async-shell-command command)))
(defun shell-command-to-list (cmd)
"Split output from shell-command to list"
(split-string (shell-command-to-string cmd) "\n" t))
I don’t want some processes to exit, when I close emacs.
;;;###autoload
(defun +my/start-inferior-process-shell-command (command)
(let ((name (format "*nohup: %s*" command)))
(start-process-shell-command name nil (format "nohup %s" command))))
;;;###autoload
(defun +my/start-inferior-process (name buffer program &rest program-args)
(let ((nohup-name (lambda (&optional text)
(if (not (null text))
(format "*nohup: %s*" text)
nil)))
(args (-some->> program-args
(s-join " "))))
(start-process
(funcall nohup-name name)
(funcall nohup-name buffer)
shell-file-name
shell-command-switch
(template "nohup 1>/dev/null 2>/dev/null <<program>> <<args>>"))))
;;;###autoload
(defun +my/nohup-shell-command (cmd &optional &rest args)
"Launch a shell command, without opening a message buffer.
The proram persists when emacs is closed."
(let ((args-str (or (-some->> args
(s-join " "))
"")))
(call-process-shell-command
(template "nohup 1>/dev/null 2>/dev/null <<cmd>> <<args-str>> &") nil nil)))
;;;###autoload
(defun +mpv/play-external-url (url)
(-when-let* ((quality-val
(-some->> (completing-read
"Max height resolution (0 for unlimited): "
'("720" "0" "480" "1080"))
(string-to-number)))
(quality-arg (if (> 0 quality-val)
(template "--ytdl-format=\"[height<=?<<quality-val>>]\"")
"")))
(message (template "Opening <<url>> at <<quality-val>> with mpv…"))
(+my/nohup-shell-command "mpv" quality-arg (s-wrap url "\""))))
;;;###autoload
(defun noop (&optional args) nil)
;;;###autoload
(defun +my/kill-and-message (x)
"Executes kill-new but with a message log side effect."
(kill-new x)
(message "Copied to clipboard: %s" x))
;;;###autoload
(defmacro without-undo (&rest forms)
"Executes FORMS with a temporary buffer-undo-list that is discarded afterwards.
Taken from http://www.emacswiki.org/emacs/UndoCommands with some
modifications."
`(let* ((buffer-undo-list)
(modified (buffer-modified-p))
(inhibit-read-only t))
(unwind-protect
(progn ,@forms)
(set-buffer-modified-p modified)) ()))
(defun +my/bool-to-enabled-string (x)
"Convert bool X to string for messaging.
t -> \"Enabled\")
nil -> \"Disabled\""
(if x "Enabled" "Disabled"))
(defun +my/bool-state-message (x)
"Log message if a bool is enabled or not"
(interactive)
(message (t! "<<(symbol-name x)>>: <<(+my/bool-to-enabled-string (symbol-value x))>>")))
Run a side effect fn
on the initial input x
.
But Return the original input x
.
;;;###autoload
(defun -tap (fn x)
"Function docstring"
(funcall fn x)
x)
;;;###autoload
(defmacro --tap (fn it)
"Anaphoric form of `-tap'."
`(-tap (lambda (it) ,fn) ,it))
Log the current input without breaking the pipe.
;;;###autoload
(defun -log (x)
"Function docstring"
(--tap (message "%s" it) x))
;;;###autoload
(defun -when (pred fn x)
"When FN equals t forward X."
(if pred
(funcall fn x)
x))
;;;###autoload
(defmacro --when (pred form xs)
"Anaphoric form of -id-when"
(declare (debug (form form)))
`(let ((it ,xs))
(if ,pred
,form
,xs)))
;;;###autoload
(defun -id-when (fn x)
"When FN equals t forward X."
(when (funcall fn x) x))
;;;###autoload
(defmacro --id-when (form xs)
"Anaphoric form of -id-when"
(declare (debug (form form)))
`(let ((it ,xs))
(when ,form ,xs)))
;;;###autoload
(defun -append (elem list)
"Append ELEM to the end of list.
This is like -snoc but it takes the ELEM as the first argument for easier composition"
(-snoc list elem))
(defun swap-list-items (LIST el1 el2)
"in LIST swap indices EL1 and EL2 in place"
(let ((tmp (elt LIST el1)))
(setf (elt LIST el1) (elt LIST el2))
(setf (elt LIST el2) tmp)))
;;;###autoload
(defun -shuffle (LIST)
"Shuffle the elements in LIST.
shuffling is done in place."
(loop for i in (reverse (number-sequence 1 (1- (length LIST))))
do (let ((j (random (+ i 1))))
(swap-list-items LIST i j)))
LIST)
;;;###autoload
(defun -f-join (x path)
"Reversed argument order for f-join"
(f-join path x))
;;;###autoload
(defun f-tildify (path)
"Replace the HOME directory in path"
(s-replace-regexp (t! "^<<(getenv \"HOME\")>>") "~" path))
;;;###autoload
(defun s-match-or (regex x)
"Return match groups or original"
(interactive)
(-if-let ((match (s-match regex x)))
(cdr match)
(list x)))
;;;###autoload
(defun s-match-or-1 (regex x)
"Return 1st match group or original."
(interactive)
(-if-let ((match (s-match regex x)))
(car (cdr match))
x))
(defun +my/buffer-contains-line (string)
(save-excursion
(save-match-data
(goto-char (point-min))
(search-forward string nil t))))
;;;###autoload
(defun +my/line-indent ()
"Get the indent of the current line."
(interactive)
(or (-some->> (substring-no-properties (thing-at-point 'line))
(s-match "^\\(\s*\\).*\n$")
(nth 1)
(length))
0))
;;;###autoload
(defun +my/buffer-line-has (regexp)
"Check for REGEXP at current line."
(save-excursion
(goto-char (point-at-bol))
(search-forward-regexp regexp (point-at-eol) t)))
;;;###autoload
(defun +my/delete-current-line ()
"Delete (not kill) the current line."
(interactive)
(save-excursion
(delete-region
(progn (forward-visible-line 0) (point))
(progn (forward-visible-line 1) (point)))))
;;;###autoload
(defun +my/map-lines (fun &optional start end)
"Map lines in buffer with FUN, fn gets called with the line contents."
(let ((start (or start (point-min)))
(end (or end (point-max)))
(lines (list)))
(save-excursion
(goto-char start)
(while (< (point) end)
(add-to-list 'lines
(funcall fun (buffer-substring (line-beginning-position) (line-end-position))))
(forward-line 1))
(erase-buffer)
(->> lines
reverse
(s-join "\n")
insert))))
;;;###autoload
(defun +file/timestamp (path)
(->> (file-attributes path)
(nth 5)))
Get the last created file in a directory.
;;;###autoload
(defun +file/latest-file-in-dir (path)
(->> (f-entries path)
(-sort (lambda (a b) (not (time-less-p (+file/timestamp a)
(+file/timestamp b)))))
(car)))
;;;###autoload
(defun +file|chmod-this-file ()
"Chmod +x the current file."
(interactive)
(shell-command (template "chmod +x \"<<(buffer-file-name)>>\"")))
Has hidden entry
(defun f-has-hidden-entry (dir)
"Check if a DIR has any hidden entries.
Return the first found file when one is found."
(--find (s-starts-with-p "." (f-filename it)) (f-entries dir)))
;;;###autoload
(defun +my|toggle-window-split-direction ()
"Toggle current window split between horizontal and vertical."
(interactive)
(if (= (count-windows) 2)
(let* ((this-win-buffer (window-buffer))
(next-win-buffer (window-buffer (next-window)))
(this-win-edges (window-edges (selected-window)))
(next-win-edges (window-edges (next-window)))
(this-win-2nd (not (and (<= (car this-win-edges)
(car next-win-edges))
(<= (cadr this-win-edges)
(cadr next-win-edges)))))
(splitter
(if (= (car this-win-edges)
(car (window-edges (next-window))))
'split-window-horizontally
'split-window-vertically)))
(delete-other-windows)
(let ((first-win (selected-window)))
(funcall splitter)
(if this-win-2nd (other-window 1))
(set-window-buffer (selected-window) this-win-buffer)
(set-window-buffer (next-window) next-win-buffer)
(select-window first-win)
(if this-win-2nd (other-window 1))))))
Lock a window so the buffer can’t be changed or it cant be deleted.
;;;###autoload
(defun +my|toggle-window-dedicated ()
"Control whether or not Emacs is allowed to display another
buffer in current window."
(interactive)
(let* ((window (get-buffer-window (current-buffer)))
(is-locked (window-dedicated-p window))
(txt (if is-locked "Window unlocked" "Window locked")))
(set-window-dedicated-p window (not is-locked))
(message (template "<<(current-buffer)>>: <<txt>>!"))))
(defun +ui/get-display-dpi (&optional display)
"Get the DPI of DISPLAY.
DISPLAY is a display name, frame or terminal, as in
`display-monitor-attributes-list'."
(cl-flet ((pyth (lambda (w h)
(sqrt (+ (* w w)
(* h h)))))
(mm2in (lambda (mm)
(/ mm 25.4))))
(let* ((atts (frame-monitor-attributes))
(pix-w (cl-fourth (assoc 'geometry atts)))
(pix-h (cl-fifth (assoc 'geometry atts)))
(pix-d (pyth pix-w pix-h))
(mm-w (cl-second (assoc 'mm-size atts)))
(mm-h (cl-third (assoc 'mm-size atts)))
(mm-d (pyth mm-w mm-h)))
(/ pix-d (mm2in mm-d)))))
(defun +ui/adjust-font (size line-space &optional font-family)
(let* ((font-family (or font-family)))
(setq-default line-spacing line-space)
(setq-default doom-font (font-spec :family font-family :size size))
(setq-default doom--font-scale nil)
(set-frame-font doom-font 'keep-size t)
(doom/reload-font)
(run-hooks 'after-setting-font-hook)))
(defun +ui/active-display-name ()
(interactive)
(-some->> (shell-command-to-string "xrandr | grep \"connected primary\"")
(s-split " ")
(nth 0)
(s-replace "-" "")))
(defun +ui|adjust-ui-to-display ()
"Adjust the UI to the current attached display."
(interactive)
(cond
((string= (system-name) "mbair")
;; Not actually a retina display, but this looks best
(+ui/adjust-font 13 5 "Fira Code"))
((string= (system-name) "Florians-iMac.local")
(+ui/adjust-font 14 10 "Menlo"))
((string= (system-name) "thinknix")
(if (string= (+ui/active-display-name) "eDP1")
;; Internal Screen
(+ui/adjust-font 15 7 "Fira Code")
;; HiDPI External
(+ui/adjust-font 18 8 "Fira Code")))
((string= (system-name) "Florians-MacBook-Air.local")
(+ui/adjust-font 14 10 "Menlo"))))
Bookmarks for projects with using a bookmarks.json file at the project root.
(defvar +bookmarks:local-bookmarks-file nil)
(setq +bookmarks:local-bookmarks-file (f-join doom-local-dir "bookmarks.json"))
(defun +bookmarks/find-bookmarks (x &optional &key other-window?)
"Jump to a X relative to the project root, go to character POS."
(-when-let* ((find-fn (if other-window? 'find-file-other-window 'find-file))
(item (car x)))
(let* ((file (-some->> (alist-get 'file item)
(--when (alist-get 'relative item)
(f-join (projectile-project-root) it))))
(buffer-is-open (when file (get-file-buffer file))))
(when file
(funcall find-fn file)
(+workspaces-add-current-buffer-h))
(when-let ((fn (alist-get 'fn item)))
(funcall fn))
;; goto, disable-relocation
;; Go to a location matched by regex
;; Unless disable-relocation is enabled and the file is already visited
(-some--> (alist-get 'goto item)
(--id-when (or (not buffer-is-open)
(not (alist-get 'disable-relocation item))) it)
(--tap (progn
(goto-char (point-min))
(cond
((eq 'integer (type-of it))
(goto-line it))
((eq 'string (type-of it))
(search-forward it))))
it))
;; action
;; Execute a function after find file
(-some--> (alist-get 'action item)
(call-interactively it))
;; goto-bol
;; Go to the beginning, since the regex search
;; will leave the cursor at the end of the search
(-some--> (alist-get 'goto-bol item)
(evil-first-non-blank)))))
(defun +bookmarks/local/save (xs)
"Save an alist as json to the +bookmarks:local-bookmarks-file"
(->> xs
(json-encode)
((lambda (x) (f-write x 'utf-8 +bookmarks:local-bookmarks-file)))))
(defun +bookmarks/find-bookmarks-other-window (x)
"Open bookmark X in other window, used with ivy action 'j'."
(+bookmarks/find-bookmarks (cdr x) :other-window? t))
(defun +bookmarks/local|save ()
(interactive)
(let ((entry `((file . ,(buffer-file-name))
(goto . ,(line-number-at-pos))
(name . ,(read-string "Bookmark Name: ")))))
(->> (+bookmarks/local/list)
(-append entry)
(+bookmarks/local/save))))
(defun +bookmarks/local/kill (x)
(->>
(--remove (cl-equalp (car (cdr x)) it) (+bookmarks/local/list))
(+bookmarks/local/save)))
(defun +bookmarks/local/rename (x)
(require 'a)
(-let ((item (car (cdr x))))
(-log (a-get (cdr item) :name))
(->>
(+bookmarks/local/list)
(--map-first (cl-equalp item it) (a-update it 'name (lambda (x) (read-string "Rename Bookmark: " x))))
(+bookmarks/local/save))))
(defun +bookmarks/local/list ()
(-some->> +bookmarks:local-bookmarks-file
(-id-when #'f-exists?)
(json-read-file)
(-map 'identity)))
;;;###autoload
(defun +bookmarks (&optional bookmarks-list)
"Either take alist BOOKMARKS-LIST or look for bookmarks.json in project root.
If found, show an ivy window with the bookmarks"
(interactive)
(-if-let*
((bookmarks
(or bookmarks-list
(+bookmarks/local/list)))
(bookmarks (--map (list (alist-get 'name it) it) bookmarks)))
(ivy-read "Bookmark: " bookmarks
:action (lambda (x) (+bookmarks/find-bookmarks (cdr x)))
:caller #'+bookmarks|find-bookmarks)
(user-error "No bookmarks file found at project root.")))
(after! ivy
(ivy-set-actions
'+bookmarks|find-bookmarks
'(("j" +bookmarks/find-bookmarks-other-window "open in other window")
("k" +bookmarks/local/kill "Remove")
("r" +bookmarks/local/rename "Rename"))))