1143 lines
49 KiB
Org Mode
1143 lines
49 KiB
Org Mode
#+title: Applications
|
||
#+author: Howard X. Abrams
|
||
#+date: 2023-12-21
|
||
#+tags: emacs
|
||
|
||
A literate programming file configuring critical applications.
|
||
|
||
#+begin_src emacs-lisp :exports none
|
||
;;; ha-applications.el --- configuring critical applications. -*- lexical-binding: t; -*-
|
||
;;
|
||
;; © 2023 Howard X. Abrams
|
||
;; Licensed under a Creative Commons Attribution 4.0 International License.
|
||
;; See http://creativecommons.org/licenses/by/4.0/
|
||
;;
|
||
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
|
||
;; Maintainer: Howard X. Abrams <howard.abrams@gmail.com>
|
||
;; Created: December 21, 2023
|
||
;;
|
||
;; While obvious, GNU Emacs does not include this file
|
||
;;
|
||
;; *NB:* Do not edit this file. Instead, edit the original literate file at:
|
||
;; ~/src/hamacs/ha-applications.org
|
||
;; And tangle the file to recreate this one.
|
||
;;
|
||
;;; Code:
|
||
#+end_src
|
||
|
||
Can we call the following /applications/? I guess.
|
||
* Agentic Interface
|
||
Ethic issues aside, I’m [[https://technobabble.bearblog.dev/fine-ill-try-ai/][trying AI]] … primarily because my company requires my participation. I appreciate the approaches from my fellow Emacsians, for while VSCode may be a fine editor, it can’t compete with my creation here.
|
||
** Agent Shell
|
||
Installing Xenodium’s [[https://github.com/xenodium/agent-shell][agent-shell]], requires his [[https://github.com/xenodium/acp.el][ACP package]] for accessing the installed [[https://agentclientprotocol.com/][ACP libraries]], and his [[https://github.com/xenodium/shell-maker][shell-maker]] package.
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(use-package acp
|
||
:straight (:type git :host github :repo "xenodium/acp.el"))
|
||
|
||
(use-package shell-maker
|
||
:straight (:type git :host github :repo "xenodium/shell-maker"))
|
||
|
||
(use-package agent-shell
|
||
:straight (:type git :host github :repo "xenodium/agent-shell")
|
||
:after acp
|
||
|
||
:custom
|
||
(agent-shell-display-action '(display-buffer-in-previous-window))
|
||
|
||
:config
|
||
(ha-leader "a i" '("agent chat" . agent-shell))
|
||
|
||
;; Evil state-specific RET behavior: insert mode = newline, normal mode = send
|
||
(evil-define-key 'insert agent-shell-mode-map (kbd "RET") #'newline)
|
||
(evil-define-key 'insert agent-shell-mode-map (kbd "C-RET") #'agent-shell-submit)
|
||
(evil-define-key 'normal agent-shell-mode-map (kbd "RET") #'comint-send-input)
|
||
|
||
;; Configure *agent-shell-diff* buffers to start in Emacs state
|
||
(add-hook 'diff-mode-hook
|
||
(lambda ()
|
||
(when (string-match-p "\\*agent-shell-diff\\*" (buffer-name))
|
||
(evil-emacs-state)))))
|
||
#+END_SRC
|
||
*** Notifications
|
||
When my /artificial intern/ completes a task, I have long since nipped out to the kitchen, put the kettle on ... buttering scones... and getting crumbs and bits of food out of those round brown straw mats that the teapot goes on.
|
||
|
||
I would like the /intern/ to notify me when it needs attention, so I noticed the [[https://github.com/zackattackz/agent-shell-notifications/][agent-shell-notifications]] project can connect the =agent-shell= project with the [[https://github.com/konrad1977/knockknock/][knock-knock]] project (which is similar to my [[https://howardism.org/Technical/Emacs/beep-for-emacs.html][beep project]]).
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(use-package knockknock
|
||
:straight (knockknock :type git :host github :repo "konrad1977/knockknock")
|
||
:init
|
||
(setq knockknock-border-color "brown")
|
||
(setq knockknock-border-width 2)
|
||
(setq knockknock-default-duration 4)
|
||
:config
|
||
(advice-add 'knockknock-notify :after #'beep-beep))
|
||
#+END_SRC
|
||
|
||
Testing it out:
|
||
|
||
#+BEGIN_SRC emacs-lisp :tangle no
|
||
(knockknock-notify :title "Attention"
|
||
:message "Claude needs attention"
|
||
:icon "nf-md-robot_confused")
|
||
#+END_SRC
|
||
|
||
The configuration for [[https://github.com/zackattackz/agent-shell-notifications/][agent-shell-notifications]] and hook it to the =agent-shell-notifications-provider=:
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(use-package agent-shell-notifications
|
||
:straight (agent-shell-notifications
|
||
:type git
|
||
:host github
|
||
:repo "zackattackz/agent-shell-notifications")
|
||
:hook
|
||
;; Enable notifications in each agent-shell buffer
|
||
(agent-shell-mode . agent-shell-notifications-mode)
|
||
|
||
:config
|
||
;; Notification display timeout in seconds (0 = never expire (the default), -1 = backend default)
|
||
;; (setq agent-shell-notifications-timeout 5)
|
||
|
||
;; Seconds to wait before notifying when the shell is already visible (default: 10)
|
||
;; (setq agent-shell-notifications-idle-timeout 30)
|
||
|
||
;; Use the knockknock backend instead of the default libnotify
|
||
(setq agent-shell-notifications-provider 'agent-shell-notifications-knockknock)
|
||
|
||
;; While that code is _supposed_ to do this, I seem to need to do this manually:
|
||
(require 'agent-shell-notifications-knockknock))
|
||
#+END_SRC
|
||
*** Agent Sidebar
|
||
Using the [[https://github.com/cmacrae/agent-shell-sidebar][agent-shell-sidebar]] project, we can easily open/close the Agent buffer window:
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(use-package agent-shell-sidebar
|
||
:after agent-shell
|
||
:straight (:host github :repo "cmacrae/agent-shell-sidebar")
|
||
:bind (("s-i" . agent-shell-sidebar-toggle-focus)
|
||
("s-I" . agent-shell-sidebar-toggle)))
|
||
#+END_SRC
|
||
*** Claude Code
|
||
To begin install the dependencies:
|
||
|
||
#+BEGIN_SRC emacs-lisp :tangle no
|
||
(use-package agent-shell
|
||
:ensure-system-package
|
||
((claude . "brew install claude-code")
|
||
(claude-agent-acp . "npm install -g @agentclientprotocol/claude-agent-acp")))
|
||
#+END_SRC
|
||
|
||
And point Emacs to it:
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(use-package agent-shell
|
||
:config
|
||
(setq agent-shell-preferred-agent-config (agent-shell-anthropic-make-claude-code-config)
|
||
agent-shell-anthropic-claude-acp-command
|
||
`,(file-expand-wildcards "/opt/homebrew/Cellar/node/*/bin/claude-agent-acp")))
|
||
;; /opt/homebrew/Cellar/node/26.0.0/bin/claude-agent-acp
|
||
#+END_SRC
|
||
|
||
*** Cursor
|
||
Cursor, through ACP constantly drops its token on the floor. This annoyance makes me want to use something else.
|
||
|
||
The Cursor interface requires installing the ACP libraries, and a suggestion to install the [[https://github.com/blowmage/cursor-agent-acp-npm][cursor-agent-acp]] project:
|
||
|
||
#+BEGIN_SRC sh
|
||
npm install -g @blowmage/cursor-agent-acp
|
||
#+END_SRC
|
||
|
||
Appears we need to install the [[https://github.com/zalab-inc/cursor_agent][cursor-agent]] CLI as well. Install it with this command:
|
||
|
||
#+BEGIN_SRC sh
|
||
curl https://cursor.com/install -fsSL | bash
|
||
#+END_SRC
|
||
|
||
Or can we do both of these through Emacs:
|
||
|
||
#+BEGIN_SRC emacs-lisp :tangle no
|
||
(use-package agent-shell
|
||
:ensure-system-package
|
||
((cursor-agent . "brew install cursor-cli")
|
||
(claude-agent-acp . "npm install -g @blowmage/cursor-agent-acp")))
|
||
#+END_SRC
|
||
|
||
Change the *default browser* to Workday’s favorite, Chrome, and login:
|
||
|
||
#+BEGIN_SRC sh
|
||
cursor-agent login
|
||
#+END_SRC
|
||
|
||
Now the Emacs configuration:
|
||
|
||
#+BEGIN_SRC emacs-lisp :tangle no
|
||
(use-package agent-shell
|
||
:custom
|
||
(agent-shell-cursor-command `,(file-expand-wildcards "/opt/homebrew/Cellar/node/*/bin/cursor-agent-acp"))
|
||
|
||
:config
|
||
(setq agent-shell-preferred-agent-config (agent-shell-cursor-make-agent-config)))
|
||
#+END_SRC
|
||
*** Gemini
|
||
First install the [[https://github.com/google-gemini/gemini-cli][gemini-cli]]:
|
||
#+BEGIN_SRC sh
|
||
brew install gemini-cli
|
||
#+END_SRC
|
||
|
||
And let’s make that the default now:
|
||
|
||
#+BEGIN_SRC emacs-lisp :tangle no
|
||
(use-package agent-shell
|
||
:config
|
||
(setq agent-shell-preferred-agent-config (agent-shell-google-make-gemini-config)))
|
||
#+END_SRC
|
||
** AI Code Interface
|
||
While the =agent-shell= offers a /vibe-codey/ interface to Chatbots, the [[https://github.com/tninja/ai-code-interface.el][ai-code-interface]] offers a more /programmatic/ interface.
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(use-package ai-code
|
||
:straight (:host github :repo "tninja/ai-code-interface.el")
|
||
:config
|
||
;; use codex as backend, other options are 'claude-code, 'gemini,
|
||
;; 'github-copilot-cli, 'opencode, 'grok, 'cursor, 'kiro,
|
||
;; 'codebuddy, 'aider, 'eca, 'agent-shell, 'claude-code-ide,
|
||
;; 'claude-code-el
|
||
(ai-code-set-backend 'gemini)
|
||
|
||
;; Enable global keybinding for the main menu
|
||
(global-set-key (kbd "C-c a") #'ai-code-menu)
|
||
|
||
;; Optional: Enable @ file completion in comments and AI sessions
|
||
(ai-code-prompt-filepath-completion-mode 1)
|
||
|
||
;; Optional: Ask AI to run test after code changes, for a tighter build-test loop
|
||
;; (setq ai-code-auto-test-type 'ask-me)
|
||
|
||
;; Optional: In AI session buffers, SPC in Evil normal state triggers the prompt-enter UI
|
||
;; (with-eval-after-load 'evil (ai-code-backends-infra-evil-setup))
|
||
|
||
;; Optional: Set up Magit integration for AI commands in Magit popups
|
||
(with-eval-after-load 'magit
|
||
(ai-code-magit-setup-transients)))
|
||
#+END_SRC
|
||
|
||
* Git and Magit
|
||
Can not live without [[https://magit.vc/][Magit]], a Git porcelain for Emacs. I stole the bulk of this work from Doom Emacs.
|
||
#+begin_src emacs-lisp
|
||
(use-package magit
|
||
;; See https://github.com/magit/magit/wiki/Emacsclient for why we need to set:
|
||
:custom (with-editor-emacsclient-executable "emacsclient")
|
||
|
||
:config
|
||
;; See https://takeonrules.com/2024/03/01/quality-of-life-improvement-for-entering-and-exiting-magit/
|
||
(setq magit-display-buffer-function
|
||
#'magit-display-buffer-fullframe-status-v1)
|
||
(setq magit-bury-buffer-function
|
||
#'magit-restore-window-configuration)
|
||
|
||
;; The following code re-instates my General Leader key in Magit.
|
||
(general-unbind magit-mode-map "SPC")
|
||
|
||
(ha-leader
|
||
"g" '(:ignore t :which-key "git")
|
||
"g /" '("Magit dispatch" . magit-dispatch)
|
||
"g ." '("Magit file dispatch" . magit-file-dispatch)
|
||
"g b" '("Magit switch branch" . magit-branch-checkout)
|
||
"g u" '("Git Update" . vc-update)
|
||
|
||
"g g" '("Magit status" . magit-status)
|
||
"g s" '("Magit status here" . magit-status-here)
|
||
"g D" '("Magit file delete" . magit-file-delete)
|
||
"g B" '("Magit blame" . magit-blame-addition)
|
||
"g C" '("Magit clone" . magit-clone)
|
||
"g F" '("Magit fetch" . magit-fetch)
|
||
"g L" '("Magit buffer log" . magit-log-buffer-file)
|
||
"g R" '("Revert file" . magit-file-checkout)
|
||
"g S" '("Git stage file" . magit-stage-file)
|
||
"g U" '("Git unstage file" . magit-unstage-file)
|
||
|
||
"g f" '(:ignore t :which-key "find")
|
||
"g f f" '("Find file" . magit-find-file)
|
||
"g f g" '("Find gitconfig file" . magit-find-git-config-file)
|
||
"g f c" '("Find commit" . magit-show-commit)
|
||
|
||
"g l" '(:ignore t :which-key "list")
|
||
"g l r" '("List repositories" . magit-list-repositories)
|
||
"g l s" '("List submodules" . magit-list-submodules)
|
||
|
||
"g o" '(:ignore t :which-key "open")
|
||
|
||
"g c" '(:ignore t :which-key "create")
|
||
"g c R" '("Initialize repo" . magit-init)
|
||
"g c C" '("Clone repo" . magit-clone)
|
||
"g c c" '("Commit" . magit-commit-create)
|
||
"g c f" '("Fixup" . magit-commit-fixup)
|
||
"g c b" '("Branch" . magit-branch-and-checkout)
|
||
|
||
"g <escape>" '(keyboard-escape-quit :which-key t)
|
||
"g C-g" '(keyboard-escape-quit :which-key t))
|
||
|
||
(general-nmap "<escape>" #'transient-quit-one))
|
||
#+end_src
|
||
** VC Diff Highlight
|
||
The [[https://github.com/dgutov/diff-hl][diff-hl project]], while more active, has more features than the [[https://github.com/syohex/emacs-git-gutter-fringe][git-gutter-fringe]] project.
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(use-package diff-hl
|
||
:custom
|
||
(diff-hl-side 'right)
|
||
(fringe-mode '(8 . 4))
|
||
(diff-hl-draw-borders nil)
|
||
|
||
:hook ((dired-mode . diff-hl-dired-mode)
|
||
(diff-hl-mode . diff-hl-flydiff-mode)))
|
||
#+END_SRC
|
||
|
||
Turning on the mode, as well as binding some new /leader/ keys:
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(use-package diff-hl
|
||
:config
|
||
(global-diff-hl-mode)
|
||
|
||
(ha-leader
|
||
"g j" '("jump hunk" . diff-hl-diff-goto-hunk)
|
||
"g ]" '("next hunk" . diff-hl-next-hunk)
|
||
"g [" '("previous hunk" . diff-hl-previous-hunk)
|
||
"g e" '("end of hunk" . diff-hl-end-of-hunk)
|
||
"g r" '("revert hunk" . diff-hl-revert-hunk)
|
||
"g s" '("show hunk" . diff-hl-show-hunk)
|
||
"g S" '("stage hunk" . diff-hl-stage-dwim)
|
||
|
||
;; Using Gerrit means I might want to view changes not from my
|
||
;; last review, but from the original changes:
|
||
"g a" '("diff amend" . diff-hl-amend-mode)))
|
||
#+END_SRC
|
||
|
||
This project (and others) can use repeat mode, but
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(repeat-mode)
|
||
#+END_SRC
|
||
|
||
** Git Delta
|
||
The [[https://scripter.co/using-git-delta-with-magit][magit-delta]] project uses [[https://github.com/dandavison/delta][git-delta]] for colorized diffs.
|
||
#+begin_src emacs-lisp
|
||
(use-package magit-delta
|
||
:ensure t
|
||
:hook (magit-mode . magit-delta-mode))
|
||
#+end_src
|
||
|
||
This requires [[https://dandavison.github.io/delta/installation.html][installing an executable]]. For instance, on my Mac:
|
||
#+begin_src sh
|
||
brew install git-delta
|
||
#+end_src
|
||
|
||
I also need to append the following to my [[file:~/.gitconfig][~/.gitconfig]] file:
|
||
#+begin_src conf
|
||
[delta]
|
||
minus-style = normal "#8f0001"
|
||
minus-non-emph-style = normal "#8f0001"
|
||
minus-emph-style = normal bold "#d01011"
|
||
minus-empty-line-marker-style = normal "#8f0001"
|
||
zero-style = syntax
|
||
plus-style = syntax "#006800"
|
||
plus-non-emph-style = syntax "#006800"
|
||
plus-emph-style = syntax "#009000"
|
||
plus-empty-line-marker-style = normal "#006800"
|
||
#+end_src
|
||
** Git with Difftastic
|
||
I’m stealing the code for this section from [[https://tsdh.org/posts/2022-08-01-difftastic-diffing-with-magit.html][this essay]] by Tassilo Horn, and in fact, I’m going to lift a lot of his explanation too, as I may need to remind myself how this works. The idea is based on using Wilfred’s excellent [[https://github.com/Wilfred/difftastic][difftastic]] tool to do a structural/syntax comparison of code changes in git. To begin, install the binary:
|
||
#+begin_src sh
|
||
brew install difftastic # and the equivalent on Linux
|
||
#+end_src
|
||
Next, we can do this, to use this as a diff tool for everything.
|
||
#+begin_src emacs-lisp
|
||
(setenv "GIT_EXTERNAL_DIFF" "difft")
|
||
#+end_src
|
||
But perhaps integrating it into Magit and selectively calling it (as it is slow). Tassilo suggests making the call to =difft= optional by first creating a helper function to set the =GIT_EXTERNAL_DIFF= to =difft=:
|
||
#+begin_src emacs-lisp
|
||
(defun th/magit--with-difftastic (buffer command)
|
||
"Run COMMAND with GIT_EXTERNAL_DIFF=difft then show result in BUFFER."
|
||
(let ((process-environment
|
||
(cons (concat "GIT_EXTERNAL_DIFF=difft --width="
|
||
(number-to-string (frame-width)))
|
||
process-environment)))
|
||
;; Clear the result buffer (we might regenerate a diff, e.g., for
|
||
;; the current changes in our working directory).
|
||
(with-current-buffer buffer
|
||
(setq buffer-read-only nil)
|
||
(erase-buffer))
|
||
;; Now spawn a process calling the git COMMAND.
|
||
(make-process
|
||
:name (buffer-name buffer)
|
||
:buffer buffer
|
||
:command command
|
||
;; Don't query for running processes when emacs is quit.
|
||
:noquery t
|
||
;; Show the result buffer once the process has finished.
|
||
:sentinel (lambda (proc event)
|
||
(when (eq (process-status proc) 'exit)
|
||
(with-current-buffer (process-buffer proc)
|
||
(goto-char (point-min))
|
||
(ansi-color-apply-on-region (point-min) (point-max))
|
||
(setq buffer-read-only t)
|
||
(view-mode)
|
||
(end-of-line)
|
||
;; difftastic diffs are usually 2-column side-by-side,
|
||
;; so ensure our window is wide enough.
|
||
(let ((width (current-column)))
|
||
(while (zerop (forward-line 1))
|
||
(end-of-line)
|
||
(setq width (max (current-column) width)))
|
||
;; Add column size of fringes
|
||
(setq width (+ width
|
||
(fringe-columns 'left)
|
||
(fringe-columns 'right)))
|
||
(goto-char (point-min))
|
||
(pop-to-buffer
|
||
(current-buffer)
|
||
`(;; If the buffer is that wide that splitting the frame in
|
||
;; two side-by-side windows would result in less than
|
||
;; 80 columns left, ensure it's shown at the bottom.
|
||
,(when (> 80 (- (frame-width) width))
|
||
#'display-buffer-at-bottom)
|
||
(window-width . ,(min width (frame-width))))))))))))
|
||
#+end_src
|
||
The crucial parts of this helper function are that we "wash" the result using =ansi-color-apply-on-region= so that the function can transform the difftastic highlighting using shell escape codes to Emacs faces. Also, note the need to possibly change the width, as difftastic makes a side-by-side comparison.
|
||
|
||
The functions below depend on [[help:magit-thing-at-point][magit-thing-at-point]], and this depends on the [[https://sr.ht/~pkal/compat/][compat]] library, so let’s grab that stuff:
|
||
#+begin_src emacs-lisp :tangle no
|
||
(use-package compat
|
||
:straight (:host github :repo "emacs-straight/compat"))
|
||
|
||
(use-package magit-section
|
||
:commands magit-thing-at-point)
|
||
#+end_src
|
||
Next, let's define our first command basically doing a =git show= for some revision which defaults to the commit or branch at point or queries the user if there's none.
|
||
#+begin_src emacs-lisp
|
||
(defun th/magit-show-with-difftastic (rev)
|
||
"Show the result of \"git show REV\" with GIT_EXTERNAL_DIFF=difft."
|
||
(interactive
|
||
(list (or
|
||
;; Use if given the REV variable:
|
||
(when (boundp 'rev) rev)
|
||
;; If not invoked with prefix arg, try to guess the REV from
|
||
;; point's position.
|
||
(and (not current-prefix-arg)
|
||
(or (magit-thing-at-point 'git-revision t)
|
||
(magit-branch-or-commit-at-point)))
|
||
;; Otherwise, query the user.
|
||
(magit-read-branch-or-commit "Revision"))))
|
||
(if (not rev)
|
||
(error "No revision specified")
|
||
(th/magit--with-difftastic
|
||
(get-buffer-create (concat "*git show difftastic " rev "*"))
|
||
(list "git" "--no-pager" "show" "--ext-diff" rev))))
|
||
#+end_src
|
||
And here the second command which basically does a =git diff=. It tries to guess what one wants to diff, e.g., when point is on the Staged changes section in a magit buffer, it will run =git diff --cached= to show a diff of all staged changes. If it can not guess the context, it'll query the user for a range or commit for diffing.
|
||
#+begin_src emacs-lisp
|
||
(defun th/magit-diff-with-difftastic (arg)
|
||
"Show the result of \"git diff ARG\" with GIT_EXTERNAL_DIFF=difft."
|
||
(interactive
|
||
(list (or
|
||
;; Use If RANGE is given, just use it.
|
||
(when (boundp 'range) range)
|
||
;; If prefix arg is given, query the user.
|
||
(and current-prefix-arg
|
||
(magit-diff-read-range-or-commit "Range"))
|
||
;; Otherwise, auto-guess based on position of point, e.g., based on
|
||
;; if we are in the Staged or Unstaged section.
|
||
(pcase (magit-diff--dwim)
|
||
('unmerged (error "unmerged is not yet implemented"))
|
||
('unstaged nil)
|
||
('staged "--cached")
|
||
(`(stash . ,value) (error "stash is not yet implemented"))
|
||
(`(commit . ,value) (format "%s^..%s" value value))
|
||
((and range (pred stringp)) range)
|
||
(_ (magit-diff-read-range-or-commit "Range/Commit"))))))
|
||
(let ((name (concat "*git diff difftastic"
|
||
(if arg (concat " " arg) "")
|
||
"*")))
|
||
(th/magit--with-difftastic
|
||
(get-buffer-create name)
|
||
`("git" "--no-pager" "diff" "--ext-diff" ,@(when arg (list arg))))))
|
||
#+end_src
|
||
|
||
What's left is integrating the new show and diff commands in Magit. For that purpose, Tasillo created a new transient prefix for all personal commands. Intriguing, but I have a hack that I can use on a leader:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-difftastic-here ()
|
||
(interactive)
|
||
(call-interactively
|
||
(if (eq major-mode 'magit-log-mode)
|
||
'th/magit-show-with-difftastic
|
||
'th/magit-diff-with-difftastic)))
|
||
|
||
(ha-leader "g d" '("difftastic" . ha-difftastic-here))
|
||
#+end_src
|
||
|
||
How much has been already integrated? Need to re-evaluate this.
|
||
** Time Machine
|
||
The [[https://github.com/emacsmirror/git-timemachine][git-timemachine]] project visually shows how a code file changes with each iteration:
|
||
#+begin_src emacs-lisp
|
||
(use-package git-timemachine
|
||
:config
|
||
(ha-leader "g t" '("git timemachine" . git-timemachine)))
|
||
#+end_src
|
||
** Gist
|
||
Using the [[https://github.com/emacsmirror/gist][gist package]] to write code snippets on [[https://gist.github.com/][Github]] seems like it can be useful, but I'm not sure how often.
|
||
|
||
#+begin_src emacs-lisp :tangle no
|
||
(use-package gist
|
||
:config
|
||
(ha-leader
|
||
"g G" '(:ignore t :which-key "gists")
|
||
"g l g" '("gists" . gist-list)
|
||
"g G l" '("list" . gist-list) ; Lists your gists in a new buffer.
|
||
"g G r" '("region" . gist-region) ; Copies Gist URL into the kill ring.
|
||
"g G R" '("private region" . gist-region-private) ; Explicitly create a private gist.
|
||
"g G b" '("buffer" . gist-buffer) ; Copies Gist URL into the kill ring.
|
||
"g G B" '("private buffer" . gist-buffer-private) ; Explicitly create a private gist.
|
||
"g c g" '("gist" . gist-region-or-buffer) ; Post either the current region, or buffer
|
||
"g c G" '("private gist" . gist-region-or-buffer-private))) ; create private gist from region or buffer
|
||
#+end_src
|
||
|
||
The gist project depends on the [[https://github.com/sigma/gh.el][gh library]]. There seems to be a problem with it.
|
||
#+begin_src emacs-lisp :tangle no
|
||
(use-package gh
|
||
:straight (:host github :repo "sigma/gh.el"))
|
||
#+end_src
|
||
|
||
** Forge
|
||
Let's extend Magit with [[https://github.com/magit/forge][Magit Forge]] for working with Github and Gitlab:
|
||
#+begin_src emacs-lisp :tangle no
|
||
(use-package forge
|
||
:after magit
|
||
:config
|
||
(ha-leader
|
||
"g '" '("Forge dispatch" . forge-dispatch)
|
||
"g f i" '("Find issue" . forge-visit-issue)
|
||
"g f p" '("Find pull request" . forge-visit-pullreq)
|
||
|
||
"g l i" '("List issues" . forge-list-issues)
|
||
"g l p" '("List pull requests" . forge-list-pullreqs)
|
||
"g l n" '("List notifications" . forge-list-notifications)
|
||
|
||
"g o r" '("Browse remote" . forge-browse-remote)
|
||
"g o c" '("Browse commit" . forge-browse-commit)
|
||
"g o i" '("Browse an issue" . forge-browse-issue)
|
||
"g o p" '("Browse a pull request" . forge-browse-pullreq)
|
||
"g o i" '("Browse issues" . forge-browse-issues)
|
||
"g o P" '("Browse pull requests" . forge-browse-pullreqs)
|
||
|
||
"g c i" '("Issue" . forge-create-issue)
|
||
"g c p" '("Pull request" . forge-create-pullreq)))
|
||
#+end_src
|
||
|
||
Every /so often/, pop over to the following URLs and generate a new token where the *Note* is =forge=, and then copy that into the [[file:~/.authinfo.gpg][~/.authinfo.gpg]]:
|
||
- [[https://gitlab.com/-/user_settings/personal_access_tokens][Gitlab]]
|
||
- [[https://github.com/settings/tokens][Github]]
|
||
|
||
Make sure this works:
|
||
|
||
#+begin_src emacs-lisp :tangle no :results replace
|
||
(ghub-request "GET" "/user" nil
|
||
:forge 'github
|
||
:host "api.github.com"
|
||
:username "howardabrams"
|
||
:auth 'forge)
|
||
|
||
#+end_src
|
||
|
||
** Magit Github
|
||
Jonathan Chu’s [[https://github.com/jonathanchu/magit-gh][magit-gh]] project is /simpler/ than [[#Forge][Forge]] (see [[https://jonathanchu.is/posts/introducing-magit-gh/][this essay]] for details).
|
||
|
||
First, install and configure the [[https://github.com/cli/cli/blob/trunk/docs/install_macos.md#homebrew][Github CLI]] program.
|
||
#+BEGIN_SRC sh
|
||
brew install gh
|
||
#+END_SRC
|
||
|
||
Create a =GITHUB_TOKEN= under =/settings/tokens=.
|
||
The required scopes are =repo=, =read:org=, =admin:public_key=.
|
||
Also, these don’t last long, so return and regenerate routinely.
|
||
|
||
Next, [[https://cli.github.com/manual/gh_auth_login][configure it]] with:
|
||
#+BEGIN_SRC sh
|
||
gh auth login --hostname ${GH_HOST:-github.com}
|
||
#+END_SRC
|
||
|
||
And pass in the =GITHUB_TOKEN= environment variable.
|
||
|
||
Verify that this works:
|
||
#+BEGIN_SRC sh
|
||
gh pr list
|
||
#+END_SRC
|
||
|
||
With the =gh= CLI working, we can install and use this project:
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(use-package magit-gh
|
||
:after magit
|
||
:straight (:type git :host github :repo "jonathanchu/magit-gh"))
|
||
#+END_SRC
|
||
|
||
|
||
** Pushing is Bad
|
||
Pushing directly to the upstream branch is /bad form/, as one should create a pull request, etc. To prevent an accidental push, we /double-check/ first:
|
||
|
||
#+begin_src emacs-lisp
|
||
(define-advice magit-push-current-to-upstream (:before (args) query-yes-or-no)
|
||
"Prompt for confirmation before permitting a push to upstream."
|
||
(when-let ((branch (magit-get-current-branch)))
|
||
(unless (yes-or-no-p (format "Push %s branch upstream to %s? "
|
||
branch
|
||
(or (magit-get-upstream-branch branch)
|
||
(magit-get "branch" branch "remote"))))
|
||
(user-error "Push to upstream aborted by user"))))
|
||
#+end_src
|
||
|
||
** Github Search?
|
||
Wanna see an example of how other’s use a particular function?
|
||
#+begin_src emacs-lisp
|
||
(defun ha-github-code-search(&optional search)
|
||
(interactive (list (read-string "Search: " (thing-at-point 'symbol))))
|
||
(let* ((language (cond ((eq major-mode 'python-mode) "Python")
|
||
((eq major-mode 'emacs-lisp-mode) "Emacs Lisp")
|
||
((eq major-mode 'yaml-mode) "Ansible")
|
||
(t "Text")))
|
||
(url (format "https://github.com/search/?q=\"%s\"+language:\"%s\"&type=Code" (url-hexify-string search)
|
||
language)))
|
||
(browse-url url)))
|
||
#+end_src
|
||
* ediff
|
||
Love me ediff, but with monitors that are wider than they are tall, let’s put the diffs side-by-side:
|
||
#+begin_src emacs-lisp
|
||
(setq ediff-split-window-function 'split-window-horizontally)
|
||
#+end_src
|
||
Frames, er, windows, are actually annoying for me, as Emacs is always in full-screen mode.
|
||
#+begin_src emacs-lisp
|
||
(setq ediff-window-setup-function 'ediff-setup-windows-plain)
|
||
#+end_src
|
||
When =ediff= is finished, it leaves the windows /borked/. This is annoying, but according to [[http://yummymelon.com/devnull/surprise-and-emacs-defaults.html][this essay]], we can fix it:
|
||
#+begin_src emacs-lisp
|
||
(defvar my-ediff-last-windows nil
|
||
"Session for storing window configuration before calling `ediff'.")
|
||
|
||
(defun my-store-pre-ediff-winconfig ()
|
||
"Store `current-window-configuration' in variable `my-ediff-last-windows'."
|
||
(setq my-ediff-last-windows (current-window-configuration)))
|
||
|
||
(defun my-restore-pre-ediff-winconfig ()
|
||
"Restore window configuration to stored value in `my-ediff-last-windows'."
|
||
(set-window-configuration my-ediff-last-windows))
|
||
|
||
(add-hook 'ediff-before-setup-hook #'my-store-pre-ediff-winconfig)
|
||
(add-hook 'ediff-quit-hook #'my-restore-pre-ediff-winconfig)
|
||
#+end_src
|
||
* Web Browsing
|
||
** EWW
|
||
Web pages look pretty good with EWW, but I'm having difficulty getting it to render a web search from DuckDuck.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package eww
|
||
:init
|
||
(setq browse-url-browser-function 'eww-browse-url
|
||
browse-url-secondary-browser-function 'browse-url-default-browser
|
||
browse-url-chrome-program "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||
|
||
eww-browse-url-new-window-is-tab nil
|
||
shr-use-colors nil
|
||
shr-use-fonts t ; I go back and forth on this one
|
||
;; shr-discard-aria-hidden t
|
||
shr-bullet "• "
|
||
shr-inhibit-images nil ; Gotta see the images?
|
||
;; shr-blocked-images '(svg)
|
||
;; shr-folding-mode nil
|
||
url-privacy-level '(email))
|
||
|
||
:config
|
||
(ha-leader "a b" '("eww browser" . eww))
|
||
|
||
(defun ha-eww-save-off-window (name)
|
||
(interactive (list (read-string "Name: " (plist-get eww-data :title))))
|
||
(rename-buffer (format "*eww: %s*" name) t))
|
||
|
||
(defun ha-eww-better-scroll (prefix)
|
||
(interactive "^p")
|
||
(forward-paragraph prefix)
|
||
;; (recenter) ... if you want the cursor in the center,
|
||
;; otherwise, this puts the paragraph at the top of window:
|
||
(recenter-top-bottom 0))
|
||
|
||
(major-mode-hydra-define eww-mode nil
|
||
("Browser"
|
||
(("G" eww-browse "Browse")
|
||
("B" eww-list-bookmarks "Bookmarks")
|
||
("q" bury-buffer "Quit"))
|
||
"History"
|
||
(("l" eww-back-url "Back" :color pink)
|
||
("r" eww-forward-url "Forward" :color pink)
|
||
("H" eww-list-histories "History"))
|
||
"Current Page"
|
||
(("b" eww-add-bookmark "Bookmark")
|
||
("g" link-hint-open-link "Jump Link")
|
||
("d" eww-download "Download"))
|
||
"Render Page"
|
||
(("e" eww-browse-with-external-browser "Open in Firefox")
|
||
("R" eww-readable "Reader Mode")
|
||
("y" eww-copy-page-url "Copy URL"))
|
||
"Navigation"
|
||
(("u" eww-top-url "Site Top")
|
||
("n" eww-next-url "Next Page" :color pink)
|
||
("p" eww-previous-url "Previous" :color pink))
|
||
"Toggles"
|
||
(("c" eww-toggle-colors "Colors")
|
||
("i" eww-toggle-images "Images")
|
||
("f" eww-toggle-fonts "Fonts"))
|
||
"Misc"
|
||
(("s" ha-eww-save-off-window "Rename")
|
||
("S" eww-switch-to-buffer "Switch to")
|
||
("-" eww-write-bookmarks "Save Bookmarks")
|
||
("M" eww-read-bookmarks "Load Bookmarks"))))
|
||
|
||
:general
|
||
(:states 'normal :keymaps 'eww-mode-map
|
||
"q" 'bury-buffer
|
||
"J" 'ha-eww-better-scroll)
|
||
(:states 'normal :keymaps 'eww-buffers-mode-map
|
||
"q" 'bury-buffer))
|
||
#+end_src
|
||
|
||
This function allows Imenu to offer HTML headings in EWW buffers, helpful for navigating long, technical documents.
|
||
#+begin_src emacs-lisp :tangle no
|
||
(use-package eww
|
||
:config
|
||
(defun unpackaged/imenu-eww-headings ()
|
||
"Return alist of HTML headings in current EWW buffer for Imenu.
|
||
Suitable for `imenu-create-index-function'."
|
||
(let ((faces '(shr-h1 shr-h2 shr-h3 shr-h4 shr-h5 shr-h6 shr-heading)))
|
||
(save-excursion
|
||
(save-restriction
|
||
(widen)
|
||
(goto-char (point-min))
|
||
(cl-loop for next-pos = (next-single-property-change (point) 'face)
|
||
while next-pos
|
||
do (goto-char next-pos)
|
||
for face = (get-text-property (point) 'face)
|
||
when (cl-typecase face
|
||
(list (cl-intersection face faces))
|
||
(symbol (member face faces)))
|
||
collect (cons (buffer-substring (point-at-bol) (point-at-eol)) (point))
|
||
and do (forward-line 1))))))
|
||
:hook (eww-mode .
|
||
(lambda ()
|
||
(setq-local imenu-create-index-function #'unpackaged/imenu-eww-headings))))
|
||
#+end_src
|
||
** SHRFace
|
||
Make my EWW browsers /look/ like an Org file with the [[https://github.com/chenyanming/shrface][shrface project]].
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package shrface
|
||
:straight (:host github :repo "chenyanming/shrface")
|
||
:config
|
||
(shrface-basic)
|
||
;; (shrface-trial)
|
||
;; (shrface-default-keybindings) ; setup default keybindings
|
||
(setq shrface-href-versatile t)
|
||
|
||
(major-mode-hydra-define+ eww-mode nil
|
||
("Headlines"
|
||
(("j" shrface-next-headline "Next Heading" :color pink)
|
||
("k" shrface-previous-headline "Previous" :color pink)
|
||
("J" shrface-headline-consult "Goto Heading")))))
|
||
#+end_src
|
||
|
||
The following connection to EWW throws errors now. Hrm.
|
||
|
||
#+begin_src emacs-lisp :tangle no
|
||
(use-package eww
|
||
:after shrface
|
||
:hook (eww-after-render #'shrface-mode))
|
||
#+end_src
|
||
|
||
** Get Pocket
|
||
The [[https://github.com/alphapapa/pocket-reader.el][pocket-reader]] project connects to the [[https://getpocket.com/en/][Get Pocket]] service.
|
||
|
||
#+begin_src emacs-lisp :tangle no
|
||
(use-package pocket-reader
|
||
:init
|
||
(setq org-web-tools-pandoc-sleep-time 1)
|
||
:config
|
||
(ha-leader "o p" '("get pocket" . pocket-reader))
|
||
|
||
;; Instead of jumping into Emacs mode to get the `pocket-mode-map',
|
||
;; we add the keybindings to the normal mode that makes sense.
|
||
:general
|
||
(:states 'normal :keymaps 'pocket-reader-mode-map
|
||
"RET" 'pocket-reader-open-url
|
||
"TAB" 'pocket-reader-pop-to-url
|
||
|
||
"*" 'pocket-reader-toggle-favorite
|
||
"B" 'pocket-reader-open-in-external-browser
|
||
"D" 'pocket-reader-delete
|
||
"E" 'pocket-reader-excerpt-all
|
||
"F" 'pocket-reader-show-unread-favorites
|
||
"M" 'pocket-reader-mark-all
|
||
"R" 'pocket-reader-random-item
|
||
"S" 'tabulated-list-sort
|
||
"a" 'pocket-reader-toggle-archived
|
||
"c" 'pocket-reader-copy-url
|
||
"d" 'pocket-reader
|
||
"e" 'pocket-reader-excerpt
|
||
"f" 'pocket-reader-toggle-favorite
|
||
"l" 'pocket-reader-limit
|
||
"m" 'pocket-reader-toggle-mark
|
||
"o" 'pocket-reader-more
|
||
"q" 'quit-window
|
||
"s" 'pocket-reader-search
|
||
"u" 'pocket-reader-unmark-all
|
||
"t a" 'pocket-reader-add-tags
|
||
"t r" 'pocket-reader-remove-tags
|
||
"t s" 'pocket-reader-tag-search
|
||
"t t" 'pocket-reader-set-tags
|
||
|
||
"g s" 'pocket-reader-resort
|
||
"g r" 'pocket-reader-refresh))
|
||
#+end_src
|
||
|
||
Use these special keywords when searching:
|
||
|
||
- =:*=, =:favorite= Return favorited items.
|
||
- =:archive= Return archived items.
|
||
- =:unread= Return unread items (default).
|
||
- =:all= Return all items.
|
||
- =:COUNT= Return at most /COUNT/ (a number) items. This limit persists until you start a new search.
|
||
- =:t:TAG=, =t:TAG= Return items with /TAG/ (you can search for one tag at a time, a limitation of the Pocket API).
|
||
** External Browsing
|
||
Browsing on a work laptop is a bit different. According to [[http://ergoemacs.org/emacs/emacs_set_default_browser.html][this page]], I can set a /default browser/ for different URLs, which is great, as I can launch my browser for personal browsing, or another browser for work access, or even EWW. To make this clear, I'm using the abstraction associated with [[https://github.com/rolandwalker/osx-browse][osx-browse]]:
|
||
#+begin_src emacs-lisp
|
||
(use-package osx-browse
|
||
:init
|
||
(setq browse-url-handlers
|
||
'(("docs\\.google\\.com" . osx-browse-url-personal)
|
||
("grafana.com" . osx-browse-url-personal)
|
||
("dndbeyond.com" . osx-browse-url-personal)
|
||
("tabletopaudio.com" . osx-browse-url-personal)
|
||
("youtu.be" . osx-browse-url-personal)
|
||
("youtube.com" . osx-browse-url-personal)
|
||
("." . eww-browse-url)))
|
||
|
||
:config
|
||
(defun osx-browse-url-personal (url &optional new-window browser focus)
|
||
"Open URL in Firefox for my personal surfing.
|
||
The parameters, URL, NEW-WINDOW, and FOCUS are as documented in
|
||
the function, `osx-browse-url'."
|
||
(interactive (osx-browse-interactive-form))
|
||
(cl-callf or browser "org.mozilla.Firefox")
|
||
(osx-browse-url url new-window browser focus)))
|
||
#+end_src
|
||
* Dired
|
||
Allow me a confession. When renaming a file or flipping an executable bit, I don’t pull up =dired= as a first thought. But I feel like I should, as can do a lot of things quicker than pulling up a shell. Especially when working [[https://www.masteringemacs.org/article/working-multiple-files-dired][with multiple files]].
|
||
Most commands are /somewhat/ straight-forward (and Prot did a pretty good [[https://www.youtube.com/watch?v=5dlydii7tAU][introduction]] to it), but to remind myself, keep in mind it has /two actions/ … mark one or more files to do something, or /flag/ one or more files to delete them. Why two? Dunno. Especially since they act the same. For instance:
|
||
|
||
1. Mark a few files with ~m~, and then type ~D~ to delete them, or …
|
||
2. Flag a few files with ~d~, and then type ~x~ to delete them.
|
||
|
||
Seems the same to me. Especially since you can type ~u~ to unmark or unflag.
|
||
|
||
Few other commands to note:
|
||
|
||
+ ~m~ :: marks a single file
|
||
+ ~%~ :: will /mark/ a bunch of files based on a regular expression
|
||
+ ~u~ :: un-mark a file, or type ~U~ to un-mark all
|
||
+ ~t~ :: to toggle the marked files. Keep files with =xyz= extension? Mark those with ~%~, and then ~t~ toggle.
|
||
+ ~C~ :: copy the current file or all marked files
|
||
+ ~D~ :: delete the current file or all marked files
|
||
+ ~R~ :: rename/move the current file or all marked files
|
||
+ ~M~ :: change the mode (=chmod=) of current or marked files, accepts symbols, like =a+x=
|
||
|
||
Couple useful settings:
|
||
|
||
#+begin_src emacs-lisp
|
||
(setq delete-by-moving-to-trash t
|
||
dired-auto-revert-buffer t
|
||
dired-vc-rename-file t) ; Why not mention to git when renaming?
|
||
#+end_src
|
||
|
||
My =ls= is an often alias and GNU’s =ls=, labeled =gls= on my Mac, isn’t consistent between Mac and Linux, so I *don’t* do:
|
||
#+begin_src emacs-lisp :tangle no
|
||
(setq insert-directory-program "gls")
|
||
#+end_src
|
||
|
||
Instead I use Emacs' built-in directory lister (which accepts the standard, =dired-listing-switches= to customize the output):
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package ls-lisp
|
||
:straight (:type built-in)
|
||
:config
|
||
(setq ls-lisp-use-insert-directory-program nil
|
||
dired-listing-switches
|
||
"-l --almost-all --human-readable --group-directories-first --no-group"))
|
||
#+end_src
|
||
|
||
And [[https://www.masteringemacs.org/article/dired-shell-commands-find-xargs-replacement][this article by Mickey Petersen]] convinced me to turn on the built-in =dired-x= (just have to tell [[file:bootstrap.org::*Introduction][straight]] that knowledge):
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package dired-x
|
||
:straight (:type built-in))
|
||
#+end_src
|
||
|
||
The advantage of =dired-x= is the ability to have [[https://www.emacswiki.org/emacs/DiredExtra#Dired_X][shell command guessing]] when selecting one or more files, and running a shell command on them with ~!~ or ~&~.
|
||
|
||
** Dirvish
|
||
The [[https://github.com/alexluigit/dirvish][dirvish]] project aims to make a prettier =dired=. And since the =major-mode= is still =dired-mode=, the decades of finger memory isn’t lost. Dirvish does require the following supporting programs, but I’ve already got those puppies installed:
|
||
#+begin_src sh
|
||
brew install coreutils fd poppler ffmpegthumbnailer mediainfo imagemagick
|
||
#+end_src
|
||
|
||
I’m beginning with dirvish to use the [[https://github.com/alexluigit/dirvish/blob/main/docs/CUSTOMIZING.org][sample configuration]] and change it:
|
||
#+begin_src emacs-lisp :tangle no
|
||
(use-package dirvish
|
||
:straight (:host github :repo "alexluigit/dirvish")
|
||
:init (dirvish-override-dired-mode)
|
||
|
||
:custom
|
||
(dirvish-quick-access-entries
|
||
'(("h" "~/" "Home")
|
||
("e" "~/.emacs.d/" "Emacs user directory")
|
||
("p" "~/personal" "Personal")
|
||
("p" "~/projects" "Projects")
|
||
("t" "~/technical" "Technical")
|
||
("w" "~/website" "Website")
|
||
("d" "~/Downloads/" "Downloads")))
|
||
|
||
:config
|
||
;; This setting is like `treemacs-follow-mode' where the buffer
|
||
;; changes based on the current file. Not sure if I want this:
|
||
;; (dirvish-side-follow-mode)
|
||
|
||
(setq dirvish-mode-line-format
|
||
'(:left (sort symlink) :right (omit yank index)))
|
||
(setq dirvish-attributes
|
||
'(all-the-icons file-time file-size collapse subtree-state vc-state git-msg))
|
||
|
||
(set-face-attribute 'dirvish-hl-line nil :background "darkmagenta"))
|
||
#+end_src
|
||
|
||
While in =dirvish-mode=, we can rebind some keys:
|
||
#+begin_src emacs-lisp :tangle no
|
||
(use-package dirvish
|
||
:bind
|
||
(:map dirvish-mode-map ; Dirvish inherits `dired-mode-map'
|
||
("a" . dirvish-quick-access)
|
||
("f" . dirvish-file-info-menu)
|
||
("y" . dirvish-yank-menu)
|
||
("N" . dirvish-narrow)
|
||
("^" . dirvish-history-last)
|
||
("h" . dirvish-history-jump) ; remapped `describe-mode'
|
||
("q" . dirvish-quit)
|
||
("s" . dirvish-quicksort) ; remapped `dired-sort-toggle-or-edit'
|
||
("v" . dirvish-vc-menu) ; remapped `dired-view-file'
|
||
("," . dirvish-dispatch)
|
||
("TAB" . dirvish-subtree-toggle)
|
||
("M-f" . dirvish-history-go-forward)
|
||
("M-b" . dirvish-history-go-backward)
|
||
("M-l" . dirvish-ls-switches-menu)
|
||
("M-m" . dirvish-mark-menu)
|
||
("M-t" . dirvish-layout-toggle)
|
||
("M-s" . dirvish-setup-menu)
|
||
("M-e" . dirvish-emerge-menu)
|
||
("M-j" . dirvish-fd-jump)))
|
||
#+end_src
|
||
** My Dired Interface
|
||
Because I can’t remember all the cool things =dired= can do, I put together a helper/cheatsheet. Typing ~,~ brings up a menu of possibilities (for others, I recommend [[https://github.com/kickingvegas/casual-dired][Casual Dired]]):
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package major-mode-hydra
|
||
:config
|
||
(major-mode-hydra-define dired-mode (:quit-key "q")
|
||
("File"
|
||
(("C" dired-do-copy "Copy")
|
||
("D" dired-do-delete "Delete")
|
||
("S" dired-do-symlink "Symlink")
|
||
("w" dired-copy-filename-as-kill "Copy name")
|
||
("!" dired-do-shell-command "Shell")
|
||
("&" dired-do-async-shell-command "Shell &")) ; Really?
|
||
"Change"
|
||
(("R" dired-do-rename "Rename")
|
||
("M" dired-do-chmod "Mode")
|
||
("O" dired-do-chown "Owner")
|
||
("G" dired-do-chgrp "Group")
|
||
("T" dired-do-touch "Mod time"))
|
||
"Directory"
|
||
(("+" dired-create-directory "New")
|
||
("i" dired-insert-subdir "Insert subdir" :color pink)
|
||
("I" dired-hide-subdir "Hide subdir" :color pink)
|
||
("g" revert-buffer "Refresh" :color pink)
|
||
("E" wdired-change-to-wdired-mode "Edit (wdired)"))
|
||
"Mark"
|
||
(("m" dired-mark "Mark" :color pink)
|
||
("u" dired-unmark "Unmark" :color pink)
|
||
("U" dired-unmark-all-marks "Unmark all" :color pink)
|
||
("t" dired-toggle-marks "Toggle marks" :color pink)
|
||
("~" dired-flag-backup-files "Mark backups" :color pink)
|
||
("r" hydra-dired-regexp-mark/body "Regexp »"))
|
||
"Navigation"
|
||
(("^" dired-up-directory "Up Directory")
|
||
("j" dired-next-line "Next File" :color pink)
|
||
("k" dired-previous-line "Previous File" :color pink)
|
||
("J" dired-next-subdir "Next subdir" :color pink)
|
||
("K" dired-previous-subdir "Previous subdir" :color pink))
|
||
"Misc"
|
||
(("x" hydra-dired-utils/body "Utils »")
|
||
("o" hydra-dired-toggles/body "Toggles »")
|
||
("a" dirvish-quick-access "Quick Access"))))
|
||
|
||
;; And some more hydras for the sub-menus:
|
||
(pretty-hydra-define hydra-dired-regexp-mark (:color blue :hint nil)
|
||
("Mark files with regexp..."
|
||
(("m" dired-mark-files-regexp "matching filenames")
|
||
("g" dired-mark-files-containing-regexp "containing text")
|
||
("d" dired-flag-files-regexp "to delete")
|
||
("c" dired-do-copy-regexp "to copy")
|
||
("r" dired-do-rename-regexp "to rename"))))
|
||
|
||
(pretty-hydra-define hydra-dired-toggles (:color blue)
|
||
("Dired Toggles"
|
||
(("d" dired-hide-details-mode "File details")
|
||
("h" dired-do-kill-lines "Hide marked")
|
||
("o" dired-omit-mode "Hide (omit) some?")
|
||
("T" image-dired "Image thumbnails"))))
|
||
|
||
(pretty-hydra-define hydra-dired-utils (:color blue :hint nil)
|
||
("Files"
|
||
(("f" dired-do-find-marked-files "open marked")
|
||
("z" dired-do-compress "(un)compress marked"))
|
||
"Rename"
|
||
(("u" dired-upcase "upcase")
|
||
("d" dired-downcase "downcase"))
|
||
"Search"
|
||
(("g" dired-do-find-regexp "grep marked")
|
||
("s" dired-do-isearch "isearch marked")) ; Maybe C-s ... even on top?
|
||
"Replace"
|
||
(("r" dired-do-find-regexp-and-replace "find/replace marked")
|
||
("R" dired-do-query-replace-regexp "query find/replace")))))
|
||
#+end_src
|
||
|
||
Notice ~E~ to turn on =wdired=, which brings =dired= to a whole new level.
|
||
|
||
I do want to change a couple of bindings, as ~j~ to pull up a =completing-read= interface for files, and then move the cursor to the on selected (why not just search) and ~k~ for /hiding/ marked files, aren’t very useful, compared to the finger memory I now have for using those two keys to move up and down lines.
|
||
|
||
#+begin_src emacs-lisp
|
||
(define-key dired-mode-map (kbd "j") 'evil-next-line)
|
||
(define-key dired-mode-map (kbd "k") 'evil-previous-line)
|
||
(define-key dired-mode-map (kbd "/") 'isearch-forward)
|
||
(define-key dired-mode-map (kbd "n") 'evil-search-next)
|
||
(define-key dired-mode-map (kbd ",") 'major-mode-hydras/dired-mode/body)
|
||
#+end_src
|
||
* Annotations
|
||
Let's try [[https://github.com/bastibe/annotate.el][annotate-mode]], which allows you to drop "notes" and then move to them (yes, serious overlap with bookmarks, which we will return to).
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package annotate
|
||
:config
|
||
(ha-leader
|
||
"t A" '("annotations" . annotate-mode)
|
||
|
||
"n" '(:ignore t :which-key "notes")
|
||
"n a" '("toggle mode" . annotate-mode)
|
||
"n n" '("annotate" . annotate-annotate)
|
||
"n d" '("delete" . annotate-delete)
|
||
"n s" '("summary" . annotate-show-annotation-summary)
|
||
"n j" '("next" . annotate-goto-next-annotation)
|
||
"n k" '("prev" . annotate-goto-previous-annotation)
|
||
|
||
;; If a shift binding isn't set, it defaults to non-shift version
|
||
;; Use SPC N N to jump to the next error:
|
||
"n N" '("next error" . flycheck-next-error)))
|
||
#+end_src
|
||
Keep the annotations simple, almost /tag-like/, and then the summary allows you to display them.
|
||
* Keepass
|
||
Use the [[https://github.com/ifosch/keepass-mode][keepass-mode]] to view a /read-only/ version of my Keepass file in Emacs:
|
||
#+begin_src emacs-lisp
|
||
(use-package keepass-mode)
|
||
#+end_src
|
||
When having your point on a key entry, you can copy fields to kill-ring using:
|
||
- ~u~ :: URL
|
||
- ~b~ :: user name
|
||
- ~c~ :: password
|
||
|
||
* PDF Viewing
|
||
Why not [[https://github.com/politza/pdf-tools][view PDF files]] better? If you have standard build tools installed on your system, run [[help:pdf-tools-install][pdf-tools-install]], as this command will an =epdfinfo= program to PDF displays.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package pdf-tools
|
||
:mode ("\\.pdf\\'" . pdf-view-mode)
|
||
:init
|
||
(setq pdf-info-epdfinfo-program
|
||
(if (file-exists-p "/opt/homebrew")
|
||
"/opt/homebrew/bin/epdfinfo"
|
||
"/usr/local/bin/epdfinfo")
|
||
|
||
;; Match my theme:
|
||
pdf-view-midnight-colors '("#c5c8c6" . "#1d1f21"))
|
||
|
||
:general
|
||
(:states 'normal :keymaps 'pdf-view-mode-map
|
||
;; Since the keys don't make sense when reading:
|
||
"J" 'pdf-view-scroll-up-or-next-page
|
||
"K" 'pdf-view-scroll-down-or-previous-page
|
||
"gp" 'pdf-view-goto-page
|
||
">" 'doc-view-fit-window-to-page))
|
||
#+end_src
|
||
|
||
Make sure the [[help:pdf-info-check-epdfinfo][pdf-info-check-epdfinfo]] function works.
|
||
|
||
The [[Evil Collection][evil-collection]] package adds the following keybindings:
|
||
- ~z d~ :: Dark mode … indispensable, see also ~z m~
|
||
- ~C-j~ / ~C-k~ :: next and previous pages
|
||
- ~j~ / ~k~ :: up and down the page
|
||
- ~h~ / ~l~ :: scroll the page left and right
|
||
- ~=~ / ~-~ :: enlarge and shrink the page
|
||
- ~o~ :: Table of contents (if available)
|
||
|
||
I’d like write notes in org files that link to the PDFs (and maybe visa versa), using the [[https://github.com/weirdNox/org-noter][org-noter]] package:
|
||
#+begin_src emacs-lisp
|
||
(use-package org-noter
|
||
:config
|
||
(major-mode-hydra-define org-noter-doc-mode-map nil
|
||
("Notes"
|
||
(("i" org-noter-insert-note "insert note")
|
||
("s" org-noter-sync-current-note "sync note")
|
||
("n" org-noter-sync-next-note "next note" :color pink)
|
||
("p" org-noter-sync-prev-note "previous note" :color pink)))))
|
||
#+end_src
|
||
|
||
To use, open a header in an org doc, and run =M-x org-noter= (~SPC o N~) and select the PDF. The =org-noter= function can be called in the PDF doc as well. In Emacs state, type ~i~ to insert a note /as a header/, or in Normal state, type ~, i~.
|
||
|
||
* Technical Artifacts :noexport:
|
||
|
||
Let's provide a name so that the file can be required:
|
||
|
||
#+begin_src emacs-lisp :exports none
|
||
(provide 'ha-applications)
|
||
;;; ha-applications.el ends here
|
||
#+end_src
|
||
|
||
|
||
#+description: A literate programming file configuring critical applications.
|
||
|
||
#+property: header-args:sh :tangle no
|
||
#+property: header-args:emacs-lisp :tangle yes
|
||
#+property: header-args :results none :eval no-export :comments no mkdirp yes
|
||
|
||
#+options: num:nil toc:t todo:nil tasks:nil tags:nil date:nil
|
||
#+options: skip:nil author:nil email:nil creator:nil timestamp:nil
|
||
#+infojs_opt: view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js
|
||
|
||
# Local Variables:
|
||
# eval: (add-hook 'after-save-hook #'org-babel-tangle t t)
|
||
# jinx-local-words: "Emacsians VSCode"
|
||
# End:
|