Compare commits

..

1 commit

Author SHA1 Message Date
Howard Abrams
f8f2be92ff Mostly works.
Magit and other parts, don't.
2025-09-09 11:19:16 -07:00
42 changed files with 1252 additions and 2980 deletions

4
.gitignore vendored
View file

@ -9,6 +9,4 @@
/elisp/gourmet.el
/elisp/wd-imaas.el
/ha-theme-results.org
/support/dashboard
/.emacs.desktop
/.*-notes.org
/support/dashboard

View file

@ -44,7 +44,7 @@ Best success comes from using the [[https://github.com/d12frosted/homebrew-emacs
I find that I need to … at least, on my work computer, install two different versions of Emacs that I use to distinguish one for “work” and the other for other activities, like IRC and [[file:ha-feed-reader.org][elfeed]]. To that end, I run the following command to install Emacs:
#+begin_src sh
brew reinstall $(brew deps emacs-plus@30)
brew reinstall emacs-plus@30 --with-mailutils
brew install emacs-plus@30 --with-native-comp --with-mailutils --with-imagemagick --with-savchenkovaleriy-big-sur-icon --with-no-frame-refocus --debug
#+end_src
And if it fails, choose =shell= and type:
#+begin_src sh

View file

@ -8,15 +8,6 @@ A literate programming file for bootstraping my Emacs Configuration.
#+begin_src emacs-lisp :exports none
;;; bootstrap.el --- file for bootstraping my Emacs Configuration
;;
;; ██████████
;; ░░███░░░░░█
;; ░███ █ ░ █████████████ ██████ ██████ █████
;; ░██████ ░░███░░███░░███ ░░░░░███ ███░░███ ███░░
;; ░███░░█ ░███ ░███ ░███ ███████ ░███ ░░░ ░░█████
;; ░███ ░ █ ░███ ░███ ░███ ███░░███ ░███ ███ ░░░░███
;; ██████████ █████░███ █████░░████████░░██████ ██████
;; ░░░░░░░░░░ ░░░░░ ░░░ ░░░░░ ░░░░░░░░ ░░░░░░ ░░░░░░
;; © 2021-2023 Howard X. Abrams
;; Licensed under a Creative Commons Attribution 4.0 International License.
;; See http://creativecommons.org/licenses/by/4.0/
@ -36,8 +27,6 @@ A literate programming file for bootstraping my Emacs Configuration.
* Introduction
This file contains all the variable definitions and library loading for the other files in my project.
I'm installing everything using the [[https://github.com/raxod502/straight.el#getting-started][straight.el]] for package installation and management. This is initialization code configured in [[file:initialize][initialize]], and calls to =use-package= now accepts a =:straight= parameter that allows me to retrieve special versions of some packages.
See the details in [[https://dev.to/jkreeftmeijer/emacs-package-management-with-straight-el-and-use-package-3oc8][this essay]].
* Initial Settings
@ -107,7 +96,7 @@ Getting tired off all the packages that I load spewing a bunch of warnings that
The following packages come with Emacs, but seems like they still need loading:
#+begin_src emacs-lisp
(use-package cl-lib
:straight (:type built-in)
:init (defun first (elt) (car elt))
:commands (first))

View file

@ -19,7 +19,7 @@
"A WAV or AU file used at the completion of a function.")
;; My replacement in case we can't play internal sounds:
(defun beep-beep (&rest _)
(defun beep-beep ()
"Play a default notification sound file.
Customize the variable, `beep-alert-sound-file' to adjust the sound."
(if (fboundp 'play-sound-internal)

View file

@ -33,7 +33,7 @@ start some music to indicate we are working, and set a timer.
Call `ha-focus-break' when finished."
(interactive)
(ha-focus-countdown-timer 25 'ha-focus-break)
(ha-focus--command "tell application \"VLC\" to play")
(ha-focus--command "tell application \"Spotify\" to play")
(if (eq major-mode 'org-mode)
(org-clock-in)
(org-clock-in-last))
@ -47,7 +47,7 @@ This also starts another break timer, that calls
`ha-focus-break-over' when finished."
(interactive)
(run-with-idle-timer 30 nil 'ha-focus-capture)
(ha-focus--command "tell application \"VLC\" to stop")
(ha-focus--command "tell application \"Spotify\" to pause")
(ha-focus-countdown-timer 5 'ha-focus-break-over)
(message "🍅 Time to take a break."))

View file

@ -26,223 +26,13 @@ A literate programming file configuring critical applications.
#+end_src
Can we call the following /applications/? I guess.
* Agentic Interface
Ethic issues aside, Im [[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 cant compete with my creation here.
** Agent Shell
Installing Xenodiums [[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
I might switch to [[https://github.com/cxa/agent-shell-macext#start-of-content][agent-shell-macext]] if I cant get KnockKnock to work well on my Linux system.
*** Agent Skills
The following pulls in [[https://github.com/xenodium/emacs-skills][emacs-skills]], a Claude Agent skills for Emacs:
#+BEGIN_SRC sh
claude plugin marketplace add xenodium/emacs-skills
claude plugin install emacs-skills@xenodium-emacs-skills
#+END_SRC
Often update with:
#+BEGIN_SRC sh
claude plugin marketplace update xenodium-emacs-skills
#+END_SRC
The most helpful is =/open= that you run afterwards to pull the changes into buffers for complete review. Use the =/highlight= variation to see the changes highlighted.
*** 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/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 Workdays 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 lets 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)
(use-package magit
:load-path "~/.emacs.d/repos/magit"
;; See https://github.com/magit/magit/wiki/Emacsclient for why we need to set:
:custom (with-editor-emacsclient-executable "emacsclient")
@ -300,7 +90,7 @@ Can not live without [[https://magit.vc/][Magit]], a Git porcelain for Emacs. I
** 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
#+BEGIN_SRC emacs-lisp :tangle no
(use-package diff-hl
:custom
(diff-hl-side 'right)
@ -313,7 +103,7 @@ The [[https://github.com/dgutov/diff-hl][diff-hl project]], while more active, h
Turning on the mode, as well as binding some new /leader/ keys:
#+BEGIN_SRC emacs-lisp
#+BEGIN_SRC emacs-lisp :tangle no
(use-package diff-hl
:config
(global-diff-hl-mode)
@ -334,7 +124,7 @@ Turning on the mode, as well as binding some new /leader/ keys:
This project (and others) can use repeat mode, but
#+BEGIN_SRC emacs-lisp
#+BEGIN_SRC emacs-lisp :tangle no
(repeat-mode)
#+END_SRC
@ -427,7 +217,7 @@ The crucial parts of this helper function are that we "wash" the result using =a
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 lets grab that stuff:
#+begin_src emacs-lisp :tangle no
(use-package compat
:straight (:host github :repo "emacs-straight/compat"))
;; :vc (:url "https://github.com/emacs-straight/compat"))
(use-package magit-section
:commands magit-thing-at-point)
@ -523,7 +313,7 @@ Using the [[https://github.com/emacsmirror/gist][gist package]] to write code sn
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"))
;; :vc (:url "https://github.com/sigma/gh.el"))
#+end_src
** Forge
@ -555,51 +345,15 @@ Let's extend Magit with [[https://github.com/magit/forge][Magit Forge]] for work
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]]
and make sure this works:
Make sure this works:
#+begin_src emacs-lisp :tangle no :results replace
#+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 Chus [[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 dont 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
#+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:
@ -613,11 +367,10 @@ Pushing directly to the upstream branch is /bad form/, as one should create a pu
(magit-get "branch" branch "remote"))))
(user-error "Push to upstream aborted by user"))))
#+end_src
** Github Search?
Wanna see an example of how others use a particular function?
#+begin_src emacs-lisp
(defun ha-github-code-search(&optional search)
(defun my-github-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")
@ -658,6 +411,7 @@ Web pages look pretty good with EWW, but I'm having difficulty getting it to ren
#+begin_src emacs-lisp
(use-package eww
:after major-mode-hydra
:init
(setq browse-url-browser-function 'eww-browse-url
browse-url-secondary-browser-function 'browse-url-default-browser
@ -756,7 +510,8 @@ Make my EWW browsers /look/ like an Org file with the [[https://github.com/cheny
#+begin_src emacs-lisp
(use-package shrface
:straight (:host github :repo "chenyanming/shrface")
;; :vc (:url "https://github.com/chenyanming/shrface")
:after major-mode-hydra
:config
(shrface-basic)
;; (shrface-trial)
@ -891,7 +646,7 @@ Instead I use Emacs' built-in directory lister (which accepts the standard, =dir
#+begin_src emacs-lisp
(use-package ls-lisp
:straight (:type built-in)
:config
(setq ls-lisp-use-insert-directory-program nil
dired-listing-switches
@ -902,7 +657,7 @@ And [[https://www.masteringemacs.org/article/dired-shell-commands-find-xargs-rep
#+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 ~&~.
@ -916,7 +671,7 @@ The [[https://github.com/alexluigit/dirvish][dirvish]] project aims to make a pr
Im 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")
;; :vc (:url "https://github.com/alexluigit/dirvish")
:init (dirvish-override-dired-mode)
:custom
@ -1053,6 +808,28 @@ I do want to change a couple of bindings, as ~j~ to pull up a =completing-read=
(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
@ -1100,6 +877,8 @@ The [[Evil Collection][evil-collection]] package adds the following keybindings:
Id 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
:load-path "/User/howard.abrams/.emacs.d/repos/org-noter"
:after major-mode-hydra
:config
(major-mode-hydra-define org-noter-doc-mode-map nil
("Notes"
@ -1133,5 +912,4 @@ Let's provide a name so that the file can be required:
# Local Variables:
# eval: (add-hook 'after-save-hook #'org-babel-tangle t t)
# jinx-local-words: "Emacsians VSCode"
# End:

View file

@ -26,12 +26,12 @@ A literate programming file for helper apps in Emacs.
#+end_src
* Introduction
The following applications are not needed. I alternate between trying to /stay in Emacs/ taking advantage of the consistent interface, and using a stand-alone app on my Workday computer.
* Social Media Apps
* Federation
** Mastodon
Glad to see the 2FA feature is working on the [[https://codeberg.org/martianh/mastodon.el][mastodon.el]] project, and even more glad to see the great birdland diaspora arrive in the land of the toots.
#+begin_src emacs-lisp
(use-package mastodon
:straight (:host codeberg :repo "martianh/mastodon.el")
;; :vc (:url "https://codeberg.org/martianh/mastodon.el")
:init
(setq mastodon-instance-url "https://pdx.social"
mastodon-active-user "howard"))
@ -41,7 +41,7 @@ I would like a dedicate perspective to Mastodon, and I would like a leader key s
#+begin_src emacs-lisp
(use-package mastodon
:config
(ha-leader "a m" `("mastodon" . ,(lambda () (interactive) (ha-tab-bar-new "mastodon" #'mastodon))))
(ha-leader "a m" `("mastodon" . ,(ha-app-perspective "mastodon" #'mastodon)))
(defun ha-mastodon-scroll-or-more ()
"Scroll a window, and at the end, get more entries in timeline."
@ -53,35 +53,35 @@ I would like a dedicate perspective to Mastodon, and I would like a leader key s
(use-package major-mode-hydra
:config
(major-mode-hydra-define mastodon-mode (:quit-key "q")
(major-mode-hydra-define mastodon-mode nil
("Timelines"
(("u" mastodon-tl-update "update")
("F" mastodon-tl-get-federated-timeline "Federated")
("H" mastodon-tl-get-home-timeline "Home")
("L" mastodon-tl-get-local-timeline "Local")
("T" mastodon-tl-get-tag-timeline "Hashtag"))
(("u" mastodon-tl--update "update")
("F" mastodon-tl--get-federated-timeline "Federated")
("H" mastodon-tl--get-home-timeline "Home")
("L" mastodon-tl--get-local-timeline "Local")
("T" mastodon-tl--get-tag-timeline "Hashtag"))
"Specials"
(("M" mastodon-notifications--get-mentions "Mentions")
("N" mastodon-notifications-get "Notifications")
("A" mastodon-tl-followed-tags-timeline "All Tags")
("S" mastodon-profile-view-bookmarks "Saved bookmarks")
("O" mastodon-profile-my-profile "My Profile"))
("A" mastodon-tl--followed-tags-timeline "All Tags")
("S" mastodon-profile--view-bookmarks "Saved bookmarks")
("O" mastodon-profile--my-profile "My Profile"))
"Post"
(("c" mastodon-toot "Compose toot")
("e" mastodon-toot-edit-toot-at-point "Edit toot")
("t" mastodon-tl-thread "Read thread")
("r" mastodon-toot-reply "Reply")
("m" mastodon-tl-dm-user "Direct Msg")
("d" mastodon-toot-delete-toot "Delete"))
("e" mastodon-toot--edit-toot-at-point "Edit toot")
("t" mastodon-tl--thread "Read thread")
("r" mastodon-toot--reply "Reply")
("m" mastodon-tl--dm-user "Direct Msg")
("d" mastodon-toot--delete-toot "Delete"))
"Toot"
(("f" mastodon-toot-toggle-favourite "Favorite")
("b" mastodon-toot-toggle-boost "Boost")
("s" mastodon-toot-toggle-bookmark "Save")
("y" mastodon-toot-copy-toot-url "Copy URL")
("Y" mastodon-toot-copy-toot-text "Copy text"))
(("f" mastodon-toot--toggle-favourite "Favorite")
("b" mastodon-toot--toggle-boost "Boost")
("s" mastodon-toot--toggle-bookmark "Save")
("y" mastodon-toot--copy-toot-url "Copy URL")
("Y" mastodon-toot--copy-toot-text "Copy text"))
"Navigation"
(("n" mastodon-tl-next-tab-item "next" :color pink)
("p" mastodon-tl-previous-tab-item "previous" :color pink)
(("n" mastodon-tl--next-tab-item "next" :color pink)
("p" mastodon-tl--previous-tab-item "previous" :color pink)
("," ha-mastodon-scroll-or-more "...more" :color pink))))))
#+end_src
@ -95,8 +95,7 @@ Lets turn on non-fixed-width fonts to make everything easier to read:
Yet another encrypted chat/VoIP client-server, but unlike Signal and Telegram, [[matrix.org][Matrix]] is act ually open source. In other words, a project for nerds. Well be using Alphapapas latest [[https://github.com/alphapapa/ement.el][ement]] project.
#+begin_src emacs-lisp
(use-package ement
:straight (:host github :repo "alphapapa/ement.el")
:after major-mode-hydra
;; :vc (:url "https://github.com/alphapapa/ement.el")
:config
(major-mode-hydra-define ement-room-mode (:quit-key "q")
("Send"
@ -118,6 +117,7 @@ Yet another encrypted chat/VoIP client-server, but unlike Signal and Telegram, [
(ement-connect :user-id username
:password password
:uri-prefix "https://matrix.org"))
(ha-leader
"a x S" '("send" . ement-send-direct-message)
"a x s" '("send" . ement-room-send-message)
@ -143,10 +143,23 @@ Perhaps we can make it more attractive:
ement-room-timestamp-format ""
ement-room-send-message-filter 'ement-room-send-org-filter))
#+end_src
* Other
** Twitter
The venerable [[https://github.com/hayamiz/twittering-mode/tree/master][twittering-mode]] allows me to follow all the twits.
#+begin_src emacs-lisp :tangle no
(use-package twittering-mode
:init
(setq twittering-use-master-password t
epa-pinentry-mode 'loopback)
:config
(defalias 'epa--decode-coding-string 'decode-coding-string)
(ha-leader "a t" '("twitter" . twit)))
#+end_src
And we are no longer using this package.
** Telega
I'm thinking the [[https://zevlg.github.io/telega.el/][Telega package]] would be better than Bitlbee for Telegram communication.
#+begin_src emacs-lisp :tangle no
#+begin_src emacs-lisp
(use-package telega
:config
(setq telega-chat-show-avatars nil
@ -162,21 +175,21 @@ I'm thinking the [[https://zevlg.github.io/telega.el/][Telega package]] would be
(when (fboundp 'evil-insert-state)
(add-hook 'telega-chat-mode-hook 'evil-insert-state))
(ha-leader "a t" `("telega" . ,(lambda () (interactive) (ha-tab-bar-new "telega" #'telega)))))
(ha-leader "a t" `("telega" . ,(ha-app-perspective "telega" #'telega))))
#+end_src
For some reason, you need [[https://github.com/Fanael/rainbow-identifiers][rainbow-identifiers]] to work, oh, I guess the docs state this.
In the Telega chats, lets turn on non-fixed-width fonts:
#+begin_src emacs-lisp :tangle no
#+begin_src emacs-lisp
(use-package telega
:hook (telega-chat-mode . mixed-pitch-mode))
#+end_src
* RPG DM
Been working on my [[https://gitlab.com/howardabrams/emacs-rpgdm][RPG DM project]] for getting Emacs helping as a /Dungeon Master's Assistant/. The idea is to be able to roll dice and whatnot. What I find most useful is the [[https://gitlab.com/howardabrams/emacs-rpgdm/-/blob/main/rpgdm-tables.el][random tables]].
#+begin_src emacs-lisp :tangle no
#+begin_src emacs-lisp
(when (f-directory? "~/src/emacs-rpgdm")
(use-package rpgdm
:straight (:local-repo "~/src/emacs-rpgdm")
:load-path "~/src/emacs-rpgdm"
:commands (rpgdm-mode rpgdm-tables-load)
:init (setq rpgdm-base (expand-file-name "~/src/emacs-rpgdm"))
:config (ha-leader "t D" '("rpg dm" . rpgdm-mode))))
@ -186,7 +199,7 @@ Working on my new replacement of my DM code:
#+begin_src emacs-lisp
(when (f-directory? "~/src/emacs-rpgtk")
(use-package rpgtk
:straight (:local-repo "~/src/emacs-rpgtk")
:load-path "~/src/emacs-rpgtk"
:after hydra
;; :commands (rpgtk-mode rpgtk-tables-load rpgtk-dice rpgtk-message)
:custom
@ -204,7 +217,7 @@ And my [[https://gitlab.com/howardabrams/emacs-ironsworn][new Ironsworn project]
(when (f-directory? "~/src/emacs-ironsworn")
(use-package rpgdm-ironsworn
:after rpgdm
:straight (:local-repo "~/src/emacs-ironsworn")
:load-path "~/src/emacs-ironsworn"
:init
(setq rpgdm-ironsworn-project (expand-file-name "~/src/emacs-ironsworn")
;; Ignore org links that call my RPG functions:
@ -214,89 +227,17 @@ And my [[https://gitlab.com/howardabrams/emacs-ironsworn][new Ironsworn project]
#+end_src
The project is interesting, and I should record a tutorial how to use it.
* Playing Music
After building a NAS out of a Raspberry Pi to hold my /ripped/ CD collection, lets control my playlists from Emacs.
A more /rich/ experience than the [[https://github.com/isamert/empv.el][empv]] project is Álvaros [[https://github.com/xenodium/ready-player][ready-player]]. First install [[https://mpv.io/][mpv]]:
#+BEGIN_SRC sh
Why not? Lets see if the [[https://github.com/isamert/empv.el][empv]] project is sufficient. First install =mpv=, as in:
#+begin_src sh
brew install mpv
#+END_SRC
And make sure we can play it:
#+BEGIN_SRC sh
mpv "/Volumes/music/Steely Dan/Aja/01 Black Cow.mp3"
#+END_SRC
And stream my favorite local Jazz radio station:
#+BEGIN_SRC sh
mpv --terminal --force-window=no --no-resume-playback \
'https://ais-sa3.cdnstream1.com/2442_128.aac/playlist.m3u8' &
#+END_SRC
And then the configuration:
#+BEGIN_SRC emacs-lisp
(use-package ready-player
:straight (:type git :host github :repo "xenodium/ready-player")
:config
(when (eq system-type 'darwin)
(set-fontset-font t nil "SF Pro Display" nil 'append)
(ready-player-macos-use-sf-symbols))
(setq ready-player-my-media-collection-location "/Volumes/music")
(defun ha-ready-player-load-playlist ()
"Open a playlist in the correct location."
(interactive)
(let ((default-directory
(expand-file-name "Playlists/"
ready-player-my-media-collection-location)))
(setq ready-player-shuffle t)
(call-interactively 'ready-player-load-m3u-playlist)))
(transient-append-suffix 'ready-player-menu "?"
'("P" "Load Playlist" ha-ready-player-load-playlist))
(defun ha-ready-player-load-directory ()
"Open a directory in the correct location."
(interactive)
(let ((default-directory
(file-name-as-directory ready-player-my-media-collection-location)))
(setq ready-player-shuffle nil)
(call-interactively 'ready-player-load-directory)))
(transient-append-suffix 'ready-player-menu "d"
'("A" "Play Album" ha-ready-player-load-directory))
(ha-leader "a r" '("ready player" . ready-player-menu))
(ready-player-mode +1))
#+END_SRC
Since ready-player doesnt support [[https://github.com/xenodium/ready-player/issues/36][adjusting the volume]], I can, at least, set the /default volume/ low enough that my speaker adjustment affects it:
#+BEGIN_SRC conf :tangle ~/.config/mpv/mpv.conf
volume=60
#+END_SRC
Would love to have the /playing/ media in a particular tab-bar and in a particular location.
#+BEGIN_SRC emacs-lisp :tangle no
(defun ha-ready-player-switch ()
"Switch to the buffer window of the ready-player."
(interactive)
(let* ((buf-list
(seq-filter (lambda (buf)
(string-match (rx string-start "ready-player: "
(+ (not "*")) string-end)
(buffer-name buf)))
(buffer-list)))
(buffer (car buf-list))
(tab (tab-bar-get-buffer-tab (buffer-name buffer) t)))
(if tab
(tab-bar-switch-to-tab (alist-get 'name tab))
(tab-bar-switch-to-tab "main"))))
#+END_SRC
#+end_src
What else?
#+begin_src emacs-lisp
(use-package empv
;; :vc (:url "https://github.com/isamert/empv.el")
:general (ha-leader
"a p" '(empv-map :wk "play music")))
#+end_src
* Technical Artifacts :noexport:
Let's =provide= a name so we can =require= this file:

View file

@ -1,7 +1,7 @@
#+TITLE: Capturing Notes with Org
#+AUTHOR: Howard X. Abrams
#+DATE: 2020-09-18
#+TAGS: emacs org
#+title: Capturing Notes with Org
#+author: Howard X. Abrams
#+date: 2020-09-18
#+tags: emacs org
A literate programming file for configuring org for capturing notes.
@ -233,7 +233,7 @@ And for even quicker work, we can have special scripts tied to special keybindin
The [[https://gitlab.com/aimebertrand/org-mac-link][org-mac-link]] project makes it easy to tell Emacs to retrieve information from other apps, e.g. the URL of the opened tab in Firefox.
#+begin_src emacs-lisp
(use-package org-mac-link
:straight (:host gitlab :repo "aimebertrand/org-mac-link")
;; :vc (:url "https://gitlab.com/aimebertrand/org-mac-link")
:config
(ha-leader "i" '("insert app info" . org-mac-link-get-link)))
#+end_src
@ -244,7 +244,6 @@ Im use [[https://github.com/deseven/icanhazshortcut][ICanHazShortcut]] to hav
#+begin_src sh :shebang "#!/bin/bash" :tangle ~/bin/emacs-capture-clock
/usr/bin/osascript ~/bin/emacs-capture-clock.scr
#+end_src
But the following Applescript does the work:
#+begin_src applescript :sheband "#!/usr/bin/osascript" :tangle ~/bin/emacs-capture-clock.scr
tell application "System Events" to set theApp to name of first application process whose frontmost is true
@ -274,7 +273,6 @@ Now we have some goodies on the clipboard, and the script uses =emacsclient= to
(ignore-errors
(delete-frame)))
#+end_src
Oh, and it this is from the Terminal program, lets wrap it in a block:
#+begin_src emacs-lisp
(defun ha-external-capture-code-to-org ()
@ -290,10 +288,136 @@ Oh, and it this is from the Terminal program, lets wrap it in a block:
(ignore-errors
(delete-frame)))
#+end_src
#+begin_src conf :tangle ~/.config/iCanHazShortcut/config.ini
[main]
config version = 2
shell = /bin/bash -l
populate_menu_with_actions = yes
show_hotkeys_in_menu = yes
check_for_updates = yes
start_on_login = yes
show_icon_in_statusbar = yes
set_workdir_with_cd = no
window_x = -988
window_y = 172
window_width = 600
window_height = 361
shortcut_column_enabled = yes
action_column_enabled = yes
command_column_enabled = yes
workdir_column_enabled = no
shortcut_column_width = 80
action_column_width = 160
command_column_width = 173
workdir_column_width = 100
[shortcut1]
shortcut = ⇧⌃⌥⌘E
action = Personal Emacs
command = open -a Emacs
workdir =
enabled = yes
[shortcut2]
shortcut = ⇧⌃⌥E
action = Work Emacs
command = FOR_WORK=yes open -a Emacs-Work
workdir =
enabled = yes
[shortcut3]
shortcut = ⇧⌃⌥⌘X
action = Emacs Capture
command = ~/bin/emacs-capture
workdir =
enabled = yes
[shortcut4]
shortcut = ⇧⌃⌥X
action = Emacs Capture Clipboard
command = ~/bin/emacs-capture-clock
workdir =
enabled = yes
[shortcut5]
shortcut = ⇧⌃⌥T
action = iTerm
command = open -a iTerm
workdir =
enabled = yes
[shortcut6]
shortcut = ⇧⌃⌥S
action = Slack
command = open -a Slack
workdir =
enabled = yes
[shortcut7]
shortcut = ⇧⌃⌥W
action = Spotify
command = open -a Spotify
workdir =
enabled = yes
[shortcut8]
shortcut = ⇧⌃⌥F
action = Firefox
command = open -a Firefox
workdir =
enabled = yes
[shortcut9]
shortcut = ⇧⌃⌥C
action = Chome
command = ~/bin/chrome.scr
workdir =
enabled = yes
[shortcut10]
shortcut = ⇧⌃⌥Q
action = Keepass
command = open -a KeepassXC
workdir =
enabled = yes
[shortcut11]
shortcut = ⇧⌃⌥Z
action = Zoom
command = open -a zoom.us
workdir =
enabled = yes
[shortcut12]
shortcut = ⌃F1
action = Mute Zoom
command = ~/bin/zoom-muter
workdir =
enabled = yes
[shortcut13]
shortcut = ⇧⌃⌥⌘M
action = Capture Meeting
command = ~/bin/emacs-capture-meeting
workdir =
enabled = yes
[shortcut14]
shortcut = ⇧⌃⌥⌘B
action = Outlook
command = open -a "Microsoft Outlook"
workdir =
enabled = yes
[shortcut15]
shortcut = ⇧⌃⌥⌘D
action = Discord
command = open -a Discord
workdir =
enabled = yes
#+end_src
Configure the *ICanHazShortcut* shortcuts to call these scripts, as in this screenshot:
[[file:screenshots/icanhazshortcuts.png]]
And here is the configuration file for that:
#+begin_src conf :tangle ~/.config/iCanHazShortcut/config.ini :mkdirp yes
[main]

View file

@ -1,354 +0,0 @@
#+TITLE: Taking Notes with Org
#+AUTHOR: Howard X. Abrams
#+DATE: 2026-07-14
#+FILETAGS: emacs hamacs
#+LASTMOD: [2026-07-14 Tue]
A literate programming file for configuring Emacs to take notes.
#+begin_src emacs-lisp :exports none
;;; ha-code-notes --- configuring Emacs to take notes. -*- lexical-binding: t; -*-
;;
;; © 2026 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
;; Created: July 14, 2026
;;
;; While obvious, GNU Emacs does not include this file or project.
;;
;; *NB:* Do not edit this file. Instead, edit the original literate file at:
;; /Users/howard.abrams/src/hamacs/ha-code-notes.org
;; And tangle the file to recreate this one.
;;
;;; Code:
#+end_src
* Introduction
Seems silly to have a full set of instructions for /taking notes/ using Org-mode, because that is what it does, but this is a bit special.
When I [[file:ha-capturing-notes.org][capture notes]] with a link to some source code, the /destination/ is a *clocked in* task. Useful, but seems to require a dedicated task and some forethought. The following project pairs a “source code file” (which can be any file, actually) and a “notes file” with headers based on the function in the original file.
#+begin_src dot :file ha-code-notes-illustration.png :exports file :results file
digraph G {
rankdir=LR;
// Make to icons side-by-side
code_file [shape=note, label="", fixedsize=true, width=1, height=1.5];
note_file [shape=note, label="", fixedsize=true, width=1, height=1.5];
// Create a couple of label to live _below_ the icons:
code_label [shape=plaintext, label="foobar.py"];
note_label [shape=plaintext, label=".foobar-note.org"];
{ rank=same; code_file; code_label; }
{ rank=same; note_file; note_label; }
code_file -> code_label [style=invis];
note_file -> note_label [style=invis];
// Connect the code and note icons:
code_file -> note_file;
}
#+end_src
#+attr_org: :width 800px
[[file:ha-code-notes-illustration.png]]
This allows me to /wax poetic/ with a parallel, but separate org file.
* Helper Functions
While this project started simple, Ive expanded the ideas, and this requires some custom helper functions. For instance, I want to add a buffer-level /property/ to an org-mode file, for instance:
#+begin_example
#+TITLE: The title of the file
#+PROPERTY: key1 value1
#+end_example
The tricky bit about this function is that if (in the above example) =key1= exists, we should /replace/ it, not add to it.
#+BEGIN_SRC emacs-lisp
(defun ha-org-set-buffer-property (key value)
"Set a buffer-level property at the top of an Org file.
If KEY already refers to a #+PROPERTY, replace it.
Otherwise, insert it at the end of the Org header lines."
(let ((property-line (format "#+PROPERTY: %s %s" key value))
(magic-re (rx (group line-start "#+PROPERTY:"
space (literal key) space
(zero-or-more any)
line-end))))
(defun process-line ()
"Helper function returns non-nil if more to process.
This works because `replace-match' and `insert' return nil,
while `forward-line' returns a non-nil value."
(cond
((looking-at magic-re) (replace-match property-line))
((looking-at "#") (forward-line))
(t (insert property-line ?\n))))
(save-excursion
(goto-char (point-min))
(while (process-line)))))
#+END_SRC
And we need the ability to read it. The Org API doesnt have a function to read it without parsing the entire structure, so we use the =org-collect-keywords= for the =PROPERTY= value, and then parse the results.
#+BEGIN_SRC emacs-lisp
(defun ha-org-get-buffer-property (key)
"Get the value of a buffer-level #+PROPERTY matching KEY."
(let* ((properties (thread-last "PROPERTY"
(list) ; Requires a list of keywords
(org-collect-keywords)
(car))) ; A list of lists? Get first entry
;; Each entry in properties is a string with the key and value:
(entry (seq-find (lambda (k) (s-starts-with? key k)) properties)))
(substring entry (1+ (s-index-of " " entry)))))
#+END_SRC
They work like:
#+BEGIN_SRC emacs-lisp :tangle no
(ha-org-set-buffer-property "foor" "bar")
(ha-org-get-buffer-property "foo") ; ⟹ "bar"
#+END_SRC
IMenu has a /mode-agnostic/ approach to jumping to /sections/. This could work whether the file is an formatted in org-mode, markdown, or even source code. This function allows me to jump to a particular header in either Org, Markdown, or other files that define the =defun= interface differently.
#+BEGIN_SRC emacs-lisp
(defun imenu-goto (header)
"Jump to the first `imenu' entry whose name contains HEADER.
Like `consult-imenu', this matches HEADER as a case-insensitive
substring rather than requiring an exact name, and jumps straight
to the first hit instead of prompting.
Note that this won't work if IMenu is stale and requires refreshing."
;; Overshadowing imenu's lookup function seems overly sneaky!
(let ((imenu-name-lookup-function
(lambda (str name)
(let ((case-fold-search t))
(string-match-p (regexp-quote str) name)))))
;; Calling `imenu' programmatically is a pain!
(when-let ((item (imenu--in-alist header (imenu--make-index-alist))))
(imenu item))))
#+END_SRC
* Code Notes
This function defines what the notes filename should look like, loads it in a side window, and adds a /back reference/ in the form of an org-mode =property=. Next it needs to either find the section that matches the function in the code we are writing about, or jumps to the bottom and creates it.
#+BEGIN_SRC emacs-lisp
(defun ha-code-notes ()
"Open an Org file based on the current opened file.
The pattern for choosing the name of the org file is:
foobar.py --> .foobar-notes.org
The Org header will be the name of the function in the original source
code file. This means you have one note section per function, which
should be fine in practice because functions are small and succinct,
right?"
(interactive)
(let* ((orig-file (buffer-file-name))
(line-num (line-number-at-pos))
(orig-parent (file-name-directory orig-file))
(orig-base (file-name-base orig-file))
;; Keep in mind the `orig-parent' has a final slash, so the
;; initial . here marks it as hidden:
(note-file (format "%s.%s-notes.org"
orig-parent orig-base))
(header (which-function)))
;; With the above local variables defined, we can open the file in
;; a window (pane) to the side:
(find-file-other-window note-file)
(ha-org-set-buffer-property "XREF" orig-file)
(goto-char (point-min)) ; jump to start of file
;; The `condition-case' is Elisp's way of a try..catch:
(condition-case nil
;; Find the first Org header that matches `header':
(re-search-forward (rx line-start
(one-or-more "*")
(one-or-more space)
(optional (or "=" "~"))
(literal header)
(optional (or "=" "~"))))
(error
;; We didn't find a header matching the function, so we jump to
;; the end of the file and create a new heading:
(goto-char (point-max))
(org-insert-heading)
(insert (format "%s" header))
(org-insert-property-drawer)
(org-set-property "XREF_LINE" (number-to-string line-num))
;; In case we want to change the section header name, we store
;; the name of the function that led us here as a property:
(org-set-property "XREF_NAME" header)
(when-let ((buf (find-buffer-visiting orig-file)))
(with-current-buffer buf
(ha-code--fringe-notes)))))
;; We are somewhere in the file, so go to the end of the block:
(org-end-of-subtree)
;; If the subtree ends mid-line, insert a newline:
(unless (eq (line-beginning-position) (point))
(end-of-line)
(insert "\n"))))
#+END_SRC
Use the builtin autoinsert feature to inject a basic template at the beginning of the notes file when we first create the notes file:
#+BEGIN_SRC emacs-lisp
(use-package autoinsert
:config
(define-auto-insert
(cons (rx "/." (one-or-more (not "/")) "-notes.org" string-end) "Org Notes Template")
'("Short description: "
"#+TITLE: "
(s-titleized-words (s-replace-regexp (rx (any "-" "_")) " "
(file-name-base (buffer-file-name))))
\n
"#+DATE:" (format-time-string "%Y-%m-%d %a")
\n
"#+LASTMOD:" (format-time-string "[%Y-%m-%d %a]")
\n \n)))
#+END_SRC
If we are /inside/ one of these note files, lets have a quick way to return back to the original “code” file:
#+BEGIN_SRC emacs-lisp
(defun ha-code-notes-return ()
"Return to the code referenced in the notes.
Essentially pretends we have a backlink without a database."
(interactive)
(let* ((filename (ha-org-get-buffer-property "XREF"))
(line-num (car (org-property-values "XREF_LINE")))
(function (or (car (org-property-values "XREF_NAME")) (which-function))))
;; If the property is set, load that file (which jumps to the
;; buffer if it is displayed), otherwise, we assume the previous
;; buffer contains it:
(if filename
(find-file-other-window filename)
(switch-to-prev-buffer))
(if line-num
(goto-line (string-to-number line-num)))
;; If the line number got out of sync so that the point is no
;; longer in the correct function, use `find-function' to
;; reposition the point:
(unless (equal (which-function) function)
(if (derived-mode-p 'prog-mode)
(xref-find-definitions function)
(imenu-goto function)))))
#+END_SRC
And give us keybinding that will either go to the notes (if we are in some code) or return to the source code (if we are in our notes):
#+BEGIN_SRC emacs-lisp
(defun ha-code-notes-dwim ()
"Open the notes buffer, or return to the code."
(interactive)
(when (buffer-file-name)
(if (string-match (rx "/." ; A hidden file
(one-or-more (not "/"))
"-notes.org" string-end)
(buffer-file-name))
(ha-code-notes-return)
(ha-code-notes))))
(ha-leader "n c" '("code notes" . ha-code-notes-dwim))
#+END_SRC
** Fringe Indicators for Notes
A code file with an associated notes file is easy to forget about. Let's mark, in the fringe, every line that has a note, so we notice it as we scroll past:
#+BEGIN_SRC emacs-lisp
(define-fringe-bitmap 'ha-code-notes-bitmap
[#b00001100
#b00010110
#b00010111
#b00101110
#b00101110
#b01011100
#b01011100
#b10110000
#b10010000
#b11100000]
nil nil 'center)
(defface ha-code-notes-face
'((t :foreground "yellow"))
"Face for the fringe marker indicating a line has an associated note.")
(defvar-local ha-code-notes-fringe-overlays nil
"Overlays marking lines in this buffer that have notes in the paired notes file.")
(defun ha-code-notes--fringe ()
"Mark, in the fringe, every line in this buffer that has a note.
Notes live in the paired `.BASE-notes.org' file (see
`ha-code-notes') as headings whose XREF/XREF_LINE
properties point back to a line in this file."
(mapc #'delete-overlay ha-code-notes--fringe-overlays)
(setq ha-code--notes--fringe-overlays nil)
(let* ((orig-file (buffer-file-name))
(note-file (and orig-file
(format "%s.%s-notes.org"
(file-name-directory orig-file)
(file-name-base orig-file))))
lines)
(when (and note-file (file-exists-p note-file))
(with-temp-buffer
(insert-file-contents note-file)
(org-mode)
(org-map-entries
(lambda ()
(when (equal (ha-org-get-buffer-property "XREF") orig-file)
(push (string-to-number (org-entry-get nil "XREF_LINE")) lines)))))
(dolist (line lines)
(save-excursion
(goto-char (point-min))
(forward-line (1- line))
(let ((ov (make-overlay (point) (point))))
(overlay-put ov 'before-string
(propertize "x" 'display
'(left-fringe ha-code-notes-bitmap
ha-code-notes-face)))
(push ov ha-code-notes--fringe-overlays)))))))
(add-hook 'find-file-hook #'ha-code-notes--fringe)
#+END_SRC
New notes and edited notes should refresh the markers too, so we hook into saving a notes file, and refresh right after inserting a new XREF property drawer:
#+BEGIN_SRC emacs-lisp
(defun ha-code-notes--fringe-notes-refresh-all ()
"Refresh fringe note markers in every buffer after saving a notes file."
(when (string-match (rx "-notes.org" string-end) (buffer-file-name))
(dolist (buf (buffer-list))
(with-current-buffer buf
(when buffer-file-name
(ha-code--fringe-notes))))))
(add-hook 'after-save-hook #'ha-code-notes--fringe-notes-refresh-all)
#+END_SRC
* Technical Artifacts :noexport:
Let's =provide= a name so we can =require= this file:
#+begin_src emacs-lisp :exports none
(provide 'ha-code-notes)
;;; ha-code-notes.el ends here
#+end_src
#+DESCRIPTION: configuring Emacs to take notes.
#+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:nil 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

View file

@ -146,7 +146,8 @@ After reading [[https://irreal.org/blog/?p=12139][Jon Sanders essay]] as well
#+begin_src emacs-lisp
(use-package emacs-everywhere
:straight (:host github :repo "tecosaur/emacs-everywhere"))
;; :vc (:url "https://github.com/tecosaur/emacs-everywhere")
)
#+end_src
This package /called outside of Emacs/, so I bound a keybinding to iCanHazShortcut:
@ -164,20 +165,16 @@ Ive often called =imenu= to easily jump to a function definition in a file (o
#+begin_src emacs-lisp
(defun ha-imenu-setup ()
"Set up the imenu customization. Use in hooks."
(condition-case err
(imenu-add-menubar-index)
(ignore-errors
(imenu-add-menubar-index)
(setq-local imenu-auto-rescan t)
(when (derived-mode-p 'prog-mode)
(setq-local imenu-sort-function 'imenu--sort-by-name))
(imenu-unavailable
(let ((inhibit-message t))
(message "Warning: %s" (error-message-string err))))))
(setq-local imenu-sort-function 'imenu--sort-by-name))))
(add-hook 'org-mode-hook 'ha-imenu-setup)
(add-hook 'markdown-mode-hook 'ha-imenu-setup)
(add-hook 'makefile-mode-hook 'ha-imenu-setup)
(add-hook 'prog-mode-hook 'ha-imenu-setup)
(add-hook 'makefile-mode-hook 'ha-imenu-setup)
#+end_src
** File Access
*** Remote Files
@ -229,7 +226,6 @@ When I push changes to my files to Gerrit and other code review, I dont want
The [[https://www.emacswiki.org/emacs/RecentFiles][recentf]] feature has been in Emacs for a long time, but it has a problem with Tramp, as we need to turn off the cleanup feature that attempts to =stat= all the files and remove them from the =recent= accessed list if they are readable. The requires recentf to open up a remote files which blocks Emacs at the most inopportune times… like when trying to reboot the machine.
#+begin_src emacs-lisp
(use-package recentf
:straight (:type built-in)
:config
(setq recentf-auto-cleanup 'never) ;; disable before we start recentf!
(recentf-mode 1))
@ -340,10 +336,13 @@ The [[https://github.com/minad/vertico][vertico]] package puts the completing re
(use-package vertico
:config (vertico-mode))
#+end_src
My issue with Vertico is when calling =find-file=, the Return key opens =dired=, instead of inserting the directory at point. This package addresses this:
#+begin_src emacs-lisp
#+begin_src emacs-lisp :tangle no
(use-package vertico-directory
:straight (el-patch :files ("~/.emacs.d/straight/repos/vertico/extensions/vertico-directory.el"))
:load-path "~/.emacs.d/repos/vertigo/extensions"
;; :straight (:files ("~/.emacs.d/straight/repos/vertico/extensions/vertico-directory.el"))
;; More convenient directory navigation commands
:bind (:map vertico-map
("RET" . vertico-directory-enter)
@ -384,7 +383,7 @@ How does it compare? Once upon a time, I enjoyed typing ~plp~ for =package-list-
#+begin_src emacs-lisp
(use-package fussy
;; :straight (:host github :repo "jojojames/fussy")
;; ;; :vc (:url "https://github.com/jojojames/fussy")
:config
(push 'fussy completion-styles)
(setq completion-category-defaults nil
@ -575,7 +574,7 @@ The [[https://github.com/minad/cape][Cape project]] deliver particular [[help:co
#+BEGIN_SRC emacs-lisp :tangle no
(use-package cape
:straight (:host github :repo "minad/cape")
;; :vc (:url "https://github.com/minad/cape")
:init
(setq completion-at-point-functions (list #'cape-emoji)))
#+END_SRC
@ -589,50 +588,20 @@ Each programming environment might need some particular love. For instance:
#+END_SRC
*** Company
The [[http://company-mode.github.io/][company project]] for completion back-ends work well with LSP.
Do I want it to display whenever there is a pause in the conversation (set =company-minimum-prefix-length= to a large number, or set =company-idle-delay=), or do I want it to only show when I push the =TAB= or other key (see =company-indent-or-complete-common=).
#+BEGIN_SRC emacs-lisp
(use-package company
:custom
(company-minimum-prefix-length 3) ; default
(company-idle-delay 0.5)
(company-tooltip-align-annotations t)
(company-tooltip-limit 9)
(company-tooltip-flip-when-above t)
(company-show-quick-access 'left)
:bind (("M-/" . company-complete)
:map company-mode-map
("M-/" . company-other-backend))
:config
(add-to-list 'company-backends 'company-yasnippet)
(set-face-attribute 'company-tooltip nil
:family "Cascadia Code NF"
:height 120)
:hook (after-init . global-company-mode))
#+END_SRC
Another idea, is I can trigger the company with a M-/ but then view /other backends/ by re-hitting that keybinding.
*** Corfu
The default completion system either inserts the first option directly in the text (without cycling, so lets hope it gets it right the first time), or presents choices in another buffer (who wants to hop to it to select an expansion).
After using [[http://company-mode.github.io/][company]] for my completion back-end, I switched to [[https://github.com/minad/corfu][corfu]] as it works with the variable-spaced font of my org files (also see [[https://takeonrules.com/2022/01/17/switching-from-company-to-corfu-for-emacs-completion/][this essay]] for my initial motivation).
#+begin_src emacs-lisp :tangle no
#+begin_src emacs-lisp
(use-package corfu
;; :vc (:url "https://github.com/minad/corfu")
:custom
(corfu-cycle t)
(corfu-separator ?\s)
:init
:config
(global-corfu-mode))
#+end_src
*** Snippets
Using [[https://github.com/joaotavora/yasnippet][yasnippet]] to expand templates into text:
@ -708,7 +677,7 @@ As I've mentioned [[http://www.howardism.org/Technical/Emacs/beep-for-emacs.html
'libnotify)))
(use-package beep
:straight (:local-repo "~/src/hamacs/elisp")
:load-path "~/src/hamacs/elisp"
:hook (after-init . ha-random-startup-message)
:commands (beep-when-finished beep-when-run-too-long)
:config
@ -725,7 +694,7 @@ As I've mentioned [[http://www.howardism.org/Technical/Emacs/beep-for-emacs.html
"Yippee"
"What's next?"
"Notification complete. Happy?"
"Beware, lest the dragons fly"))))
"Beware lest the dragons fly"))))
(beep-when-finished msg)))
#+end_src
@ -809,37 +778,6 @@ If you hit the following keys /before/ you select a target, you get special acti
- ~z~ :: =zap-to-char= … kill from current point to the target
Im not thinking of ideas of what would be useful, e.g. ~v~ to highlight from cursor to target, etc.
What is missing is copying the entire line:
#+BEGIN_SRC emacs-lisp
(use-package avy
:config
(defun avy-action-copy-whole-line (pt)
(save-excursion
(goto-char pt)
(cl-destructuring-bind (start . end)
(bounds-of-thing-at-point 'line)
(copy-region-as-kill start end)))
(select-window
(cdr
(ring-ref avy-ring 0)))
t)
(defun avy-action-copy-rest-line (pt)
(save-excursion
(goto-char pt)
(let ((start pt)
(end (line-end-position)))
(copy-region-as-kill start end)))
(select-window
(cdr
(ring-ref avy-ring 0)))
t)
(setf (alist-get ?w avy-dispatch-alist) 'avy-action-copy
(alist-get ?W avy-dispatch-alist) 'avy-action-copy-whole-line
(alist-get ?N avy-dispatch-alist) 'avy-action-copy-rest-line))
#+END_SRC
Want to know something amazing. In a Terminal, like =vterm= or =eshell=, I run ~s-g~ and pinpoint the UUID in the output of a long command. Then type ~y~ and then ~C-y~ to paste that ID without even moving the mouse.
*** Link Hint, the Link Jumper
The [[info:emacs#Goto Address mode][Goto Address]] mode (see this [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Goto-Address-mode.html][online link]]) turns URLs into clickable links. Nice feature and built into Emacs, but it requires using the mouse or moving to the URL and hitting ~Return~ (if you like this idea, check out [[https://xenodium.com/actionable-urls-in-emacs-buffers/][Álvaro Ramírez's configuration]] for this).
@ -957,6 +895,16 @@ I like the idea of dropping returnable bookmarks, but with /good defaults/ for t
(bookmark-set label)))
#+END_SRC
The built-in behavior doesnt honor either /projects/ or /perspectives/, but I use [[https://codeberg.org/ideasman42/emacs-bookmark-in-project][bookmark-in-project]] package to make a =project=-specific bookmarks and use that to jump to only bookmarks in the current project.
#+BEGIN_SRC emacs-lisp
(use-package bookmark-in-project
:bind
(("C-x r m" . bookmark-in-project-toggle)
("C-x r M" . ha-bookmark-set)))
#+END_SRC
** Minor Keybinding Annoys
I like ~C-a~ to go to the beginning of the line, but what about getting to the beginning of text on that line? In Evil, you have ~^~ for beginning of line, and ~0~ for first text. Why not have ~C-a~ toggle between them both:
@ -981,13 +929,15 @@ Since I wasnt using all the features that [[https://github.com/bbatsov/projec
:config
(ha-leader
"p" '(:ignore t :which-key "projects")
"p p" '("switch project" . ha-tab-bar-new-project)
"p W" '("initialize workspace" . ha-workspace-initialize)
"p n" '("new project space" . ha-project-persp)
"p !" '("run cmd in project root" . project-shell-command)
"p &" '("run cmd async" . project-async-shell-command)
"p a" '("add new project" . project-remember-projects-under)
"p d" '("dired" . project-dired)
"p k" '("kill project buffers" . project-kill-buffers)
"p p" '("switch project" . project-switch-project)
"p x" '("remove known project" . project-forget-project)
"p f" '("find file" . project-find-file)
@ -1001,246 +951,204 @@ Since I wasnt using all the features that [[https://github.com/bbatsov/projec
"p s" '("project shell" . project-shell)))
#+end_src
** Workspaces
A /workspace/ (at least to me) requires a quick jump to a collection of buffer windows organized around a project or task. Later versions of Emacs use [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Tab-Bars.html][Tab Bars]] which group windows and buffers in a perspective. The code that follows is a Poor Persons Workspace package. Also lets dive into the end section of [[https://www.masteringemacs.org/article/demystifying-emacs-window-manager][Mickey Petersen's essay]] on the subject.
A /workspace/ (at least to me) requires a quick jump to a collection of buffer windows organized around a project or task. For this, I'm basing my work on the [[https://github.com/nex3/perspective-el][perspective.el]] project.
Couple notes:
- Function, =tab-bar-switch-to-tab=, switches or /creates/ a tab. We will always use this.
- We can switch to a tab by number with =tab-bar-select-tab=
#+BEGIN_SRC emacs-lisp
(setq tab-bar-show 1 ; hide bar if <= 1 tabs open
tab-bar-close-button-show nil ; hide tab close / X button
tab-bar-new-tab-choice "*dashboard*" ; buffer to show in new tabs
tab-bar-tab-hints t ; show tab numbers
tab-bar-separator " "
tab-bar-button-relief 0
;; Jump to a tab by numbers (see the keybindings set later):
tab-bar-select-tab-modifiers '(super control))
#+END_SRC
Ive struggled to /programmatically/ create sane workspaces, so lets just save them off:
#+BEGIN_SRC emacs-lisp
(desktop-save-mode 1)
#+END_SRC
New workspace is a tab with a specific name that opens up a specific buffer or application. My motive for such a complicated function allows me to pre-create tabs with already running applications or files.
I build a Hydra to dynamically list the current projects as well as select the project.
To do this, we need a way to generate a string of the perspectives in alphabetical order:
#+begin_src emacs-lisp
(defun ha-tab-bar-new (name &optional bff)
"Create a new tab with a NAME.
With a non-nil BFF, call BFF as a function or switch
to the BFF buffer or the files listed."
(interactive "sWorkspace Name: ")
(tab-bar-switch-to-tab name)
(when bff
(cond
((listp bff) (find-file (car bff))
(dolist (f (cdr bff))
(split-window-right)
(find-file f)))
((fboundp bff) (call-interactively bff))
((bufferp bff) (switch-to-buffer bff)))))
#+end_src
Create a new tab associated with a project:
#+begin_src emacs-lisp
(defun ha-tab-bar-new-project (project-dir)
"Create a new tab/workspace based on a project.
The project is defined by the PROJECT-DIR directory."
(interactive (list (completing-read "Project: " (project-known-project-roots))))
(let ((name (project-name (project-current nil project-dir)))
(default-directory project-dir))
(ha-tab-bar-new name)
(project-switch-project project-dir)))
#+end_src
If we close a tab that is a project, we want to close all the buffers associated with it. I wouldnt do this if it wasnt so easy to re-create them:
#+begin_src emacs-lisp
(defun ha-tab-bar-delete (tab-name)
"Delete a tab, TAB-NAME, and all buffers associated with it."
(interactive
(list (completing-read "Close tab by name: "
(mapcar (lambda (tab)
(alist-get 'name tab))
(funcall tab-bar-tabs-function)))))
(dolist (buf (ha-tab-bar-buffers tab-name))
(kill-buffer buf))
(tab-bar-close-tab-by-name tab-name))
(defun ha-tab-bar-buffers (tab-name)
"Return list of buffers associated with TAB-NAME."
(seq-filter (lambda (b)
(thread-last b
(tab-bar-get-buffer-tab)
(alist-get 'name)
(string-equal tab-name)))
(buffer-list)))
#+end_src
And lets make one tab to be /special/:
#+BEGIN_SRC emacs-lisp
(defun ha-switch-to-special ()
"Change the perspective to the Sprint/Notes project."
(interactive)
(tab-bar-switch-to-tab "main"))
#+END_SRC
And some shortcut keys from the =general= project:
#+BEGIN_SRC emacs-lisp
(use-package emacs
:config
(general-nmap :prefix "SPC"
"<tab>" '(:ignore t :which-key "workspaces")
"<tab> <tab>" '("switch" . tab-switch)
"<tab> p" '("new project" . ha-tab-bar-new-project)
"<tab> n" '("new space" . ha-tab-bar-new)
"<tab> u" '("update names" . ha-tab-bar-update-names)
"<tab> d" '("delete space" . ha-tab-bar-delete)
"<tab> t" '("toggle tab-bar" . tab-bar-mode)
"<tab> w" '("special space" . ha-switch-to-special)
"<tab> `" '("recent" . tab-bar-switch-to-recent-tab))
(global-set-key (kbd "s-C-t") 'ha-tab-bar-new)
(global-set-key (kbd "s-C-[") 'tab-bar-switch-to-prev-tab)
(global-set-key (kbd "s-C-]") 'tab-bar-switch-to-next-tab)
(tab-bar-mode 1))
#+END_SRC
I want to quickly jump, by the number shown on the tab, to that grouping. The following two functions create leader sequences with the name of the tab group:
#+BEGIN_SRC emacs-lisp
(defun ha-tab-bar-update-names (&optional changed-tab)
"Create normal-mode keybindings for the tab groupings.
This creates `SPC TAB 1' to jump to the first tab, etc."
(interactive)
;; Remove all previously created keybindings:
(ignore-errors
(dolist (indx (number-sequence 0 9))
(general-nmap :prefix "SPC" (format "<tab> %d" indx) nil)))
;; Loop through the existing tabs, create keys for each:
(seq-do-indexed 'ha-tab-bar-update-tab-keybinding (tab-bar-tabs)))
(defun ha-tab-bar-update-tab-keybinding (tab-deets indx)
"Create a keybinding to jump to tab described by TAB-DEETS.
The key sequence, `SPC' `TAB' then INDX."
(when (< indx 10)
(let ((name (alist-get 'name tab-deets)))
(general-nmap :prefix "SPC"
;; As indx is starts with 0, we need to create keybindings to
;; match the tab labels by incrementing it by one ... unless,
;; we are at 10, where we use 0 instead:
(format "<tab> %d" (if (= indx 9) 0 (1+ indx)))
`(,name . (lambda () (interactive)
"Switch to a tab-bar by number"
(setq ha-tab-bar-previous
(tab-bar-tab-name-current))
(tab-bar-select-tab ,(1+ indx))))))))
#+END_SRC
Any time I create or delete a new tab, we can call =ha-tab-bar-update-names=:
#+BEGIN_SRC emacs-lisp
(advice-add #'tab-bar-new-tab :after #'ha-tab-bar-update-names)
(advice-add #'tab-bar-close-tab :after #'ha-tab-bar-update-names)
(advice-add #'tab-bar-close-other-tabs :after #'ha-tab-bar-update-names)
(add-hook #'desktop-after-read-hook #'ha-tab-bar-update-names)
#+END_SRC
*** Add Numbers ot Tab Bar Mode
Christian Tietze had a [[https://christiantietze.de/posts/2022/02/emacs-tab-bar-numbered-tabs/][great idea]] for making the tab-bar numbers more distinguished from the labels on the tabs (he has a [[https://christiantietze.de/posts/2022/12/sf-symbols-emacs-tab-numbers/][follow up essay]] about using a different font, but since I need the portability of Unicode for Linux, I may need to synthesize both).
#+BEGIN_SRC emacs-lisp
(defface ha/tab-bar-numbers
(if (ha-running-on-macos?)
'((t
:inherit tab-bar-tab-face
:family "SF Compact"
:weight light))
;; On Linux, we use the dingbat unicode with default font:
'((t
:inherit tab-bar-tab-face
:weight light)))
"Face for tab numbers in both active and inactive tabs.")
#+END_SRC
First, create a variable that contains the Unicode values for the numbers. I am using the negative versions of the circle numbers from the Unicode dingbats collection:
#+BEGIN_SRC emacs-lisp
(defvar ha/circle-numbers-alist
(if (ha-running-on-macos?)
'((0 . "􀃈")
(1 . "􀃊")
(2 . "􀃌")
(3 . "􀃎")
(4 . "􀘙")
(5 . "􀃒")
(6 . "􀑵")
(7 . "􀃖")
(8 . "􀃘")
(9 . "􀑷")
(10 . "􀃈"))
;; Keeping these around for the Mac just in case.
;; '((0 . "⓿")
;; (1 . "❶")
;; (2 . "❷")
;; (3 . "❸")
;; (4 . "❹")
;; (5 . "❺")
;; (6 . "❻")
;; (7 . "❼")
;; (8 . "❽")
;; (9 . "❾"))
'((0 . "🄌")
(1 . "➊")
(2 . "➋")
(3 . "➌")
(4 . "➍")
(5 . "➎")
(6 . "➏")
(7 . "➐")
(8 . "➑")
(9 . "➒")))
"Alist of integers to strings of circled unicode numbers.")
#+END_SRC
And then use this function to replace the standard =tab-bar-tab-name-format-function=:
#+BEGIN_SRC emacs-lisp
(defun ha/tab-bar-tab-name-format-default (tab i)
"Replacement for `tab-bar-tab-name-format-function'.
Places a special symbol for the initial digit."
(let ((current-p (eq (car tab) 'current-tab)))
(defun ha--persp-label (num names)
"Return string of numbered elements.
NUM is the starting number and NAMES is a list of strings."
(when names
(concat
;; First, add the tab number with a custom face
(propertize
(if (and tab-bar-tab-hints (< i 10)) (alist-get i ha/circle-numbers-alist) "")
'face 'ha/tab-bar-numbers)
;; Fixed-width gap so active (variable-pitch) and inactive tabs match
(propertize " " 'face 'tab-bar)
;; Add tab name with the face returned by tab-bar-tab-face-function
(propertize
(concat
" "
(alist-get 'name tab)
(or (and tab-bar-close-button-show
(not (eq tab-bar-close-button-show
(if current-p 'non-selected 'selected)))
tab-bar-close-button)
"")
" ")
'face (funcall tab-bar-tab-face-function tab)))))
(format " %d: %s%s" ; Shame that the following doesn't work:
num ; (propertize (number-to-string num) :foreground "#00a0")
(car names) ; Nor does surrounding the number with underbars.
(setq tab-bar-tab-name-format-function
#'ha/tab-bar-tab-name-format-default)
#+END_SRC
(if (equal (car names) (persp-name (persp-curr))) "*" ""))
(ha--persp-label (1+ num) (cdr names)))))
(defun ha-persp-labels ()
"Return a string of numbered elements from a list of names."
(ha--persp-label 1 (sort (hash-table-keys (perspectives-hash)) 's-less?)))
#+end_src
Build the hydra as well as configure the =perspective= project.
#+begin_src emacs-lisp
(use-package perspective
:custom
(persp-modestring-short t)
(persp-show-modestring t)
:config
(setq persp-suppress-no-prefix-key-warning t)
(persp-mode)
(defhydra hydra-workspace-leader (:color blue :hint nil) "
Workspaces- %s(ha-persp-labels)
_n_: new project _r_: rename _a_: add buffer _l_: load worksp
_]_: next worksp _d_: delete _b_: goto buffer _s_: save worksp
_[_: previous _W_: init all _k_: remove buffer _`_: to last worksp "
("TAB" persp-switch-quick)
("RET" persp-switch)
("`" persp-switch-last)
("1" (persp-switch-by-number 1))
("2" (persp-switch-by-number 2))
("3" (persp-switch-by-number 3))
("4" (persp-switch-by-number 4))
("5" (persp-switch-by-number 5))
("6" (persp-switch-by-number 6))
("7" (persp-switch-by-number 7))
("8" (persp-switch-by-number 8))
("9" (persp-switch-by-number 9))
("0" (persp-switch-by-number 0))
("n" ha-project-persp)
("N" persp-switch)
("]" persp-next :color pink)
("[" persp-prev :color pink)
("d" persp-kill)
("W" ha-workspace-initialize)
("a" persp-add-buffer)
("b" persp-switch-to-buffer)
("k" persp-remove-buffer)
("K" persp-kill-buffer)
("m" persp-merge)
("u" persp-unmerge)
("i" persp-import)
("r" persp-rename)
("s" persp-state-save)
("l" persp-state-load)
("w" ha-switch-to-special) ; The most special perspective
("q" nil)
("C-g" nil)))
#+end_src
Lets give it a binding:
#+begin_src emacs-lisp
(ha-leader "TAB" '("workspaces" . hydra-workspace-leader/body))
#+end_src
When called, it /can/ look like:
[[file:screenshots/projects-hydra.png]]
The /special/ perspective is a nice shortcut to the one I use the most:
#+begin_src emacs-lisp
(defun ha-switch-to-special ()
"Change to the projects perspective."
(interactive)
(persp-switch "projects"))
#+end_src
I often want a workspace dedicated to an /application/, so this function:
#+begin_src emacs-lisp
(defun ha-app-perspective (name func)
"Generate new perspective NAME, automatically running FUNC."
(lambda ()
(interactive)
(let ((already-started? (seq-contains-p (persp-names) name 'equal)))
(persp-switch name)
(unless already-started?
(call-interactively func)))))
#+end_src
And I can then use it like:
#+begin_src emacs-lisp :tangle no
(ha-leader "a x" `("to foobar" . ,(ha-app-perspective "foobar" #'foobar)))
#+end_src
*** Predefined Workspaces
Let's describe a list of startup project workspaces. This way, I don't need the clutter of the recent state, but also get back to a state of mental normality.
Granted, this list is essentially a list of projects that I'm currently developing, so I expect this to change often.
#+begin_src emacs-lisp
(defvar ha-workspace-projects-personal nil "List of default projects with a name.")
(add-to-list 'ha-workspace-projects-personal
'("projects" "~/projects" ("breathe.org" "tasks.org")))
(add-to-list 'ha-workspace-projects-personal
'("personal" "~/personal" ("general.org")))
(add-to-list 'ha-workspace-projects-personal
'("technical" "~/technical" ("ansible.org")))
(add-to-list 'ha-workspace-projects-personal
'("hamacs" "~/src/hamacs" ("README.org" "ha-config.org")))
#+end_src
Given a list of information about project-workspaces, can we create them all?
#+begin_src emacs-lisp
(defun ha-persp-exists? (name)
"Return non-nill if a perspective of NAME exists."
(when (fboundp 'perspectives-hash)
(seq-contains (hash-table-keys (perspectives-hash)) name)))
(defun ha-workspace-initialize (&optional projects)
"Precreate workspace projects from a PROJECTS list.
Each entry in the list is a list containing:
- name (as a string)
- project root directory
- a optional list of files to display"
(interactive)
(unless projects
(setq projects ha-workspace-projects-personal))
(dolist (project projects)
(seq-let (name root files) project
(unless (ha-persp-exists? name)
(message "Creating workspace: %s (from %s)" name root)
(ha-project-persp root name files))))
(persp-switch "main"))
#+end_src
Often, but not always, I want a perspective based on an actual Git repository, e.g. a project. Emacs calls these transients.
#+begin_src emacs-lisp
(defun ha-project-persp (project &optional name files)
"Create a new perspective, and then switch to the PROJECT.
If NAME is not given, then figure it out based on the name of the
PROJECT. If FILES aren't specified, then see if there is a
README. Otherwise, pull up Dired."
(interactive (list (completing-read "Project: "
(project-known-project-roots))))
(when (f-directory-p project)
(unless name
(setq name (f-filename project)))
(persp-switch name)
(let ((recent-files (thread-last recentf-list
(--filter (s-starts-with? project it))
(-take 3)))
(readme-org (f-join project "README.org"))
(readme-md (f-join project "README.md"))
(readme-rst (f-join project "README.rst")))
(cond
(files (ha--project-show-files project files))
(recent-files (ha--project-show-files project recent-files))
((f-exists? readme-org) (find-file readme-org))
((f-exists? readme-md) (find-file readme-md))
((f-exists? readme-rst) (find-file readme-rst))
(t (dired project))))))
#+end_src
When starting a new perspective, and I specify more than one file, this function splits the window horizontally for each file.
#+begin_src emacs-lisp
(defun ha--project-show-files (root files)
"Display a list of FILES in a project ROOT directory.
Each file gets its own window (so don't make the list of files
long)."
(when files
(let ((default-directory root)
(file (car files))
(more (cdr files)))
(message "Loading files from %s ... %s and %s" root file more)
(when (f-exists? file)
(find-file file))
(when more
(split-window-horizontally)
(ha--project-show-files root more)))))
#+end_src
* Pretty Good Encryption
For details on using GnuPG in Emacs, see Mickey Petersens [[https://www.masteringemacs.org/article/keeping-secrets-in-emacs-gnupg-auth-sources][GnuPG Essay]].
@ -1258,7 +1166,6 @@ Also, as [[https://www.bytedude.com/gpg-in-emacs/][bytedude]] mentions, I need t
#+begin_src emacs-lisp
(use-package epa-file
:straight (:type built-in)
:custom
(epg-debug t)
(auth-source-debug t)
@ -1268,18 +1175,22 @@ Also, as [[https://www.bytedude.com/gpg-in-emacs/][bytedude]] mentions, I need t
;; Make sure we prompt in the minibuffer for the password:
(epg-pinentry-mode 'loopback)
;; I trust my Emacs session, so I don't bother expiring my pass:
(auth-source-cache-expiry nil)
(auth-source-cache-expiry nil))
#+end_src
Need to make sure that Emacs will handle the prompts, and turn it on:
#+begin_src emacs-lisp
(use-package epa-file
:config
(setenv "GPG_AGENT_INFO" nil)
(ignore-error (epa-file-enable)))
(epa-file-enable))
#+end_src
Since I already (at this point in my file) have Org installed and running, the following code configures the encryption of certain header sections (see [[https://orgmode.org/worg/org-tutorials/encrypting-files.html][this tutorial]]). Headers with a =:crypt:tag (see =org-crypt-tag-matcher= to change it) will be encrypted.
To temporarily read an encrypted part, and call =M-x org-decrypt-entry= when the cursor is inside that section. Saving the file, will re-encrypt it.
#+begin_src emacs-lisp :tangle no
#+begin_src emacs-lisp
(use-package org
:config
(require 'org-crypt)

View file

@ -145,10 +145,12 @@ The [[https://github.com/emacs-dashboard/emacs-dashboard][emacs-dashboard]] proj
(string-replace "\n" "" smaller-version)))
(setq dashboard-startup-banner
;; Choose a random image from my collection of startup images:
(thread-first "~/src/hamacs/support/dashboard"
(directory-files t (rx ".png"))
(seq-random-elt)))
(if (ha-emacs-for-work?)
"~/src/hamacs/support/teal-sticker.png"
;; Choose a random image from my collection of startup images:
(thread-first "~/src/hamacs/support/dashboard"
(directory-files t (rx ".png"))
(seq-random-elt))))
(setq dashboard-banner-logo-title
(format "Emacs %s — %s"
@ -167,12 +169,12 @@ The [[https://github.com/emacs-dashboard/emacs-dashboard][emacs-dashboard]] proj
dashboard-set-heading-icons t
dashboard-footer-messages (list (ha--dad-joke)))
:config
(dashboard-setup-startup-hook)
;; Real shame that :config is incompatible with :hook, otherwise:
;; :hook (dashboard-after-initialize . ha-dashboard)
:config
(tab-bar-switch-to-tab "main")
(dashboard-setup-startup-hook))
(add-hook 'dashboard-after-initialize-hook 'ha-dashboard))
#+end_src
This dashboard project requires [[https://github.com/purcell/page-break-lines][page-break-lines]] (which is a nice project):
@ -262,7 +264,6 @@ The =dashboard= project hooks to [[help:emacs-startup-hook][emacs-startup-hook]]
(defun ha-dashboard ()
"Shows the extra stuff with the dashboard."
(interactive)
(tab-bar-switch-to-tab "main")
(switch-to-buffer "*dashboard*")
(setq-local mode-line-format nil)
(delete-other-windows)

View file

@ -44,8 +44,8 @@ Once I made demonstrations /within/ Emacs with my [[https://github.com/howardabr
#+begin_src emacs-lisp :tangle no
(use-package demo-it
:straight (:local-repo "~/src/demo-it")
;; :straight (:host github :repo "howardabrams/demo-it")
:load-path "~/src/demo-it"
;; :vc (:url "https://github.com/howardabrams/demo-it")
:commands (demo-it-create demo-it-start demo-it-hide-mode-line
demo-it--presentation-display-set)
:custom (demo-it--insert-test-speed :faster))
@ -194,7 +194,7 @@ With some a startup bug that I havent been able to resolve, Im not using i
#+begin_src emacs-lisp :tangle no
(use-package dslide
:straight (dslide :host github :repo "positron-solutions/dslide")
;; :vc (:url "https://github.com/positron-solutions/dslide")
:commands (dslide-deck-start dslide-deck-stop)
:custom
(dslide-start-from 'point)
@ -244,7 +244,7 @@ Call the =ha-slide-notes-update= function automatically after updating a slide.
#+BEGIN_SRC emacs-lisp :tangle no
(use-package dslide
:straight (dslide :host github :repo "positron-solutions/dslide")
;; :vc (:url "https://github.com/positron-solutions/dslide")
:commands (dslide-narrow-hook)
:hook (dslide-narrow . 'ha-slide-notes-update))
#+END_SRC
@ -256,8 +256,7 @@ The [[https://github.com/positron-solutions/moc][Master of Ceremonies]] package
(use-package default-text-scale)
(use-package moc
:straight (:type git :host github
:repo "positron-solutions/moc"))
;; :vc (:url "https://github.com/positron-solutions/moc"))
#+END_SRC
Select text, and call =moc-focus= (call =moc-focus-quit= to stop). Highlight more text, and call =moc-focus-highlight= to brighten it, or =moc-focus-obscure= to hide it.
@ -272,7 +271,7 @@ The [[https://github.com/trevorpogue/topspace][topspace]] project can pad the to
#+BEGIN_SRC emacs-lisp
(use-package topspace
:straight (:type git :host github :repo "trevorpogue/topspace"))
;; :vc (:url "https://github.com/trevorpogue/topspace"))
#+END_SRC
*** Showing Something associated with a Headline
@ -307,7 +306,7 @@ To do this, add =:DSLIDE_ACTIONS: dslide-action-highlight-paragraphs= to the pro
#+begin_src elisp emacs-lisp :tangle no
(use-package dslide
:straight (:host github :repo "positron-solutions/dslide")
;; :vc (:url "https://github.com/positron-solutions/dslide")
:config
(defclass dslide-action-highlight-paragraphs (dslide-action)
((overlays :initform nil))

View file

@ -47,7 +47,7 @@ Let's turn off the menu and other settings:
Let's install and load some of packages from the [[https://github.com/hlissner/doom-emacs][Doom Emacs]] project, like [[https://github.com/seagle0128/doom-modeline][doom-modeline]] and maybe the themes:
#+begin_src emacs-lisp
(use-package doom-modeline
:config
:init
(setq doom-modeline-minor-modes nil
doom-modeline-buffer-encoding nil
doom-modeline-major-mode-color-icon t
@ -56,82 +56,53 @@ Let's install and load some of packages from the [[https://github.com/hlissner/d
doom-modeline-modal 'evil
doom-modeline-lsp-icon t
doom-modeline-percent-position nil)
;; Weird bug:
(defvar helm-ag-show-status-function nil "Workaround")
(doom-modeline-mode 1))
#+end_src
* Window Dimmer
To make the active window /more noticeable/, we /dim/ the in-active windows with the [[https://github.com/gonewest818/dimmer.el][dimmer project]].
#+begin_src emacs-lisp
(use-package dimmer
:custom (dimmer-adjustment-mode :foreground)
:config
;; I get issues with Magit and Dimmer, so lets turn off this feature in certain windows:
(dimmer-configure-which-key) ; Do not dim these special windows
(dimmer-configure-hydra)
(dimmer-configure-magit)
:custom (dimmer-adjustment-mode :foreground))
#+end_src
(dimmer-mode t))
I get issues with Magic and Dimmer, so lets turn off this feature in certain windows:
#+begin_src emacs-lisp
(use-package dimmer
:config
(dimmer-configure-which-key) ; Do not dim these special windows
(dimmer-configure-hydra)
(dimmer-configure-magit)
(dimmer-mode t))
#+end_src
As an interesting alternative, check out the [[https://www.emacs.dyerdwelling.family/emacs/20240208164549-emacs-selected-window-accent-mode-now-on-melpa/][selected-window-accent]] project.
** Ultra Scroll
The [[https://github.com/jdtsmith/ultra-scroll][ultra-scroll]] project allows smoother scrolling of text and images. While this splits text at the top/bottom of buffer windows, we no longer work within a 80x24 text matrix. Large images would either be "there or not" which resulted large jumps and large distractions.
The [[https://github.com/jdtsmith/ultra-scroll][ultra-scroll]] project allows smoother scrolling of text and images. While this splits text at the top/bottom of buffer windows, we no longer work within a 80x24 text matrix. Large images would
either be "there or not" which resulted large jumps and large distractions.
#+BEGIN_SRC emacs-lisp
(use-package ultra-scroll
:straight (:type git :host github :repo "jdtsmith/ultra-scroll")
;; :vc (:url "https://github.com/jdtsmith/ultra-scroll")
:config
(setq scroll-conservatively 101 ; important!
pixel-scroll-precision-interpolate-page t
scroll-margin 0)
scroll-margin 0)
(ultra-scroll-mode 1))
#+END_SRC
** Find the Bloody Cursor
Large screen, lots of windows, so where is the cursor? While I used to use =hl-line+=, Sacha Chuas [[https://fediscience.org/@ericsfraga/116279043710841253][suggestion]] solves the problem of the built-in =hl-line= that /highlights paragraphs/ in Org with a single function:
#+BEGIN_SRC emacs-lisp
(defun get-visual-line-range ()
"Identify current visual line (for highlighting mostly)
Use the visual line functions to define the range in the buffer
that should be highlighted when ~hl-line-mode~ is enabled. By
default, the whole line in the file is highlighted. This is not
particularly useful in org files where I have whole paragraphs in a
single file line."
(let (b e)
(save-excursion
(beginning-of-visual-line)
(setq b (point))
(end-of-visual-line)
(setq e (point)))
(cons b e)))
(setq hl-line-range-function #'get-visual-line-range)
(global-hl-line-mode 1)
#+END_SRC
The real trick should /visually/ highlight the current line without being too light and interfering with seeing the text or being too obnoxious (see my [[file:ha-theme.org::*Dark Theme][Dark Theme]]).
I found that the prolific [[https://protesilaos.com/][Protesilaos Stavrou]] [[https://protesilaos.com/codelog/2022-03-14-emacs-pulsar-demo/][introduced his Pulsar project]] is just what I need. Specifically, I might /loose the cursor/ and need to have it highlighted (using ~F8~), but also, this automatically highlights the cursor line with specific /actions/ , like changing windows.
Large screen, lots of windows, so where is the cursor? While I used to use =hl-line+=, I found that the prolific [[https://protesilaos.com/][Protesilaos Stavrou]] [[https://protesilaos.com/codelog/2022-03-14-emacs-pulsar-demo/][introduced his Pulsar project]] is just what I need. Specifically, I might /loose the cursor/ and need to have it highlighted (using ~F8~), but also, this automatically highlights the cursor line with specific /actions/ , like changing windows.
#+begin_src emacs-lisp
(use-package pulsar
:straight (:host github :repo "protesilaos/pulsar")
:after winum ace-window
;; :vc (:url "https://github.com/protesilaos/pulsar")
:custom
(pulsar-face 'pulsar-generic)
(pulsar-delay 0.15)
:config
(set-face-attribute 'pulsar-generic nil :background "orange")
(dolist (built-in-function '(recenter-top-bottom move-to-window-line-top-bottom reposition-window
bookmark-jump other-window delete-window delete-other-windows
forward-page backward-page scroll-up-command scroll-down-command
@ -188,7 +159,7 @@ Am I ever really ever satisfied with any font? I regularly change my font based
#+begin_src emacs-lisp
(use-package mixed-pitch
;; :straight (:host github :repo "jabranham/mixed-pitch")
;; ;; :vc (:url "https://github.com/jabranham/mixed-pitch")
:config
(add-to-list 'mixed-pitch-fixed-pitch-faces 'org-property-value)
(add-to-list 'mixed-pitch-fixed-pitch-faces 'org-special-keyword)
@ -418,7 +389,7 @@ This project replaces [[https://github.com/domtronn/all-the-icons.el][all-the-ic
#+BEGIN_SRC emacs-lisp
(use-package nerd-icons
:straight (nerd-icons :type git :host github :repo "rainstormstudio/nerd-icons.el")
;; :vc (:url "https://github.com/rainstormstudio/nerd-icons.el")
:custom
;; The Nerd Font you want to use in GUI defaults to fixed-font:
(nerd-icons-font-family ha-fixed-font))
@ -477,15 +448,10 @@ This replaces the /title generator/ for [[file:ha-config.org::*Leader Sequences]
#+BEGIN_SRC emacs-lisp
(setq major-mode-hydra-title-generator
'(lambda (&optional mode)
(unless mode (setq mode major-mode))
(let ((title (major-mode-hydra-title mode))
(default (nerd-icons-mdicon "nf-md-apple_keyboard_command"
:v-adjust 0.05))
(icon (nerd-icons-icon-for-mode (or mode major-mode)
:v-adjust 0.05)))
(concat (if (eq icon mode) default icon)
" " title " Commands"))))
(let ((title (major-mode-hydra-title mode)))
(s-concat ; (s-repeat 5 " ")
(nerd-icons-icon-for-mode (or mode major-mode) :v-adjust 0.05)
" " title " Commands"))))
#+END_SRC
Transition:
@ -672,7 +638,7 @@ In code, if you drop a specific /text/ labels, we can highlight them with [[http
#+begin_src emacs-lisp
(use-package hl-todo
:straight (:host github :repo "tarsius/hl-todo")
;; :vc (:url "https://github.com/tarsius/hl-todo")
:config
(setq hl-todo-keyword-faces
`(("TODO" . ,(face-foreground 'warning))
@ -688,16 +654,14 @@ Suggests to bind some keys to =hl-todo-next= in order to jump from tag to tag, b
#+begin_src emacs-lisp
(use-package consult-todo
:straight (:host github :repo "liuyinz/consult-todo")
;; :vc (:url "https://github.com/liuyinz/consult-todo")
:init
(defconst consult-todo--narrow
'((?t . "TODO")
(?f . "FIXME")
(?n . "NOTE"))
"Mapping of narrow and keywords.")
;; :config
;; (evil-define-key '(normal) 'global "g t" '("jump todos" . consult-todo))
)
:general (:states 'normal "g t" '("jump todos" . consult-todo)))
#+end_src
* Full Size Frame
Taken from [[https://emacsredux.com/blog/2020/12/04/maximize-the-emacs-frame-on-startup/][this essay]], I figured I would start the initial frame automatically in fullscreen, but not any subsequent frames (as this could be part of the capturing system).

View file

@ -36,7 +36,7 @@ Next type ~s~ to view and organize mail I've never seen before. We need to focus
- ~P~ :: receipts go to this *Paper Trail*, which takes a *tag* as the name of the store
- ~f~ :: mailing lists and other email that might be nice to read go to *The Feed*
* Email Addresses
** Email Addresses
The configuration files below expect email addresses (I store passwords and other encrypted information elsewhere). These email addresses are /not/ private, but I figured I would annoy any screenscraping spam-inducing crawlers out there, while still allowing others to follow my lead on configuring Emacs and Email.
#+name: email-address-1
@ -54,18 +54,19 @@ The configuration files below expect email addresses (I store passwords and othe
(rot13-string "ubjneq@shmmlgbnfg.pbz")
#+end_src
To use these, we set the =:noweb yes= (to pull in the /name/ of the code block) but put a pair of parens after the name to have it evaluated. For instance, the configuration for sending mail through the MUA in Emacs. Note that port 465 requires implicit SSL/TLS (not STARTTLS), so we set =smtpmail-stream-type= to ~ssl~ and let =auth-source= (e.g. =~/.authinfo.gpg=) supply the credentials for =<<email-address-1>>=:
To use these, we set the =:noweb yes= (to pull in the /name/ of the code block) but put a pair of parens after the name to have it evaluated. For instance, the configuration for sending mail through the MUA in Emacs:
#+begin_src emacs-lisp :noweb yes
(setq send-mail-function 'smtpmail-send-it
message-send-mail-function 'smtpmail-send-it
smtpmail-smtp-server "smtp.mailbox.org"
smtpmail-smtp-service 465
smtpmail-stream-type 'ssl)
smtpmail-starttls-credentials '(("smtp.gmail.com" 587 nil nil))
smtpmail-auth-credentials `(("smtp.gmail.com" 587 ,<<email-address-1>> nil))
smtpmail-default-smtp-server "smtp.gmail.com"
smtpmail-smtp-server "smtp.gmail.com"
smtpmail-smtp-service 587)
#+end_src
* Sending Mail
#+BEGIN_SRC emacs-lisp
(require 'smtpmail)
(require 'smtpmail)
#+END_SRC
* Installation and Basic Configuration
@ -131,8 +132,8 @@ Also, let's do some basic configuration of Emacs' mail system:
#+end_src
Create a special mail perspective:
#+begin_src emacs-lisp :tangle no
(ha-leader "a M" `("mail" . ,(ha-tab-bar-new "mail" #'notmuch)))
#+begin_src emacs-lisp
(ha-leader "a M" `("mail" . ,(ha-app-perspective "mail" #'notmuch)))
#+end_src
* Configuration
Do I want to sign messages by default? Nope.
@ -172,7 +173,6 @@ There are global settings:
#+begin_src conf :tangle ~/.mbsyncrc :noweb yes
# Note: We now tangle this file from ~/src/hamacs/ha-email.org
Create Both
Expunge Both
SyncState *
MaxMessages 100
Sync All # New ReNew Flags
@ -184,41 +184,37 @@ The file generally can have a =Pass= entry for the encrypted passcode, but to sh
#+begin_src conf :tangle ~/.mbsyncrc :noweb yes
# PERSONAL ACCOUNT
IMAPAccount personal
Host imap.gmail.com
User <<email-address-1()>> # Substitute your own email address here
PassCmd "gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/.mailpass-personal.gpg"
SSLType IMAPS
AuthMechs LOGIN
IMAPStore personal-remote
Account personal
Host imap.mailbox.org
User <<email-address-1()>> # Substitute your own email address here
PassCmd "gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/.mailpass-personal.gpg"
SSLType STARTTLS
MaildirStore personal-local
Path ~/.mail/personal/
Inbox ~/.mail/personal/INBOX
SubFolders Verbatim
Flatten .
Channel mailbox
Master :mailbox-remote:
Slave :mailbox-local:
Patterns * !Archives
Channel personal-inbox
Far :personal-remote:
Near :personal-local:
Patterns * !"[Gmail]/Drafts" !"[Gmail]/Spam"
Expunge Both
# Patterns "inbox"
# ExpireUnread no
## Channel personal-inbox
## Far :personal-remote:
## Near :personal-local:
## # Patterns * !"[Gmail]/Drafts" !"[Gmail]/Spam"
## Expunge Both
## # Patterns "inbox"
## # ExpireUnread no
##
## Channel personal-sent
## # Far :personal-remote:"[Gmail]/Sent Mail"
## Near :personal-local:sent
## ExpireUnread yes
##
## Channel personal-trash
## # Far :personal-remote:"[Gmail]/Trash"
## Near :personal-local:trash
## ExpireUnread yes
Channel personal-sent
Far :personal-remote:"[Gmail]/Sent Mail"
Near :personal-local:sent
ExpireUnread yes
Channel personal-trash
Far :personal-remote:"[Gmail]/Trash"
Near :personal-local:trash
ExpireUnread yes
#+end_src
I have other email accounts that could use or ignore.
@ -741,13 +737,13 @@ The idea of linking org documents to email could be nice, however, the =ol-notmu
#+begin_src emacs-lisp :tangle no
(use-package ol-notmuch
:after org
:straight (:type built-in)
:config (add-to-list 'org-modules 'ol-notmuch))
#+end_src
To use, read a message and save a link to it with ~SPC o l~. Next, in an org document, create a link with ~, l~. Now, you can return to the message from that document with ~, o~. Regardless, I may need to store a local copy when I upgrade Org.
* Display Configuration
Using the [[https://github.com/seagle0128/doom-modeline][Doom Modeline]] to add notifications:
#+begin_src emacs-lisp :tangle no
#+begin_src emacs-lisp
(use-package doom-modeline
:config
(setq doom-modeline-mu4e t))

View file

@ -30,7 +30,7 @@ If you find the documentation lacking, I [[http://www.howardism.org/Technical/Em
Tell straight to use the built-in =eshell=:
#+begin_src emacs-lisp
(use-package eshell
:straight (:type built-in)
:hook (eshell-mode . ha-eshell-setup))
#+end_src
@ -976,7 +976,7 @@ The problem with this trick is that =rx= outputs an Emacs-compatible regular exp
The [[https://github.com/joddie/pcre2el][pcre2el]] project can convert from a Lisp regular expression to a [[http://www.pcre.org/][PCRE]] (Perl Compatible Regular Expression), acceptable by [[https://github.com/BurntSushi/ripgrep][ripgrep]].
#+begin_src emacs-lisp
(use-package pcre2el
:straight (:host github :repo "joddie/pcre2el")
;; :vc (:url "https://github.com/joddie/pcre2el")
:config
(defmacro prx (&rest expressions)
"Convert the rx-compatible regular EXPRESSIONS to PCRE.
@ -1378,7 +1378,7 @@ The [[https://codeberg.org/akib/emacs-eat][Emulate a Terminal]] project provides
#+begin_src emacs-lisp :tangle no
(use-package eat
:after eshell
:straight (:repo "https://codeberg.org/akib/emacs-eat")
;; :vc (:url "https://codeberg.org/akib/emacs-eat")
:hook (eshell-load . eat-eshell-visual-command-mode))
#+end_src
@ -1787,7 +1787,7 @@ Sometimes you need to change something about the current file you are editing...
Here is where we associate all the functions and their hooks with =eshell=, through the magic of =use-package=.
#+begin_src emacs-lisp
(use-package eshell
:straight (:type built-in)
:custom (eshell-banner-message '(ha-eshell-banner))
:init
(setq eshell-error-if-no-glob t

View file

@ -54,7 +54,9 @@ I split the configuration of Evil mode into sections. First, global settings:
evil-want-keybinding nil ; work with evil-collection
evil-want-integration t
evil-want-C-u-scroll nil
evil-want-C-i-jump nil))
evil-want-C-i-jump nil
evil-escape-key-sequence "jk"
evil-escape-unordered-key-sequence t))
#+end_src
The Escape key act like ~C-g~ and always go back to normal mode?
@ -191,29 +193,12 @@ I am not a long term VI user, and dont have much need for any of its control
;; I have better window control:
(kbd "C-w") 'sp-kill-region))
#+end_src
** Evil Escape
I find that myself not noticing Im in /insert/ mode, and start typing =j= repeatedly to go down a line, and end up typing something like: =jjjjjjjjjjjj=.
To remedy this, Im using the [[https://www.emacswiki.org/emacs/KeyChord][KeyChord]] code:
#+BEGIN_SRC emacs-lisp
(use-package key-chord
:config
(setq key-chord-two-keys-delay 0.1)
(key-chord-define evil-insert-state-map "jk" 'evil-normal-state)
(key-chord-define evil-insert-state-map "jj" 'evil-normal-state)
(key-chord-define evil-insert-state-map "kk" 'evil-normal-state)
(key-chord-mode 1))
#+END_SRC
** Evil Text Object Line
Delete a line, ~d d~ is in basic VI. Since some commands use text objects, and the basic text object doesnt include lines, the [[https://github.com/emacsorphanage/evil-textobj-line][evil-textobj-line]] project adds that:
#+begin_src emacs-lisp
(use-package evil-textobj-line)
#+end_src
Now ~v i l~ selects the line without initial/trailing whitespace, and ~v a l~ selects the line with spaces, and ~S-v~ selects the line with the final carriage return. Is this much of an improvement? Well, wrapping a line in parenthesis, is just ~y s i l )~
Now ~v i l~ and ~v a l~ works as youd expect, but does this improve on ~S-v~?
** Text Objects based on Indentation
The [[https://github.com/TheBB/evil-indent-plus][evil-indent-plus]] project creates text objects based on the indentation level, similar to how the ~b~ works with “blocks” of code.
#+begin_src emacs-lisp
@ -355,16 +340,6 @@ And the keybindings:
Why not use ~f~? Im reserving the ~f~ for a tree-sitter version that is not always available for all modes… yet.
* Evil Extensions
** Anzu
[[https://github.com/emacsorphanage/anzu][Anzu]] provides a minor mode which displays current match and total matches information in the mode-line in various search modes.
#+BEGIN_SRC emacs-lisp
(use-package anzu
:ensure t
:config
(global-anzu-mode +1))
#+END_SRC
** Evil Exchange
I often use the Emacs commands, ~M-t~ and whatnot to exchange words and whatnot, but this requires a drop out of normal state mode. The [[https://github.com/Dewdrops/evil-exchange][evil-exchange]] project attempts to do something similar, but in a VI-way, and the /objects/ do not need to be adjacent.
@ -427,7 +402,7 @@ Where I like to align, is on variable assignments, e.g.
If you press ~RETURN~ for the /character/ to align, =evil-lion= package simply calls the built-in [[help:align][align]] function. This function chooses a regular expression based on a list of /rules/, and aligning Lisp variables requires a complicated regular expression. Extend [[elisp:(describe-variable 'align-rules-list)][align-rules-list]]:
#+begin_src emacs-lisp
(use-package align
:straight (:type built-in)
:config
(add-to-list 'align-rules-list
`("lisp-assignments"
@ -514,8 +489,9 @@ Notes:
** Evil Jump, er Better Jump
The [[https//github.com/gilbertw1/better-jumper][better-jumper project]] replaces the [[https://github.com/bling/evil-jumper][evil-jumper project]], essentially allowing you jump back to various movements. While I already use ~g ;~ to jump to the last change, this jumps /to the jumps/ … kinda. Im having a difficult time determining /what jumps/ are remembered.
#+begin_src emacs-lisp
#+begin_src emacs-lisp :tangle no
(use-package better-jumper
;; :vc (:url "https//github.com/gilbertw1/better-jumper")
:config
(better-jumper-mode +1)

View file

@ -87,7 +87,7 @@ According to Ben Maughan and [[http://pragmaticemacs.com/emacs/to-eww-or-not-to-
And some global keys to display them in the =apps= menu:
#+begin_src emacs-lisp
(ha-leader "a f" `("feed reader" . ,(ha-tab-bar-new "elfeed" #'elfeed)))
(ha-leader "a f" `("feed reader" . ,(ha-app-perspective "elfeed" #'elfeed)))
#+end_src
* The Feeds :elfeed:
The [[https://github.com/remyhonig/elfeed-org][elfeed-org]] project configures =elfeed= to read the RSS feeds from an Org file … like this one!

View file

@ -335,8 +335,9 @@ And the collection of useful operations:
"b O" '("other" . project-switch-buffer-to-other-window)
"b i" '("ibuffer" . ibuffer)
"b I" '("ibuffer" . ibuffer-other-window)
"b k" '("persp remove" . persp-remove-buffer)
"b N" '("new" . evil-buffer-new)
"b d" '("delete" . kill-buffer)
"b d" '("delete" . persp-kill-buffer*)
"b r" '("revert" . revert-buffer)
"b s" '("save" . save-buffer)
"b S" '("save all" . evil-write-all)
@ -360,15 +361,10 @@ And the collection of useful operations:
"b C-g" '(keyboard-escape-quit :which-key t))
#+end_src
* Bookmarks
The built-in behavior of bookmarks doesnt honor either /projects/ or /perspectives/, but the [[https://codeberg.org/ideasman42/emacs-bookmark-in-project][bookmark-in-project]] package makes =project=-specific bookmarks.
This expands on my global [[file:ha-config.org::*Bookmarks][Bookmarks]]:
Expand on my [[file:ha-config.org::*Bookmarks][Bookmarks]] with the following key sequences:
#+begin_src emacs-lisp
(use-package bookmark-in-project
:bindd
(("C-x r m" . bookmark-in-project-toggle)
("C-x r M" . ha-bookmark-set))
:config
(ha-leader
;; Set or delete a bookmark associated with project:
@ -380,31 +376,6 @@ This expands on my global [[file:ha-config.org::*Bookmarks][Bookmarks]]:
"b <down>" '("next mark" . bookmark-in-project-jump-next)
"b <up>" '("previous mark" . bookmark-in-project-jump-previous)))
#+end_src
** Annotations
Let's try [[https://github.com/bastibe/annotate.el][annotate-mode]], which allows you to drop "notes" displayed /inline/ with the buffer.
#+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-annotation)
;; Can't seem to get anything useful out of this function:
"n s" '("summary" . annotate-show-annotation-summary)
;; The next and previous only work for the current file:
"n j" '("next" . annotate-goto-next-annotation)
"n k" '("prev" . annotate-goto-previous-annotation)))
#+end_src
Keep the annotations simple, almost /tag-like/, and then the summary doesnt wrap. For longer notes, see my [[file:ha-capturing-notes.org::*Code Notes][Code Notes]] section.
The annotations are stored as s-expressions in the file, =~/.emacs.d/annotations=. Lets see how well this scales.
* Centering
After reading [[https://mbork.pl/2024-04-15_Improving_recenter-top-bottom_and_reposition-window][this essay]], I got to thinking that it would be nice to position the text in a buffer /near the top/, but show context based on some specific, textual /things/. My thought is to have a function that prompts for the thing (like the current paragraph, function, etc), but also create thing-specific functions.
@ -550,7 +521,6 @@ The goal here is toggle switches and other miscellaneous settings.
"t T" '("tramp mode" . tramp-mode)
"t v" '("visual" . visual-line-mode)
"t w" '("whitespace" . whitespace-mode)
"t <tab>" '("tab-bar" . tab-bar-mode)
"t <escape>" '(keyboard-escape-quit :which-key t)
"t C-g" '(keyboard-escape-quit :which-key t))
@ -576,10 +546,13 @@ And put it on the toggle menu:
(ha-leader "t n" '("narrow" . ha-narrow-dwim))
#+end_src
* Window Operations
While it comes with Emacs, the =tab-bar= feature keeps track of all window configurations within a tab, allowing me to revert situations where I accidentally delete all the windows.
While it comes with Emacs, I use [[https://www.emacswiki.org/emacs/WinnerMode][winner-mode]] to undo window-related changes:
#+begin_src emacs-lisp
(tab-bar-history-mode)
(use-package winner
:custom
(winner-dont-bind-my-keys t)
:config
(winner-mode +1))
#+end_src
** Ace Window
Use the [[https://github.com/abo-abo/ace-window][ace-window]] project to jump to any window you see.
@ -907,7 +880,7 @@ Remember these keys in the *Help* buffer:
Let's make Info behave a little more VI-like:
#+begin_src emacs-lisp
(use-package info
:straight (:type built-in)
:general
(:states 'normal :keymaps 'Info-mode-map
"B" 'Info-bookmark-jump

View file

@ -102,7 +102,7 @@ Quick way to start and jump to my IRC world.
And some global keys to display them:
#+begin_src emacs-lisp
(ha-leader "a i" `("irc" . ,(ha-tab-bar-new "irc" #'ha-erc)))
(ha-leader "a i" `("irc" . ,(ha-app-perspective "irc" #'ha-erc)))
#+end_src
And a quick shortcuts to call it:
@ -121,7 +121,7 @@ And a quick shortcuts to call it:
#+end_src
* Display Configuration
Using the [[https://github.com/seagle0128/doom-modeline][Doom Modeline]] to add notifications:
#+begin_src emacs-lisp :tangle no
#+begin_src emacs-lisp
(setq doom-modeline-irc t
doom-modeline-irc-stylize 'identity)
#+end_src

View file

@ -2,7 +2,7 @@
#+author: Howard Abrams
#+date: 2024-07-07
#+filetags: emacs hamacs
#+lastmod: [2026-08-05 Wed]
#+lastmod: [2025-09-09 Tue]
A literate programming file for literate programming in Emacs Org Files.
@ -51,7 +51,7 @@ Since we are using Emacs, the downsides of using a literate approach (even for p
** Surround with Org Block
This simple change allows me to highlight any text, and surround it in an org block. We
#+BEGIN_SRC snippet :tangle ~/.emacs.d/snippets/org-mode/surround-org-block :eval none
#+BEGIN_SRC snippet :tangle ~/.emacs.d/snippets/org-mode/surround-org-block
# name: surround-org-block
# --
,#+BEGIN_${1:SRC} ${2:emacs-lisp} $0
@ -90,7 +90,7 @@ Actually, I probably want all the parameters (if any) to come along for the ride
This allows us to now split a block into two parts:
#+BEGIN_SRC snippet :tangle ~/.emacs.d/snippets/org-mode/split-org-block :eval none
#+BEGIN_SRC snippet :tangle ~/.emacs.d/snippets/org-mode/split-org-block
,# -*- mode: snippet -*-
,# name: split-org-block
,# key: #split
@ -712,8 +712,7 @@ Not every header should be a destination, as many of my org files have duplicate
"Install"
"Overview"
"Summary"
"Technical Artifacts"
"[["))
"Technical Artifacts"))
"Regular expression matching headers to purge.")
#+end_src
@ -778,20 +777,12 @@ Before we dive into the implementation of this function, lets write a test to
'("Somefile∷ Parent﹥ Subparent﹥ Deep Heading"
"ha-somefile.org" 73)))
(setq ha-hamacs-edit-prev-head-list '("Parent" "Subparent" "Subby"))
(should (equal
(ha-hamacs-edit--process-entry
"ha-somefile.org:73:**** Deep Heading")
'("Somefile∷ Parent﹥ Subparent﹥ Subby﹥ Deep Heading"
"ha-somefile.org" 73)))
(setq ha-hamacs-edit-prev-head-list '("Parent" "Subparent"
"Subby" "Deepsubby"))
(should (equal
(ha-hamacs-edit--process-entry
"ha-somefile.org:73:***** Deepest Heading")
'("Somefile∷ Parent﹥ Subparent﹥ Subby﹥ Deepsubby﹥ Deepest Heading"
'("Somefile∷ ... Deepest Heading"
"ha-somefile.org" 73))))
#+end_src
@ -871,11 +862,13 @@ I would like to make the /filename/ more readable, I use the =s-match= again, to
#+begin_src emacs-lisp
(defun ha-hamacs-edit--file-title (file)
"Return a more readable string from FILE."
(s-with file
(s-match ha-hamacs-edit-file-to-title)
(second)
(s-replace "-" " ")
(s-titleize)))
(if file
(s-with file
(s-match ha-hamacs-edit-file-to-title)
(second)
(s-replace "-" " ")
(s-titleize))
file))
(defvar ha-hamacs-edit-file-to-title
(rx (optional (or "README-" "ha-"))

View file

@ -31,7 +31,7 @@ The RSS needs UUIDs:
#+BEGIN_SRC emacs-lisp results silent
(use-package uuidgen
:straight (:host github :repo "emacsmirror/uuidgen"))
;; :vc (:url "https://github.com/emacsmirror/uuidgen"))
(defun org-icalendar-create-uid (&rest ignored)
"Returns a UUID."
@ -42,7 +42,7 @@ While the following packages come with Emacs, they aren't necessarily loaded:
#+begin_src emacs-lisp :results silent
(use-package ox-rss
:straight (:host github :repo "emacsmirror/ox-rss"))
;; :vc (:url "https://github.com/emacsmirror/ox-rss"))
(use-package org
:config
@ -64,7 +64,7 @@ Render my code with my font colors:
Also, we need Jack, and his HTML prowess:
#+begin_src emacs-lisp
(use-package jack
:straight (:host github :repo "tonyaldon/jack")
;; :vc (:url "https://github.com/tonyaldon/jack")
:commands (jack-html))
#+end_src

View file

@ -2,7 +2,9 @@
#+author: Howard X. Abrams
#+date: 2020-09-25
#+tags: emacs work
A literate program for configuring org files for work-related notes.
#+begin_src emacs-lisp :exports none
;;; org-sprint --- Configuring org files for work-related notes. -*- lexical-binding: t; -*-
;;
@ -23,51 +25,31 @@ A literate program for configuring org files for work-related notes.
;;; Code:
#+end_src
* Introduction
At the beginning of each Sprint, I create a new org file dedicated to it. This workflow/technique strikes a balance between a single ever-growing file, and a thousand little ones. This also gives me a sense of continuity, as the filename of each sprint is date-based.
I want a single keybinding that always displays the current Sprint note file, regardless of the Sprint. This means, I need to have functions that can calculate what this is.
To have the Org Capture features to be able to write to correct locations in the current file, I need each file to follow a particular format. I create a [[file:templates/sprint.org][sprint note template]] that will be automatically expanded with a new sprint.
This template needs the following functions:
- =sprint-current-name= to be both the numeric label as well as the nickname
- =sprint-date-range= to include a org-formatted date range beginning and ending the sprint
- =sprint-date-from-start= return a date for pre-scheduled and recurring meetings
* Naming Sprints
I give each sprint a nickname, based on a /theme/ of some sorts, alphabetized. Since our sprints are every two weeks, this allows me to go through the alphabet once. Yeah, my group likes to boringly /number/ the sprints, so I do both… for myself.
At the beginning of the year, I choose a theme, and make a list for the upcoming sprints. In the org file, this is a list, that gets /tangled/ into an actual Emacs LIsp list. This is pretty cool.
#+begin_src emacs-lisp :var sprint-names=sprint-names-2026
#+begin_src emacs-lisp :var sprint-names=sprint-names-2025
(defvar sprint-nicknames sprint-names
"List of 26 Sprint Nicknames from A to Z.")
#+end_src
** 2026
Migrating to a new team should include something different.
#+name: sprint-names-2026
- Alchemists Amber
- Bubblegloop Bogs
- Crumpet Crossroads
- Dandelion Drifters
- Elfin Espresso
- Firefly Fandango
- Giggling Goblins
- Hobgoblin Hideaway
- Inkwell Isles
- Jesters Junction
- Knapsack Knolls
- Lavender Labyrinths
- Moonbeam Meadow
- Nectar Nibblers
- Owls Overlook
- Pixie Dust Pipeline
- Quizzical Quagmires
- Runaway Radishes
- Starfruit Springs
- Thimbleweed Thicket
- Unicorn Unwinding
- Velvet Valley
- Whistling Willows
- Xanadu Xylophones
- Yawning Yearlings
- Zephyr Zigzags
** 2025
This year is the animals representing corporate slogans:
#+name: sprint-names-2025
- Accelerated and Adaptive Ant
- Buy-in Bee and Blue-sky Bull
@ -126,6 +108,7 @@ How about Tabaxi names this year … especially since my next character has to b
- Zip in the Wind
** 2023
How about a list of Ent names?
#+name: sprint-names-2023
- ashskin
- birchblossom
@ -154,7 +137,9 @@ How about a list of Ent names?
- yewbiquitous
- zelkova
** 2022
Fun sprint names for 2022 lists my favorite D&D monsters, also see [[https://list.fandom.com/wiki/List_of_monsters][this list of monsters]] from mythology and other sources:
#+name: sprint-names-2022
- ankheg
- beholder
@ -184,6 +169,7 @@ Fun sprint names for 2022 lists my favorite D&D monsters, also see [[https://lis
- zombie
** 2021
Choosing Sprint Names based on [[https://www.imagineforest.com/blog/funniest-words-in-the-english-language/][Funny or Silly Words]]:
#+name: sprint-names-2021
- abibliophobia :: The fear of running out of reading materials to read
- bamboozled :: To trick or confuse someone
@ -212,7 +198,9 @@ Choosing Sprint Names based on [[https://www.imagineforest.com/blog/funniest-wor
- yerk :: Pull or push something with a sudden movement.
- zazzy :: Something that is shiny and flashy
** 2020
New names from [[https://en.m.wikipedia.org/wiki/List_of_dinosaur_genera][list of dinosaurs]].
#+name: sprint-names-2020
- ankylosaurus
- brontosaurus
@ -240,8 +228,11 @@ New names from [[https://en.m.wikipedia.org/wiki/List_of_dinosaur_genera][list o
- xiaotingia
- yi
- zuul
** 2019
Came up with a list of somewhat well-known cities throughout the world (at least, they had to have a population of 100,000 or more), but I didn't want any real obvious ones.
#+name: sprint-names-2019
- achy-aachen
- bare-bacabal
@ -269,8 +260,10 @@ Came up with a list of somewhat well-known cities throughout the world (at least
- xenophobic-xichang
- yellow-yamaguchi
- zippy-zinder
* Sprint Boundaries
Function to help in calculating dates and other features of a two-week sprint that starts on Tuesday and ends on a Monday… how we work at my job.
Emacs have an internal rep of a time.
#+begin_src emacs-lisp
(defun get-date-time (date)
@ -284,40 +277,81 @@ Emacs have an internal rep of a time.
(apply 'encode-time))
date))
#+end_src
** Sprint Numbering
Each year, specify the first day of the first sprint of the year:
#+BEGIN_SRC emacs-lisp
;; CHANGEME Each year as this should update:
(defvar sprint-start-date (get-date-time "2026-01-06")
(defvar sprint-start-date (get-date-time "2025-01-14")
"The date of the first day of the first sprint of the year.
See `sprint-range'.")
#+END_SRC
My company has sprints two weeks long that we can calculate from
My Sprint starts on Tuesday, but this sometimes changed, so let's make this a variable:
#+begin_src emacs-lisp
(defvar sprint-length (* 14 24 60 60)
"The length of a sprint, in seconds. This is 2 weeks long")
(defvar sprint-starting-day 2 "The day of the week the sprint begins, where 0 is Sunday.")
#+end_src
The number of the sprint comes from the number of /bi-weeks/ (14 day increments) from the =sprint-start-date=. We can calculate the number of seconds from this /start date/ and divide it by the =sprint-length=:
We seem to never start our Sprints correctly, and we seem to like offsets:
#+begin_src emacs-lisp
;; CHANGEME Each year as this never matches:
(defvar sprint-offset-value 1 "The number of the first sprint.")
#+end_src
We label our sprint based on the week number that it starts. Note that on a Monday, I want to consider that we are still numbering from last week.
#+begin_src emacs-lisp
(defun sprint-week-num (&optional date)
"Return the week of the current year (or DATE), but starting
the week at Tuesday to Monday."
(let* ((d (get-date-time date))
(dow (nth 6 (decode-time d))) ; Day of the week 0=Sunday
(week (thread-last d ; Week number in the year
(format-time-string "%U")
string-to-number)))
(if (>= dow sprint-starting-day)
(1+ week)
week)))
#+end_src
Let's have these tests to make of this /weekly/ perspective:
#+begin_src emacs-lisp :tangle no
(ert-deftest sprint-week-num-test ()
(should (= (sprint-week-num "2025-01-13") 2)) ; Monday previous week
(should (= (sprint-week-num "2025-01-14") 3)) ; Monday previous week
(should (= (sprint-week-num "2024-01-01") 0)) ; Monday previous week
(should (= (sprint-week-num "2024-01-02") 1)) ; Tuesday ... this week
(should (= (sprint-week-num "2024-01-09") 2)) ; Monday, next week, part of last
(should (= (sprint-week-num "2024-01-10") 3))) ; Tuesday next week
#+end_src
My company has sprints two weeks long, we could be see that on even week numbers, the /sprint/ is actually the previous week's number.
This year, my PM decided to start the sprints sequentially starting with 11, so Ive decided to follow my own naming convention for my filenames.
#+begin_src emacs-lisp
(defun sprint-number (&optional date)
"Return the number of 14-day intervals since SPRINT-START-DATE to DATE.
DATE is a string in YYYY-MM-DD format."
(unless date
(setq date (format-time-string "%Y-%m-%d")))
(let* ((end-time (get-date-time date))
(diff-seconds (float-time (time-subtract end-time sprint-start-date))))
;; After calculating the number of 'bi-weeks' (2 week sprint increments),
;; we add one to return the correct sprint for the networking team:
;; (1+ (floor (/ diff-seconds sprint-length)))
;; Or for the platform storage team:
(1+ (* 2 (floor (/ diff-seconds sprint-length))))))
"Return the current sprint number, with some assumptions that
each sprint is two weeks long, starting on Tuesday."
(let* ((num (sprint-week-num date))
;; Depending on how late we wait to start the sprint, the
;; new sprint may be on an oddp or evenp week:
(bucket (if (cl-oddp num) num (1- num))))
(thread-first bucket
;; Make 2 week sprints sequential:
(/ 2)
;; Sprint offset number:
(- sprint-offset-value))))
#+end_src
And some tests to verify that:
#+begin_src emacs-lisp :tangle no
(ert-deftest sprint-number-test ()
(should (= (sprint-number "2026-01-12") 0))
(should (= (sprint-number "2026-01-13") 1))
(should (= (sprint-number "2026-01-20") 1)))
(should (= (sprint-number "2025-01-13") 0))
(should (= (sprint-number "2025-01-14") 0))
(should (= (sprint-number "2025-01-27") 0))
(should (= (sprint-number "2025-01-29") 1)))
#+end_src
** Sprint File Name
I create my org-file notes based on the Sprint number.
@ -330,6 +364,7 @@ I create my org-file notes based on the Sprint number.
(format-time-string "%Y" d)
(sprint-number d)))))
#+end_src
So given a particular date, I should expect to be able to find the correct Sprint file name:
#+begin_src emacs-lisp :tangle no
(ert-deftest sprint-current-file-test ()
@ -338,7 +373,9 @@ So given a particular date, I should expect to be able to find the correct Sprin
(should (s-ends-with? "Sprint-2024-13.org" (sprint-current-file "2024-02-01")))
(should (s-ends-with? "Sprint-2024-14.org" (sprint-current-file "2024-02-13"))))
#+end_src
Daily note-taking goes into my sprint file notes, so this interactive function makes an easy global short-cut key.
#+begin_src emacs-lisp
(defun sprint-current-find-file (&optional date)
"Load the `org-mode' note associated with my current sprint."
@ -349,32 +386,33 @@ Daily note-taking goes into my sprint file notes, so this interactive function m
(add-to-list 'org-agenda-files filename)
(find-file filename)))
#+end_src
The /name/ and /nickname/ of the sprint will be used in the =#+TITLE= section, and it looks something like: =Sprint 2019-07 (darling-dadu)=
#+begin_src emacs-lisp
(defun sprint-current-name (&optional date)
"Return the default name of the current sprint (based on DATE)."
(let* ((d (get-date-time date))
(sprint-num (sprint-number d))
(nickname (nth (/ (1- sprint-num) 2) sprint-nicknames)))
(nickname (nth (1- sprint-num) sprint-nicknames)))
(format "Sprint %s-%02d :: %s"
(format-time-string "%Y" d)
(sprint-number d)
nickname)))
#+end_src
Current sprint name:
#+BEGIN_SRC emacs-lisp :tangle no
(sprint-current-name)
#+END_SRC
These test won't pass any more, as the nickname of the sprint changes from year to year.
#+begin_src emacs-lisp :tangle no
(ert-deftest sprint-current-name-test ()
(should (equal "Sprint 2024-01 :: Art of Shadows" (sprint-current-name "2024-01-02")))
(should (equal "Sprint 2024-04 :: Daydream at Night" (sprint-current-name "2024-02-14"))))
#+end_src
** Sprint Start and End
I want to print the beginning and ending of the sprint, where we have a sprint number or a data, and we can give the dates that bound the sprint. This odd function calculates this based on knowing the date of the /first Tuesday/ of the year, so I need to begin the year changing this value. I should fix this.
#+begin_src emacs-lisp
(defun sprint-range (&optional number-or-date)
"Return a list of three entries, start of the current sprint,
@ -392,7 +430,9 @@ I want to print the beginning and ending of the sprint, where we have a sprint n
(sprint-end (time-add sprint-next (- day-length))))
(list sprint-start sprint-end sprint-next)))
#+end_src
Format the start and end so that we can insert this directly in the org file:
#+begin_src emacs-lisp
(defun sprint-date-range (&optional number-or-date)
"Return `org-mode' formatted date range for a given sprint.
@ -404,6 +444,7 @@ Format the start and end so that we can insert this directly in the org file:
(end (format-time-string formatter sprint-end)))
(format "[%s]--[%s]" start end))))
#+end_src
And validate with a test:
#+begin_src emacs-lisp
(ert-deftest sprint-date-range ()
@ -418,8 +459,11 @@ And validate with a test:
(should (equal (sprint-date-range 5)
(sprint-date-range "2024-01-31"))))
#+end_src
** Pre-scheduled Dates
Due to the regularity of the sprint cadence, I can pre-schedule meetings and other deadlines by /counting/ the number of days from the start of the sprint:
#+begin_src emacs-lisp
(defun sprint-date-from-start (days &optional formatter)
"Return formatted date string from number of DAYS from the start of the sprint."
@ -430,14 +474,18 @@ Due to the regularity of the sprint cadence, I can pre-schedule meetings and oth
(format-time-string formatter adate)
(format-time-string "%Y-%m-%d %a" adate))))
#+end_src
* Other Date Functions
The following functions /were/ helpful at times. But I'm not sure I will use them.
#+begin_src emacs-lisp :tangle no
(defun sprint-num-days (time-interval)
"Converts a TIME-INTERVAL to a number of days."
(let ((day-length (* 3600 24)))
(round (/ (float-time time-interval) day-length))))
#+end_src
#+begin_src emacs-lisp :tangle no
(defun sprint-day-range (&optional date)
"Returns a list of two values, the number of days from the
@ -449,6 +497,7 @@ The following functions /were/ helpful at times. But I'm not sure I will use the
(ending (time-subtract sprint-end now)))
(list (sprint-num-days starting) (sprint-num-days ending)))))
#+end_src
#+begin_src emacs-lisp :tangle no
(ert-deftest sprint-day-range ()
;; This sprint starts on 2/13 and ends on 2/26
@ -456,25 +505,33 @@ The following functions /were/ helpful at times. But I'm not sure I will use the
(should (equal '(-1 12) (sprint-day-range "2020-02-14")))
(should (equal '(-13 0) (sprint-day-range "2020-02-26"))))
#+end_src
#+begin_src emacs-lisp :tangle no
(defun sprint-day-start (&optional date)
"Return a relative number of days to the start of the current sprint. For instance, if today was Friday, and the sprint started on Tuesday, this would return -1."
(first (sprint-day-range date)))
(defun sprint-day-end (&optional date)
"Return a relative number of days to the end of the current sprint. For instance, if today was Monday, and the sprint will end on Monday, this would return 3."
(second (sprint-day-range date)))
#+end_src
* Technical Artifacts :noexport:
Let's =provide= a name so we can =require= this file:
#+begin_src emacs-lisp :exports none
(provide 'ha-org-sprint)
;;; ha-org-sprint.el ends here
#+end_src
Before you can build this on a new system, make sure that you put the cursor over any of these properties, and hit: ~C-c C-c~
#+description: A literate program for configuring org files for work-related notes.
#+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:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js

View file

@ -27,6 +27,7 @@ A literate programming file for making Org file more readable.
#+end_src
* Introduction
I like having org-mode files look more like a word processor than having it look like programming code. But that is me. The end results:
[[file:screenshots/org-as-word-processor.png]]
* General Org Settings
Since I use ellipsis in my writing… to /change/ how org renders a collapsed heading.
@ -101,7 +102,7 @@ And hook this function to Org:
The list of things to try:
#+BEGIN_SRC emacs-lisp
(setq org-hide-leading-stars t) ; or t
(setq org-hide-leading-stars nil) ; or t
#+END_SRC
** Markup View
@ -109,7 +110,7 @@ The variable, =org-hide-emphasis-markers=, is key to pretending that Emacs can b
#+begin_src emacs-lisp
(use-package org-appear
:straight (:type git :host github :repo "awth13/org-appear")
;; :vc (:url "https://github.com/awth13/org-appear")
:init (setq org-appear-trigger 'manual)
:hook
((org-mode . (lambda ()
@ -330,7 +331,7 @@ The [[https://github.com/minad/org-modern][org-modern]] project attempts to do a
#+begin_src emacs-lisp
(use-package org-modern
:straight (:host github :repo "minad/org-modern")
;; :vc (:url "https://github.com/minad/org-modern")
:after org
:hook ((org-mode . org-modern-mode)
(org-agenda-finalize . org-modern-agenda))
@ -338,12 +339,11 @@ The [[https://github.com/minad/org-modern][org-modern]] project attempts to do a
(org-modern-table nil)
(org-pretty-entities t)
(setq org-modern-fold-stars '(("▸" . "▾")
("▹" . "▿")
("▸" . "▾")
("▹" . "▿")
("▸" . "▾")))
("▹" . "▿")
("▸" . "▾")
("▹" . "▿")
("▸" . "▾")))
:config
(set-face-attribute 'org-modern-todo nil :inverse-video nil)
(set-face-attribute 'org-ellipsis nil :inherit 'default :box nil :underline nil)
(set-face-attribute 'org-modern-symbol nil :family "Iosevka"))
#+end_src
@ -367,7 +367,7 @@ According to an idea by [[https://jft.home.blog/2019/07/17/use-unicode-symbol-to
The [[https://github.com/TonCherAmi/org-padding][org-padding]] project looks places extra space before and after headers and blocks (essentially leading), to create a more word-processor-y experience. Great idea, however, I have spent a lot of extra time entering blank lines before and after my headers and blocks:
#+begin_src emacs-lisp
(use-package org-padding
:straight (:host github :repo "TonCherAmi/org-padding")
;; :vc (:url "https://github.com/TonCherAmi/org-padding")
:hook (org-mode . org-padding-mode)
:config
(setq org-padding-block-begin-line-padding '(0.5 . 0.3)

View file

@ -3,7 +3,7 @@
#+date: 2020-09-18
#+tags: emacs org
#+startup: inlineimages
#+lastmod: [2026-08-10 Mon]
#+lastmod: [2025-09-08 Mon]
A literate programming file for configuring org-mode and those files.
@ -32,7 +32,7 @@ Org is a /large/ complex beast with a gazillion settings, so I discuss these lat
#+begin_src emacs-lisp :noweb yes
(use-package org
;; TODO: Using the latest org-mode
;; :straight (:type built-in)
;;
:mode (("\\.org" . org-mode))
:init
<<variables>>
@ -65,7 +65,6 @@ Begin by initializing these org variables:
org-edit-src-content-indentation 2 ; Doom Emacs sets this to 0,
; but uses a trick to make it
; appear indented.
org-use-sub-superscripts '{}
org-imenu-depth 4
sentence-end-double-space nil ; I jump around by sentences, but seldom have two spaces.
@ -154,7 +153,7 @@ What should we do if we are at the end of a line?
- In a table? Create a new row.
- In a list, create a new item.
I should break this function into smaller bits:
I should break this function into smaller bits ...
#+begin_src emacs-lisp
(defun ha-org-special-return (&optional ignore)
@ -180,7 +179,7 @@ I should break this function into smaller bits:
(org-return)))))
#+end_src
How do we know if we are in a list item? Lists end with two empty lines, so we need to make sure we are also not at the beginning of a line to avoid a loop where a new entry gets created with one empty line.
How do we know if we are in a list item? Lists end with two blank lines, so we need to make sure we are also not at the beginning of a line to avoid a loop where a new entry gets created with one blank line.
#+begin_src emacs-lisp
(defun org-really-in-item-p ()
"Return item beginning position when in a plain list, nil otherwise.
@ -224,11 +223,10 @@ I need to add a /blocked/ state, and wouldnt /doing/ be better than /in progr
(add-hook 'org-after-todo-state-change-hook 'ha-org-clock-todo-change)
#+end_src
And I would like to have cute little icons for those states:
#+name: org-font-lock
#+begin_src emacs-lisp :tangle no
#+begin_src emacs-lisp
(dolist (m '(org-mode org-journal-mode))
(font-lock-add-keywords m ; A bit silly but my headers are now
`(("^\\*+ \\(TODO\\) " ; shorter, and that is nice canceled
@ -247,7 +245,6 @@ And I would like to have cute little icons for those states:
("^ +\\([-*]\\) "
(0 (prog1 () (compose-region (match-beginning 1) (match-end 1) "•")))))))
#+end_src
** Meetings
I've notice that while showing a screen while taking meeting notes, I don't always like showing other windows, so I created this function to remove distractions during a meeting.
@ -281,7 +278,7 @@ Came up with a great way to search a project for Org-specific files, and wrote [
#+begin_src emacs-lisp
(use-package org-find-file
:straight nil
:config
(ha-leader "f o" '("load org" . org-find-file)))
#+end_src
@ -412,9 +409,8 @@ And lets see how it works:
User-Agent: ${user-agent}
#+end_src
Results in:
#+begin_src js :tangle no
#+results:
#+begin_src js
{
"Emacs Lisp": 15327,
"Shell": 139
@ -435,9 +431,8 @@ And lets try this:
User-Agent: ${user-agent}
#+end_src
Results in the same output:
#+begin_src js :tangle no
#+results:
#+begin_src js
{
"Emacs Lisp": 15327,
"Shell": 139
@ -494,8 +489,12 @@ Need to install and configure Emacs to work with [[https://plantuml.com/][PlantU
After installing the [[https://github.com/skuro/plantuml-mode][plantuml-mode]], we need to reference the location:
#+begin_src emacs-lisp
(use-package deflate
:vc (:url "https://github.com/skuro/deflate.git"))
(use-package plantuml-mode
:straight (:host github :repo "skuro/plantuml-mode")
;; :vc (:url "https://github.com/skuro/plantuml-mode")
:after deflate
:init
(setq org-plantuml-jar-path (expand-file-name "~/bin/plantuml.jar")))
#+end_src
@ -548,8 +547,7 @@ Of course, since we are dealing with Emacs, where we assimilate any good idea. J
#+begin_src emacs-lisp
(use-package pikchr-mode
:straight (:local-repo "~/src/pikchr-mode")
;; :straight (:host github :repo "kljohann/pikchr-mode")
;; :vc (:url "https://github.com/kljohann/pikchr-mode")
:custom
(pikchr-executable "~/bin/pikchr"))
#+end_src
@ -737,7 +735,7 @@ I have a special version of tweaked [[file:elisp/ox-confluence.el][Confluence ex
#+begin_src emacs-lisp
(use-package ox-confluence
:after org
:straight nil ; Located in my "elisp" directory
; Located in my "elisp" directory
:config
(ha-leader :keymaps 'org-mode-map
"o E" '("to confluence" . ox-export-to-confluence)))
@ -748,7 +746,7 @@ I have a special version of tweaked [[file:elisp/ox-confluence.el][Confluence ex
Im not afraid of HTML, but I like the idea of doing my HTML work in a Lisp-like way using the [[https://github.com/tonyaldon/jack][jack-html project]]:
#+begin_src emacs-lisp
(use-package jack
:straight (:host github :repo "tonyaldon/jack")
;; :vc (:url "https://github.com/tonyaldon/jack")
:commands (jack-html))
#+end_src
@ -796,13 +794,16 @@ Splitting out HTML snippets is often a way that I can transfer org-formatted con
hard-newline))))))
#+end_src
** Focused Work
:LOGBOOK:
CLOCK: [2022-02-11 Fri 11:05]--[2022-02-11 Fri 11:21] => 0:16
:END:
I've been working on my own [[http://www.howardism.org/Technical/Emacs/focused-work.html][approach to focused work]],
#+begin_src emacs-lisp
(use-package async)
(use-package ha-focus
:straight (:type built-in)
:config
(ha-leader
"o f" '("begin focus" . ha-focus-begin)
@ -812,49 +813,8 @@ I've been working on my own [[http://www.howardism.org/Technical/Emacs/focused-w
("S-<f12>" . ha-focus-interrupt)
("s-<f12>" . ha-focus-timer-left)))
#+end_src
Need to be able to clock out of a task even if no /clock-in/ has occurred.
#+begin_src emacs-lisp
(defun ha-clock-out ()
"Safely clock out of the current task.
This will not throw an error if no active clock exists."
(interactive)
(ignore-errors
(org-clock-out)
(message "Clocked out.")))
#+end_src
I call this function from [[file:hammerspoon.org::*Clocking out a Task][my Hammerspoon configuration]] to clock out of a task when I close my laptop.
What was I working on? Opening the laptop after a break seldom answers that, so this reports whatever clock is still running, and when none is, the one I stopped last, which is the more likely question after a break.
#+begin_src emacs-lisp
(defun ha-display-latest-clocked-task ()
"Display the task clocked in now, or the one most recently clocked out."
(interactive)
(cond
((org-clocking-p)
(message "Currently clocked in: %s"
(substring-no-properties (org-clock-get-clock-string))))
((and org-clock-out-time
(stringp org-clock-heading)
(not (string-empty-p org-clock-heading)))
(let ((seconds (float-time (time-subtract nil org-clock-out-time))))
(message "Clocked out of \"%s\" %s ago" org-clock-heading
(if (< seconds 60)
"less than a minute"
(format-seconds "%D %H %M%z" seconds)))))
(t (message "No task is currently clocked in."))))
#+end_src
The =%z= at the /end/ of the format drops the larger units while they read zero, turning =0 days 0 hours 25 minutes= into =25 minutes=. It has nothing to lean on when every unit is zero, so the first minute gets phrased by hand:
** Spell Checking
Let's hook some spell checking into org files, and actually all text files. Im making this particularly delicious.
Let's hook some spell-checking into org files, and actually all text files. Im making this particularly delicious.
*** abbrev
First, we turn on =abbrev-mode=. While this package comes with Emacs, check out [[https://masteringemacs.org/article/correcting-typos-misspellings-abbrev][Mickey Petersen's overview]] of using this package for auto-correcting typos.
@ -874,7 +834,7 @@ The idea is that you can correct a typo /and remember/ it. Perhaps calling [[hel
*** jinx
Once upon a time, I used [[https://www.emacswiki.org/emacs/FlySpell][flyspell]] mode to highlight the misspelled words, and the venerable [[https://www.emacswiki.org/emacs/InteractiveSpell][ispell]] for correcting. To be able to correct spelling mistakes /from a distance/, without navigation, I wrote a function that took advantage of Evils [[help:evil-prev-flyspell-error][evil-prev-flyspell-error]] to jump back to the last spelling mistake.
Now, Im using [[https://github.com/minad/jinx][jinx]], as it is the /complete basket/. It spellchecks based on the fontlock face and uses an external [[https://github.com/AbiWord/enchant][enchant program]] (to make spell checking fast and asynchronous). Like =flymake=, Jinx does on-the-fly spell checking of code comments and strings.
Now, Im using [[https://github.com/minad/jinx][jinx]], as it is the /complete basket/. It spellchecks based on the fontlock face and uses an external [[https://github.com/AbiWord/enchant][enchant program]] (to make spell-checking fast and asynchronous). Like =flymake=, Jinx does on-the-fly spellchecking of code comments and strings.
I keep =jinx-correct= bound to ~C-;~ à la flyspell because it is so darn helpful. Supports checking documents with mixed languages.
@ -894,7 +854,7 @@ And the Emacs interface to that:
#+begin_src emacs-lisp
(use-package jinx
:straight (:host github :repo "minad/jinx" :files (:defaults "jinx-mod.c" "emacs-module.h"))
;; :vc (:url "https://github.com/minad/jinx" :files (:defaults "jinx-mod.c" "emacs-module.h"))
:hook (emacs-startup . global-jinx-mode)
:bind (("C-;" . jinx-correct-nearest)
("s-;" . jinx-correct-nearest)
@ -934,6 +894,7 @@ The key-bindings, keystrokes, and key-connections work well with a hyper-command
:bind ("s-t" . powerthesaurus-lookup-dwim))
#+END_SRC
*** Definitions
Since the /definitions/ do not work, so let's use the [[https://github.com/abo-abo/define-word][define-word]] project:
@ -979,103 +940,21 @@ Once in the dictionary buffer, acquiesce these keybindings:
Also note that the dictionary has links to other pages, so ~n~ and ~TAB~ jumps to the next link and ~RET~ opens that link.
** Grammar and Prose Linting
Flagging clichés, weak phrasing and other poor grammar choices.
Flagging cliches, weak phrasing and other poor grammar choices.
We are trying a lot of checkers, as each have their specialties. To do this, we /chain/ them with a call to =flycheck-add-next-checker=:
We are trying a lot of checkers, so we need to /chain/ them with a call to =flycheck-add-next-checker=:
=write-good= —> =proselint= —> =textlint= —> =jinx= (for spell checking)
*** Ignoring Code Blocks
None of these checkers understand Org, so each one grades my source blocks as if they were English. This becomes especially problematic when a Lisp quote turns off =proselint= for the rest of the file. I solved this issue in Markdown with [[file:ha-programming.org::*Remark][Remark]] and a code-stripping plugin, but the Org checkers have no such pipeline.
/Every/ checker gets the buffer through one of two functions: =flycheck-process-send-buffer= for the ones reading standard input, and =flycheck-save-buffer-to-file= for the ones handed a temporary file. Both work on whatever buffer happens to be current, so rather than wrap all three tools, I =advise= the pair to cover every checker in the chain, including any I add later.
The blanking replaces each character with a space instead of deleting it. Line numbers, columns, and character offsets all survive, so the positions the checkers report still line up with the real buffer:
#+begin_src emacs-lisp
(defcustom ha-org-prose-ignored-blocks '("src" "example" "export")
"Org block types whose contents the prose checkers should never see.
Note the absence of `quote' and `verse', as those hold prose worth
grading."
:type '(repeat string)
:group 'flycheck)
(defun ha-org--blank-region (start end)
"Replace each non-newline character between START and END with a space.
To keep the line number positions correct, I can't just delete a code block,
but need to replace the code block contents with innocuous spaces, but keep
the newline characters."
(save-excursion
(goto-char start)
(while (< (point) end)
(let ((eol (min end (line-end-position))))
(unless (= eol (point))
(let ((len (- eol (point))))
(delete-region (point) eol)
(insert (make-string len ?\s)))))
(forward-line 1))))
(defun ha-org--blank-inline-code ()
"Blank inline verbatim and code markup in the current buffer.
Only group 2 of `org-verbatim-re', the markup and its delimiters,
gets blanked, leaving the surrounding punctuation it also matches."
(goto-char (point-min))
(while (re-search-forward org-verbatim-re nil t)
(ha-org--blank-region (match-beginning 2) (match-end 2))
;; Stop short of the trailing character, since it doubles as the
;; leading character of whatever construct comes next.
(goto-char (match-end 2))))
(defun ha-org-prose-only-text ()
"Return this buffer's text with code blocks and inline code blanked."
(let ((text (buffer-substring-no-properties (point-min) (point-max))))
(with-temp-buffer
(insert text)
(goto-char (point-min))
(let* ((case-fold-search t)
(types (regexp-opt ha-org-prose-ignored-blocks t))
(start-re (concat "^[ \t]*#\\+begin_" types "\\b")))
(while (re-search-forward start-re nil t)
(let ((type (match-string 1))
(start (match-beginning 0)))
(when (re-search-forward
(concat "^[ \t]*#\\+end_" (regexp-quote type) "\\b.*$") nil t)
(ha-org--blank-region start (point))))))
;; After the blocks, since their contents no longer contain markup.
(ha-org--blank-inline-code)
(buffer-string))))
#+end_src
Another issue I encounter (again solved differently for Markdown files using [[file:ha-programming.org::*Remark][Remark]]) involves ignoring these grammar and /proselinters/ for verbatim-formatted code. This allows me to write about =just= as a program, and not have it flagged.
Both advised functions read the current buffer, so the trick is to make a scratch buffer current that holds the blanked text. The =derived-mode-p= test happens /before/ the temporary buffer exists, since that buffer is in =fundamental-mode=:
#+begin_src emacs-lisp
(defun ha-flycheck--org-prose-only (orig &rest args)
"Call ORIG with code blocks blanked, when checking an Org buffer."
(if (derived-mode-p 'org-mode)
(let ((text (ha-org-prose-only-text)))
(with-temp-buffer
(insert text)
(apply orig args)))
(apply orig args)))
(advice-add 'flycheck-process-send-buffer :around #'ha-flycheck--org-prose-only)
(advice-add 'flycheck-save-buffer-to-file :around #'ha-flycheck--org-prose-only)
#+end_src
This matters more than tidiness for =proselint=. Since version 0.16, it pairs quote characters across the whole document and silently discards any complaint landing between a pair. Lisp is full of quoted symbols and strings, so those pairings run wild: 80% of this file counts as “quoted”, and the warnings in it never appear. Blanking the code drops that figure to a fraction of a percent.
=write-good= —> =proselint= —> =textlint= —> =languagetool=?
*** Writegood
The [[https://github.com/bnbeckwith/writegood-mode][writegood-mode]] is effective at highlighting passive voice and weasel words.
The [[https://github.com/bnbeckwith/writegood-mode][writegood-mode]] is effective at highlighting passive and weasel words.
#+begin_src emacs-lisp
(use-package writegood-mode)
#+end_src
And it reports obnoxious messages.
For instance:
#+begin_quote
The cake was eaten by the children.
#+end_quote
We install the =write-good= npm:
Note: Instead of hooking the =writegood-mode= to Org files, I will hook it to =flycheck= instead.
We install the =write-good= NPM:
#+begin_src shell
npm install -g write-good
#+end_src
@ -1085,9 +964,7 @@ And check that the following works:
write-good --text="So it is what it is."
#+end_src
Instead of hooking the =writegood-mode= to Org files, I =hook= it to =flycheck= instead.
Note that =markdown-mode= is absent from the list of modes, as Markdown files go through [[file:ha-programming.org::*Remark][Remark]] instead, which runs =write-good= over the prose while ignoring the code blocks:
Now, lets connect it to flycheck:
#+begin_src emacs-lisp
(use-package flycheck
:config
@ -1097,31 +974,21 @@ Note that =markdown-mode= is absent from the list of modes, as Markdown files go
:standard-input nil
:error-patterns
((warning line-start (file-name) ":" line ":" column ":" (message) line-end))
:modes (org-mode text-mode))
:modes (markdown-mode org-mode text-mode))
(add-to-list 'flycheck-checkers 'write-good))
#+end_src
*** Proselint
The [[https://github.com/amperser/proselint/][proselint]] project checks for poor English phrasings, clichés, and corporate-speak. For instance:
#+begin_quote
Let's touch base and circle back.
#+end_quote
Install it on a Mac via:
With overlapping goals to =write-good=, the [[https://github.com/amperser/proselint/][proselint]] project, once installed, can check for some English phrasings. I like =write-good= better, but I want this available for its level of /pedantic-ness/.
#+begin_src sh
brew install proselint
#+end_src
And on Linux:
#+BEGIN_SRC sh
sudo apt install python3-proselint
#+END_SRC
Next, create a configuration file, =~/.config/proselint/config= file, to turn on/off checks:
#+begin_src js :tangle ~/.config/proselint/config.json :mkdirp yes
{
"checks": {
@ -1132,8 +999,7 @@ Next, create a configuration file, =~/.config/proselint/config= file, to turn on
}
#+end_src
And tell [[https://www.flycheck.org/][flycheck]] to use Proselint:
And tell [[https://www.flycheck.org/][flycheck]] to use this:
#+begin_src emacs-lisp
(use-package flycheck
:config
@ -1141,20 +1007,10 @@ And tell [[https://www.flycheck.org/][flycheck]] to use Proselint:
;; And create the chain of checkers so that both work:
(flycheck-add-next-checker 'write-good 'proselint))
#+end_src
Without this block tangling, =write-good= has no /next/ checker, and since it sits at the front of =flycheck-checkers=, it becomes the one Org buffers select. Flycheck then stops there, and every checker chained behind it never runs.
*** Textlint
The [[https://textlint.github.io/][textlint]] project comes with =flycheck=, and flags undefined acronyms (which annoyingly display at the beginning of the file), using the wrong punctuation, and “profane” words. For instance:
#+begin_quote
NASA launched a new rocket balls deep to the moon.
#+end_quote
This requires an executable:
The [[https://textlint.github.io/][textlint]] project comes with =flycheck=, as long as there is an executable:
#+begin_src sh :results silent
sudo npm install -g textlint
# And all the rules
sudo npm install -g textlint-rule-alex
sudo npm install -g textlint-rule-diacritics
@ -1162,112 +1018,75 @@ This requires an executable:
sudo npm install -g textlint-rule-max-comma
sudo npm install -g textlint-rule-no-start-duplicated-conjunction
sudo npm install -g textlint-rule-period-in-list-item
sudo npm install -g textlint-rule-stop-words
sudo npm install -g textlint-rule-terminology
sudo npm install -g textlint-rule-unexpanded-acronym
#+end_src
I create a configuration file in my home directory:
#+begin_src js :tangle ~/.textlintrc
{
"filters": {},
"rules": {
"abbr-within-parentheses": false,
"alex": { "severity": "warning" },
"alex": true,
"common-misspellings": false,
"diacritics": { "severity": "warning" },
"en-max-word-count": { "severity": "warning" },
"max-comma": { "severity": "warning" },
"no-start-duplicated-conjunction": { "severity": "warning" },
"period-in-list-item": { "severity": "warning" },
"stop-words": false,
"terminology": { "severity": "warning" },
"unexpanded-acronym": { "severity": "warning" },
"diacritics": true,
"en-max-word-count": true,
"max-comma": true,
"no-start-duplicated-conjunction": true,
"period-in-list-item": true,
"stop-words": true,
"terminology": true,
"unexpanded-acronym": true,
"write-good": false
}
}
#+end_src
Every rule carries an explicit =severity=, as textlint defaults each one to /error/, and =flycheck-parse-eslint= turns that into an Emacs error. Left alone, a quibble over a hyphen would outrank a misspelling. Note the /object/ form: a rule set to =true= takes no options, so the severity has to replace that value rather than sit beside it.
Add =textlint= to the /chain/ for Org files. The built-in checker hands textlint a temporary file named after the buffer, and textlint /silently skips/ any file whose extension it does not know. Since =.org= is not on that list, it reports nothing and exits successfully, which looks identical to a clean buffer…ugh.
Piping the buffer in under a name textlint accepts avoids that. The =stdin.txt= name is cosmetic, as =flycheck-parse-eslint= overwrites it with the real buffers file name:
Add =textlint= to the /chain/ for Org files:
#+begin_src emacs-lisp
(use-package flycheck
:config
(setq flycheck-textlint-config (format "%s/.textlintrc" (getenv "HOME")))
(flycheck-define-checker org-textlint
"A prose checker for Org files using textlint.
Textlint skips files whose extension it does not know, and `.org' is
not among them, so the buffer arrives on standard input under a name
the text plugin accepts."
:command ("textlint"
(config-file "--config" flycheck-textlint-config)
"--format" "json"
"--plugin" "@textlint/text"
"--stdin" "--stdin-filename" "stdin.txt")
:standard-input t
:error-parser flycheck-parse-eslint
:modes (org-mode))
(add-to-list 'flycheck-checkers 'org-textlint t)
(flycheck-add-next-checker 'proselint 'org-textlint t))
(flycheck-add-next-checker 'proselint 'textlint t))
#+end_src
Note: The text plugin has no notion of Org, but that no longer matters, since [[*Ignoring Code Blocks][the advice above]] hands it prose with the code already blanked.
*** Spelling
I like correcting spelling through the [[*jinx][jinx]] interface, but would also like to jump through misspellings using the =flycheck= UI.
Note: Since =flycheck= only /reports/ problems (like a misspelling), I still require =jinx-correct= to repair the problem.
*** Language Tool
Another flycheck feature is to use [[http://languagetool.org][LanguageTool]] connection to [[https://github.com/emacs-languagetool/flycheck-languagetool][flycheck-languagetool]]:
Jinx already does the work and leaves an overlay on each misspelling, so the checker collects them:
#+BEGIN_SRC emacs-lisp :tangle no
(use-package flycheck-languagetool
:ensure t
:hook (text-mode . flycheck-languagetool-setup)
:init
(setq flycheck-languagetool-server-jar (expand-file-name "/opt/homebrew/Cellar/languagetool/6.6/libexec/languagetool-commandline.jar")
flycheck-languagetool-server-args (expand-file-name "~/.config/languagetool/config.properties")))
#+END_SRC
#+begin_src emacs-lisp
Where it can read: [[file:~/.config/languagetool/config.properties][config.properties]].
And connect it to the chain:
#+BEGIN_SRC emacs-lisp :tangle no
(use-package flycheck
:after jinx
:config
(defun ha-jinx--flycheck-errors (checker)
"Return a `flycheck-error' for each misspelling jinx finds."
;; Jinx separates prose from code by font-lock face, and font-lock
;; only fontifies what has been displayed. Without this, the rest of
;; the buffer arrives faceless and every symbol reads as misspelled.
(font-lock-ensure)
(jinx--check-region (point-min) (point-max))
(mapcar (lambda (ov)
(let ((start (overlay-start ov))
(end (overlay-end ov)))
(flycheck-error-new-at-pos
start 'error
(format "Misspelled: %s"
(buffer-substring-no-properties start end))
:id "jinx"
:checker checker
:end-pos end)))
(jinx--get-overlays (point-min) (point-max))))
:config (flycheck-add-next-checker 'textlint 'languagetool t)
(flycheck-define-generic-checker 'jinx
"A spell checker reporting what jinx and enchant already found."
:start (lambda (checker callback)
(condition-case err
(funcall callback 'finished (ha-jinx--flycheck-errors checker))
(error (funcall callback 'errored (error-message-string err)))))
;; Without jinx enabled there are no dictionaries to ask.
:predicate (lambda () (bound-and-true-p jinx-mode))
:modes '(org-mode text-mode markdown-mode gfm-mode))
;; May have to specify a Java on one of my Mac machines:
(when (file-exists-p "/opt/homebrew/opt/openjdk")
(add-to-list 'exec-path "/opt/homebrew/opt/openjdk/bin")))
#+END_SRC
(add-to-list 'flycheck-checkers 'jinx t)
(flycheck-add-next-checker 'org-textlint 'jinx t))
#+end_src
This check complains about whitespace in Org files (duh), so lets create a configuration file where we can disable that rule (and any other we can require):
Note that jinx underlining the words itself, /on top of/ what flycheck now draws. Turning that off would mean would apply that everywhere — including the programming buffers where this prose-chain never runs, and I would need jinxs flagging. In other words, the doubled underline seems the smaller cost.
#+BEGIN_SRC conf :tangle ~/.config/languagetool/config.properties :mkdirp ~/.config/languagetool
disabledRuleIds: WHITESPACE
#+END_SRC
Gotta admit that Language Tool doesnt seem to help much. $ 100 ?
** Perfect Sentence
Jordan Peterson's writing app, [[https://essay.app/guide][Essay]], inspired Chris Maloranas [[https://www.youtube.com/watch?v=E-yk_V5TnNU][video tutorial]] showing the ability to extrude a single sentence into another buffer, edit different versions of that sentence, and replace one version into the original buffer. Similar to how org-mode edits blocks.
Chris Maloranas [[https://www.youtube.com/watch?v=E-yk_V5TnNU][video tutorial]] demonstrates the ability to extrude a single sentence into another buffer, edit different versions of that sentence, and replace one version into the original buffer. For instance, how org-mode edits blocks.
Thought I might work on it, but I want my version more resilient and not as dependent on the context.
Malorana based this idea on Jordan Peterson's writing app, [[https://essay.app/guide][Essay]]. Thought I might work on it, but I want my version more resilient and not as dependent on the context.
When we create a new buffer, we set the following /buffer-local/ variables, so we know where to return:
@ -1350,11 +1169,9 @@ In that case, we need to select the text before hitting the ~C-c C-c~ sequence.
(delete-region orig-start orig-end) ; Or call `kill-region' to put on clipboard?
(insert chosen-sentence))))
#+end_src
The [[help:kill-region][kill-region]] function takes the original text and places it on the [[help:kill-ring][kill-ring]] (the clipboard). But since we already copied that when we created the buffer, we call [[help:delete-region][delete-region]] instead. Especially since if we felt like we made a mistake, we could just undo the changes.
The [[help:kill-region][kill-region]] function takes the original text and places it on the [[help:kill-ring][kill-ring]] (the clipboard). But since we already copied that when we created the buffer, we call [[help:delete-region][delete-region]] instead. If we felt like we made a mistake, we can undo the changes.
With my limited experience, I seldom replace the sentence. Instead, I want to /copy/ the sentence and work on that:
With my limited experience, I seldom enter completely difference sentences. Instead, I want to /copy/ the sentence and work on that. Lets make a function to duplicate it.
#+begin_src emacs-lisp
(defun ha-sentence-duplicate ()
(interactive)
@ -1366,7 +1183,7 @@ With my limited experience, I seldom replace the sentence. Instead, I want to /c
(goto-char starting-point))))
#+end_src
When creating this new editing buffer, we need keybindings that exist for this buffer, in other words, a [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Defining-Minor-Modes.html][minor mode]]:
When creating this new editing buffer, we need keybindings that exist only for this buffer, in other words, a [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Defining-Minor-Modes.html][minor mode]]:
#+begin_src emacs-lisp
(defvar ha-sentence-buffer-mode-map (make-sparse-keymap) "Keymap for `my-mode'.")
(define-key ha-sentence-buffer-mode-map (kbd "C-c C-c") #'ha-sentence-choose)
@ -1398,9 +1215,7 @@ And something else while in Evil mode:
#+begin_src emacs-lisp
(ha-leader "x b" '("edit sentence" . ha-sentence-break))
#+end_src
While Malorana might get around to turning [[https://git.chrismaiorana.com/?p=sentinel.git;a=blob;f=sentin.el;h=2738eff6ac2b0877576bafe88878683a7eff3125;hb=refs/heads/master][his code]] into a package, new features need to include:
Perhaps he might get around to turning [[https://git.chrismaiorana.com/?p=sentinel.git;a=blob;f=sentin.el;h=2738eff6ac2b0877576bafe88878683a7eff3125;hb=refs/heads/master][his code]] into a package. Features needed include:
- Adding an overlay to the original text, help:org-src--make-source-overlay
** Distraction-Free Writing
@ -1438,7 +1253,7 @@ The [[https://github.com/rnkn/olivetti][olivetti project]] sets wide margins and
Trying out [[https://protesilaos.com/][Protesilaos Stavrou]]s [[https://protesilaos.com/emacs/logos][logos project]] as a replacement for [[https://github.com/joostkremers/writeroom-mode][Writeroom-mode]]:
#+begin_src emacs-lisp
(use-package logos
:straight (:host gitlab :repo "protesilaos/logos")
;; :vc (:url "https://gitlab.com/protesilaos/logos")
:init
(setq logos-outlines-are-pages t
logos-outline-regexp-alist
@ -1481,5 +1296,5 @@ Before you can build this on a new system, make sure that you put the cursor ove
#+infojs_opt: view:nil toc:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js
# Local Variables:
# jinx-local-words: "Braganza Graphviz Malorana Maloranas Proselint Somers Textlint Writegood flycheck flyspell fontlock"
# jinx-local-words: "Braganza Graphviz Maloranas Proselint Somers Textlint Writegood flycheck flyspell fontlock"
# End:

View file

@ -35,7 +35,7 @@ The obvious keybindings are ~M-h/j/k/l~ … but that is used … well, somewhat.
#+begin_src emacs-lisp :tangle no
(use-package spatial-navigate
:straight (:repo "https://codeberg.org/ideasman42/emacs-spatial-navigate")
;; :vc (:url "https://codeberg.org/ideasman42/emacs-spatial-navigate")
:config
(pretty-hydra-define spatial-navigate (:color amaranth :quit-key "q")
("Box"
@ -62,7 +62,7 @@ The [[https://github.com/antonj/Highlight-Indentation-for-Emacs][Highlight-Inden
#+begin_src emacs-lisp
(use-package highlight-indentation
:straight (:host github :repo "antonj/Highlight-Indentation-for-Emacs")
;; :vc (:url "https://github.com/antonj/Highlight-Indentation-for-Emacs")
:hook ((yaml-mode . highlight-indentation-mode)
(python-mode . highlight-indentation-mode)))
#+end_src
@ -119,7 +119,7 @@ Allow this mode in Org blocks:
And we hook
#+begin_src emacs-lisp
(use-package yaml-pro
:straight (:host github :repo "zkry/yaml-pro")
;; :vc (:url "https://github.com/zkry/yaml-pro")
:after yaml-mode
:hook ((yaml-mode . yaml-pro-mode)))
#+end_src
@ -224,7 +224,7 @@ Do I consider all YAML files an Ansible file needing [[https://github.com/k1LoW/
#+begin_src emacs-lisp
(use-package ansible
:straight (:host gitlab :repo "emacs-ansible/emacs-ansible")
;; :vc (:url "https://gitlab.com/emacs-ansible/emacs-ansible")
;; :mode ((rx (or "playbooks" "roles") (one-or-more any) ".y" (optional "a") "ml") . ansible-mode)
:config
(setq ansible-vault-password-file "~/.ansible-vault-passfile")
@ -242,7 +242,7 @@ Since most Ansible files are a combination of YAML and Jinja, the [[https://gith
#+BEGIN_SRC emacs-lisp
(use-package poly-ansible
:straight (:host github :repo "emacsmirror/poly-ansible")
;; :vc (:url "https://github.com/emacsmirror/poly-ansible")
:after ansible)
#+END_SRC

View file

@ -247,7 +247,7 @@ The /real problem/ is trying to remember all the [[https://github.com/clojure-em
And of course, we want to put this with org blocks:
#+begin_src emacs-lisp
(use-package ob-clojure
:straight (:type built-in)
:custom
(org-babel-clojure-backend 'cider)
:config

View file

@ -71,7 +71,11 @@ And we should extend it with the [[https://github.com/xuchunyang/elisp-demos][el
Wilfreds [[https://github.com/Wilfred/suggest.el][suggest]] function helps you find the right function. Basically, you type in the parameters of a function, and then the desired output, and it will write the function call.
#+begin_src emacs-lisp
(use-package suggest)
(use-package spinner
:vc (:url "https://github.com/Malabarba/spinner.el"))
(use-package suggest
:after spinner)
#+end_src
* Navigation
** Goto Definitions
@ -280,7 +284,7 @@ These are all good, but the primary keys I need to figure out, are the s-express
Wilfreds [[https://github.com/Wilfred/emacs-refactor/tree/master#elisp][emacs-refactor]] package can be helpful if you turn on =context-menu-mode= and …
#+begin_src emacs-lisp
(use-package emr
;; :straight (:host github :repo "Wilfred/emacs-refactor")
;; ;; :vc (:url "https://github.com/Wilfred/emacs-refactor")
:config
(pretty-hydra-define+ lisp-refactor nil
("To 𝛌"
@ -366,3 +370,4 @@ Let's =provide= a name so we can =require= this file:
#+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:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js
exit

View file

@ -60,7 +60,7 @@ The [[https://github.com/mihaimaruseac/hindent][hindent package]] looks interest
* Haskell and Org
#+begin_src emacs-lisp
(use-package ob-haskell
:straight (:type built-in)
:config
(add-to-list 'org-babel-load-languages '(haskell . t)))
#+end_src

View file

@ -33,9 +33,8 @@ While Emacs supplies a Python editing environment, well still use =use-packag
#+begin_src emacs-lisp
(use-package python
:after flycheck
:mode (((rx ".flake8" eol) . conf-mode)
((rx "Pipfile" eol) . conf-mode)
((rx ".wsgi" eol) . python-mode))
:mode ("[./]flake8\\'" . conf-mode)
:mode ("/Pipfile\\'" . conf-mode)
:init
(setq python-indent-guess-indent-offset-verbose nil
flycheck-flake8-maximum-line-length 120)
@ -137,14 +136,7 @@ Next, after reading David Vujics [[https://davidvujic.blogspot.com/2025/03/ar
# c.InteractiveShellApp.exec_lines = ['%autoreload 2']
#+END_SRC
** Isolated Python Environments
While the Python community (and my work at my company) had difficulty transitioning from Python 2 to 3, I often run into issues needing a particular Python version and modules. After playing around with different approaches, Im finding:
* Docker environments are nicely isolated, but annoying to work from outside the container
* The Builtin =venv= is works well for different library modules, but not for different versions
* The =pyenv= deals with different Python versions, but is overkill for library isolation
While the [[https://github.com/marcwebbie/auto-virtualenv][auto-virtualenv]] project attempts to resolve this, Im using the [[file:ha-programming.org::*Virtual Environments with direnv][direnv project]] abstraction for situations where I need project-specific isolation in more than just Python.
*** Virtual Environments
** Virtual Environment
Use the built-in module, venv, to create isolated Python environments for specific projects, enabling you to manage dependencies separately.
Create a virtual environment, either in the projects directory, or in a global spot:
@ -168,8 +160,8 @@ Now, do what you need to do with this isolation:
#+BEGIN_SRC sh :tangle no
pip install -r test-requirements.txt
#+END_SRC
*** Managing Python Versions
[[https://github.com/pyenv/pyenv][Pyenv]] is a tool for managing multiple versions of Python on your machine, allowing you to switch between them easily (see [[https://realpython.com/intro-to-pyenv/][this essay]]). On a Mac, installed it via Homebrew:
** Virtual Environment with new Python Version
Pyenv is a tool for managing multiple versions of Python on your machine, allowing you to switch between them easily. On a Mac, installed it via Homebrew:
#+BEGIN_SRC sh
brew install readline xz
@ -221,56 +213,16 @@ Also, you need the following in your =~/.config/direnv/direnvrc= file (which I h
fi
}
#+end_src
Tell Emacs about [[https://github.com/pythonic-emacs/pyenv-mode][pyenv-mode]]:
#+BEGIN_SRC emacs-lisp
(use-package pyenv-mode
:config
(defun setup-pyenv ()
"Pyenv."
(setenv "WORKON_HOME" "~/.pyenv/versions")
(pyenv-mode +1)))
#+END_SRC
Now specify the =pyenv= Python version by calling [[help:pyenv-mode-set][pyenv-mode-set]]:
#+begin_example
M-x pyenv-mode-set
#+end_example
When you run inferior Python processes (like =run-python=), the process will start inside the specified Python installation. You can unset the current version with:
#+begin_example
M-x pyenv-mode-unset
#+end_example
Or, we can do it automatically when we get into a project (if the project has a =.python-version= file):
#+BEGIN_SRC emacs-lisp
(use-package pyenv-mode
:config
(defun project-pyenv-mode-set (&rest _)
"Set pyenv version matching project name."
(ignore-errors
(let* ((filename (thread-first
(project-current)
(project-root)
(file-name-concat ".python-version")))
(version (when (file-exists-p filename)
(with-temp-buffer
(insert-file-contents filename)
(buffer-string)))))
(when version
(pyenv-mode-set version)
(pyenv-mode-unset)))))
;; Either set/unset the pyenv version whenever changing tabs:
(add-hook 'tab-bar-tab-post-select-functions 'project-pyenv-mode-set))
#+END_SRC
*** Docker Environment
Docker allows you to isolate your project's environment. The downside is that you are using Docker and probably a bloated container. On my work laptop, a Mac, this creates a behemoth virtual machine that immediately spins the fans like a wind tunnel.
** Editing Python Code
Lets integrate this [[https://github.com/wbolster/evil-text-object-python][Python support for evil-text-object]] project:
#+begin_src emacs-lisp
(when (fboundp 'evil-define-text-object)
(use-package evil-text-object-python
:hook (python-mode . evil-text-object-python-add-bindings)))
#+end_src
This allows me to delete a Python “block” using ~dal~.
** Docker Environment
Docker really allows you to isolate your project's environment. The downside is that you are using Docker and probably a bloated container. On my work laptop, a Mac, this creates a behemoth virtual machine that immediately spins the fans like a wind tunnel.
But, but... think of the dependencies!
@ -284,14 +236,6 @@ Your project's =.envrc= file would contain something like:
container_layout
#+end_src
** Editing Python Code
Lets integrate this [[https://github.com/wbolster/evil-text-object-python][Python support for evil-text-object]] project:
#+begin_src emacs-lisp
(when (fboundp 'evil-define-text-object)
(use-package evil-text-object-python
:hook (python-mode . evil-text-object-python-add-bindings)))
#+end_src
This allows me to delete a Python “block” using ~dal~.
** Unit Tests
#+begin_src emacs-lisp
(use-package python-pytest
@ -318,6 +262,64 @@ This allows me to delete a Python “block” using ~dal~.
("Misc"
(("t" python-tests/body "Tests..."))))))
#+end_src
* Elpy
The [[https://elpy.readthedocs.io/en/latest/introduction.html][Elpy Project]] expands on the =python-mode=.
#+BEGIN_SRC emacs-lisp
(use-package elpy
:config
(elpy-enable))
#+END_SRC
Lets expand our =major-mode-hydra= with some extras:
#+begin_src emacs-lisp
(use-package major-mode-hydra
:after elpy
:config
(pretty-hydra-define python-evaluate (:color blue :quit-key "q"
:title ha-python-eval-title)
("Section"
(("F" elpy-shell-send-defun "Function")
("E" elpy-shell-send-statement "Statement")
(";" python-shell-send-string "Expression"))
"Entirety"
(("B" elpy-shell-send-buffer "Buffer")
("r" elpy-shell-send-region-or-buffer "region"))
"And Step..."
(("f" elpy-shell-send-defun-and-step "Function" :color pink)
("e" elpy-shell-send-statement-and-step "Statement" :color pink))))
(pretty-hydra-define+ python-refactor nil
("Elpy"
(("r" elpy-refactor-rename "Rename")
("i" elpy-refactor-inline "Inline var")
("v" elpy-refactor-extract-variable "To variable")
("f" elpy-refactor-extract-function "To function")
("a" elpy-refactor-mode "All..."))))
(major-mode-hydra-define+ python-mode (:quit-key "q" :color blue)
("Server"
(("s" elpy-shell-switch-to-shell "Go to Server")
("C" elpy-config "Config Elpy"))
"Edit"
(("f" elpy-black-fix-code "Fix/format code"))
"Docs"
(("d" elpy-eldoc-documentation "Describe Symbol")
("D" elpy-doc "Docs Symbol")))))
#+end_src
* Anaconda
The [[https://github.com/pythonic-emacs/anaconda-mode][anaconda-mode project]] seems as good as Elpy, but also include Evil keybindings.
#+BEGIN_SRC emacs-lisp :tangle no
(use-mode anaconda-mode
:hook ((python-mode . anaconda-mode)
(python-mode ../ anaconda-eldoc-mode)))
#+END_SRC
Since we are using
* LSP Integration of Python
** Dependencies
Each Python project's =requirements-dev.txt= file would reference the [[https://pypi.org/project/python-lsp-server/][python-lsp-server]] (not the /unmaintained/ project, =python-language-server=):
@ -343,60 +345,100 @@ Each Python project's =requirements-dev.txt= file would reference the [[https://
# ...
#+end_src
** Pyright
Im using the Microsoft-supported [[https://github.com/Microsoft/pyright][pyright]] package instead, adding it to the global Python scope.
Im using the Microsoft-supported [[https://github.com/Microsoft/pyright][pyright]] package instead. Adding this to my =requirements.txt= files:
#+begin_src conf :tangle no
pyright
#+end_src
While the [[https://github.com/emacs-lsp/lsp-pyright][pyright package]] works with LSP, this isnt needed, as =eglot= works directly with it.
** eglot
The built-in Eglot package natively supports [[https://github.com/Microsoft/pyright][pyright]] via =eglot-server-programs= (no extra package needed):
The [[https://github.com/emacs-lsp/lsp-pyright][pyright package]] works with LSP.
#+begin_src emacs-lisp
(use-package eglot
:straight nil
:after python
:defer t
:hook (python-mode . eglot-ensure)
:config
(add-to-list 'eglot-server-programs
'((python-mode python-ts-mode) . ("pyright-langserver" "--stdio"))))
#+begin_src emacs-lisp :tangle no
(use-package lsp-pyright
:hook (python-mode . (lambda () (require 'lsp-pyright)))
:init (when (executable-find "python3")
(setq lsp-pyright-python-executable-cmd "python3")))
#+end_src
Keybindings for the eglot-based LSP sub-menu, wired into the existing =python-mode= hydra:
*** Keybindings
Now that the [[file:ha-programming.org::*Language Server Protocol (LSP) Integration][LSP Integration]] is complete, we can stitch the two projects together, by calling =lsp=. I oscillate between automatically turning on LSP mode with every Python file, but I sometimes run into issues when starting, so I conditionally turn it on.
#+begin_src emacs-lisp
(defvar ha-python-lsp-title (font-icons 'faicon "python" :title "Python LSP"))
(use-package eglot
:straight nil
:after major-mode-hydra
:config
(defun ha-setup-python-lsp ()
"Configure the keybindings for LSP in Python."
(interactive)
(pretty-hydra-define python-lsp (:color blue :quit-key "q"
:title ha-python-lsp-title)
("Server"
(("D" eglot-shutdown "Disconnect")
("R" eglot-reconnect "Reconnect")
("?" eglot-show-workspace-configuration "Describe"))
(("D" lsp-disconnect "Disconnect")
("R" lsp-workspace-restart "Restart")
("S" lsp-workspace-shutdown "Shutdown")
("?" lsp-describe-session "Describe"))
"Refactoring"
(("a" eglot-code-actions "Code Actions")
("o" eglot-code-action-organize-imports "Organize Imports")
("r" eglot-rename "Rename"))
"Navigation"
(("." xref-find-definitions "Definition")
("/" xref-find-references "References")
("t" eglot-find-typeDefinition "Type Definition")
("i" eglot-find-implementation "Implementation"))
"Docs"
(("d" eldoc-doc-buffer "Eldoc Buffer")
("h" eglot-inlay-hints-mode "Inlay Hints")
("=" eglot-format-buffer "Format Buffer"))))
(("a" lsp-execute-code-action "Code Actions")
("o" lsp-organize-imports "Organize Imports")
("l" lsp-avy-lens "Avy Lens"))
"Toggles"
(("b" lsp-headerline-breadcrumb-mode "Breadcrumbs")
("d" lsp-ui-doc-mode "Documentation Popups")
("m" lsp-modeline-diagnostics-mode "Modeline Diagnostics")
("s" lsp-ui-sideline-mode "Sideline Mode"))
""
(("t" lsp-toggle-on-type-formatting "Type Formatting")
("h" lsp-toggle-symbol-highlight "Symbol Highlighting")
("L" lsp-toggle-trace-io "Log I/O"))))
(pretty-hydra-define+ python-goto (:quit-key "q")
("LSP"
(("g" lsp-find-definition "Definition")
("d" lsp-find-declaration "Declaration")
("r" lsp-find-references "References")
("t" lsp-find-type-definition "Type Definition"))
"Peek"
(("D" lsp-ui-peek-find-definitions "Definitions")
("I" lsp-ui-peek-find-implementation "Implementations")
("R" lsp-ui-peek-find-references "References")
("S" lsp-ui-peek-find-workspace-symbol "Symbols"))
"LSP+"
(("u" lsp-ui-imenu "UI Menu")
("i" lsp-find-implementation "Implementations")
("h" lsp-treemacs-call-hierarchy "Hierarchy")
("E" lsp-treemacs-errors-list "Error List"))))
(major-mode-hydra-define+ python-mode nil
("Server"
(("l" python-lsp/body "LSP..."))
"Edit"
(("=" lsp-format-region "Format"))
"Navigate"
(("A" lsp-workspace-folders-add "Add Folder")
("R" lsp-workspace-folders-remove "Remove Folder"))
"Docs"
(("D" lsp-describe-thing-at-point "Describe LSP Symbol")
("h" lsp-ui-doc-glance "Glance Help")
("H" lsp-document-highlight "Highlight"))))
(call-interactively 'lsp))
(use-package lsp-mode
:config
(major-mode-hydra-define+ python-mode (:quit-key "q")
("Server"
( ;; ("L" eglot-ensure "Start eglot") --> Not needed with eglot-ensure
("l" python-lsp/body "LSP...")))))
(("L" ha-setup-python-lsp "Start LSP Server")))))
;; ----------------------------------------------------------------------
;; Missing Symbols to be integrated?
;; "0" '("treemacs" . lsp-treemacs-symbols)
;; "/" '("complete" . completion-at-point)
;; "k" '("check code" . python-check)
;; "Fb" '("un-blacklist folder" . lsp-workspace-blacklist-remove)
;; "hs" '("signature help" . lsp-signature-activate)
;; "tT" '("toggle treemacs integration" . lsp-treemacs-sync-mode)
;; "ta" '("toggle modeline code actions" . lsp-modeline-code-actions-mode)
;; "th" '("toggle highlighting" . lsp-toggle-symbol-highlight)
;; "tl" '("toggle lenses" . lsp-lens-mode)
;; "ts" '("toggle signature" . lsp-toggle-signature-auto-activate)
#+end_src
* Project Configuration
I work with a lot of projects with my team where I need to /configure/ the project such that LSP and my Emacs setup works. Let's suppose I could point a function at a project directory, and have it /set it up/:
@ -422,9 +464,17 @@ I work with a lot of projects with my team where I need to /configure/ the proje
(shell-command "pipconf --local")
(shell-command "pip install --upgrade pip"))
(message "Configuring pip for eglot/pyright")
(message "Configuring pip for LSP")
(with-temp-file "requirements-dev.txt"
(insert "pyright\n"))
(insert "python-lsp-server[all]\n")
;; Let's install these extra packages individually ...
(insert "pyls-flake8\n")
;; (insert "pylsp-mypy")
;; (insert "pyls-isort")
;; (insert "python-lsp-black")
;; (insert "pyls-memestra")
(insert "pylsp-rope\n"))
(shell-command "pip install -r requirements-dev.txt")))
#+end_src
* Major Mode Hydra

View file

@ -229,6 +229,37 @@ The [[https://github.com/r0man/ruby-test-mode][ruby-test-mode]] project aims a r
"t A" '("test all" . ruby-test-run)
"t a" '("retest" . ruby-test-rerun)))
#+end_src
** Robe
The [[https://github.com/dgutov/robe][Robe project]] can be used instead of [[file:ha-programming.org::*Language Server Protocol (LSP) Integration][LSP]].
#+begin_src emacs-lisp
(use-package robe
:config
(ha-local-leader 'ruby-mode-map
"w" '(:ignore t :which-key "robe")
"ws" '("start" . robe-start))
;; The following leader-like keys, are only available when I have
;; started LSP, and is an alternate to Command-m:
:general
(:states 'normal :keymaps 'robe-mode-map
", w r" '("restart" . lsp-reconnect)
", w b" '("events" . lsp-events-buffer)
", w e" '("errors" . lsp-stderr-buffer)
", w q" '("quit" . lsp-shutdown)
", w l" '("load file" . ruby-load-file)
", l r" '("rename" . lsp-rename)
", l f" '("format" . lsp-format)
", l a" '("actions" . lsp-code-actions)
", l i" '("imports" . lsp-code-action-organize-imports)
", l d" '("doc" . lsp-lookup-documentation)))
#+end_src
Do we want to load Robe /automatically/?
#+begin_src emacs-lisp
(use-package robe :hook (ruby-mode . robe-mode))
#+end_src
** Bundler
The [[https://github.com/endofunky/bundler.el][Bundler project]] integrates [[https://bundler.io/][bundler]] to install a projects Gems.
@ -264,34 +295,6 @@ Seems that to understand and edit Cucumber /feature/ definitions, you need [[htt
** RSpec
https://github.com/pezra/rspec-mode
* LSP
The built-in Eglot package natively supports [[https://github.com/castwide/solargraph][Solargraph]] via =eglot-server-programs= (no extra package needed):
#+begin_src emacs-lisp
(use-package eglot
:straight nil
:after ruby-mode
:defer t
:hook (ruby-mode . eglot-ensure))
#+end_src
Keybindings for the eglot-based LSP sub-menu, wired into the =ruby-mode= local leader:
#+begin_src emacs-lisp
(use-package eglot
:straight nil
:after ruby-mode
:config
(ha-local-leader 'ruby-mode-map
"w" '(:ignore t :which-key "eglot")
"wD" '("disconnect" . eglot-shutdown)
"wR" '("reconnect" . eglot-reconnect)
"w?" '("describe" . eglot-show-workspace-configuration)
"wa" '("code actions" . eglot-code-actions)
"wo" '("organize imports" . eglot-code-action-organize-imports)
"wr" '("rename" . eglot-rename)
"w=" '("format buffer" . eglot-format-buffer)))
#+end_src
Need to install [[https://github.com/castwide/solargraph][Solargraph]] for the LSP server experience:
#+begin_src sh
gem install solargraph
@ -300,7 +303,29 @@ Or add it to your =Gemfile=:
#+begin_src ruby
gem 'solargraph', group: :development
#+end_src
Once installed, [[*eglot][eglot]] finds and talks to =solargraph= automatically; no further Emacs configuration is needed.
* XRef Interface with GNU Global
The [[http://www.gnu.org/software/global/][GNU Global]] has the ability to generate a tags file for large, multi-project Ruby code bases.
First, issue these two:
#+begin_src sh :dir ~/work/gourmet
find . -name .git | while read DOTGIT
do
REPO=$(dirname $DOTGIT)
(cd $REPO && git pull origin master)
done
find . -name "*.rb" > gtags.files
gtags --gtagslabel=new-ctags --file gtags.files
#+end_src
And now we need the GNU Global for Emacs, we are using the most up-to-date version of [[https://github.com/leoliu/ggtags][ggtags]].
#+begin_src emacs-lisp
(use-package ggtags
:hook ((ruby-mode . #'ggtags-mode)))
#+end_src
Careful observers will note that
* Technical Artifacts :noexport:
Let's =provide= a name so we can =require= this file:

View file

@ -68,10 +68,24 @@ The [[https://github.com/brotzeit/rustic][rustic project]] incorporates [[https:
#+begin_src emacs-lisp
(use-package rustic
:config
:init
(setq rustic-lsp-client 'eglot
rustic-analyzer-command '("/opt/homebrew/bin/rust-analyzer")
rustic-format-on-save t))
rustic-analyzer-command '("/opt/homebrew/bin/rust-analyzer"))
;; :bind (:map rustic-mode-map
;; ("M-j" . lsp-ui-imenu)
;; ("M-?" . lsp-find-references)
;; ("C-c C-c l" . flycheck-list-errors)
;; ("C-c C-c a" . lsp-execute-code-action)
;; ("C-c C-c r" . lsp-rename)
;; ("C-c C-c q" . lsp-workspace-restart)
;; ("C-c C-c Q" . lsp-workspace-shutdown)
;; ("C-c C-c s" . lsp-rust-analyzer-status)
;; ("C-c C-c e" . lsp-rust-analyzer-expand-macro)
;; ("C-c C-c d" . dap-hydra)
;; ("C-c C-c h" . lsp-ui-doc-glance))
:config
(setq rustic-format-on-save t))
#+end_src
** The Playground
@ -88,6 +102,7 @@ The [[https://github.com/dryman/toml-mode.el][toml-mode]] adds syntax highlighti
#+begin_src emacs-lisp
(use-package toml-mode)
#+end_src
* LSP
* Technical Artifacts :noexport:
Let's =provide= a name so we can =require= this file:

View file

@ -57,7 +57,7 @@ The [[https://www.nongnu.org/geiser/][geiser project]] attempts to be the interf
Do we need a Scheme work for Org Babel? According to [[https://orgmode.org/worg/org-contrib/babel/languages/ob-doc-scheme.html][this document]], we just need to make sure we add the =:session= variable to start the REPL.
#+begin_src emacs-lisp
(use-package ob-scheme
:straight (:type built-in)
:config
(add-to-list 'org-babel-load-languages '(scheme . t)))
@ -122,7 +122,7 @@ While Racket, as a Scheme, should work with Geiser (below), lets also get [[h
Can we get Racket working with Org?
#+begin_src emacs-lisp
(use-package ob-racket
:straight (:host github :repo "DEADB17/ob-racket")
;; :vc (:url "https://github.com/DEADB17/ob-racket")
:after org
:config
(add-to-list 'org-babel-load-languages '(racket . t)))

View file

@ -28,32 +28,6 @@ A literate programming file for helping me program.
Configuration for programming interfaces and workflows that behave similarly.
* General
The following work for all programming languages.
** Virtual Environments with Mise
The [[https://github.com/eki3z/mise.el][mise.el]] project overcomes the [[https://mise.jdx.dev/ide-integration.html#emacs][need for shims]] and other hoops when working with [[https://mise.jdx.dev/walkthrough.html][Mise]].
#+BEGIN_SRC emacs-lisp
(use-package mise
:straight (:type git :host github :repo "eki3z/mise.el")
:hook (after-init . global-mise-mode))
#+END_SRC
Now, per project, create a =mise.toml= that contains tools, environment variables, etc.
#+BEGIN_SRC toml
[env]
AWS_DEFAULT_REGION = "us-gov-east-1"
[tools]
python = {version='3', virtualenv='~/.venv/govcloud_lambda'}
#+END_SRC
Or create the entries by using CLI commands:
#+BEGIN_SRC sh :tangle no
mise set AWS_DEFAULT_REGION=us-gov-east-1
#+END_SRC
** Virtual Environments with direnv
Farm off commands into /virtual environments/:
#+begin_src emacs-lisp
@ -84,7 +58,7 @@ For all programming languages, I would like to now default to absolute line numb
While Emacs has options for viewing and moving around code, sometimes, we could /collapse/ all functions, and then start to expand them one at a time. For this, we could enable the built-in [[https://www.emacswiki.org/emacs/HideShow][hide-show feature]]:
#+begin_src emacs-lisp :tangle no
(use-package hide-show
:straight (:type built-in)
:init
(setq hs-hide-comments t
hs-hide-initial-comment-block t
@ -138,10 +112,10 @@ I appreciate calling =hi-lock-face-symbol-at-point= (or =highlight-symbol-at-poi
:config
(setq ahs-idle-interval 0.1)
(set-face-attribute ahs-face nil :foreground 'unspecified :background 'unspecified
(set-face-attribute ahs-face nil :foreground nil :background nil
:weight 'ultra-bold :slant 'italic)
(set-face-attribute ahs-plugin-default-face nil :foreground 'unspecified
:background 'unspecified :weight 'bold :slant 'normal))
(set-face-attribute ahs-plugin-default-face nil :foreground nil
:background nil :weight 'bold :slant 'normal))
#+end_src
Instead of calling =global-auto-highlight-symbol-mode=, we should just hook it to the =prog-mode=:
@ -238,7 +212,7 @@ Why use [[https://www.flycheck.org/][flycheck]] over the built-in =flymake=? Spe
#+begin_src emacs-lisp
(use-package flycheck
:straight (:host github :repo "flycheck/flycheck")
;; :vc (:url "https://github.com/flycheck/flycheck")
:init
(setq next-error-message-highlight t)
:bind (:map flycheck-error-list-mode-map
@ -370,7 +344,7 @@ Normally, you would need to add all the projects to directory clones in =~/src=
https://github.com/tree-sitter/tree-sitter-go
https://github.com/tree-sitter/tree-sitter-javascript
https://github.com/tree-sitter/tree-sitter-templ
https://github.com/tree-sitter-grammars/tree-sitter-yaml
https://github.com/ikatyang/tree-sitter-yaml
https://github.com/tree-sitter/tree-sitter-json
https://github.com/tree-sitter/tree-sitter-css
https://github.com/tree-sitter/tree-sitter-python
@ -422,11 +396,11 @@ In most cases,the =npm install= /usually/ works, but I may work on some sort of
#+end_src
At this point, we can now parse stuff using: =tree-sitter parse <source-code-file>=
**** Emacs Part
Note that Emacs already has the ability to download and install grammars, so following instructions from Mickey Petersens essay on [[https://www.masteringemacs.org/article/combobulate-structured-movement-editing-treesitter][using Tree-sitter with Combobulate]]:
However, Emacs already has the ability to download and install grammars, so following instructions from Mickey Petersens essay on [[https://www.masteringemacs.org/article/combobulate-structured-movement-editing-treesitter][using Tree-sitter with Combobulate]]:
#+begin_src emacs-lisp
(when (treesit-available-p)
(use-package treesit
:straight (:type built-in)
:preface
(setq treesit-language-source-alist
'((bash "https://github.com/tree-sitter/tree-sitter-bash")
@ -460,7 +434,7 @@ Note that Emacs already has the ability to download and install grammars, so fol
(toml "https://github.com/tree-sitter/tree-sitter-toml")
;; (tsx "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src")
;; (typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src")
(yaml "https://github.com/tree-sitter-grammars/tree-sitter-yaml")))
(yaml "https://github.com/ikatyang/tree-sitter-yaml")))
(defun mp-setup-install-grammars ()
"Install Tree-sitter grammars if they are absent."
@ -506,7 +480,7 @@ I like [[file:ha-programming-elisp.org::*Clever Parenthesis][Clever Parenthesis]
#+begin_src emacs-lisp
(when (treesit-available-p)
(use-package combobulate
:straight (:host github :repo "mickeynp/combobulate")
;; :vc (:url "https://github.com/mickeynp/combobulate")
:after treesit
:hook ((yaml-ts-mode . combobulate-mode)
;; (css-ts-mode . combobulate-mode)
@ -683,21 +657,86 @@ I have two different /jumping/ systems, the [[info:emacs#Xref][Xref interface]]
The [[https://microsoft.github.io/language-server-protocol/][LSP]] is a way to connect /editors/ (like Emacs) to /languages/ (like Lisp)… wait, no. While originally designed for VS Code and probably Python, we can abstract away [[https://github.com/davidhalter/jedi][Jedi]] and the [[http://tkf.github.io/emacs-jedi/latest/][Emacs integration to Jedi]] (and duplicate everything for Ruby, and Clojure, and…).
Emacs has two LSP projects, and while I have used [[LSP Mode]], but since I dont have heavy IDE requirements, I am finding that [[eglot]] to be simpler.
*** LSP
#+begin_src emacs-lisp
(use-package eglot
:straight nil
:commands eglot-ensure
(use-package lsp-mode
:commands (lsp lsp-deferred)
:init
;; Let's make lsp-doctor happy with these settings:
(setq gc-cons-threshold (* 100 1024 1024)
read-process-output-max (* 1024 1024)
company-idle-delay 0.0 ; Are thing fast enough to do this?
lsp-keymap-prefix "s-m")
:config
(setq eglot-autoshutdown t))
(global-set-key (kbd "s-m") 'lsp)
(ha-local-leader :keymaps 'prog-mode-map
"w" '(:ignore t :which-key "lsp")
"l" '(:ignore t :which-key "lsp")
"ws" '("start" . lsp))
;; The following leader-like keys, are only available when I have
;; started LSP, and is an alternate to Command-m:
:general
(:states 'normal :keymaps 'lsp-mode-map
", w r" '("restart" . lsp-reconnect)
", w b" '("events" . lsp-events-buffer)
", w e" '("errors" . lsp-stderr-buffer)
", w q" '("quit" . lsp-shutdown)
", w Q" '("quit all" . lsp-shutdown-all)
", l r" '("rename" . lsp-rename)
", l f" '("format" . lsp-format)
", l a" '("actions" . lsp-code-actions)
", l i" '("imports" . lsp-code-action-organize-imports)
", l d" '("doc" . lsp-lookup-documentation))
:hook ((lsp-mode . lsp-enable-which-key-integration)))
#+end_src
I will want to start adding commands under my =,= mode-specific key sequence leader, but in the meantime, all LSP-related keybindings are available under ~⌘-m~. See [[https://emacs-lsp.github.io/lsp-mode/page/keybindings/][this page]] for the default keybindings.
The [[https://github.com/flycheck/flycheck-eglot][flycheck-eglot]] package bridges eglot diagnostics into flycheck, so existing checkers and keybindings continue to work unchanged:
Using the [[https://github.com/seagle0128/doom-modeline][Doom Modeline]] to add notifications:
#+begin_src emacs-lisp
(use-package flycheck-eglot
:after (flycheck eglot)
(use-package doom-modeline
:config
(global-flycheck-eglot-mode 1))
(setq doom-modeline-lsp t
doom-modeline-env-version t))
#+end_src
**** UI
The [[https://github.com/emacs-lsp/lsp-ui][lsp-ui]] project offers much of the display and interface to LSP. Seems to make the screen cluttered.
#+begin_src emacs-lisp
(use-package lsp-ui
:commands lsp-ui-mode
:config
(setq lsp-ui-sideline-ignore-duplicate t
lsp-ui-sideline-show-hover t
lsp-ui-sideline-show-diagnostics t)
:hook (lsp-mode . lsp-ui-mode))
#+end_src
*** Company Completion
The [[https://github.com/tigersoldier/company-lsp][company-lsp]] offers a [[http://company-mode.github.io/][company]] completion backend for [[https://github.com/emacs-lsp/lsp-mode][lsp-mode]]:
#+begin_src emacs-lisp :tangle no
(use-package company-lsp
:config
(push 'company-lsp company-backends))
#+end_src
To options that might be interesting:
- =company-lsp-async=: When set to non-nil, fetch completion candidates asynchronously.
- =company-lsp-enable-snippet=: Set it to non-nil if you want to enable snippet expansion on completion. Set it to nil to disable this feature.
*** LSP iMenu
The [[https://github.com/emacs-lsp/lsp-ui/blob/master/lsp-ui-imenu.el][lsp-imenu]] project offers a =lsp-ui-imenu= function for jumping to functions:
#+begin_src emacs-lisp :tangle no
(use-package lsp-ui-imenu
:after lsp-ui
:config
(ha-local-leader :keymaps 'prog-mode-map
"g" '(:ignore t :which-key "goto")
"g m" '("imenu" . lsp-ui-imenu))
(add-hook 'lsp-after-open-hook 'lsp-enable-imenu))
#+end_src
** General Code Editing
*** iEdit
@ -1011,8 +1050,9 @@ So many configuration files to track:
("\\.setup.*\\'" . conf-space-mode)))
#+end_src
** JSON
Im interested in the [[https://github.com/emacs-tree-sitter/tree-sitter-langs][tree-sitter]] extensions for JSON, e.g. =json-ts-mode=, that comes with Emacs 29, but instead of /searching/ for particular data in a JSON buffer, what about whittling that data with [[https://jqlang.github.io/jq/][jq]]?
While interested in the [[https://github.com/emacs-tree-sitter/tree-sitter-langs][tree-sitter]] extensions for JSON, e.g. =json-ts-mode=, that comes with Emacs 29, Ill deal with what is bundled now.
However, what about taking a buffer of JSON data, and whittling it down with [[https://jqlang.github.io/jq/][jq]]?
#+begin_src emacs-lisp
(defun ha-json-buffer-to-jq (query)
"Runs JSON buffer with QUERY through an external `jq' program.
@ -1049,7 +1089,7 @@ This means, that some data like:
"data": {
"name": "Create And Wait for Service Image",
"description": "Creates a new Service Image using IMaaS",
"long_description": "This job creates a new service image with name ... blah, blah, blah",
"long_description": "This job creates a new yawxway service image with name yawxway-howard.abrams-test and docker-dev-artifactory.workday.com/dev/yawxway-service:latest docker url in development folder",
"job_id": "5e077245-0f4a-4dc9-b473-ce3ec0b811ba",
"state": "success",
"progress": "100",
@ -1093,14 +1133,12 @@ I can type, ~, j~ and then type =.data.timeout.seconds= and end up with:
300
#+end_src
** Markdown
Most projects =README= files and other documentation use [[https://jblevins.org/projects/markdown-mode/][markdown-mode]]. Note that the /preview/ uses =multimarkdown=, which should be /pre-installed/. For instance:
Most project =README= files and other documentation use [[https://jblevins.org/projects/markdown-mode/][markdown-mode]]. Note that the /preview/ is based on =multimarkdown=, when needs to be /pre-installed/, for instance:
#+begin_src sh
brew install multimarkdown
#+end_src
Also, I like Markdown to look like a word processor, similarly to my org files:
Also, I like Markdown is look like a word processor, similarly to my org files:
#+begin_src emacs-lisp
(use-package markdown-mode
:mode ((rx ".md" string-end) . gfm-mode)
@ -1125,8 +1163,51 @@ Also, I like Markdown to look like a word processor, similarly to my org files:
Note that the markdown-specific commands use the ~C-c C-c~ and ~C-c C-s~ prefixes.
*** Rendering with Pandoc
We call both the =markdown-command= and the =markdown-open-command= variables to render (and preview) a Markdown file (~C-c C-c o~), and calls the following scripts (which in turn, call =pandoc= as I depend on this for other org-related features):
Lets make sure that [[https://www.flycheck.org/en/latest/languages.html#markdown][markdown]] is proper using [[https://pypi.org/project/pymarkdownlnt/][PyMarkdown]]. First, get the script installed globally:
#+begin_src sh
pip install pymarkdown
#+end_src
And then we can use it. For some reason, the =pymarkdown= (which I need to use from work) doesnt seem to be part of the version of Flycheck available on Melpa, so…
#+begin_src emacs-lisp
(use-package markdown-mode
:after flycheck
:config
(setq flycheck-markdown-pymarkdown-config
(expand-file-name ".pymarkdown.yml" (getenv "HOME")))
(flycheck-may-enable-checker 'markdown-pymarkdown))
#+end_src
Ugh
#+begin_src emacs-lisp
(flycheck-def-config-file-var flycheck-markdown-pymarkdown-config
markdown-pymarkdown nil
:package-version '(flycheck . "34"))
(flycheck-define-checker markdown-pymarkdown
"Markdown checker using PyMarkdown.
See URL `https://pypi.org/project/pymarkdownlnt/'."
:command ("pymarkdown"
(config-file "--config" flycheck-markdown-pymarkdown-config)
"scan"
source)
:error-patterns
((error line-start
(file-name) ":" line
(? ":" column) ": " (id (one-or-more alnum))
": " (message) line-end))
:error-filter
(lambda (errors)
(flycheck-sanitize-errors
(flycheck-remove-error-file-names "(string)" errors)))
:modes (markdown-mode gfm-mode))
#+end_src
Both the =markdown-command= and the =markdown-open-command= variables are called to render (and preview) a Markdown file (~C-c C-c o~), and calls the following scripts (which in turn, call =pandoc= as I depend on this for other org-related features):
#+begin_src sh :tangle ~/bin/markdown :shebang "#!/usr/bin/env bash" :tangle-mode u+x
pandoc --to=html --from=gfm $*
@ -1145,250 +1226,6 @@ We call both the =markdown-command= and the =markdown-open-command= variables to
fi
#+end_src
*** PyMarkdown
My teams have standardized on “linting” markdown files using [[https://pypi.org/project/pymarkdownlnt/][PyMarkdown]], specifying a =.pymarkdown.yml= to configure what is allowed. For instance:
#+BEGIN_SRC yaml :tangle no
---
# Enable front-matter extensions, as Hugo needs them.
extensions:
front-matter:
enabled: true
plugins:
md013:
enabled: true
line_length: 80
heading_line_length: 80
code_block_line_length: 160
#+END_SRC
Flycheck ships the =markdown-pymarkdown= checker itself, so all it wants is the configuration file.
Note that PyMarkdown does /not/ look for =.pymarkdown.yml= on its own — it only reads a configuration passed with =--config=. Naming the file without a directory hands the search to flycheck, whose =flycheck-locate-config-file-functions= walks the ancestor directories with =locate-dominating-file= and then falls back to my home directory. A repository that defines its own rules therefore gets them, and everything else gets my defaults:
#+begin_src emacs-lisp
(use-package flycheck
:config
(setq flycheck-markdown-pymarkdown-config ".pymarkdown.yml"))
#+end_src
*** Remark
The [[https://github.com/remarkjs/remark][Remark]] project can transform markdown with plugins, evaluating and changing through its pipeline. This means I can apply [[file:ha-org.org::*Writegood][Writegood]] intelligently to a Markdown file, ignoring code blocks, etc. Prose is the /only/ job it has here, and [[*Remark and Flycheck][flycheck chains the two together]].
The =remark= executable itself lives globally, since flycheck calls it by name:
#+begin_src sh
npm install -g remark-cli
#+end_src
The /plugins/, however, can not. The configuration file below uses ESM =import= statements, and Node resolves those specifiers relative to /importing the file/, as globally installed packages are not on the path. The =~/.config/remark= directory has to be a self-contained package with its own =node_modules=, with a =package.json= file.
Note the ="type": "module"= entry belongs here too, otherwise Node parses each =.js= file as CommonJS, fails, and re-parses it as a module while complaining:
#+begin_src js :tangle ~/.config/remark/package.json :mkdirp yes
{
"name": "remark-config",
"private": true,
"type": "module",
"dependencies": {
"remark-frontmatter": "^5.0.0",
"remark-lint-write-good": "^1.2.0",
"unist-util-visit": "^5.1.0"
}
}
#+end_src
After tangling that the above file, we install the plugins /into the =.config/remark= directory/:
#+begin_src sh :dir ~/.config/remark
npm install
#+end_src
Lets create a /code-stripping/ plugin that removes all text but the prose of a Markdown document. We can then validate the prose with =write-good= on the results:
#+begin_src js :tangle ~/.config/remark/ignore-code.js :mkdirp yes
import { visit } from 'unist-util-visit';
// Bare URLs and autolinks land in `text` nodes, where write-good
// grades the path segments as if they were prose. A `[label](url)`
// needs no help here, since the url of a link node never reaches
// the prose checker anyway.
const URL_PATTERN = /\b(?:https?:\/\/|www\.)[^\s<>()[\]{}'"]+/gi;
/** Spaces of equal length, so line numbers and offsets survive. */
const blank = (text) => ' '.repeat(text.length);
export default function remarkIgnoreCode() {
return (tree) => {
visit(tree, ['code', 'inlineCode'], (node) => {
// Replaces code block and backtick content with spaces of
// equal length. This silences linters while preserving line
// numbers and offsets.
node.value = blank(node.value);
});
visit(tree, 'text', (node) => {
node.value = node.value.replace(URL_PATTERN, (match) => {
// Trailing punctuation stays, so sentence boundaries are
// not merged.
const url = match.replace(/[.,;:!?]+$/, '');
return blank(url) + match.slice(url.length);
});
});
};
}
#+end_src
A path like =/a/very/long/= reads as weasel words to =write-good=, so blanking the URL is what keeps =very= and =obviously= out of the report. The =[label](url)= form is left alone deliberately: its label /is/ prose worth grading, and its url was never visited. Trailing punctuation survives so that =…/foo.= still ends a sentence, rather than running it into the next one.
**** Configuring the pipeline
The other plugins have been made, so Remarks configuration file (=.config/remark/remarkrc.js=) pulls them in, and creates an ordered plugin pipeline. Note the absence of any =remark-preset-lint-*= entry: those presets check structure, which is PyMarkdowns half of the work, and they disagree with it often enough to be worth leaving out. Left in, they wanted every ATX heading rewritten as setext:
#+begin_src js :tangle ~/.config/remark/.remarkrc.js
import remarkFrontmatter from 'remark-frontmatter';
import remarkWriteGood from 'remark-lint-write-good';
import remarkIgnoreCode from './ignore-code.js';
export default {
plugins: [
// 1. Recognise YAML front matter, so its metadata is not
// read as prose
remarkFrontmatter,
// 2. Erase backticks/code blocks before the prose check
remarkIgnoreCode,
// 3. Check English writing quality, the sole
// job remark has, since PyMarkdown already covers
// structure and formatting.
remarkWriteGood
]
};
#+end_src
The order matters, since plugins run as transformers in sequence: =remarkIgnoreCode= has to blank the code out before =remarkWriteGood= gets a look at what is left.
And =remarkFrontmatter= earns its place, rather than being tidiness. Without it, a =---= delimiter usually turns the metadata into a setext heading, which =write-good= happens to skip — but only by luck. Put a blank line in the front matter and the first key becomes an ordinary paragraph, at which point =title:= gets graded as prose:
#+begin_src markdown :tangle no
---
title: This value is very obviously scanned as prose
description: short
---
#+end_src
Worth knowing what =write-good= looks at, since it only visits =paragraph= nodes: headings are never checked, and blockquotes are skipped on purpose, on the reasoning that quoted words are somebody elses.
Verify on the command line using the =rc-path= parameter:
#+begin_src sh :tangle no
remark --rc-path ~/.config/remark/.remarkrc.js README.md
#+end_src
**** Remark and Flycheck
Remarks default report groups messages under a file name header and gives each one a =line:column-line:column= range, which takes more regular expression than I care to write. Instead, lets hand it a [[https://github.com/vfile/vfile-reporter][vfile reporter]] of our own that prints one message per line in a shape flycheck parses with a single pattern:
#+begin_src js :tangle ~/.config/remark/flycheck-reporter.js
/**
* A vfile reporter that emits one message per line in a format that is
* trivial for Emacs' flycheck to parse with a single regular expression:
*
* LINE:COLUMN:SEVERITY:[ruleId] reason
*
* The file name is deliberately omitted; flycheck lints one buffer at a
* time and attributes patterns without a file name to that buffer, which
* avoids the mismatch between the buffer's directory and remark's cwd.
*/
export default function flycheckReporter(files) {
const lines = [];
for (const file of [files].flat()) {
for (const message of file.messages) {
// `fatal` is true for errors, false for warnings, null/undefined for info.
const severity =
message.fatal === true
? 'error'
: message.fatal === false
? 'warning'
: 'info';
const rule = message.ruleId || message.source || 'remark';
// Newlines would break the one-message-per-line contract.
const reason = String(message.reason).replace(/\s*\n\s*/g, ' ');
lines.push(
`${message.line || 1}:${message.column || 1}:${severity}:[${rule}] ${reason}`
);
}
}
return lines.join('\n');
}
#+end_src
Which we can check by hand:
#+begin_src sh
remark --no-color --no-stdout \
--rc-path ~/.config/remark/.remarkrc.js \
--report ~/.config/remark/flycheck-reporter.js README.md
#+end_src
Two flags there earn their keep. Remark writes the /transformed/ document to standard output by default, and since =remarkIgnoreCode= replaces code with whitespace, that output is a mangled copy of the file. The =--no-stdout= parameter throws it away, guaranteeing a stray =--output= can never overwrite the real file with it. Also =--no-color= keeps ANSI escapes out of what flycheck has to parse.
Note that both paths are absolute. Remark resolves a /reporter/ relative to the current directory, but a configuration file relative to the file being linted, so neither would be found from an arbitrary project directory.
Note that =flycheck-define-checker= only /defines/ the checker; the =add-to-list= is what puts it in rotation. The trailing =t= appends rather than prepends, which is the whole trick to chaining: flycheck picks the /first/ checker in =flycheck-checkers= that claims the buffer, so leaving =markdown-remark= behind the built-in =markdown-pymarkdown= lets PyMarkdown go first and remark follow it.
#+begin_src emacs-lisp
(use-package flycheck
:config
(flycheck-def-config-file-var flycheck-markdown-remark-config markdown-remark
(expand-file-name ".remarkrc.js" "~/.config/remark")
:package-version '(flycheck . "34"))
(defcustom flycheck-markdown-remark-reporter
(expand-file-name "flycheck-reporter.js" "~/.config/remark")
"Path to the vfile reporter that formats remark messages for flycheck."
:type 'string
:group 'flycheck)
(flycheck-define-checker markdown-remark
"A Markdown prose checker using remark and write-good.
See URL `https://github.com/remarkjs/remark'."
:command ("remark"
"--no-color"
"--no-stdout"
(config-file "--rc-path" flycheck-markdown-remark-config)
"--report" (eval flycheck-markdown-remark-reporter)
source)
:error-patterns
((error line-start line ":" column ":error:["
(id (one-or-more (not (any "]")))) "] " (message) line-end)
(warning line-start line ":" column ":warning:["
(id (one-or-more (not (any "]")))) "] " (message) line-end)
(info line-start line ":" column ":info:["
(id (one-or-more (not (any "]")))) "] " (message) line-end))
:modes (markdown-mode gfm-mode))
(add-to-list 'flycheck-checkers 'markdown-remark t)
;; Structure first, then prose, in one report.
(flycheck-add-next-checker 'markdown-pymarkdown 'markdown-remark)
;; Then terminology/consistency, since flycheck's built-in textlint
;; checker already declares markdown-mode/gfm-mode and picks the
;; @textlint/markdown plugin for them -- no new checker needed.
(flycheck-add-next-checker 'markdown-remark 'textlint t))
#+end_src
Naming =markdown-remark= as a plain symbol, rather than a =(level . checker)= cons cell, means the prose check runs whatever PyMarkdown found. Were it written =(warning . markdown-remark)=, a single formatting error would swallow the prose report along with it.
Since remark now runs =write-good= over the prose (and more carefully, as it skips the code), the [[file:ha-org.org::*Writegood][write-good]] checker no longer lists =markdown-mode= among its modes, and the two do not double up.
[[file:ha-org.org::*Textlint][Textlint]] runs last in the chain. Its bundled Markdown plugin parses the AST itself, so it skips fenced code without any =ignore-code.js=-style help, and =flycheck-textlint-plugin-alist= already maps =markdown-mode=/=gfm-mode= to =@textlint/markdown=.
*** Markdown and Polymode
Using [[https://polymode.github.io/][polymode]], lets add syntax coloring to Markdown code blocks similar to what we do with Org:
#+begin_src emacs-lisp
@ -1459,12 +1296,12 @@ While I don't like writing them, I can't get away from them. Check out the goodi
While filename extensions work fine most of the time, I don't like to pre-pend =.sh= to the shell scripts I write, and instead, would like to associate =shell-mode= with all files in a =bin= directory:
#+begin_src emacs-lisp
(use-package sh-mode
:straight (:type built-in)
:mode (rx (or (seq ".sh" eol)
"/bin/"))
:init
(setq sh-basic-offset 4
sh-indentation 4)
(setq sh-basic-offset 2
sh-indentation 2)
:config
(ha-auto-insert-file (rx (or (seq ".sh" eol)
"/bin/"))

View file

@ -28,7 +28,7 @@ A literate configuration for accessing remote systems.
[[https://www.emacswiki.org/emacs/TrampMode][Tramp]] allows almost all Emacs features to execute on a remote system.
#+begin_src emacs-lisp
(use-package tramp
:straight (:type built-in)
:config
;; Use remote PATH on tramp (handy for eshell).
@ -42,7 +42,7 @@ A literate configuration for accessing remote systems.
Will Schenk has [[https://willschenk.com/articles/2020/tramp_tricks/][a simple extension]] to allow editing of files /inside/ a Docker container:
#+begin_src emacs-lisp
(use-package tramp
:straight (:type built-in)
:config
(push '("docker" . ((tramp-login-program "docker")
(tramp-login-args (("exec" "-it") ("%h") ("/bin/sh")))
@ -50,19 +50,16 @@ Will Schenk has [[https://willschenk.com/articles/2020/tramp_tricks/][a simple e
(tramp-remote-shell-args ("-i") ("-c"))))
tramp-methods)
(defun ha-tramp-completion-docker (orig-fun &rest args)
"Advice for `tramp-completion-handle-file-name-all-completions`.
Return active Docker container names when completing for `/docker:`."
(if (equal (nth 1 args) "/docker:")
(defadvice tramp-completion-handle-file-name-all-completions
(around dotemacs-completion-docker activate)
"(tramp-completion-handle-file-name-all-completions \"\" \"/docker:\" returns
a list of active Docker container names, followed by colons."
(if (equal (ad-get-arg 1) "/docker:")
(let* ((command "docker ps --format '{{.Names}}:'")
(dockernames-raw (shell-command-to-string command))
(dockernames (split-string dockernames-raw "\n" t)))
dockernames)
;; Call the original function
(apply orig-fun args)))
(advice-add 'tramp-completion-handle-file-name-all-completions
:around #'ha-tramp-completion-docker))
(dockernames (split-string dockernames-raw "\n")))
(setq ad-return-value dockernames))
ad-do-it)))
#+end_src
Keep in mind you need to /name/ your Docker session, with the =—name= option. I actually do more docker work on remote systems (as Docker seems to make my fans levitate my laptop over the desk). Granted, the =URL= is a bit lengthy, for instance:
@ -75,7 +72,7 @@ Which means, I need to put it as a link in an org file.
#+begin_src emacs-lisp
(use-package tramp-sh
:after tramp
:straight (:type built-in)
:custom (tramp-use-ssh-controlmaster-options nil))
#+end_src
* Remote Terminals
@ -157,37 +154,18 @@ VTerm has an issue (at least for me) with ~M-Backspace~ not deleting the previou
#+end_src
The advantage of running terminals in Emacs is the ability to copy text without a mouse. For that, hit ~C-c C-t~ to enter a special copy-mode. If I go into this mode, I might as well also go into normal mode to move the cursor. To exit the copy-mode (and copy the selected text to the clipboard), hit ~Return~.
An odd use case I have: working in a Terminal session in one buffer window, and reading instructions in another. I want to copy commands from the document into the session using Avy to mark the command. Normally, I would use ~s-g~ to call [[file:ha-config.org::*Jump with Avy][avy-goto-char-timer]], and typing the /beginning of the command/, but before /jumping/, I press ~N~ to copy the line into the clipboard (kill-ring), and then /paste/ that into the Terminal using ~C-y~. One-too-many keystrokes.
Now, I can type: ~s-G~ (with the shift), select the line with Avy, and type ~N~ to have it copy/paste into the buffer:
#+BEGIN_SRC emacs-lisp
(use-package vterm
:config
(defun ha-vterm-avy-paste-line ()
"docstring"
(interactive)
(let ((buf (current-buffer)))
(call-interactively 'avy-goto-char-timer)
(when (equal buf (current-buffer))
(vterm-yank))))
(general-def 'vterm-mode-map "s-G" 'ha-vterm-avy-paste-line))
#+END_SRC
** Eat
While not as fast as [[https://github.com/akermu/emacs-libvterm][vterm]], the [[https://codeberg.org/akib/emacs-eat][Emulate a Terminal]] project (eat) is fast enough, and doesnt require a dedicate library that requires re-compilation. While offering [[https://elpa.nongnu.org/nongnu-devel/doc/eat.html][online documentation]], Im glad for an [[info:eat#Top][Info version]].
#+BEGIN_SRC emacs-lisp :tangle no
(use-package eat
:straight (:host codeberg :repo "akib/emacs-eat"
:files ("*.el" ("term" "term/*.el") "*.texi"
"*.ti" ("terminfo/e" "terminfo/efo/e/*")
("terminfo/65" "terminfo/65/*")
("integration" "integration/*")
(:exclude ".dir-locals.el" "*-tests.el")))
;; :straight (:host codeberg :repo "akib/emacs-eat"
;; :files ("*.el" ("term" "term/*.el") "*.texi"
;; "*.ti" ("terminfo/e" "terminfo/efo/e/*")
;; ("terminfo/65" "terminfo/65/*")
;; ("integration" "integration/*")
;; (:exclude ".dir-locals.el" "*-tests.el")))
;; :vc (:url "https://codeberg.org/akib/emacs-eat")
:commands (eat eat-make eat-project)
:bind (:map eat-semi-char-mode-map
("C-c C-t" . ha-eat-narrow-to-shell-prompt-dwim))
@ -237,6 +215,14 @@ Let's begin by defining some variables used for communication between the functi
See =ha-ssh-add-favorite-host= for easily adding to this list.")
#+end_src
Also, let's make it easy for me to change my default shell:
#+begin_src emacs-lisp
(defvar ha-shell "bash" ;; Eat works better with Bash/Zsh
;; (string-trim (shell-command-to-string "type -p fish"))
"The executable to the shell I want to use locally.")
#+end_src
** Terminal Abstractions
Could I abstract the different ways I start terminals in Emacs? The =ha-ssh-term= starts either a [[VTerm]]
or [[Eat]] terminals, depending on what is available. This replaces (wraps) the default [[help:make-term][make-term]].
@ -247,11 +233,11 @@ or [[Eat]] terminals, depending on what is available. This replaces (wraps) the
The PROGRAM, if non-nil, is executed, otherwise, this is `ha-shell'.
STARTFILE is the initial text given to the PROGRAM, and the
SWITCHES are the command line options."
(unless program (setq program ha-shell))
(cond
((fboundp 'vterm) (progn (vterm name)
(when program
(vterm-send-string (append program switches))
(vterm-send-return))))
(vterm-send-string (append program switches))
(vterm-send-return)))
((fboundp 'eat) (progn (switch-to-buffer
(apply 'eat-make (append (list name program startfile)
switches)))
@ -315,7 +301,7 @@ For the sake of my demonstrations, I use =ha-shell= to start a terminal with a p
default-directory)))
(buf-name (format "*%s*" win-name)))
(setq ha-latest-ssh-window-name buf-name)
(ha-make-term win-name))) ; Lisp-2 FTW!?
(ha-make-term win-name ha-shell))) ; Lisp-2 FTW!?
#+end_src
Now that Emacs can /host/ a Terminal shell, I would like to /programmatically/ send commands to the running terminal, e.g. =(ha-shell-send "ls *.py")= I would really like to be able to send and execute a command in a terminal from a script.
@ -340,6 +326,8 @@ Now that Emacs can /host/ a Terminal shell, I would like to /programmatically/ s
(t (progn
(insert command)
(term-send-input))))))
(ha-shell-send "exit")
#+end_src
Let's have a quick way to bugger out of the terminal:

View file

@ -311,6 +311,7 @@ Can we *see* our colors?
#+END_SRC
* Dark Theme
Lets make a /theme/:
#+BEGIN_SRC emacs-lisp
@ -322,20 +323,15 @@ Lets make a /theme/:
'hamacs
`(default ((t (:foreground ,default-fg :background ,default-bg))))
`(fringe ((t :background ,default-bg)))
`(tab-bar ((t :foreground ,default-fg :background ,default-bg)))
`(tab-line ((t :foreground ,default-fg :background ,default-bg)))
`(window-divider ((t :foreground "black")))
`(cursor ((t (:foreground ,gray-10 :background ,cursor))))
`(region ((t (:background ,region))))
`(hl-line ((t (:background ,gray-20))))
`(mode-line ((t (:background ,active :foreground "white"))))
`(mode-line-active ((t (:background ,active))))
`(mode-line-inactive ((t (:background ,inactive))))
`(which-func ((t (:foreground ,orange-lt))))
`(tab-bar ((t :foreground ,default-fg :background ,inactive :inherit variable-pitch)))
`(tab-line ((t :foreground ,default-fg :background ,inactive)))
`(tab-bar-tab ((t (:inherit variable-pitch :background ,active :weight bold))))
`(tab-bar-tab-inactive ((t (:inherit variable-pitch :background ,inactive :weight normal))))
`(doom-modeline-buffer-path ((t (:foreground ,almond))))
`(doom-modeline-buffer-file ((t (:foreground "white" :weight bold))))
@ -458,24 +454,7 @@ Lets make a /theme/:
`(elfeed-search-tag-face ((t (:foreground ,slate))))))
#+END_SRC
[[file:ha-theme-results.png]]
To update a feature live with all attributes, do something like:
#+BEGIN_SRC emacs-lisp :tangle no
(set-face-attribute 'tab-bar-tab-inactive nil
:inherit variable-pitch
:background "#462200")
#+END_SRC
Or, you can use the helper functions to change a feature directly:
#+BEGIN_SRC emacs-lisp :tangle no
(set-face-background 'tab-bar-tab-inactive "#462200")
#+END_SRC
* Technical Artifacts :noexport:
Let's =provide= a name so we can =require= this file:

View file

@ -1,373 +0,0 @@
#+title: Hammerspoon Configuration
#+author: Howard X. Abrams
#+date: 2025-11-24
#+filetags: emacs hamacs
#+lastmod: [2026-08-12 Wed]
A literate programming file for configuring Hammerspoon.
* Introduction
Ever since I got a Mac, Ive used various tools to /script/ it. With my limited number of applications, my UI needs are simple (I mean, what more do you need once Emacs is in full screen). That said, Ive been using [[https://hammerspoon.org][Hammerspoon]] for those few needs. While I refer to the [[https://www.hammerspoon.org/docs/index.html][standard documentation]], I often steal snippets of code from others.
Simple to use. Use =hs.execute= to run a script, =ha.alert.show= to print a small message on the screen, and for something longer:
#+BEGIN_SRC lua :tangle no
hs.notify.new({title="Hammerspoon", informativeText="Hello World"}):send()
#+END_SRC
** The Command Line
The =hs= binary talks to a running Hammerspoon over a Mach port, and that port only exists while the =hs.ipc= module is loaded. Without this line, the tool answers every request with /cant access Hammerspoon message port/, however well it is installed:
#+BEGIN_SRC lua
require("hs.ipc")
#+END_SRC
For instance, using the [[*Zoom][Zoom spoon]], I can toggle the mute button from a script:
#+BEGIN_SRC sh :tangle no
hs -c "spoon.Zoom:toggleMute()" # your Zoom spoon, from anywhere
#+END_SRC
Or callable from Emacs.
Other ideas:
1. Tail the Hammerspoon console in a terminal with =hs -C=, which shows anything called with =printf=.
2. While I already built out [[https://howardism.org/Technical/Emacs/beep-for-emacs.html][a notification system]], I could use Hammerspoon to replace =beep= CLI:
#+BEGIN_SRC sh
make build; hs -c 'hs.notify.new({title="Build", informativeText="done"}):send()'
#+END_SRC
3. Query Hammerspoon to query the system:
#+BEGIN_SRC bash :results replace raw output
hs -c 'return hs.wifi.currentNetwork()'
#+END_SRC
This should return =iTerm2=, or =Emacs= when running from my literate config:
#+BEGIN_SRC sh :results replace raw output
hs -c "return hs.application.frontmostApplication():name()"
#+END_SRC
Or screwing with my monitors:
#+BEGIN_SRC sh :results replace raw output
hs -c "return hs.screen.mainScreen():name()"
#+END_SRC
Or the volume level as a float value:
#+BEGIN_SRC sh :results replace raw output
hs -c "return hs.audiodevice.defaultOutputDevice():volume()"
#+END_SRC
Flags worth knowing, from the binary's usage text:
* =-i= interactive REPL
* =-q= quiet (only the result, ideal for scripting)
* =-c= repeatable
* =-A= auto-launch Hammerspoon if not running, and a bare file path to run a =.lua= file.
* Shortcut Keybindings
Ive created left and right ~Meh~ keys on my Moonlander keyboard:
- Left Meh Key: ~C-M-S-s~ (Control Option/Meta Command Shift)
- Right Meh Key: ~C-M-S~ (Control Option/Meta Shift)
To create specific key bindings for starting applications, I can:
#+BEGIN_SRC lua
----------------------------------------------------------------------
-- Launcher replaces iCanHazShortcuts
hs.hotkey.bind({"alt", "ctrl", "shift"}, "T", function()
hs.application.launchOrFocus("iTerm")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "S", function()
hs.application.launchOrFocus("Slack")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "F", function()
hs.application.launchOrFocus("Vivaldi")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "C", function()
-- hs.osascript.applescriptFromFile("~/bin/chrome.scr")
hs.execute("~/bin/chrome.scr")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "B", function()
hs.application.launchOrFocus("Microsoft Outlook")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "Z", function()
hs.application.launchOrFocus("zoom.us")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "Q", function()
hs.application.launchOrFocus("KeepassXC")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "G", function()
hs.application.launchOrFocus("Ghostty")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "W", function()
hs.application.launchOrFocus("VLC")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "E", function()
hs.execute("FOR_WORK=yes open -a /Applications/Emacs.app")
end)
-- Special Emacs Guys
-- Right Meh key:
hs.hotkey.bind({"alt", "ctrl", "shift"}, "X", function()
hs.execute("~/bin/emacs-capture")
end)
-- Left Meh key:
hs.hotkey.bind({"cmd", "alt", "ctrl", "shift"}, "X", function()
hs.execute("~/bin/emacs-capture-clock")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "M", function()
hs.execute("~/bin/emacs-capture-meeting")
end)
-- Current music system is actually in Emacs:
hs.hotkey.bind({"alt", "ctrl", "shift"}, "R", function()
hs.execute("/opt/homebrew/bin/emacsclient -s work -e '(ready-player-toggle-play-stop)'")
end)
#+END_SRC
* Zoom
/Spoons/ are library extensions to Hammerspoon: usally small Lua scripts, like =init.lua=, stored in the =Spoons= subdirectory. Installable from a =git clone=, typically. To use the Zoom spoon, clone it:
#+BEGIN_SRC sh :dir ~/.hammerspoon/Spoons :tangle no :results silent
git clone https://github.com/jpf/Zoom.spoon.git
#+END_SRC
I noticed that this library does most of its work by calling Zooms menus, and with the latest version of Zoom, they changed the /case/ of the menu, meaning that I needed to create a pull request.
From this [[https://developer.okta.com/blog/2020/10/22/set-up-a-mute-indicator-light-for-zoom-with-hammerspoon][nice essay]], we create a menu bar item that shows the status, as well as allowing me to click it to toggle:
#+BEGIN_SRC lua
zoomStatusMenuBarItem = hs.menubar.new(true)
zoomStatusMenuBarItem:setClickCallback(function()
spoon.Zoom:toggleMute()
end)
updateZoomStatus = function(event)
hs.printf("updateZoomStatus(%s)", event)
if (event == "from-running-to-meeting") then
zoomStatusMenuBarItem:returnToMenuBar()
elseif (event == "muted") then
zoomStatusMenuBarItem:setTitle("🔴")
elseif (event == "unmuted") then
zoomStatusMenuBarItem:setTitle("🟢")
elseif (event == "from-meeting-to-running") then
zoomStatusMenuBarItem:removeFromMenuBar()
end
end
#+END_SRC
Now we can load, instantiate it, as well as create a callback loop to call =updateZoomStatus=:
#+BEGIN_SRC lua
hs.loadSpoon("Zoom")
spoon.Zoom:setStatusCallback(updateZoomStatus)
spoon.Zoom:start()
#+END_SRC
And bind a key to the mute ability that works good for both keyboards:
#+BEGIN_SRC lua
hs.hotkey.bind({"cmd", "alt", "ctrl", "shift"}, "M", function()
spoon.Zoom:toggleMute()
end)
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "M", function()
spoon.Zoom:toggleMute()
end)
#+END_SRC
Lovely bit of code and shows the true power of Hammerspoon to fix the various app issues.
* Automatically Adjust Volume
Leaving my home office and hopping on the train can be socially awkward when my laptop suddenly screams, so if I leave my home, I just readjust the volume and turn it off:
#+BEGIN_SRC lua
----------------------------------------------------------------------
-- When leaving house, turn off the volume:
wifiWatcher = nil
homeSSID = "Intertubes"
workSSID = "workdaysecure"
hotspotSSID = "Pixie Dust"
lastSSID = hs.wifi.currentNetwork()
function ssidChangedCallback()
newSSID = hs.wifi.currentNetwork()
if newSSID == homeSSID and lastSSID ~= homeSSID then
-- joined our home WiFi network
hs.audiodevice.defaultOutputDevice():setVolume(25)
elseif newSSID == workSSID and lastSSID ~= workSSID then
-- joined our work WiFi network
hs.audiodevice.defaultOutputDevice():setVolume(0)
elseif newSSID == hotspotSSID and lastSSID ~= hotspotSSID then
-- joined our hotspot WiFi network
hs.audiodevice.defaultOutputDevice():setVolume(0)
elseif newSSID ~= homeSSID and lastSSID == homeSSID then
-- departed our home WiFi network
hs.audiodevice.defaultOutputDevice():setVolume(0)
end
lastSSID = newSSID
end
wifiWatcher = hs.wifi.watcher.new(ssidChangedCallback)
wifiWatcher:start()
#+END_SRC
Why yes, Im /looking for features/ to use Hammerspoon.
* Clocking out a Task
I want to use Orgs task clocking ability, but I often forget to /clock out/. This code allows me to clock out whenever my computer goes to sleep … which works well when closing the laptop lid:
#+BEGIN_SRC lua
function sleepWatch(eventType)
if (eventType == hs.caffeinate.watcher.systemWillSleep) then
if hs.application.find("Emacs") then
hs.execute("/opt/homebrew/bin/emacsclient --socket work --eval '(ha-clock-out)'")
end
end
end
sleepWatcher = hs.caffeinate.watcher.new(sleepWatch)
sleepWatcher:start()
#+END_SRC
Discovered a subtle bug, solving it meant assigning the =watcher= to a variable, rather than starting it and dropping the value. For instance, if I call the =start= this way:
#+BEGIN_SRC lua :tangle no
hs.caffeinate.watcher.new(sleepWatch):start()
#+END_SRC
Since nothing else refers to that object once the line finishes, Lua may collect it, and a collected watcher stops delivering events without a word about it.
Note if I want to do something for Wake:
#+BEGIN_SRC lua :tangle no
if (eventType == hs.caffeinate.watcher.systemDidWake) then
...
end
#+END_SRC
Oh, and here is a helper for [[file:laptop_keyboard.kbd][kanata]] to put the display to sleep:
#+BEGIN_SRC lua
hs.hotkey.bind({}, "F16", function()
hs.execute("pmset displaysleepnow")
end)
#+END_SRC
* Monitors
My company gave me a nice monitor … maybe a little too nice, as I dont care to shift my neck to the extreme sides (serious first-world problem), so here I can /center/ a window Keeping it my field of view:
#+BEGIN_SRC lua
----------------------------------------------------------------------
-- Centering a window on the large monitors at Work:
function centerWindow()
local win = hs.window.focusedWindow()
if not win then return end -- Safety check in case no window is focused
local app = win:application()
local screen = hs.screen.find("DELL P3424WE")
local maxBounds = screen:frame() -- This gets the absolute coordinates of that specific monitor
-- 2. Define your desired size
local desiredW = 2200
local desiredH = 1470
if app then
local name = app:name()
if name == "Slack" or name == "iTerm2" then
desiredH = 1200
end
end
-- 3. Calculate relative position based on the external monitor's bounds
-- This centers the window perfectly on the target screen
local f = {
x = maxBounds.x + ((maxBounds.w - desiredW) / 2),
y = maxBounds.y, -- + ((maxBounds.h - desiredH) / 2),
w = desiredW,
h = desiredH
}
win:setFrame(f)
hs.alert.show("Centered Window")
end
hs.hotkey.bind({"cmd", "alt", "ctrl", "shift"}, "Y", centerWindow)
#+END_SRC
Similarly, I sometimes want a window as large as possible, but flushed to the edges of the monitor (a maximized frame, not macOS' native /full screen/):
#+BEGIN_SRC lua
----------------------------------------------------------------------
-- Making a window as large as possible on its current monitor:
function fullWindow()
local win = hs.window.focusedWindow()
if not win then return end -- Safety check in case no window is focused
local screen = win:screen()
win:setFrame(screen:frame())
hs.alert.show("Full Window")
end
hs.hotkey.bind({"cmd", "alt", "ctrl", "shift"}, "U", fullWindow)
#+END_SRC
* Auto Reload Configuration
Whenever my configuration file is altered (or with a ~Meh-R~ key), I reload the configuration:
#+BEGIN_SRC lua
----------------------------------------------------------------------
-- Automatically reload the Hammerspoon configuration
hs.hotkey.bind({"cmd", "alt", "ctrl", "shift"}, "R", function()
hs.reload()
hs.alert.show("Reloaded Hammerspoon Config")
end)
function reloadConfig(files)
doReload = false
for _,file in pairs(files) do
if file:sub(-4) == ".lua" then
doReload = true
end
end
if doReload then
hs.reload()
end
end
configWatcher = hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", reloadConfig)
configWatcher:start()
#+END_SRC
Every watcher in this file now lives in a variable of its own, for the reason described under [[*Clocking out a Task][clocking out]]. This one failing is the quietest of the three, as a dead configuration watcher looks exactly like a file that saved without incident.
* Technical Artifacts :noexport:
#+BEGIN_SRC lua
hs.alert.show("Hammerspoon Configuration")
#+END_SRC
#+DESCRIPTION: Literate Hammerspoon configuration
#+PROPERTY: header-args:sh :tangle no
#+PROPERTY: header-args:lua :tangle ~/.hammerspoon/init.lua
#+PROPERTY: header-args :results none :eval no-export :comments no mkdirp yes
#+OPTIONS: num:nil toc:nil 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:
# jinx-local-words: "Lua"
# End:

View file

@ -23,15 +23,6 @@ gpg --homedir ~/.emacs.d/elpa/gnupg --receive-keys 066DAFCB81E42C40
cat > "$HAMACS_DEST/early-init.el" <<EOF
;;; early-init.el --- Hamacs Early Init -*- lexical-binding: t; -*-
;;
;; ▄████████ ▄▄▄▄███▄▄▄▄ ▄████████ ▄████████ ▄████████
;; ███ ███ ▄██▀▀▀███▀▀▀██▄ ███ ███ ███ ███ ███ ███
;; ███ █▀ ███ ███ ███ ███ ███ ███ █▀ ███ █▀
;; ▄███▄▄▄ ███ ███ ███ ███ ███ ███ ███
;; ▀▀███▀▀▀ ███ ███ ███▀███████████ ███ ▀███████████
;; ███ █▄ ███ ███ ███ ███ ███ ███ █▄ ███
;; ███ ███ ███ ███ ███ ███ ███ ███ ███ ▄█ ███
;; ██████████ ▀█ ███ █▀ ███ █▀ ████████▀ ▄████████▀
;;
;;; Commentary:
;;
;; This is my early Emacs configuration file. See init.el for the real
@ -78,14 +69,6 @@ echo "Created $HAMACS_DEST/early-init.el"
cat > "$HAMACS_DEST/init.el" <<EOF
;;; init.el --- Hamacs Init -*- lexical-binding: t; -*-
;;
;; :::::::::: :::: :::: ::: :::::::: ::::::::
;; :+: +:+:+: :+:+:+ :+: :+: :+: :+: :+: :+:
;; +:+ +:+ +:+:+ +:+ +:+ +:+ +:+ +:+
;; +#++:++# +#+ +:+ +#+ +#++:++#++: +#+ +#++:++#++
;; +#+ +#+ +#+ +#+ +#+ +#+ +#+
;; #+# #+# #+# #+# #+# #+# #+# #+# #+#
;; ########## ### ### ### ### ######## ########
;;
;;; Commentary:
;;
;; This is my Emacs Bootloader. Simply put, I initialize the package
@ -96,8 +79,23 @@ cat > "$HAMACS_DEST/init.el" <<EOF
(defvar hamacs-source-dir "$HAMACS_DIR" "Where we be.")
(defvar hamacs-package-repos
(file-name-concat user-emacs-directory "repos")
"Repository location for all packages.")
(defun load-path-repo (repo)
(expand-file-name repo hamacs-package-repos))
(defun hamacs-package-repos-update-load-path ()
"Update the \`load-path' with clone repositories."
(interactive)
(dolist (repo (directory-files hamacs-package-repos 'full "[A-z].*"))
(add-to-list 'load-path repo nil 'string-equal)))
(hamacs-package-repos-update-load-path)
;; Bug fixes for ORG (there always seems to be something):
(defvar native-comp-deferred-compilation-deny-list nil)
;; (defvar native-comp-deferred-compilation-deny-list nil)
;; Allow the installation of unsigned packages, but verify the
;; signature if possible:
@ -115,38 +113,7 @@ cat > "$HAMACS_DEST/init.el" <<EOF
(add-to-list 'package-archives
'("elpa-dev" . "https://elpa.gnu.org/devel/"))
;; Configure straight https://github.com/raxod502/straight.el#getting-started
(defvar bootstrap-version)
(let ((bootstrap-file
(expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
(bootstrap-version 6))
(unless (file-exists-p bootstrap-file)
(with-current-buffer
(url-retrieve-synchronously
"https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"
'silent 'inhibit-cookies)
(goto-char (point-max))
(eval-print-last-sexp)))
(load bootstrap-file nil 'nomessage))
(straight-use-package 'use-package)
;; While that enables the :straight t extension to use-package, let's just have that be the default:
(use-package straight
:custom (straight-use-package-by-default t
straight-default-vc 'git)
:init
;; Eglot and its dependencies ship with Emacs. Tell straight to treat them as
;; built-in so nothing (e.g. flycheck-eglot's dependency header) re-pulls the
;; GNU-ELPA copies, which would shadow the built-ins and trip eglot's
;; `require-with-check' ("Feature `project' is now provided by a different file").
(setq straight-built-in-pseudo-packages
(append '(eglot project flymake xref jsonrpc external-completion eldoc)
straight-built-in-pseudo-packages)))
;; See the details in https://dev.to/jkreeftmeijer/emacs-package-management-with-straight-el-and-use-package-3oc8
(setq use-package-always-ensure nil)
(use-package org
;; TODO: Using the latest org-mode

View file

@ -1,132 +0,0 @@
;; -*- mode:lisp; -*-
;;
;; Here is a keyboard setup for an Apple Macbook keyboard to work with
;; "home row mods" where holding down keys on the home row can act
;; like a modifier, so holding down `d` and hitting `p` results in a
;; capital `P` being entered.
;;
;; This also includes a dedicated Hyper and Mega modifier layers,
;; where holding down the `m' key and striking `a' creates a
;; Control+Shift+Option+a key combination. This is useful for fancy
;; hotkeys for Hammerspoon and the like.
;;
;; Installation:
;; brew install kanata
;;
;; Running:
;; sudo kanata --cfg ~/src/hamacs/laptop_keyboard.kbd
(deflocalkeys-macos
ì 13)
;; While this works on both my Macbook running MacOS and Linux, I have
;; yet to figure out how to get the same file to work in both, as I
;; have to modify the code in the `defcfg' section below.
(defcfg
;; linux-dev-names-include ("Apple Internal Keyboard / Trackpad")
macos-dev-names-include ("Apple Internal Keyboard / Trackpad"))
;; What keys are we re-defining? This is my Macbook Pro layouts, as
;; both laptops are this hardware.
;;
;; Notice lmet/rmet = Apple's Command Key
;; lalt/ralt = Option, or a real meta key
(defsrc
esc f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12
` 1 2 3 4 5 6 7 8 9 0 - = bspc
tab q w e r t y u i o p [ ] \
caps a s d f g h j k l ; ' ret
lsft z x c v b n m , . / rsft
fn lctl lalt lmet spc rmet ralt)
(defvar
tap-time 200
hold-time 200)
;; Also includes a symbol layer accessible while holding down either
;; the `h' or `g' keys.
(defalias
;; Home row mods
a (tap-hold $tap-time $hold-time a lmet)
s (tap-hold $tap-time $hold-time s lctl)
d (tap-hold $tap-time $hold-time d lsft)
f (tap-hold $tap-time $hold-time f lalt)
g (tap-hold $tap-time $hold-time g (layer-while-held SYMBOLS))
h (tap-hold $tap-time $hold-time h (layer-while-held SYMBOLS))
j (tap-hold $tap-time $hold-time j ralt)
k (tap-hold $tap-time $hold-time k rsft)
l (tap-hold $tap-time $hold-time l rctl)
; (tap-hold $tap-time $hold-time ; rmet)
caps (tap-hold $tap-time $hold-time esc lctl)
;; ----------------------------------------------------
;; HYPER ALIASES (Shift + Ctrl + Option)
;; ----------------------------------------------------
hq (multi lsft lctl lalt q) hw (multi lsft lctl lalt w) he (multi lsft lctl lalt e) hr (multi lsft lctl lalt r) ht (multi lsft lctl lalt t)
hy (multi lsft lctl lalt y) hu (multi lsft lctl lalt u) hi (multi lsft lctl lalt i) ho (multi lsft lctl lalt o) hp (multi lsft lctl lalt p)
ha (multi lsft lctl lalt a) hs (multi lsft lctl lalt s) hd (multi lsft lctl lalt d) hf (multi lsft lctl lalt f) hg (multi lsft lctl lalt g)
hh (multi lsft lctl lalt h) hj (multi lsft lctl lalt j) hk (multi lsft lctl lalt k) hl (multi lsft lctl lalt l) h; (multi lsft lctl lalt ;)
hz (multi lsft lctl lalt z) hx (multi lsft lctl lalt x) hc (multi lsft lctl lalt c) hv (multi lsft lctl lalt v) hb (multi lsft lctl lalt b)
hn (multi lsft lctl lalt n) hm (multi lsft lctl lalt m) h, (multi lsft lctl lalt ,) h. (multi lsft lctl lalt .) h/ (multi lsft lctl lalt /)
;; ----------------------------------------------------
;; MEGA ALIASES (Shift + Ctrl + Option + Cmd)
;; ----------------------------------------------------
mq (multi lsft lctl lalt lmet q) mw (multi lsft lctl lalt lmet w) me (multi lsft lctl lalt lmet e) mr (multi lsft lctl lalt lmet r) mt (multi lsft lctl lalt lmet t)
my (multi lsft lctl lalt lmet y) mu (multi lsft lctl lalt lmet u) mi (multi lsft lctl lalt lmet i) mo (multi lsft lctl lalt lmet o) mp (multi lsft lctl lalt lmet p)
ma (multi lsft lctl lalt lmet a) ms (multi lsft lctl lalt lmet s) md (multi lsft lctl lalt lmet d) mf (multi lsft lctl lalt lmet f) mg (multi lsft lctl lalt lmet g)
mh (multi lsft lctl lalt lmet h) mj (multi lsft lctl lalt lmet j) mk (multi lsft lctl lalt lmet k) ml (multi lsft lctl lalt lmet l) m; (multi lsft lctl lalt lmet ;)
mz (multi lsft lctl lalt lmet z) mx (multi lsft lctl lalt lmet x) mc (multi lsft lctl lalt lmet c) mv (multi lsft lctl lalt lmet v) mb (multi lsft lctl lalt lmet b)
mn (multi lsft lctl lalt lmet n) mm (multi lsft lctl lalt lmet m) m, (multi lsft lctl lalt lmet ,) m. (multi lsft lctl lalt lmet .) m/ (multi lsft lctl lalt lmet /)
m (tap-hold $tap-time $hold-time m (layer-while-held HYPER))
v (tap-hold $tap-time $hold-time v (layer-while-held MEGA)))
;; The base layer is fairly normal, except we all out aliases defined
;; above, os the @a is both a `tap-hold' feature as well as a regular
;; `a` key:
(deflayer base
esc f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12
` 1 2 3 4 5 6 7 8 9 0 - = bspc
tab q w e r t y u i o p [ ] \
@caps @a @s @d @f @g @h @j @k @l @; ' ret
lsft z x c @v b n @m , . / rsft
@h lctl lalt lmet spc rmet ralt)
;; The other layer is our `symbols' which allows me to hold down the
;; `g' key to invoke a magical VI-like h/j/k/l arrow keys:
(deflayer SYMBOLS
esc 🔅 🔆 F3 F4 f5 f16 ◀◀ ▶⏸ ▶▶ 🔇 🔉 🔊
_ f11 f12 f13 f14 f15 f16 f17 f18 f19 f20 _ _ _
_ S-1 S-2 { } S-\ F16 F17 F18 F19 pgup _ _ _
_ S-3 S-4 S-9 S-0 ` left down up right pgdn _ _
_ S-5 S-6 [ ] S-` F11 F12 F13 F14 F15 _
_ _ _ _ _ _ _)
;; The other layer is our `magic' which allows me to hold down the
;; `m' key to invoke a magical VI-like h/j/k/l arrow keys:
(deflayer HYPER
_ _ _ _ _ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _ _ _ _ _ _
_ @hq @hw @he @hr @ht @hy @hu @hi @ho @hp _ _ _
_ @ha @hs @hd @hf @hg @hh @hj @hk @hl @h; _ _
_ @hz @hx @hc @hv @hb @hn @hm _ _ _ _
_ _ _ _ _ _ _)
(deflayer MEGA
_ _ _ _ _ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _ _ _ _ _ _
_ @mq @mw @me @mr @mt @my @mu @mi @mo @mp _ _ _
_ @ma @ms @md @mf @mg @mh @mj @mk @ml @m; _ _
_ @mz @mx @mc @mv @mb @mn @mm _ _ _ _
_ _ _ _ _ _ _)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 KiB

After

Width:  |  Height:  |  Size: 308 KiB

View file

@ -2,4 +2,4 @@
# name: title
# key: title
# --
#+TITLE: ${0}
#+title: ${0}

105
tmux.org
View file

@ -1,105 +0,0 @@
#+title: Tmux Configuration
#+author: Howard X. Abrams
#+date: 2026-01-15
#+filetags: emacs hamacs
#+lastmod: [2026-01-15 Thu]
I use [[https://github.com/tmux/tmux/wiki][tmux]] infrequently as I'm usually running a bunch of Terminals from within Emacs. Laugh all you want, but Emacs is my terminal multiplexer. That said, frequent network connectivity and long-running jobs make a =tmux= helpful on a remote server.
* Remote Installation
Log in an install =tmux= on a RedHat-based system with a =yum= command:
#+BEGIN_SRC sh :dir /ssh:docker:
sudo yum install -y tmux
#+END_SRC
And on my Ubuntu systems:
#+BEGIN_SRC sh :dir /ssh:docker:
sudo dpkg install tmux
#+END_SRC
* Remote Configuration
Copy the following to =$HOME/~/.tmux.conf=:
#+BEGIN_SRC conf :tangle /ssh:docker:/home/vagrant/.tmux.conf
set-option -g prefix C-`
set -g status-right "| docker "
set-option -g allow-rename off
# Start windows and panes at 1, not 0
set -g base-index 1
set -g pane-base-index 1
# Bind function keys.
bind -n F1 select-window -t 1
bind -n F2 select-window -t 2
bind -n F3 select-window -t 3
bind -n F4 select-window -t 4
bind -n F5 select-window -t 5
bind -n F6 select-window -t 6
bind -n F7 select-window -t 7
bind -n F8 select-window -t 8
bind -n F9 select-window -t 9
bind -n F10 select-window -t 10
#+END_SRC
Not sure why =screen= and =tmux= chose such awful prefix keybindings as ~C-a~ and ~C-b~, so I change this to ~C-`~.
I'm not a tmux power user, as changing windows is about all I do, so I bind the function keys to change tabs.
The following =~/.bash_aliases= enhance tmux:
#+BEGIN_SRC sh :tangle /ssh:docker:/home/vagrant/.bash_aliases
#!/bin/sh
# The following aliases have been installed on this machine.
# - `nw` to create a new Tmux window. Give er a command.
alias nw='tmux new-window'
# - `root` for a TMUX window as the root user.
alias root='nw -n root "sudo su -" '
#+END_SRC
When using Docker sessions on the remote system, the =go= and =logs= opens a new window:
#+BEGIN_SRC sh
# - `go` to connect to a docker container
function go {
nw -n "$1" "sudo docker exec -it $1 bash"
}
# - `logs` for displaying the logs from a container
function logs {
nw -n $1-logs "sudo docker logs -f $*"
}
#+END_SRC
After writing a number of helper aliases, I find it helpful to see those aliases when I roll into a remote server, so put this in the =.bashrc= file:
#+BEGIN_SRC sh
if [ "$HOME/.bash_aliases" ]
then
source "$HOME/.bash_aliases"
grep '^# ' "$HOME/.bash_aliases" | sed 's/^# *//'
echo
fi
#+END_SRC
Note: this can interfere with Tramp.
* Local Access
For a collection of long-running and highly verbose programs (like a remote Anisble run), I use iTerm on a MacOS with a special binding to create Mac-widgets in tmux.
#+BEGIN_SRC sh
ssh -t laptop 'tmux -CC attach || tmux -CC'
#+END_SRC
#+DESCRIPTION: Code to be used for configuring Tmux.
#+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:nil 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

View file

@ -21,7 +21,6 @@ Regardless, I keep my shell configuration conspicuously light.
Lets create the following files, and the configuration below will be injected into one of them:
- =~/.zshenv= :: Usually run for every zsh
- =~/.zprofile= :: Usually run for login shells (this includes the system-wide =/etc/zprofile=)
- =~/.zshrc= :: Run for interactive shells … default file when tangling
- =~/.zlogin= :: Run for login shells … seems to run as often as =.zshrc=
@ -44,24 +43,8 @@ Lets create the following files, and the configuration below will be injected
* Path
The all important =PATH= environment variable, needs my special =bin= directory.
#+BEGIN_SRC zsh :tangle ~/.zshenv
export PATH=$HOME/bin:$HOME/.local/bin:$PATH
#+END_SRC
My Apple Macbook screws up my =PATH= by having =/etc/profile= (that runs after my =~/.zshenv=) /pre-pend/ system directories like =/bin= and =/usr/bin= /after/ Ive set up my =PATH= environment variable. So, in my own =.zprofile= (which runs afterwards), I reverse it using the lovely =tac= program:
#+BEGIN_SRC sh :tangle ~/.zprofile :shebang #!/bin/zsh
if [[ -f /etc/zprofile ]]
then
# Reverse the PATH variable
reversed_path=$(echo $PATH | tr ':' '\n' | tac | tr '\n' ':')
# Reset the path after removing the trailing colon:
export PATH=${reversed_path%:}
# Output the reversed PATH
# echo "Reversed PATH: $reversed_path"
fi
#+BEGIN_SRC zsh :export ~/.zshenv
export PATH=$HOME/bin:/usr/local/bin:$PATH
#+END_SRC
* Options
@ -111,10 +94,7 @@ setopt MENU_COMPLETE
When using Homebrew on a Mac, we need to add its =PATH=:
#+BEGIN_SRC zsh :tangle ~/.zshenv
if [[ -d /opt/homebrew ]]
then
eval $(/opt/homebrew/bin/brew shellenv zsh)
fi
eval $(/opt/homebrew/bin/brew shellenv zsh)
#+END_SRC
This adds the following environment variables, along with expanding the =PATH=.
@ -218,35 +198,24 @@ The [[https://github.com/zsh-users/zsh-syntax-highlighting][ZShell Syntax Highli
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting
#+END_SRC
Using the =colorize= plugin (see [[Plugins][plugins section below]]), we customize with this variable setting:
#+BEGIN_SRC zsh
export ZSH_COLORIZE_STYLE="coffee"
#+END_SRC
** Language Support
Anything special for particular languages.
*** Python
Not overly impressed, for to get =pyenv= to work, we need to add this code:
#+BEGIN_SRC zsh :tangle ~/.zshenv
#+BEGIN_SRC zsh
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)"
#+END_SRC
And call the =pyenv= to initialize it:
#+BEGIN_SRC zsh
# eval "$(pyenv init --path)"
#+END_SRC
** Plugins
Configure the plugins, making sure to not use =git=, as the aliases are a pain to remember when I already have a superior Git interface in Emacs.
- [[https://github.com/hsienjan/colorize][colorize]] :: syntax-highlight file contents, so install [[https://pygments.org/download/][Pygments]] first. Then call =ccat= and =cless= (see [[Aliases]]).
- [[https://github.com/ptavares/zsh-direnv][direnv]] :: to support the [[https://direnv.net/][direnv]] virtual environment project.
- [[https://github.com/ohmyzsh/ohmyzsh/blob/master/plugins/mise/mise.plugin.zsh][mise]] :: to support the [[https://mise.jdx.dev/getting-started.html][mise]] virtual environment and tool project (instead of direnv). See the [[https://mise.jdx.dev/walkthrough.html][walk-through]].
- [[https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/gnu-utils][gnu-utils]] :: bind the GNU flavor for standard utils, like =gfind= to the normal version, e.g. =find=.
- [[https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/iterm2][iterm2]] :: while fully configured below, configures the interaction with the MacOS application, [[https://www.iterm2.com/][iTerm2]].
- [[https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/macos][macos]] :: adds new functions that work better with MacOS terminals and the Finder. I like:
@ -259,7 +228,7 @@ Configure the plugins, making sure to not use =git=, as the aliases are a pain t
To have a plugin /install/, add its name to the =plugins= array variable /before/ we =source= the OMZ script:
#+begin_SRC zsh
plugins=(colorize direnv gnu-utils iterm2 macos mise virtualenv zbell zsh-syntax-highlighting)
plugins=(colorize direnv gnu-utils iterm2 macos pyenv virtualenv zbell zsh-syntax-highlighting)
#+END_SRC
Notice the =iterm2= plugin as well as the =macos= plugins that would be nice to figure out how to make them optionally added (although I believe they check themselves).
@ -273,26 +242,6 @@ The trick is to install some base-level plugins, and then, on my work computer,
fi
#+END_SRC
I would like to have a history /per project/, so that when I start a session for a project, my history has where I left off /for that project/ and not everything. I guess Im not the only one who [[https://github.com/ivan-cukic/zsh-per-project-history][thought of this idea]]. We first need to add his plugin to the list of supplied plugins, so do this once:
#+BEGIN_SRC zsh :tangle no
git clone https://github.com/ivan-cukic/zsh-per-project-history ~/.oh-my-zsh/plugins/per-project-history
#+END_SRC
His idea is to have a variable array, =PER_PROJECT_HISTORY_TAGS= that lists files that should identify the start of a project, and while his default seems sufficient, I am not found of the spammy message, so:
#+BEGIN_SRC zsh
declare -a PER_PROJECT_HISTORY_TAGS
export PER_PROJECT_HISTORY_TAGS=(.envrc .git)
declare -r PER_PROJECT_HISTORY_TAGS
#+END_SRC
we just need to add =per-history= to the list of plugins:
#+BEGIN_SRC zsh
plugins+=(per-project-history)
#+END_SRC
Now that Ive filled in the =plugins= variable, load OMZ and the plugins:
#+BEGIN_SRC zsh
@ -325,80 +274,6 @@ Oh use the absolute /over-the-top/ bling associated with Oh My Zshells /theme
#+END_SRC
I keep the prompt simple since all of the /gunk/ we typically put in a prompt is better placed in [[https://iterm2.com/documentation-status-bar.html][iTerm2's Status Bar]].
* Homebrew
When using Homebrew on a Mac, we need to add its =PATH= and environment variables. This is typically done by running the command:
#+BEGIN_SRC zsh :tangle no
eval $(brew shellenv zsh)
#+END_SRC
We want to add the path and environment variables into the =~/.zshenv= file, but this file should not contain any logic or code. So, lets run the command /from Emacs/, and store the results in the file.
The full script to run is:
#+BEGIN_SRC zsh :tangle no :results file :file ~/.zshenv_brew
echo '# -*- mode:sh; -*-'
if which brew >/dev/null
then
if [[ -d /opt/homebrew ]]
then
/opt/homebrew/bin/brew shellenv zsh
else
brew shellenv zsh
fi
fi
#+END_SRC
Seems that if I want the GNU versions (instead of the old ones supplied by Apple), I have to do it myself:
#+BEGIN_SRC zsh :tangle no :results file :file ~/.zshenv_gnu
echo '# -*- mode:sh; -*-'
if which brew >/dev/null
then
for PKG in binutils gettext unzip openssl texinfo mysql-client openjdk
do
if PKG_INSTALL=$(brew --prefix $PKG)
then
echo export PATH=$PKG_INSTALL/bin:'$PATH'
fi
done
for PKG in coreutils ed findutils gnu-indent gnu-sed gnu-tar grep make
do
if PKG_INSTALL=$(brew --prefix $PKG)
then
echo export PATH=$PKG_INSTALL/libexec/gnubin:'$PATH'
fi
done
fi
#+END_SRC
And linking all the GNU libraries:
#+BEGIN_SRC zsh :tangle no :results file :file ~/.zshenv_lib
echo '# -*- mode:sh; -*-'
if which brew >/dev/null
then
for PKG in readline openssl xz binutils ctags libgccjit imagemagick
do
if PKG_INSTALL=$(brew --prefix $PKG)
echo export LDFLAGS=\"-L$PKG_INSTALL/lib '$LDFLAGS'\"
echo export CPPFLAGS=\"-I$PKG_INSTALL/include '$CPPFLAGS'\"
done
echo export LDFLAGS=\"'$LDFLAGS' -L$(brew --prefix)/lib\"
echo export CPPFLAGS=\"'$CPPFLAGS' -I$(brew --prefix)/include\"
fi
#+END_SRC
And pull in all the results into the =~/.zshenv= file (why yes, this could be inlined):
#+BEGIN_SRC zsh :tangle ~/.zshenv
[[ -f $HOME/.zshenv_brew ]] && source $HOME/.zshenv_brew
[[ -f $HOME/.zshenv_gnu ]] && source $HOME/.zshenv_gnu
[[ -f $HOME/.zshenv_lib ]] && source $HOME/.zshenv_lib
#+END_SRC
* iTerm2
On Mac systems, I like the [[https://www.iterm2.com/][iTerm2 application]], and we can enable [[https://iterm2.com/documentation-shell-integration.html][shell integration]], either via the old school way, or just rely on [[https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/iterm2][the /plugin/ ]]above:
@ -417,25 +292,15 @@ Favorite feature is the [[https://iterm2.com/documentation-status-bar.html][Stat
Currently, I show the currently defined Kube namespace.
#+BEGIN_SRC zsh
function iterm2_python_version() {
echo $(pyenv version-name):$(echo "$VIRTUAL_ENV" | sed "
s|^$HOME|~|
s|^~/src/wpc-gerrit.inday.io/||
s|^~/work/||
s|^~/.venv/||
s|/\.venv$||
s|\.venv$||")
}
function iterm2_print_user_vars() {
# iterm2_set_user_var kubecontext $($ yq '.users[0].name' ~/.kube/config):$(kubectl config view --minify --output 'jsonpath={..namespace}')
# iterm2_set_user_var kubecontext $($ yq '.users[0].name' ~/.kube/config):$(kubectl config view --minify --output 'jsonpath={..namespace}')
# Correct version:
# iterm2_set_user_var kubecontext $(kubectl config current-context):$(kubectl config view --minify --output 'jsonpath={..namespace}')
# Faster version:
iterm2_set_user_var kubecontext $(awk '/^current-context:/{print $2;exit;}' <~/.kube/config)
# Correct version:
# iterm2_set_user_var kubecontext $(kubectl config current-context):$(kubectl config view --minify --output 'jsonpath={..namespace}')
# Faster version:
iterm2_set_user_var kubecontext $(awk '/^current-context:/{print $2;exit;}' <~/.kube/config)
iterm2_set_user_var pycontext $(iterm2_python_version)
iterm2_set_user_var pycontext "$(pyenv version-name):$(echo $VIRTUAL_ENV | sed 's/.*.venv\///')"
}
#+END_SRC
@ -459,12 +324,10 @@ While it /should/ figure out (as Emacs keybindings are the default), this is how
bindkey -e
#+END_SRC
Where be the =emacsclient=? It should, at this point, be in our path.
And how should we call it?
Where be the =emacsclient=, and how should we call it?
#+BEGIN_SRC zsh :tangle ~/.zshenv
export EMACS_SOCKET_NAME=personal
export EMACS="emacsclient --socket-name personal"
#+END_SRC
Which needs to be overwritten on my Work computer:
@ -472,15 +335,15 @@ Which needs to be overwritten on my Work computer:
#+BEGIN_SRC zsh :tangle ~/.zshenv
if hostname | grep AL33 >/dev/null
then
export EMACS_SOCKET_NAME=work
export EMACS="emacsclient --socket-name work"
fi
#+END_SRC
The =EDITOR= variable that some programs use to edit files from the command line:
#+BEGIN_SRC zsh :tangle ~/.zshenv
export EDITOR="emacsclient --tty"
export VISUAL="emacsclient --create-frame"
export EDITOR="$EMACS --tty"
export VISUAL="$EMACS --create-frame"
#+END_SRC
With these variables defined, we can create simple aliases:
@ -488,8 +351,8 @@ With these variables defined, we can create simple aliases:
#+BEGIN_SRC zsh
alias e="$EDITOR"
alias te="$EDITOR"
alias ee="emacsclient --create-frame"
alias eee="emacsclient --create-frame --no-wait"
alias ee="$EMACS --create-frame"
alias eee="$EMACS --create-frame --no-wait"
#+END_SRC
** Vterm
@ -544,7 +407,7 @@ For instance:
Assuming weve installed [[https://github.com/lsd-rs/lsd][lsd]], lets make an alias for it:
#+BEGIN_SRC zsh
if which lsd >/dev/null
if whence lsd >/dev/null
then
alias ls=lsd
fi
@ -568,13 +431,6 @@ And an abstraction for transitory endpoints over SSH:
alias ossh="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o loglevel=ERROR"
#+END_SRC
And other ones that I use:
#+BEGIN_SRC zsh
alias os=openstack
alias k=kubectl
#+END_SRC
* Final Message
For sensitive work-related environment variables, store them elsewhere, and load them:
@ -590,7 +446,6 @@ To let us know we read the =~/.zshrc= file:
#+description: A literate programming file for configuring Zshell.
#+property: header-args:zsh :tangle ~/.zshrc
#+property: header-args:sh :tangle no
#+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