diff options
Diffstat (limited to 'users/tazjin/emacs/config/functions.el')
-rw-r--r-- | users/tazjin/emacs/config/functions.el | 248 |
1 files changed, 137 insertions, 111 deletions
diff --git a/users/tazjin/emacs/config/functions.el b/users/tazjin/emacs/config/functions.el index 9bb6772a27..68a384d20f 100644 --- a/users/tazjin/emacs/config/functions.el +++ b/users/tazjin/emacs/config/functions.el @@ -2,9 +2,7 @@ (require 'dash) (require 'map) -(defun load-file-if-exists (filename) - (if (file-exists-p filename) - (load filename))) +(require 'gio-list-apps) ;; native module! (defun goto-line-with-feedback () "Show line numbers temporarily, while prompting for the line number input" @@ -17,29 +15,19 @@ (goto-line target))) (setq-local display-line-numbers nil))) -;; These come from the emacs starter kit - (defun esk-add-watchwords () (font-lock-add-keywords nil '(("\\<\\(FIX\\(ME\\)?\\|TODO\\|DEBUG\\|HACK\\|REFACTOR\\|NOCOMMIT\\)" 1 font-lock-warning-face t)))) +(add-hook 'prog-mode-hook 'esk-add-watchwords) + (defun esk-sudo-edit (&optional arg) (interactive "p") (if (or arg (not buffer-file-name)) (find-file (concat "/sudo:root@localhost:" (read-file-name "File: "))) (find-alternate-file (concat "/sudo:root@localhost:" buffer-file-name)))) -;; Open Fefes blog -(defun fefes-blog () - (interactive) - (eww "https://blog.fefe.de/")) - -;; Open the NixOS man page -(defun nixos-man () - (interactive) - (man "configuration.nix")) - ;; Get the nix store path for a given derivation. ;; If the derivation has not been built before, this will trigger a build. (defun nix-store-path (derivation) @@ -59,9 +47,6 @@ (buffer-name) require-final-newline)) -;; Helm includes a command to run external applications, which does -;; not seem to exist in ivy. This implementation uses some of the -;; logic from Helm to provide similar functionality using ivy. (defun list-external-commands () "Creates a list of all external commands available on $PATH while filtering NixOS wrappers." @@ -82,9 +67,9 @@ '(("google-chrome" . "--force-device-scale-factor=1.4")) "This setting lets me add additional flags to specific commands - that are run interactively via `ivy-run-external-command'.") + that are run interactively via `run-external-command'.") -(defun run-external-command (cmd) +(defun run-external-command--handler (cmd) "Execute the specified command and notify the user when it finishes." (let* ((extra-flags (cdr (assoc cmd external-command-flag-overrides))) @@ -96,59 +81,46 @@ (when (string= event "finished\n") (message "%s process finished." process)))))) -(defun ivy-run-external-command () +(defun run-external-command () "Prompts the user with a list of all installed applications and lets them select one to launch." (interactive) (let ((external-commands-list (list-external-commands))) - (ivy-read "Command:" external-commands-list - :require-match t - :history 'external-commands-history - :action #'run-external-command))) - -(defun ivy-password-store (&optional password-store-dir) - "Custom version of password-store integration with ivy that - actually uses the GPG agent correctly." + (run-external-command--handler + (completing-read "Command: " external-commands-list + nil ;; predicate + t ;; require-match + nil ;; initial-input + ;; hist + 'external-commands-history)))) + +(defun password-store-lookup (&optional password-store-dir) + "Interactive password-store lookup function that actually uses +the GPG agent correctly." (interactive) - (ivy-read "Copy password of entry: " - (password-store-list (or password-store-dir (password-store-dir))) - :require-match t - :keymap ivy-pass-map - :action (lambda (entry) - (let ((password (auth-source-pass-get 'secret entry))) - (password-store-clear) - (kill-new password) - (setq password-store-kill-ring-pointer kill-ring-yank-pointer) - (message "Copied %s to the kill ring. Will clear in %s seconds." - entry (password-store-timeout)) - (setq password-store-timeout-timer - (run-at-time (password-store-timeout) - nil 'password-store-clear)))))) - -(defun ivy-browse-repositories () - "Select a git repository and open its associated magit buffer." - (interactive) - (ivy-read "Repository: " - (magit-list-repos) - :require-match t - :sort t - :action #'magit-status)) - -(defun bottom-right-window-p () - "Determines whether the last (i.e. bottom-right) window of the - active frame is showing the buffer in which this function is - executed." - (let* ((frame (selected-frame)) - (right-windows (window-at-side-list frame 'right)) - (bottom-windows (window-at-side-list frame 'bottom)) - (last-window (car (seq-intersection right-windows bottom-windows)))) - (eq (current-buffer) (window-buffer last-window)))) + (let* ((entry (completing-read "Copy password of entry: " + (password-store-list (or password-store-dir + (password-store-dir))) + nil ;; predicate + t ;; require-match + )) + (password (or (let ((epa-suppress-error-buffer t)) + (auth-source-pass-get 'secret entry)) + (error "failed to decrypt '%s', wrong password?" entry)))) + (password-store-clear) + (kill-new password) + (setq password-store-kill-ring-pointer kill-ring-yank-pointer) + (message "Copied %s to the kill ring. Will clear in %s seconds." + entry (password-store-timeout)) + (setq password-store-timeout-timer + (run-at-time (password-store-timeout) + nil 'password-store-clear)))) (defhydra mc/mark-more-hydra (:color pink) - ("<up>" mmlte--up "Mark previous like this") + ("<up>" mc/mmlte--up "Mark previous like this") ("<down>" mc/mmlte--down "Mark next like this") ("<left>" mc/mmlte--left (if (eq mc/mark-more-like-this-extended-direction 'up) "Skip past the cursor furthest up" @@ -174,26 +146,15 @@ (mc/mmlte--down) (mc/mark-more-hydra/body)))) -(defun memespace-region () - "Make a meme out of it." +(setq mc/cmds-to-run-for-all '(kill-region paredit-newline)) - (interactive) - (let* ((start (region-beginning)) - (end (region-end)) - (memed - (message - (s-trim-right - (apply #'string - (-flatten - (nreverse - (-reduce-from (lambda (acc x) - (cons (cons x (-repeat (+ 1 (length acc)) 32)) acc)) - '() - (string-to-list (buffer-substring-no-properties start end)))))))))) - - (save-excursion (delete-region start end) - (goto-char start) - (insert memed)))) +(setq mc/cmds-to-run-once '(mc/mark-dwim + mc/mark-more-hydra/mc/mmlte--down + mc/mark-more-hydra/mc/mmlte--left + mc/mark-more-hydra/mc/mmlte--right + mc/mark-more-hydra/mc/mmlte--up + mc/mark-more-hydra/mmlte--up + mc/mark-more-hydra/nil)) (defun insert-todo-comment (prefix todo) "Insert a comment at point with something for me to do." @@ -237,11 +198,16 @@ (if prefix (text-scale-adjust 0) (set-face-attribute 'default nil :height (or to 120)))) -(defun scrot-select () +(defun screenshot-select (filename) "Take a screenshot based on a mouse-selection and save it to ~/screenshots." - (interactive) - (shell-command "scrot '$a_%Y-%m-%d_%s.png' -s -e 'mv $f ~/screenshots/'")) + (interactive "sScreenshot filename: ") + (let* ((path (f-join "~/screenshots" + (format "%s-%d.png" + (if (string-empty-p filename) "shot" filename) + (time-convert nil 'integer))))) + (shell-command (format "maim --select %s" path)) + (message "Wrote screenshot to %s" path))) (defun graph-unread-mails () "Create a bar chart of unread mails based on notmuch tags. @@ -289,6 +255,17 @@ (add-to-list 'project-find-functions #'find-depot-project) +(defun find-cargo-project (dir) + "Attempt to find the current project in `project-find-functions' +by looking for a `Cargo.toml' file." + (when dir + (unless (equal "/" dir) + (if (f-exists-p (f-join dir "Cargo.toml")) + (cons 'transient dir) + (find-cargo-project (f-parent dir)))))) + +(add-to-list 'project-find-functions #'find-cargo-project) + (defun magit-find-file-worktree () (interactive) "Find a file in the current (ma)git worktree." @@ -296,31 +273,80 @@ (magit-read-file-from-rev "HEAD" "Find file") #'pop-to-buffer-same-window)) -(defun songwhip--handle-result (status &optional cbargs) - ;; TODO(tazjin): Inspect status, which looks different in practice - ;; than the manual claims. - (if-let* ((response (json-parse-string - (buffer-substring url-http-end-of-headers (point-max)))) - (sw-path (ht-get* response "data" "path")) - (link (format "https://songwhip.com/%s" sw-path)) - (select-enable-clipboard t)) - (progn - (kill-new link) - (message "Copied Songwhip link (%s)" link)) - (warn "Something went wrong while retrieving Songwhip link!") - ;; For debug purposes, the buffer is persisted in this case. - (setq songwhip--debug-buffer (current-buffer)))) - -(defun songwhip-lookup-url (url) - "Look up URL on Songwhip and copy the resulting link to the clipboard." - (interactive "sEnter source URL: ") - (let ((songwhip-url "https://songwhip.com/api/") - (url-request-method "POST") - (url-request-extra-headers '(("Content-Type" . "application/json"))) - (url-request-data - (json-serialize `((country . "GB") - (url . ,url))))) - (url-retrieve "https://songwhip.com/api/" #'songwhip--handle-result nil t t) - (message "Requesting Songwhip URL ... please hold the line."))) +(defun zoxide-open-project () + "Query Zoxide for paths, and open the result as appropriate (magit or dired)." + (interactive) + (zoxide-open-with + nil + (lambda (path) + (condition-case err (magit-status-setup-buffer path) + (magit-outside-git-repo (dired path)))))) + +(defun toggle-nix-test-and-exp () + "Switch between the .nix and .exp file in a Tvix/Nix test." + (interactive) + (let* ((file (buffer-file-name)) + (other (if (s-suffix? ".nix" file) + (s-replace-regexp ".nix$" ".exp" file) + (if (s-suffix? ".exp" file) + (s-replace-regexp ".exp$" ".nix" file) + (error "Not a .nix/.exp file!"))))) + (find-file other))) + +(defun reliably-switch-buffer () + "Reliably and interactively switch buffers, without ending up in a +situation where the buffer was renamed during selection and an +empty new buffer is created. + +This is done by, in contrast to most buffer-switching functions, +retaining a list of the buffer *objects* and their associated +names, instead of only their names (which might change)." + + (interactive) + (let* ((buffers (seq-map (lambda (b) (cons (buffer-name b) b)) + (seq-filter (lambda (b) (not (string-prefix-p " " (buffer-name b)))) + (buffer-list)))) + + ;; Annotate buffers that display remote files. I frequently + ;; want to see it, because I might have identically named + ;; files open locally and remotely at the same time, and it + ;; helps with differentiating them. + (completion-extra-properties + '(:annotation-function + (lambda (name) + (if-let* ((file (buffer-file-name (cdr (assoc name buffers)))) + (remote (file-remote-p file))) + (format " [%s]" remote))))) + + (name (completing-read "Switch to buffer: " (seq-map #'car buffers))) + (selected (or (cdr (assoc name buffers)) + ;; Allow users to manually select invisible buffers ... + (get-buffer name)))) + (switch-to-buffer (or selected name) nil 't))) + +(defun run-xdg-app () + "Use `//users/tazjin/gio-list-apps' to retrieve a list of +installed (and visible) XDG apps, and let users launch them." + (interactive) + (let* ((apps (taz-list-xdg-apps)) + + ;; Display the command that will be run as an annotation + (completion-extra-properties + '(:annotation-function (lambda (app) (format " [%s]" (cdr (assoc app apps))))))) + + (run-external-command--handler (cdr (assoc (completing-read "App: " apps nil t) apps))))) + +(defun advice-remove-all (sym) + "Remove all advices from symbol SYM." + (interactive "aFunction symbol: ") + (advice-mapc (lambda (advice _props) (advice-remove sym advice)) sym)) + +(defun M-x-always-same-window () + "Run `execute-extended-command', but ensure that whatever it does +always opens in the same window in which the command was invoked." + (interactive) + (let ((display-buffer-overriding-action + '((display-buffer-same-window) . ((inhibit-same-window . nil))))) + (call-interactively #'execute-extended-command))) (provide 'functions) |