Compare commits
1 commit
main
...
no-straigh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8f2be92ff |
42 changed files with 1252 additions and 2980 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -9,6 +9,4 @@
|
|||
/elisp/gourmet.el
|
||||
/elisp/wd-imaas.el
|
||||
/ha-theme-results.org
|
||||
/support/dashboard
|
||||
/.emacs.desktop
|
||||
/.*-notes.org
|
||||
/support/dashboard
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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."))
|
||||
|
||||
|
|
|
|||
|
|
@ -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, I’m [[https://technobabble.bearblog.dev/fine-ill-try-ai/][trying AI]] … primarily because my company requires my participation. I appreciate the approaches from my fellow Emacsians, for while VSCode may be a fine editor, it can’t compete with my creation here.
|
||||
** Agent Shell
|
||||
Installing Xenodium’s [[https://github.com/xenodium/agent-shell][agent-shell]], requires his [[https://github.com/xenodium/acp.el][ACP package]] for accessing the installed [[https://agentclientprotocol.com/][ACP libraries]], and his [[https://github.com/xenodium/shell-maker][shell-maker]] package.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(use-package acp
|
||||
:straight (:type git :host github :repo "xenodium/acp.el"))
|
||||
|
||||
(use-package shell-maker
|
||||
:straight (:type git :host github :repo "xenodium/shell-maker"))
|
||||
|
||||
(use-package agent-shell
|
||||
:straight (:type git :host github :repo "xenodium/agent-shell")
|
||||
:after acp
|
||||
|
||||
:custom
|
||||
(agent-shell-display-action '(display-buffer-in-previous-window))
|
||||
|
||||
:config
|
||||
(ha-leader "a i" '("agent chat" . agent-shell))
|
||||
|
||||
;; Evil state-specific RET behavior: insert mode = newline, normal mode = send
|
||||
(evil-define-key 'insert agent-shell-mode-map (kbd "RET") #'newline)
|
||||
(evil-define-key 'insert agent-shell-mode-map (kbd "C-RET") #'agent-shell-submit)
|
||||
(evil-define-key 'normal agent-shell-mode-map (kbd "RET") #'comint-send-input)
|
||||
|
||||
;; Configure *agent-shell-diff* buffers to start in Emacs state
|
||||
(add-hook 'diff-mode-hook
|
||||
(lambda ()
|
||||
(when (string-match-p "\\*agent-shell-diff\\*" (buffer-name))
|
||||
(evil-emacs-state)))))
|
||||
#+END_SRC
|
||||
*** Notifications
|
||||
When my /artificial intern/ completes a task, I have long since nipped out to the kitchen, put the kettle on ... buttering scones... and getting crumbs and bits of food out of those round brown straw mats that the teapot goes on.
|
||||
|
||||
I would like the /intern/ to notify me when it needs attention, so I noticed the [[https://github.com/zackattackz/agent-shell-notifications/][agent-shell-notifications]] project can connect the =agent-shell= project with the [[https://github.com/konrad1977/knockknock/][knock-knock]] project (which is similar to my [[https://howardism.org/Technical/Emacs/beep-for-emacs.html][beep project]]).
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(use-package knockknock
|
||||
:straight (knockknock :type git :host github :repo "konrad1977/knockknock")
|
||||
:init
|
||||
(setq knockknock-border-color "brown")
|
||||
(setq knockknock-border-width 2)
|
||||
(setq knockknock-default-duration 4)
|
||||
:config
|
||||
(advice-add 'knockknock-notify :after #'beep-beep))
|
||||
#+END_SRC
|
||||
|
||||
Testing it out:
|
||||
|
||||
#+BEGIN_SRC emacs-lisp :tangle no
|
||||
(knockknock-notify :title "Attention"
|
||||
:message "Claude needs attention"
|
||||
:icon "nf-md-robot_confused")
|
||||
#+END_SRC
|
||||
|
||||
The configuration for [[https://github.com/zackattackz/agent-shell-notifications/][agent-shell-notifications]] and hook it to the =agent-shell-notifications-provider=:
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(use-package agent-shell-notifications
|
||||
:straight (agent-shell-notifications
|
||||
:type git
|
||||
:host github
|
||||
:repo "zackattackz/agent-shell-notifications")
|
||||
:hook
|
||||
;; Enable notifications in each agent-shell buffer
|
||||
(agent-shell-mode . agent-shell-notifications-mode)
|
||||
|
||||
:config
|
||||
;; Notification display timeout in seconds (0 = never expire (the default), -1 = backend default)
|
||||
;; (setq agent-shell-notifications-timeout 5)
|
||||
|
||||
;; Seconds to wait before notifying when the shell is already visible (default: 10)
|
||||
;; (setq agent-shell-notifications-idle-timeout 30)
|
||||
|
||||
;; Use the knockknock backend instead of the default libnotify
|
||||
(setq agent-shell-notifications-provider 'agent-shell-notifications-knockknock)
|
||||
|
||||
;; While that code is _supposed_ to do this, I seem to need to do this manually:
|
||||
(require 'agent-shell-notifications-knockknock))
|
||||
#+END_SRC
|
||||
|
||||
I might switch to [[https://github.com/cxa/agent-shell-macext#start-of-content][agent-shell-macext]] if I can’t 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 Workday’s favorite, Chrome, and login:
|
||||
|
||||
#+BEGIN_SRC sh
|
||||
cursor-agent login
|
||||
#+END_SRC
|
||||
|
||||
Now the Emacs configuration:
|
||||
|
||||
#+BEGIN_SRC emacs-lisp :tangle no
|
||||
(use-package agent-shell
|
||||
:custom
|
||||
(agent-shell-cursor-command `,(file-expand-wildcards "/opt/homebrew/Cellar/node/*/bin/cursor-agent-acp"))
|
||||
|
||||
:config
|
||||
(setq agent-shell-preferred-agent-config (agent-shell-cursor-make-agent-config)))
|
||||
#+END_SRC
|
||||
*** Gemini
|
||||
First install the [[https://github.com/google-gemini/gemini-cli][gemini-cli]]:
|
||||
#+BEGIN_SRC sh
|
||||
brew install gemini-cli
|
||||
#+END_SRC
|
||||
|
||||
And let’s make that the default now:
|
||||
|
||||
#+BEGIN_SRC emacs-lisp :tangle no
|
||||
(use-package agent-shell
|
||||
:config
|
||||
(setq agent-shell-preferred-agent-config (agent-shell-google-make-gemini-config)))
|
||||
#+END_SRC
|
||||
** AI Code Interface
|
||||
While the =agent-shell= offers a /vibe-codey/ interface to Chatbots, the [[https://github.com/tninja/ai-code-interface.el][ai-code-interface]] offers a more /programmatic/ interface.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(use-package ai-code
|
||||
:straight (:host github :repo "tninja/ai-code-interface.el")
|
||||
:config
|
||||
;; use codex as backend, other options are 'claude-code, 'gemini,
|
||||
;; 'github-copilot-cli, 'opencode, 'grok, 'cursor, 'kiro,
|
||||
;; 'codebuddy, 'aider, 'eca, 'agent-shell, 'claude-code-ide,
|
||||
;; 'claude-code-el
|
||||
(ai-code-set-backend 'gemini)
|
||||
|
||||
;; Enable global keybinding for the main menu
|
||||
(global-set-key (kbd "C-c a") #'ai-code-menu)
|
||||
|
||||
;; Optional: Enable @ file completion in comments and AI sessions
|
||||
(ai-code-prompt-filepath-completion-mode 1)
|
||||
|
||||
;; Optional: Ask AI to run test after code changes, for a tighter build-test loop
|
||||
;; (setq ai-code-auto-test-type 'ask-me)
|
||||
|
||||
;; Optional: In AI session buffers, SPC in Evil normal state triggers the prompt-enter UI
|
||||
;; (with-eval-after-load 'evil (ai-code-backends-infra-evil-setup))
|
||||
|
||||
;; Optional: Set up Magit integration for AI commands in Magit popups
|
||||
(with-eval-after-load 'magit
|
||||
(ai-code-magit-setup-transients)))
|
||||
#+END_SRC
|
||||
|
||||
* Git and Magit
|
||||
Can not live without [[https://magit.vc/][Magit]], a Git porcelain for Emacs. I stole the bulk of this work from Doom Emacs.
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(use-package magit)
|
||||
(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 let’s 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 Chu’s [[https://github.com/jonathanchu/magit-gh][magit-gh]] project is /simpler/ than [[#Forge][Forge]] (see [[https://jonathanchu.is/posts/introducing-magit-gh/][this essay]] for details).
|
||||
|
||||
First, install and configure the [[https://github.com/cli/cli/blob/trunk/docs/install_macos.md#homebrew][Github CLI]] program.
|
||||
#+BEGIN_SRC sh
|
||||
brew install gh
|
||||
#+END_SRC
|
||||
|
||||
Create a =GITHUB_TOKEN= under =/settings/tokens=.
|
||||
The required scopes are =repo=, =read:org=, =admin:public_key=.
|
||||
Also, these don’t last long, so return and regenerate routinely.
|
||||
|
||||
Next, [[https://cli.github.com/manual/gh_auth_login][configure it]] with:
|
||||
#+BEGIN_SRC sh
|
||||
gh auth login --hostname ${GH_HOST:-github.com}
|
||||
#+END_SRC
|
||||
|
||||
And pass in the =GITHUB_TOKEN= environment variable.
|
||||
|
||||
Verify that this works:
|
||||
#+BEGIN_SRC sh
|
||||
gh pr list
|
||||
#+END_SRC
|
||||
|
||||
With the =gh= CLI working, we can install and use this project:
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(use-package magit-gh
|
||||
:after magit
|
||||
:straight (:type git :host github :repo "jonathanchu/magit-gh"))
|
||||
#+END_SRC
|
||||
|
||||
|
||||
#+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 other’s 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
|
|||
I’m beginning with dirvish to use the [[https://github.com/alexluigit/dirvish/blob/main/docs/CUSTOMIZING.org][sample configuration]] and change it:
|
||||
#+begin_src emacs-lisp :tangle no
|
||||
(use-package dirvish
|
||||
:straight (:host github :repo "alexluigit/dirvish")
|
||||
;; :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:
|
|||
I’d like write notes in org files that link to the PDFs (and maybe visa versa), using the [[https://github.com/weirdNox/org-noter][org-noter]] package:
|
||||
#+begin_src emacs-lisp
|
||||
(use-package org-noter
|
||||
: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:
|
||||
|
|
|
|||
171
ha-aux-apps.org
171
ha-aux-apps.org
|
|
@ -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 @@ Let’s 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. We’ll be using Alphapapa’s 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 chat’s, let’s 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, let’s control my playlists from Emacs.
|
||||
A more /rich/ experience than the [[https://github.com/isamert/empv.el][empv]] project is Álvaro’s [[https://github.com/xenodium/ready-player][ready-player]]. First install [[https://mpv.io/][mpv]]:
|
||||
|
||||
#+BEGIN_SRC sh
|
||||
Why not? Let’s 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 doesn’t 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:
|
||||
|
|
|
|||
|
|
@ -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 @@ I’m 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, let’s 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, let’s 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]
|
||||
|
|
|
|||
|
|
@ -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, I’ve 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 doesn’t 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, let’s 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
|
||||
551
ha-config.org
551
ha-config.org
|
|
@ -146,7 +146,8 @@ After reading [[https://irreal.org/blog/?p=12139][Jon Sander’s 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 @@ I’ve 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 don’t 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 let’s 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
|
||||
I’m 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 doesn’t 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 wasn’t 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 wasn’t 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 Person’s Workspace package. Also let’s 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
|
||||
|
||||
I’ve struggled to /programmatically/ create sane workspaces, so let’s 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 wouldn’t do this if it wasn’t 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 let’s 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
|
||||
|
||||
Let’s 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 Petersen’s [[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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
15
ha-demos.org
15
ha-demos.org
|
|
@ -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 haven’t been able to resolve, I’m 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))
|
||||
|
|
|
|||
|
|
@ -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 let’s 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 let’s 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 Chua’s [[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).
|
||||
|
|
|
|||
72
ha-email.org
72
ha-email.org
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
38
ha-evil.org
38
ha-evil.org
|
|
@ -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 don’t 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 I’m in /insert/ mode, and start typing =j= repeatedly to go down a line, and end up typing something like: =jjjjjjjjjjjj=.
|
||||
|
||||
To remedy this, I’m 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 doesn’t 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 you’d 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~? I’m 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. I’m 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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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 doesn’t 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 doesn’t 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=. Let’s 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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, let’s 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-"))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
- Alchemist’s Amber
|
||||
- Bubblegloop Bogs
|
||||
- Crumpet Crossroads
|
||||
- Dandelion Drifters
|
||||
- Elfin Espresso
|
||||
- Firefly Fandango
|
||||
- Giggling Goblins
|
||||
- Hobgoblin Hideaway
|
||||
- Inkwell Isles
|
||||
- Jester’s Junction
|
||||
- Knapsack Knolls
|
||||
- Lavender Labyrinths
|
||||
- Moonbeam Meadow
|
||||
- Nectar Nibblers
|
||||
- Owl’s 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 I’ve 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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
357
ha-org.org
357
ha-org.org
|
|
@ -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 wouldn’t /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 let’s 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 let’s 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
|
|||
I’m 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. I’m making this particularly delicious.
|
||||
Let's hook some spell-checking into org files, and actually all text files. I’m 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 Evil’s [[help:evil-prev-flyspell-error][evil-prev-flyspell-error]] to jump back to the last spelling mistake.
|
||||
|
||||
Now, I’m 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, I’m 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, let’s 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 ball’s 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 buffer’s 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 let’s 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 jinx’s 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 doesn’t seem to help much. $ 100 ?
|
||||
** Perfect Sentence
|
||||
Jordan Peterson's writing app, [[https://essay.app/guide][Essay]], inspired Chris Malorana’s [[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 Malorana’s [[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. Let’s 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 Malorana’s Proselint Somers Textlint Writegood flycheck flyspell fontlock"
|
||||
# jinx-local-words: "Braganza Graphviz Malorana’s Proselint Somers Textlint Writegood flycheck flyspell fontlock"
|
||||
# End:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -71,7 +71,11 @@ And we should extend it with the [[https://github.com/xuchunyang/elisp-demos][el
|
|||
Wilfred’s [[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
|
|||
Wilfred’s [[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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -33,9 +33,8 @@ While Emacs supplies a Python editing environment, we’ll 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 Vujic’s [[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, I’m 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, I’m 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 project’s 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
|
||||
Let’s 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
|
||||
Let’s 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
|
||||
|
||||
Let’s 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
|
||||
I’m using the Microsoft-supported [[https://github.com/Microsoft/pyright][pyright]] package instead, adding it to the global Python scope.
|
||||
|
||||
I’m 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 isn’t 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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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), let’s 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)))
|
||||
|
|
|
|||
|
|
@ -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 Petersen’s 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 Petersen’s 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 don’t 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
|
||||
I’m 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, I’ll 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):
|
||||
Let’s 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) doesn’t 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
|
||||
|
||||
Let’s 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 Remark’s 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 PyMarkdown’s 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 else’s.
|
||||
|
||||
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
|
||||
Remark’s 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, let’s 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]], let’s 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/"))
|
||||
|
|
|
|||
|
|
@ -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 doesn’t require a dedicate library that requires re-compilation. While offering [[https://elpa.nongnu.org/nongnu-devel/doc/eat.html][online documentation]], I’m 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:
|
||||
|
|
|
|||
27
ha-theme.org
27
ha-theme.org
|
|
@ -311,6 +311,7 @@ Can we *see* our colors?
|
|||
#+END_SRC
|
||||
|
||||
* Dark Theme
|
||||
|
||||
Let’s make a /theme/:
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
|
|
@ -322,20 +323,15 @@ Let’s 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 @@ Let’s 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:
|
||||
|
|
|
|||
373
hammerspoon.org
373
hammerspoon.org
|
|
@ -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, I’ve 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, I’ve 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 /can’t 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
|
||||
I’ve 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 Zoom’s 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, I’m /looking for features/ to use Hammerspoon.
|
||||
* Clocking out a Task
|
||||
I want to use Org’s 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 don’t 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:
|
||||
67
initialize
67
initialize
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -2,4 +2,4 @@
|
|||
# name: title
|
||||
# key: title
|
||||
# --
|
||||
#+TITLE: ${0}
|
||||
#+title: ${0}
|
||||
105
tmux.org
105
tmux.org
|
|
@ -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
|
||||
187
zshell.org
187
zshell.org
|
|
@ -21,7 +21,6 @@ Regardless, I keep my shell configuration conspicuously light.
|
|||
Let’s 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 @@ Let’s 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/ I’ve 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 I’m 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 I’ve 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 Zshell’s /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, let’s 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 we’ve installed [[https://github.com/lsd-rs/lsd][lsd]], let’s 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue