Compare commits

..

No commits in common. "6155e58879b3ee70272d81614793e1eccccb010a" and "30af02232a42a024e58d93d5703f0804cb1dea4f" have entirely different histories.

13 changed files with 371 additions and 475 deletions

View file

@ -43,7 +43,6 @@ 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 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:

View file

@ -19,7 +19,7 @@
"A WAV or AU file used at the completion of a function.")
;; My replacement in case we can't play internal sounds:
(defun beep-beep ()
(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)
@ -29,12 +29,12 @@ Customize the variable, `beep-alert-sound-file' to adjust the sound."
(defvar beep-speech-executable "say %s"
"An OS-dependent shell string to speak. Replaces `%s' with a phrase.")
(defun beep-speak (phrase)
(defun beep--speak (phrase)
"Call a program to speak the string, PHRASE.
Customize the variable, `beep-speech-executable'."
(let ((command (format beep-speech-executable phrase)))
(ignore-errors
(call-process-shell-command command))))
(save-window-excursion
(async-shell-command command))))
(defun beep-when-finished (phrase &optional to-speak)
"Notify us with string, PHRASE, to grab our attention.
@ -42,8 +42,8 @@ Useful after a long process has completed, but use sparingly,
as this can be pretty distracting."
(when (functionp 'alert)
(alert phrase :title "Completed"))
(beep-beep)
(beep-speak (or to-speak phrase))
(beep--beep)
(beep--speak (or to-speak phrase))
(message phrase))
(defun compile-and-notify ()
@ -57,12 +57,31 @@ See `beep-when-finished' for details."
(defvar beep-func-too-long-time 5
"The number of seconds a function runs before it is considered taking too much time, and needing to be alerted when it has finished.")
(defun beep--after-function (func)
"Call the function, FUNC, interactively, and notify us when completed."
(let ((start-time (current-time))
duration)
(call-interactively func)
(setq duration (thread-first
(current-time)
(time-subtract start-time)
decode-time
first))
(when (> duration beep-func-too-long-time)
(beep-when-finished (format "The function, %s, has finished." func)))))
(defun recompile-and-notify ()
"Call `recompile' and notify us when finished.
See `beep-when-finished' for details."
(interactive)
(beep--after-function 'recompile))
(global-set-key (kbd "C-c c") 'recompile-and-notify)
(global-set-key (kbd "C-c C") 'compile-and-notify)
(defun beep-when-runs-too-long (orig-function &rest args)
"Notifies us about the completion of ORIG-FUNCTION.
Useful as after advice to long-running functions, for instance:
Useful as after advice to long-running functions, for instance:
(advice-add 'org-publish :around #'beep-when-runs-too-long)"
(let ((start-time (current-time))

View file

@ -29,10 +29,6 @@ A literate programming file for configuring Emacs.
;; loading sequence.
;;
;;; Code:
;; Used functions defined elsewhere:
(defun font-icons (collection label &rest args)
(or (plist-get args :title) label))
#+end_src
* Basic Configuration
I begin configuration of Emacs that isnt /package-specific/.
@ -291,36 +287,6 @@ This function can be called interactively with a URL and a directory (and it att
#+end_src
** Completing Read User Interface
After using Ivy, I am going the route of a =completing-read= interface that extends the original Emacs API, as opposed to implementing backend-engines or complete replacements.
One enhancement to =completing-read= is to allow either a property list or an associate list for choices, and then return the /value/.
#+BEGIN_SRC emacs-lisp
(defun completing-read-alist (prompt collection
&optional predicate require-match
initial-input hist def inherit-input-method)
"List `completing-read', but COLLECTION is an alist, and it returns value.
The is, the _associative bit_.
PROMPT is a string to prompt with; normally it ends in a colon and a space.
PREDICATE, REQUIRE-MATCH, HIST and INHERIT-INPUT-METHOD is the same.
DEF is the default return without a match."
(let ((x (completing-read prompt collection predicate require-match
initial-input hist def inherit-input-method)))
(alist-get x collection x nil 'equal)))
#+END_SRC
This means (and I use this fairly often), that the /key/ is shows as a choice, the function returns the /value/.
#+BEGIN_SRC emacs-lisp :tangle no
(completing-read-alist "Choose a language: "
'(("Emacs Lisp" . "elisp.org")
("Python" . "python.org")
("Visual Basic" . "visual-basic.org")
;; ...
))
#+END_SRC
*** Vertico
The [[https://github.com/minad/vertico][vertico]] package puts the completing read in a vertical format, and like [[https://github.com/raxod502/selectrum#vertico][Selectrum]], it extends Emacs built-in functionality, instead of adding a new process. This means all these projects work together.
#+begin_src emacs-lisp
@ -813,43 +779,43 @@ Magnar Sveen's [[https://github.com/magnars/expand-region.el][expand-region]] pr
The built-in =isearch= is fantastically simple and useful, bound to ~C-s~, but why not bind searching for the current symbol?
#+BEGIN_SRC emacs-lisp
(global-set-key (kbd "M-s M-s") 'isearch-forward-thing-at-point)
(global-set-key (kbd "m-s m-s") 'isearch-forward-thing-at-point)
#+END_SRC
I like Charles Chois [[https://github.com/kickingvegas/casual][Casual Suite]], and his original [[https://github.com/kickingvegas/cc-isearch-menu][cc-isearch-menu]] was great at seeing the /buried/ features. Ive duplicated the features using [[https://github.com/jerrypnz/major-mode-hydra.el][pretty-hydra]]. In the middle of a search, type ~⌘-s~ (Command-s), and menu of options I dont remember appear.
#+begin_src emacs-lisp :tangle no
(defvar ha-isearch--title (font-icons 'faicon "magnifying_glass"
:title "Search Options"))
(defvar ha-isearch--title (font-icons 'faicon "magnifying_glass"
:title "Search Options"))
(pretty-hydra-define isearch-mode
(:color amaranth :quit-key "C-s" :title ha-isearch--title)
("Movement"
(("n" isearch-repeat-forward "Forward")
("p" isearch-repeat-backward "Backward")
("j" avy-isearch "Jump" :color blue))
"Expand"
((">" isearch-yank-symbol-or-char "Full symbol")
("z" isearch-yank-until-char "Until char")
("$" isearch-yank-line "Full line"))
"Replace"
(("R" isearch-query-replace "Literal")
("Q" isearch-query-replace-regexp "Regexp")
("<return>" isearch-exit "Stay" :color blue))
"Toggles"
(("w" isearch-toggle-word "Word only" :toggle t)
("s" isearch-toggle-symbol "Full symbol" :toggle t)
("r" isearch-toggle-regexp "Regexp" :toggle t)
("c" isearch-toggle-case-fold "Case Sensitive" :toggle t))
"Highlight"
(("H" isearch-highlight-regexp "Matches" :color blue)
("L" isearch-highlight-lines-matching-regexp "Lines" :color blue))
"Other"
(("e" isearch-edit-string "Edit")
("o" isearch-occur "Occur" :color blue)
("C-g" isearch-abort "Abort" :color blue))))
(pretty-hydra-define isearch-mode
(:color amaranth :quit-key "C-s" :title ha-isearch--title)
("Movement"
(("n" isearch-repeat-forward "Forward")
("p" isearch-repeat-backward "Backward")
("j" avy-isearch "Jump" :color blue))
"Expand"
((">" isearch-yank-symbol-or-char "Full symbol")
("z" isearch-yank-until-char "Until char")
("$" isearch-yank-line "Full line"))
"Replace"
(("R" isearch-query-replace "Literal")
("Q" isearch-query-replace-regexp "Regexp")
("<return>" isearch-exit "Stay" :color blue))
"Toggles"
(("w" isearch-toggle-word "Word only" :toggle t)
("s" isearch-toggle-symbol "Full symbol" :toggle t)
("r" isearch-toggle-regexp "Regexp" :toggle t)
("c" isearch-toggle-case-fold "Case Sensitive" :toggle t))
"Highlight"
(("H" isearch-highlight-regexp "Matches" :color blue)
("L" isearch-highlight-lines-matching-regexp "Lines" :color blue))
"Other"
(("e" isearch-edit-string "Edit")
("o" isearch-occur "Occur" :color blue)
("C-g" isearch-abort "Abort" :color blue))))
(define-key isearch-mode-map (kbd "s-s") 'isearch-mode/body)
(define-key isearch-mode-map (kbd "s-s") 'isearch-mode/body)
#+end_src
Pressing ~C-g~ aborts, and ~Return~ exits leaving the point in place. Typing ~C-s~ in the menu stops the menu to continue searching.

View file

@ -381,7 +381,8 @@ 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")
:straight (nerd-icons :type git :host github :repo "rainstormstudio/nerd-icons.el"
:files (:defaults "data"))
:custom
;; The Nerd Font you want to use in GUI defaults to fixed-font:
(nerd-icons-font-family ha-fixed-font))
@ -391,39 +392,39 @@ The way we access the /font icons/ has always been … odd; needing to specify t
#+BEGIN_SRC emacs-lisp
(defun font-icons (collection label &rest args)
"Abstraction over `nerd-icons' project.
LABEL is a short icon description merged with COLLECTION to identify an
icon to use. For instance, 'faicon or 'octicon.
"Abstraction over `nerd-icons' project.
LABEL is a short icon description merged with COLLECTION to
identify an icon to use. For instance, 'faicon or 'octicon.
ARGS, a plist, contain the title, sizing and other information.
ARGS, a plist, contain the title, sizing and other information.
For instance:
For instance:
(font-icons 'faicon \"file\" :title \"File Management\")
The goal is to take:
The goal is to take:
(all-the-icons-octicon \"git-branch\")
And reformat to:
And reformat to:
(font-icons 'octicon \"git-branch\")"
(let* ((func (intern (format "nerd-icons-%s" collection)))
(short (cl-case collection
('octicon "oct")
('faicon "fa")
('mdicon "md")
('codicon "cod")
('sucicon "custom")
('devicon "dev")
(t collection)))
(title (plist-get args :title))
(space (plist-get args :space))
(icon (format "nf-%s-%s" short
(string-replace "-" "_" label))))
(let* ((func (intern (format "nerd-icons-%s" collection)))
(short (cl-case collection
('octicon "oct")
('faicon "fa")
('mdicon "md")
('codicon "cod")
('sucicon "custom")
('devicon "dev")
(t collection)))
(title (plist-get args :title))
(space (plist-get args :space))
(icon (format "nf-%s-%s" short
(string-replace "-" "_" label))))
;; With the appropriate nerd-icons function name,
;; an expanded icon name, we get the icon string:
(concat (apply func (cons icon args))
(cond
((and title space) (concat (s-repeat space " ") title))
(title (concat " " title))))))
;; With the appropriate nerd-icons function name,
;; an expanded icon name, we get the icon string:
(concat (apply func (cons icon args))
(cond
((and title space) (concat (s-repeat space " ") title))
(title (concat " " title))))))
#+END_SRC
This replaces the /title generator/ for [[file:ha-config.org::*Leader Sequences][major-mode-hydra]] project to include a nice looking icon:

View file

@ -64,6 +64,7 @@ The [[https://github.com/antonj/Highlight-Indentation-for-Emacs][Highlight-Inden
(use-package highlight-indentation
:straight (:host github :repo "antonj/Highlight-Indentation-for-Emacs")
:hook ((yaml-mode . highlight-indentation-mode)
(yaml-ts-mode . highlight-indentation-mode)
(python-mode . highlight-indentation-mode)))
#+end_src
@ -101,39 +102,41 @@ Doing a lot of [[https://github.com/yoshiki/yaml-mode][YAML work]], but the =ya
, so Ive switch to [[https://github.com/zkry/yaml-pro][yaml-pro]] that is now based on Tree Sitter. Lets make sure the Tree-Sitter version works:
#+begin_src emacs-lisp
(use-package yaml-mode
:after mixed-pitch
:mode ((rx ".yamllint")
(rx ".y" (optional "a") "ml" string-end))
:hook (yaml-mode . (lambda () (mixed-pitch-mode -1)))
:mode-hydra
((:foreign-keys run)
("Simple"
(("l" ha-yaml-next-section "Next section")
("h" ha-yaml-prev-section "Previous")))))
(when (treesit-available-p)
(use-package yaml-ts-mode
:straight (:type built-in)
:after mixed-pitch
:mode ((rx ".yamllint")
(rx ".y" (optional "a") "ml" string-end))
:hook (yaml-ts-mode . (lambda () (mixed-pitch-mode -1)))
:mode-hydra
((:foreign-keys run)
("Simple"
(("l" ha-yaml-next-section "Next section")
("h" ha-yaml-prev-section "Previous"))))))
#+end_src
Allow this mode in Org blocks:
#+begin_src emacs-lisp :results silent
(add-to-list 'org-babel-load-languages '(yaml . t))
(add-to-list 'org-babel-load-languages '(yaml-ts . t))
#+end_src
And we hook
#+begin_src emacs-lisp
(use-package yaml-pro
:straight (:host github :repo "zkry/yaml-pro")
:after yaml-mode
:hook ((yaml-mode . yaml-pro-mode)))
:after yaml-ts-mode
:hook ((yaml-ts-mode . yaml-pro-ts-mode)
(yaml-mode . yaml-pro-mode)))
#+end_src
Since I can never remember too many keybindings for particular nodes, we create a Hydra just for it.
#+begin_src emacs-lisp
(defvar ha-yaml--title (font-icons 'devicon "yaml" :title "YAML Mode"))
(use-package major-mode-hydra
:after yaml-pro
:config
(major-mode-hydra-define yaml-mode (:title ha-yaml--title :foreign-keys run)
(major-mode-hydra-define yaml-ts-mode (:foreign-keys run)
("Navigation"
(("u" yaml-pro-ts-up-level "Up level" :color pink) ; C-c C-u
("J" yaml-pro-ts-next-subtree "Next subtree" :color pink) ; C-c C-n
@ -168,12 +171,10 @@ Jinja is a /template/ system that integrates /inside/ formats like JSON, HTML or
The [[https://polymode.github.io/][polymode]] project /glues/ modes like [[https://github.com/paradoxxxzero/jinja2-mode][jinja2-mode]] to [[https://github.com/yoshiki/yaml-mode][yaml-mode]].
I adapted this code from the [[https://github.com/emacsmirror/poly-ansible][poly-ansible]] project:
#+begin_src emacs-lisp :tangle no
#+begin_src emacs-lisp
(use-package polymode
:after yaml-mode jinja2-mode
:config
(define-hostmode poly-yaml-hostmode :mode 'yaml-mode)
(define-hostmode poly-yaml-hostmode :mode 'yaml-ts-mode)
(defcustom pm-inner/jinja2
(pm-inner-chunkmode :mode #'jinja2-mode
@ -194,23 +195,23 @@ I adapted this code from the [[https://github.com/emacsmirror/poly-ansible][poly
:hostmode 'poly-yaml-hostmode
:innermodes '(pm-inner/jinja2))
(major-mode-hydra-define+ yaml-mode nil
(major-mode-hydra-define+ yaml-ts-mode nil
("Extensions" (("j" poly-yaml-jinja2-mode "Jinja2")))))
#+end_src
We need to make sure the =mixed-pitch-mode= doesnt screw things up.
#+begin_src emacs-lisp :tangle no
#+begin_src emacs-lisp
(add-hook 'poly-yaml-jinja2-mode-hook (lambda () (mixed-pitch-mode -1)))
#+end_src
We /can/ hook this up to Org, via:
#+begin_src emacs-lisp :tangle no
#+begin_src emacs-lisp
(add-to-list 'org-babel-load-languages '(poly-yaml-jinja2 . t))
#+end_src
Now we can use either =yaml= or =poly-yaml-jinja2= (which perhaps we should make an alias?):
Now we can use either =yaml-ts= or =poly-yaml-jinja2= (which perhaps we should make an alias?):
#+begin_src poly-yaml-jinja2 :tangle no
---
@ -229,16 +230,15 @@ Now we can use either =yaml= or =poly-yaml-jinja2= (which perhaps we should make
#+end_src
* Ansible
Do I consider all YAML files an Ansible file needing [[https://github.com/k1LoW/emacs-ansible][ansible-mode]]? Maybe we just have a toggle for when we want the Ansible feature.
#+begin_src emacs-lisp
(use-package ansible
:straight (:host gitlab :repo "emacs-ansible/emacs-ansible")
:straight (:host github :repo "k1LoW/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")
(major-mode-hydra-define+ yaml-mode nil
("Extensions" (("a" ansible-mode "Ansible"))))
(ha-leader "t y" 'ansible-mode))
(major-mode-hydra-define+ yaml-ts-mode nil
("Extensions" (("a" ansible "Ansible"))))
(ha-leader "t y" 'ansible))
#+end_src
The [[help:ansible-vault-password-file][ansible-vault-password-file]] variable needs to change /per project/, so lets use the =.dir-locals.el= file, for instance:
@ -246,23 +246,15 @@ The [[help:ansible-vault-password-file][ansible-vault-password-file]] variable n
((nil . ((ansible-vault-password-file . "playbooks/.vault-password"))))
#+end_src
Since most Ansible files are a combination of YAML and Jinja, the [[https://github.com/emacsmirror/poly-ansible][poly-ansible]] project addresses this similar to =web-mode=:
#+BEGIN_SRC emacs-lisp
(use-package poly-ansible
:straight (:host github :repo "emacsmirror/poly-ansible")
:after ansible)
#+END_SRC
The YAML files get access Ansibles documentation using the [[https://github.com/emacsorphanage/ansible-doc][ansible-doc]] project (that accesses the [[https://docs.ansible.com/ansible/latest/cli/ansible-doc.html][ansible-doc interface]]):
#+begin_src emacs-lisp
(use-package ansible-doc
:after yaml-mode
:hook (yaml-mode . ansible-doc-mode)
:after yaml-ts-mode
:hook (yaml-ts-mode . ansible-doc-mode)
:config
;; (add-to-list 'exec-path (expand-file-name "~/.local/share/mise/installs/python/3.10/bin/ansible-doc"))
(major-mode-hydra-define+ yaml-mode nil
(major-mode-hydra-define+ yaml-ts-mode nil
("Documentation"
(("D" ansible-doc "Ansible")))))
#+end_src

View file

@ -322,7 +322,6 @@ Install the binary for the [[https://tree-sitter.github.io/][tree-sitter project
#+begin_src sh
brew install tree-sitter npm # Since most support packages need that too.
#+end_src
The tree-sitter project does not install any language grammars by default—after all, it would have no idea which particular languages to parse and analyze!
Next, using the =tree-sitter= command line tool, create the [[/Users/howard.abrams/Library/Application Support/tree-sitter/config.json][config.json]] file:
@ -343,17 +342,14 @@ Normally, you would need to add all the projects to directory clones in =~/src=
git pull origin
npm install
done <<EOL
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/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-json
https://github.com/tree-sitter/tree-sitter-python
https://github.com/tree-sitter/tree-sitter-bash
https://github.com/tree-sitter/tree-sitter-ruby
https://github.com/camdencheek/tree-sitter-dockerfile
https://github.com/alemuller/tree-sitter-make
https://github.com/ikatyang/tree-sitter-yaml
https://github.com/Wilfred/tree-sitter-elisp
EOL
#+end_src
@ -382,18 +378,15 @@ In most cases,the =npm install= /usually/ works, but I may work on some sort of
NAME=$(pwd | sed 's/.*-//')
git pull origin
npm install || make install # Various build processes!?
npm install || cargo build || make install # Various build processes!?
echo "Do we need to copy the library into ~/.emacs.d/tree-sitter/$NAME ?"
if [[ -e libtree-sitter-$NAME.dylib ]]
then
cp libtree-sitter-$NAME.dylib ~/.emacs.d/tree-sitter
fi
if [[ -e libtree-sitter-$NAME.so ]]
then
cp libtree-sitter-$NAME.so ~/.emacs.d/tree-sitter
fi
# if [ "$(uname -o)" = "Darwin" ]
# then
# cp libtree-sitter-$NAME.dylib ~/.emacs.d/tree-sitter
# else
# cp libtree-sitter-$NAME.so ~/.emacs.d/tree-sitter
# fi
done
#+end_src
At this point, we can now parse stuff using: =tree-sitter parse <source-code-file>=
@ -419,7 +412,6 @@ However, Emacs already has the ability to download and install grammars, so foll
;; (elixir "https://github.com/elixir-lang/tree-sitter-elixir" "main" "src")
;; (erlang "https://github.com/WhatsApp/tree-sitter-erlang" "main" "src")
(go "https://github.com/tree-sitter/tree-sitter-go")
(templ "https://github.com/vrischmann/tree-sitter-templ")
;; (haskell "https://github.com/tree-sitter/tree-sitter-haskell" "master" "src")
(html "https://github.com/tree-sitter/tree-sitter-html")
;; (java "https://github.com/tree-sitter/tree-sitter-java" "master" "src")

View file

@ -61,7 +61,6 @@ Will Schenk has [[https://willschenk.com/articles/2020/tramp_tricks/][a simple e
(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:
#+begin_example
/ssh:kolla-compute1.cedev13.d501.eng.pdx.wd|sudo:kolla-compute1.cedev13.d501.eng.pdx.wd|docker:kolla_toolbox:/
@ -78,19 +77,14 @@ Which means, I need to put it as a link in an org file.
* Remote Terminals
Sure =iTerm= is nice for connecting and running commands on remote systems, however, it lacks a command line option that allows you to select and manipulate the displayed text without a mouse. This is where Emacs can shine.
Interactive Functions:
- ha-shell :: create a local shell in default-directory. This is an abstraction mostly used for my demonstrations, otherwise, I can just call the =make-term= or =eat= directly.
- ha-ssh :: create a shell on remote system
*Feature One:*
When calling the =ha-ssh= function, it opens a =vterm= window which, unlike other terminal emulators in Emacs, merges both Emacs and Terminal behaviors. Essentially, it just works. It =vterm= isn't installed, it falls back to either =eat= or good ol =term=.
When calling the =ha-ssh= function, it opens a =vterm= window which, unlike other terminal emulators in Emacs, merges both Emacs and Terminal behaviors. Essentially, it just works. It =vterm= isn't installed, it falls back to =term=.
Preload a list of favorite/special hostnames with multiple calls to:
#+begin_src emacs-lisp :tangle no
(ha-ssh-add-favorite-host "Devbox 42" "10.0.1.42")
(ha-ssh-add-favorite-host "Devbox 42" "10.0.1.42")
#+end_src
Then calling =ha-ssh= function, a list of hostnames is available to quickly jump on a system (with the possibility of fuzzy matching if you have Helm or Ivy installed).
@ -106,27 +100,25 @@ Use the /favorite host/ list to quickly edit a file on a remote system using Tra
Working with remote shell connections programmatically, for instance:
#+begin_src emacs-lisp :tangle no
(let ((win-name "some-host"))
(ha-ssh "some-host.in.some.place" win-name)
(ha-ssh-send "source ~/.bash_profile" win-name)
(ha-ssh-send "clear" win-name))
;; ...
(ha-ssh-exit win-name)
(let ((win-name "some-host"))
(ha-ssh "some-host.in.some.place" win-name)
(ha-ssh-send "source ~/.bash_profile" win-name)
(ha-ssh-send "clear" win-name))
;; ...
(ha-ssh-exit win-name)
#+end_src
Actually the =win-name= in this case is optional, as it will use a good default.
** VTerm
I'm not giving up on Eshell, but I am playing around with [[https://github.com/akermu/emacs-libvterm][vterm]], and it is pretty good, but I use it primarily as a more reliable approach for remote terminal sessions.
VTerm has an issue (at least for me) with ~M-Backspace~ not deleting the previous word, and yeah, I want to make sure that both keystrokes do the same thing.
#+begin_src emacs-lisp :tangle no
#+begin_src emacs-lisp
(use-package vterm
:config
(ha-leader
"p t" '("terminal" . (lambda () (interactive) (ha-shell (project-root (project-current))))))
(dolist (k '("<C-backspace>" "<M-backspace>"))
(define-key vterm-mode-map (kbd k)
(lambda () (interactive) (vterm-send-key (kbd "C-w")))))
@ -151,50 +143,6 @@ 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~.
** Eat
While not as fast as [[https://github.com/akermu/emacs-libvterm][vterm]], the [[https://codeberg.org/akib/emacs-eat][Emulate a Terminal]] project (eat) is fast enough, and doesnt require a dedicate library that requires re-compilation. While offering [[https://elpa.nongnu.org/nongnu-devel/doc/eat.html][online documentation]], Im glad for an [[info:eat#Top][Info version]].
#+BEGIN_SRC emacs-lisp
(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")))
:bind (:map eat-semi-char-mode-map
("C-c C-t" . ha-eat-narrow-to-shell-prompt-dwim))
:config
(defun ha-eat-narrow-to-shell-prompt-dwim ()
(interactive)
(if (buffer-narrowed-p) (widen) (eat-narrow-to-shell-prompt)))
(ha-leader
"p t" '("terminal" . eat-project)))
#+END_SRC
The largest change, is like the venerable [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Term-Mode.html][term mode]], we have different modes:
- =semi-char= :: This DWIM mode works halfway between an Emacs buffer and a terminal. Use ~C-c C-e~ to go to =emacs= mode.
- =emacs= :: Good ol Emacs buffer, use ~C-c C-j~ to go back to =semi-char= mode.
- =char= :: Full terminal mode, use ~M-RET~ to pop back to =semi-char= mode.
- =line= :: Line-oriented mode, not sure why Id use it.
Cool stuff:
- ~C-n~ / ~C-p~ :: scrolls the command history
- ~C-c C-n~ / ~C-c C-p~ :: jumps to the various prompts
What about Evil mode?
TODO: Like =eshell=, the Bash in an EAT terminal has a command =_eat_msg= that takes a handler, and a /message/. Then set up an alist of =eat-message-handler-alist= to decide what to do with it.
TODO: Need to /subtlize/ the =eat-term-color-bright-green= and other settings as it is way too garish.
Make sure you add the following for Bash:
#+BEGIN_SRC bash :tangle no
[ -n "$EAT_SHELL_INTEGRATION_DIR" ] && \
source "$EAT_SHELL_INTEGRATION_DIR/bash"
#+END_SRC
** Variables
Let's begin by defining some variables used for communication between the functions.
@ -211,161 +159,187 @@ Let's begin by defining some variables used for communication between the functi
#+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"))
(defvar ha-ssh-shell (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]].
#+BEGIN_SRC emacs-lisp
(defun ha-make-term (name &optional program startfile &rest switches)
"Create a terminal buffer NAME based on available emulation.
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)
(vterm-send-string (append program switches))
(vterm-send-return)))
((fboundp 'eat) (progn (switch-to-buffer
(apply 'eat-make (append (list name program startfile)
switches)))
(setq-local ha-eat-terminal eat-terminal)))
(t (switch-to-buffer
(apply 'make-term (append (list name program startfile)
switches))))))
#+END_SRC
** Interactive Interface to Remote Systems
The function, =ha-ssh= pops up a list of /favorite hosts/ and then uses the =vterm= functions to automatically SSH into the chosen host:
#+begin_src emacs-lisp
(defun ha-ssh (hostname &optional window-name)
"Start a SSH session to a given HOSTNAME (with an optionally specified WINDOW-NAME).
If called interactively, it presents the user with a list
returned by =ha-ssh-choose-host=."
(interactive (list (ha-ssh-choose-host)))
(unless window-name
(setq window-name (format "ssh: %s" hostname)))
(setq ha-latest-ssh-window-name (format "*%s*" window-name))
(ha-make-term window-name "ssh" nil hostname)
(pop-to-buffer ha-latest-ssh-window-name))
(defun ha-ssh (hostname &optional window-name)
"Start a SSH session to a given HOSTNAME (with an optionally specified WINDOW-NAME).
If called interactively, it presents the user with a list
returned by =ha-ssh-choose-host=."
(interactive (list (ha-ssh-choose-host)))
(unless window-name
(setq window-name (format "ssh: %s" hostname)))
(setq ha-latest-ssh-window-name (format "*%s*" window-name))
;; I really like this =vterm= interface, so if I've got it loaded, let's use it:
(if (not (fboundp 'vterm))
;; Should we assume the =ssh= we want is on the PATH that started Emacs?
(make-term window-name "ssh" nil hostname)
(vterm ha-latest-ssh-window-name)
(vterm-send-string (format "ssh %s" hostname))
(vterm-send-return))
(pop-to-buffer ha-latest-ssh-window-name))
#+end_src
Of course, we need a function that =interactive= can call to get that list, and my thought is to call =helm= if it is available, otherwise, assume that ido/ivy will take over the =completing-read= function:
#+begin_src emacs-lisp
(defun ha-ssh-choose-host ()
"Prompts the user for a host, and if it is in the cache, return
its IP address, otherwise, return the input given.
This is used in calls to =interactive= to select a host."
(completing-read-alist "Hostname: " ha-ssh-favorite-hostnames nil 'confirm
nil 'ha-ssh-host-history))
(defun ha-ssh-choose-host ()
"Prompts the user for a host, and if it is in the cache, return
its IP address, otherwise, return the input given.
This is used in calls to =interactive= to select a host."
(let ((hostname
;; We call Helm directly if installed, only so that we can get better
;; labels in the window, otherwise, the =completing-read= call would be fine.
(if (fboundp 'helm-comp-read)
(helm-comp-read "Hostname: " ha-ssh-favorite-hostnames
:name "Hosts"
:fuzzy t :history ha-ssh-host-history)
(completing-read "Hostname: " ha-ssh-favorite-hostnames nil 'confirm nil 'ha-ssh-host-history))))
(alist-get hostname ha-ssh-favorite-hostnames hostname nil 'equal)))
#+end_src
Before we leave this section, I realize that I would like a way to /add/ to my list of hosts:
#+begin_src emacs-lisp
(defun ha-ssh-add-favorite-host (hostname ip-address)
"Add a favorite host to your list for easy pickin's."
(interactive "sHostname: \nsIP Address: ")
(add-to-list 'ha-ssh-favorite-hostnames (cons hostname ip-address)))
#+end_src
** Programmatic Interface
For the sake of my demonstrations, I use =ha-shell= to start a terminal with a particular =name=. Then, I can send commands into it.
Simply calling =vterm= fails to load my full environment, so this allows me to start the terminal in a particular directory (defaulting to the root of the current project):
#+begin_src emacs-lisp
(defun ha-shell (&optional directory name)
"Creates a terminal window using `ha-make-term'.
Stores the name, for further calls to `ha-shell-send', and
`ha-shell-send-lines'."
"Creates and tidies up a =vterm= terminal shell in side window."
(interactive (list (read-directory-name "Starting Directory: " (project-root (project-current)))))
(let* ((default-directory (or directory default-directory))
(win-name (or name (replace-regexp-in-string (rx (+? any)
(group (1+ (not "/")))
(optional "/") eol)
"\\1"
default-directory)))
(buf-name (format "*%s*" win-name)))
(let* ((win-name (or name (ha-shell--name-from-dir directory)))
(buf-name (format "*%s*" win-name))
(default-directory (or directory default-directory)))
(setq ha-latest-ssh-window-name buf-name)
(ha-make-term win-name ha-shell))) ; Lisp-2 FTW!?
(if (not (fboundp 'vterm))
(make-term win-name ha-ssh-shell)
(vterm buf-name))))
#+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.
Before we leave this section, I realize that I would like a way to /add/ to my list of hosts:
#+begin_src emacs-lisp
(defun ha-shell-send (command &optional name)
"Send COMMAND to existing shell terminal based on DIRECTORY.
If you want to refer to another session, specify the correct NAME.
This is really useful for scripts and demonstrations."
(unless name
(setq name ha-latest-ssh-window-name))
(save-window-excursion
(pop-to-buffer name)
(goto-char (point-max))
(cond
((eq major-mode 'vterm-mode) (progn
(vterm-send-string command)
(vterm-send-return)))
((eq major-mode 'eat-mode) (eat-term-send-string
ha-eat-terminal (concat command "\n")))
(t (progn
(insert command)
(term-send-input))))))
(defun ha-ssh-add-favorite-host (hostname ip-address)
"Add a favorite host to your list for easy pickin's."
(interactive "sHostname: \nsIP Address: ")
(add-to-list 'ha-ssh-favorite-hostnames (cons hostname ip-address)))
#+end_src
Let's have a quick way to bugger out of the terminal:
#+begin_src emacs-lisp
(defun ha-shell-exit (&optional name)
"End the SSH session specified by NAME (or if not, the latest session)."
(interactive)
(unless (or (eq major-mode 'vterm-mode) ; Already in a term?
(eq major-mode 'eat-mode) ; Just close this.
(eq major-mode 'term-mode))
(unless name
(setq name ha-latest-ssh-window-name))
(pop-to-buffer name))
(defun ha-ssh-exit (&optional window-name)
"End the SSH session specified by WINDOW-NAME (or if not, the latest session)."
(interactive)
(unless (string-match-p "v?term" (buffer-name))
(unless window-name
(setq window-name ha-latest-ssh-window-name))
(pop-to-buffer window-name))
(ignore-errors
(term-send-eof))
(kill-buffer name)
(delete-window))
(ignore-errors
(term-send-eof))
(kill-buffer window-name)
(delete-window))
#+end_src
** Programmatic Interface
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")=
Since every project perspective may have a shell terminal, lets see if I can figure which shell buffer to send—based on the =current-directory=.
#+begin_src emacs-lisp
(defun ha-shell-send (command &optional directory)
"Send COMMAND to existing shell terminal based on DIRECTORY.
If the shell doesn't already exist, start on up by calling
the `ha-shell' function.
The real work for this is done by `ha-ssh-send'.
If DIRECTORY is nil, use the project root from project."
(let ((buf (ha-shell--buf-from-dir directory)))
(unless buf
(setq buf (ha-shell directory)))
(ha-ssh-send command buf)))
(defun ha-shell--buf-from-dir (directory)
"Return Terminal buffer associated with DIRECTORY.
Or nil if no buffer has been found."
(let* ((win-name (ha-shell--name-from-dir directory))
(win-rx (rx "*" (literal win-name) "*"))
(bufs (seq-filter (lambda (b) (when (string-match win-rx (buffer-name b)) b))
(buffer-list))))
(first bufs)))
(defun ha-shell--name-from-dir (&optional directory)
"Return an appropriate title for a terminal based on DIRECTORY.
If DIRECTORY is nil, use the `project-name'."
(unless directory
(setq directory (project-name (project-current))))
(let ((name
;; Most of the time I just want the base project name, but in
;; my "work" directory, the projects are too similar, and I
;; need two levels of directories to distinguish them as a
;; project.
(if (s-contains? "/work/" directory)
(thread-last directory
(s-split "/")
(-remove 's-blank-str?)
(-take-last 2)
(s-join "/"))
(file-name-base (directory-file-name directory)))))
(format "Terminal: %s" name)))
#+end_src
For example:
Perhaps a Unit test is in order:
#+begin_src emacs-lisp :tangle no
(ert-deftest ha--terminal-name-from-dir-test ()
(should
(string= (ha-shell--name-from-dir "~/src/hamacs/") "Terminal: hamacs"))
(should
(string= (ha-shell--name-from-dir "~/work/foo/bar") "Terminal: foo/bar"))
(should
(string= (ha-shell--name-from-dir) "Terminal: hamacs")))
#+end_src
#+BEGIN_SRC emacs-lisp :tangle no
(ha-shell)
(ha-shell-send "date")
(ha-shell-exit)
#+END_SRC
The previous functions (as well as my own end of sprint demonstrations) often need to issue some commands to a running terminal session, which is a simple wrapper around a /send text/ and /send return/ sequence:
#+begin_src emacs-lisp
(defun ha-ssh-send (phrase &optional window-name)
"Send command PHRASE to the currently running SSH instance.
If you want to refer to another session, specify the correct WINDOW-NAME.
This is really useful for scripts and demonstrations."
(unless window-name
(setq window-name ha-latest-ssh-window-name))
(save-window-excursion
(pop-to-buffer window-name)
(if (fboundp 'vterm)
(progn
(vterm-send-string phrase)
(vterm-send-return))
(progn
(term-send-raw-string phrase)
(term-send-input)))))
#+end_src
As you may know, Im big into /literate devops/ where I put my shell commands in org files. However, I also work as part of a team that for some reason, doesnt accept Emacs as their One True Editor. At least, I am able to talk them into describing commands in Markdown files, e.g. =README.md=. Instead of /copying-pasting/ into the shell, could I /send/ the /current command/ to that shell?
#+begin_src emacs-lisp
(defun ha-shell-send-line (prefix &optional name)
(defun ha-ssh-send-line (prefix)
"Copy the contents of the current line in the current buffer,
and call `ha-sshell-send' with it. After sending the contents, it
returns to the current location. PREFIX is the number of lines."
and call =ha-ssh-send= with it. After sending the contents, it
returns to the current line."
(interactive "P")
(dolist (line (ha-ssh--line-or-block prefix))
;; (sit-for 0.25)
(ha-shell-send line)))
;; The function =save-excursion= doesn't seem to work...
(let ((buf (current-buffer)))
(dolist (line (ha-ssh--line-or-block prefix))
;; (sit-for 0.25)
(ha-ssh-send line))
(pop-to-buffer buf)))
#+end_src
What does /current command/ mean? The current line? A good fall back. Selected region? Sure, if active, but that seems like more work. In a Markdown file, I can gather the entire source code block, just like in an Org file.
So the following function may be a bit complicated in determining what is this /current code/:
#+begin_src emacs-lisp
(defun ha-ssh--line-or-block (num-lines)
"Return a list of the NUM-LINES from current buffer.
@ -408,7 +382,6 @@ So the following function may be a bit complicated in determining what is this /
#+end_src
In Markdown (and org), I might have initial spaces that should be removed (but not all initial spaces):
#+begin_src emacs-lisp
(defun ha-ssh--line-cleanup (str)
"Return STR as a list of strings."
@ -419,47 +392,43 @@ In Markdown (and org), I might have initial spaces that should be removed (but n
(trim-amount (when (string-match (rx bol (group (* space))) first-line)
(length (match-string 1 first-line)))))
(mapcar (lambda (line) (substring line trim-amount)) lst-contents)))
#+end_src
And some tests to validate:
#+BEGIN_SRC emacs-lisp :tangle no
(ert-deftest ha-ssh--line-cleanup-test ()
(should (equal (ha-ssh--line-cleanup "bob") '("bob")))
(should (equal (ha-ssh--line-cleanup " bob") '("bob")))
(should (equal (ha-ssh--line-cleanup "bob\nfoo") '("bob" "foo")))
(should (equal (ha-ssh--line-cleanup " bob\n foo") '("bob" "foo")))
(should (equal (ha-ssh--line-cleanup " bob\n foo") '("bob" " foo"))))
#+END_SRC
#+end_src
** Editing Remote Files
TRAMP, when it works, is amazing that we can give it a reference to a remote directory, and have =find-file= magically autocomplete.
#+begin_src emacs-lisp
(defun ha-ssh-find-file (hostname)
"Constructs a ssh-based, tramp-focus, file reference, and then calls =find-file=."
(interactive (list (ha-ssh-choose-host)))
(let ((tramp-ssh-ref (format "/ssh:%s:" hostname))
(other-window (when (equal current-prefix-arg '(4)) t)))
(ha-ssh--find-file tramp-ssh-ref other-window)))
(defun ha-ssh-find-file (hostname)
"Constructs a ssh-based, tramp-focus, file reference, and then calls =find-file=."
(interactive (list (ha-ssh-choose-host)))
(let ((tramp-ssh-ref (format "/ssh:%s:" hostname))
(other-window (when (equal current-prefix-arg '(4)) t)))
(ha-ssh--find-file tramp-ssh-ref other-window)))
(defun ha-ssh--find-file (tramp-ssh-ref &optional other-window)
"Calls =find-file= after internally completing a file reference based on TRAMP-SSH-REF."
(let ((tramp-file (read-file-name "Find file: " tramp-ssh-ref)))
(if other-window
(find-file-other-window tramp-file)
(find-file tramp-file))))
(defun ha-ssh--find-file (tramp-ssh-ref &optional other-window)
"Calls =find-file= after internally completing a file reference based on TRAMP-SSH-REF."
(let ((tramp-file (read-file-name "Find file: " tramp-ssh-ref)))
(if other-window
(find-file-other-window tramp-file)
(find-file tramp-file))))
#+end_src
We can even edit it as root:
#+begin_src emacs-lisp
(defun ha-ssh-find-file-root (hostname)
"Constructs a ssh-based, tramp-focus, file reference, and then calls =find-file=."
(interactive (list (ha-ssh-choose-host)))
(let ((tramp-ssh-ref (format "/ssh:%s|sudo:%s:" hostname hostname))
(other-window (when (equal current-prefix-arg '(4)) t)))
(ha-ssh--find-file tramp-ssh-ref other-window)))
(defun ha-ssh-find-root (hostname)
"Constructs a ssh-based, tramp-focus, file reference, and then calls =find-file=."
(interactive (list (ha-ssh-choose-host)))
(let ((tramp-ssh-ref (format "/ssh:%s|sudo:%s:" hostname hostname))
(other-window (when (equal current-prefix-arg '(4)) t)))
(ha-ssh--find-file tramp-ssh-ref other-window)))
#+end_src
** OpenStack Interface
@ -469,13 +438,22 @@ Instead of making sure I have a list of remote systems already in the favorite h
We'll give =openstack= CLI a =--format json= option to make it easier for parsing:
#+begin_src emacs-lisp
(use-package json)
(use-package json)
#+end_src
Need a variable to hold all our interesting hosts. Notice I use the word /overcloud/, but this is a name I've used for years to refer to /my virtual machines/ that I can get a listing of, and not get other VMs that I don't own.
#+begin_src emacs-lisp
(defvar ha-ssh-overcloud-cache-data nil
"A vector of associated lists containing the servers in an Overcloud.")
#+end_src
If our cache data is empty, we could automatically retrieve this information, but only on the first time we attempt to connect. To do this, we'll =advice= the =ha-ssh-choose-host= function defined earlier:
#+begin_src emacs-lisp
(defun ha-ssh-overcloud-query-for-hosts ()
"If the overcloud cache hasn't be populated, ask the user if we want to run the command."
(when (not ha-ssh-favorite-hostnames)
(when (not ha-ssh-overcloud-cache-data)
(when (y-or-n-p "Cache of Overcloud hosts aren't populated. Retrieve hosts?")
(call-interactively 'ha-ssh-overcloud-cache-populate))))
@ -488,9 +466,9 @@ We'll do the work of getting the /server list/ with this function:
(defun ha-ssh-overcloud-cache-populate (cluster)
"Given an `os-cloud' entry, stores all available hostnames.
Calls `ha-ssh-add-favorite-host' for each host found."
(interactive (list (completing-read "Cluster: " '(devprod501 devprod502))))
(interactive (list (completing-read "Cluster: " '(devprod1 devprod501 devprod502))))
(message "Calling the `openstack' command...this will take a while. Grab a coffee, eh?")
(let* ((command (format "openstack --os-cloud %s server list --no-name-lookup -f json" cluster))
(let* ((command (format "openstack --os-cloud %s server list --no-name-lookup --insecure -f json" cluster))
(json-data (thread-last command
(shell-command-to-string)
(json-read-from-string))))
@ -503,23 +481,38 @@ We'll do the work of getting the /server list/ with this function:
(message "Call to `openstack' complete. Found %d hosts." (length json-data))))
#+end_src
In case I change my virtual machines, I can repopulate that cache:
#+begin_src emacs-lisp
(defun ha-ssh-overcloud-cache-repopulate ()
"Repopulate the cache based on redeployment of my overcloud."
(interactive)
(setq ha-ssh-overcloud-cache-data nil)
(call-interactively 'ha-ssh-overcloud-cache-populate))
#+end_src
The primary interface:
#+begin_src emacs-lisp
(defun ha-ssh-overcloud (hostname)
"Log into an overcloud host given by HOSTNAME. Works better if
you have previously run =ssh-copy-id= on the host. Remember, to
make it behave like a real terminal (instead of a window in
Emacs), hit =C-c C-k=."
(interactive (list (ha-ssh-choose-host)))
(when (not (string-match-p "\." hostname))
(setq hostname (format "%s.%s" hostname (getenv "OS_PROJECT_NAME"))))
(defun ha-ssh-overcloud (hostname)
"Log into an overcloud host given by HOSTNAME. Works better if
you have previously run =ssh-copy-id= on the host. Remember, to
make it behave like a real terminal (instead of a window in
Emacs), hit =C-c C-k=."
(interactive (list (ha-ssh-choose-host)))
(when (not (string-match-p "\." hostname))
(setq hostname (format "%s.%s" hostname (getenv "OS_PROJECT_NAME"))))
(let ((window-label (or (thread-last ha-ssh-favorite-hostnames
(rassoc hostname)
(car))
hostname)))
(ha-ssh hostname window-label)))
(let ((window-label (or (-some->> ha-ssh-favorite-hostnames
(rassoc hostname)
car)
hostname)))
(ha-ssh hostname window-label)
(sit-for 1)
(ha-ssh-send "sudo -i")
(ha-ssh-send (format "export PS1='\\[\\e[34m\\]%s\\[\e[m\\] \\[\\e[33m\\]\\$\\[\\e[m\\] '"
window-label))
(ha-ssh-send "clear")))
#+end_src
* Keybindings
This file, so far, as been good-enough for a Vanilla Emacs installation, but to hook into Doom's leader for some sequence binding, this code isn't:
@ -530,11 +523,13 @@ This file, so far, as been good-enough for a Vanilla Emacs installation, but to
"a s o" '("overcloud" . ha-ssh-overcloud)
"a s l" '("local shell" . ha-shell)
"a s s" '("remote shell" . ha-ssh)
"a s p" '("project shell" . eat-project)
"a s p" '("project shell" . (lambda () (interactive) (ha-shell (project-root (project-current)))))
"a s q" '("quit shell" . ha-ssh-exit)
"a s f" '("find-file" . ha-ssh-find-file)
"a s r" '("find-root" . ha-ssh-find-root)
"a s b" '("send line" . ha-ssh-send-line))
"a s b" '("send line" . ha-ssh-send-line)
"p t" '("project vterm" . (lambda () (interactive) (ha-shell (project-root (project-current))))))
#+end_src
* Technical Artifacts :noexport:
Provide a name so we can =require= the file:

111
pud.org
View file

@ -2,7 +2,7 @@
#+author: Howard X. Abrams
#+date: 2025-01-18
#+filetags: emacs hamacs
#+lastmod: [2025-03-03 Mon]
#+lastmod: [2025-03-01 Sat]
A literate programming file for a Comint-based MUD client.
@ -48,21 +48,19 @@ The default connects to *Moss n Puddles*, my own MUD which I invite you to jo
:group 'processes)
(defcustom pud-worlds
'(["Moss-n-Puddles" telnet "howardabrams.com" 4000])
'(["Moss-n-Puddles" "howardabrams.com" 4000 "" ""])
"List of worlds you play in.
You need to define the worlds you play in before you can get
started. In most worlds, you can start playing using a guest account.
Each element WORLD of the list has the following form:
\[CONN-TYPE NAME HOST PORT CHARACTER PASSWORD LOGIN-STR]
\[NAME HOST PORT CHARACTER PASSWORD CONNECTION-STR]
NAME identifies the connection, HOST and PORT specify the network
connection, CHARACTER and PASSWORD are used to connect automatically.
The CONN-TYPE can be either 'telnet or 'ssh.
The LOGIN-STR is a string with two `%s' where this substitutes
The CONNECTION-STR is a string with two `%s' where this substitutes
the username and password respectively. Sends this to the server after
establishing a connection. This can be blank for the default.
If given, make sure to have a trailing `\n' to automatically send.
@ -72,19 +70,11 @@ The default connects to *Moss n Puddles*, my own MUD which I invite you to jo
:type '(repeat
(vector :tag "Server World"
(string :tag "Name")
(radio :tag "Type"
(const :tag "Telnet" :value telnet)
(const :tag "SSH" :value ssh))
(string :tag "Hostname")
(integer :tag "Port num")
(string :tag "Username" :value "guest")
(string :tag "Password")
(string :tag "Login String"
:format "%t: %v%h"
:doc "The login string to send after connection.
This should probably have a \`\\n' at the end to submit it.
If blank or nil, use the \`pud-default-connection-string'.
For example: connect %s %s\\n")))
(string :tag "Host")
(integer :tag "Port")
(string :tag "Char" :value "guest")
(string :tag "Pass")
(string :tag "Connect String" :value "connect %s %s")))
:group 'pud)
#+END_SRC
@ -94,23 +84,24 @@ For instance:
(use-package pud
:custom
(pud-worlds
'(["Remote Moss-n-Puddles" 'ssh "howardabrams.com" 4000 "bobby"]
'(["Remote Moss-n-Puddles" "howardabrams" 4000 "bobby"]
; ↑ No password? Should be in .authinfo.gpg
["Local Root" 'telnet "localhost" 4000 "suzy" "some-pass"]
["Local Root" "localhost" 4000 "suzy" "some-pass"]
; ↑ This has the password in your custom settings.
; ↓ Password from authinfo, special connection string:
["Local User" 'telnet "localhost" 4000 "rick" nil "login %s %s"])))
["Local User" "localhost" 4000 "rick" nil "login %s %s"])))
#+END_SRC
Hidden:
#+BEGIN_SRC emacs-lisp :tangle no :eval no
(setq pud-worlds
'(["Moss-n-Puddles" ssh "howardabrams.com" 4004 "howard" "" "\\nconnect %s %s\\n"]
["Moss-n-Puddles" ssh "howardabrams.com" 4004 "rick" "" "\\nconnect %s %s\\n"]
["Local-Moss" telnet "localhost" 4000 "howard" "" ""]
["Local-Moss" telnet "localhost" 4000 "rick" "" ""]))
(setq pud-worlds
'(["moss-n-puddles" "howardabrams.com" 4000 "howard"]
["moss-n-puddles" "howardabrams.com" 4000 "rick"]
["moss-n-puddles" "howardabrams.com" 4000 "darol"]
["local-evennia" "localhost" 4000 "howard"]
["local-evennia" "localhost" 4000 "rick"]))
#+END_SRC
Seems like MUDs have a standard login sequence, but they dont have to. Here is the default that a user can override in their =pud-worlds= listing:
@ -118,7 +109,7 @@ Seems like MUDs have a standard login sequence, but they dont have to. Here i
#+BEGIN_SRC emacs-lisp
(defcustom pud-default-connection-string "connect %s %s\n"
"The standard connection string to substitute the username and password."
:type '(string)
:type '(string :tag "Connect String" :value "connect %s %s\n")
:group 'pud)
#+END_SRC
@ -170,14 +161,14 @@ The following functions are accessibility functions to the world entry.
(defun pud-world-name (world)
"Return the name for WORLD as a string."
(if (vectorp world)
(if (or (length< world 5) (null (aref world 4)) (string-blank-p (aref world 4)))
(if (or (length< world 4) (null (aref world 3)) (string-blank-p (aref world 3)))
(aref world 0)
(concat (aref world 4) "@" (aref world 0)))
(concat (aref world 3) "@" (aref world 0)))
world))
(defun pud-world-network (world)
"Return the network details for WORLD as a cons cell (HOST . PORT)."
(list (aref world 2) (format "%s" (aref world 3))))
(list (aref world 1) (format "%s" (aref world 2))))
(defun pud-world-creds (world)
"Return the username and password from WORLD.
@ -191,7 +182,7 @@ The following functions are accessibility functions to the world entry.
(list (plist-get auth-results :user)
(funcall (plist-get auth-results :secret)))
;; No match? Just return values from world:
(list (aref world 4) (aref world 5)))))
(list (aref world 3) (aref world 4)))))
#+END_SRC
And some basic functions I should expand.
@ -205,25 +196,22 @@ And some basic functions I should expand.
(should (string-equal (pud-world-name ["foobar" "localhost" "4000" "guest" "guest"]) "guest@foobar")))
(ert-deftest pud-world-network-test ()
(should (equal (pud-world-network ["foobar" telnet "overthere" "4000" "guest" "guest"]) '("overthere" "4000")))
(should (equal (pud-world-network ["foobar" ssh "overthere" 4000 "guest" "guest"]) '("overthere" "4000"))))
(should (equal (pud-world-network ["foobar" "overthere" "4000" "guest" "guest"]) '("overthere" "4000")))
(should (equal (pud-world-network ["foobar" "overthere" 4000 "guest" "guest"]) '("overthere" "4000"))))
(ert-deftest pud-world-creds-test ()
;; Test with no match in authinfo!
(should (equal
(pud-world-creds ["some-place" telnet "some-home" 4000 "a-user" "a-pass"])
(pud-world-creds ["first" "some-home" 4000 "a-user" "a-pass"])
'("a-user" "a-pass")))
;; This test works if the following line is in .authinfo:
;; machine localhost port 4000 login george password testpass
(should (equal
(pud-world-creds ["nudder-place" ssh "localhost" 4000 "george"])
(pud-world-creds ["first" "localhost" 4000 "george"])
'("george" "testpass"))))
#+END_SRC
* Basics
:LOGBOOK:
CLOCK: [2025-03-03 Mon 11:57]--[2025-03-03 Mon 12:10] => 0:13
:END:
Using Comint, and hoping to have the ANSI colors displayed.
#+BEGIN_SRC emacs-lisp
@ -234,51 +222,17 @@ Using Comint, and hoping to have the ANSI colors displayed.
Im going to use good ol fashion =telnet= for the connection:
#+BEGIN_SRC emacs-lisp
(defcustom pud-telnet-path "telnet"
"Path to the program used by `run-pud' to connect using telnet."
:type '(string)
:group 'pud)
(defcustom pud-ssh-path "ssh"
"Path to the program used by `run-pud' to connect using ssh."
:type '(string)
:group 'pud)
(defvar pud-cli-file-path "telnet" ; ssh!?
"Path to the program used by `run-pud'.")
#+END_SRC
The pud-cli-arguments, holds a list of commandline arguments: the port.
#+BEGIN_SRC emacs-lisp
(defvar pud-cli-arguments nil
"A list of arguments to use before the connection.")
"A list of arguments to use before the telnet location.")
#+END_SRC
Command string to use, given a =world= with a connection type:
#+BEGIN_SRC emacs-lisp
(defun pud-cli-command (world)
"Return a command string to pass to the shell.
The WORLD is a vector with the hostname, see `pud-worlds'."
(seq-let (host port) (pud-world-network world)
(message "Dealing with: %s %s %s" host port (aref world 1))
(cl-case (aref world 1)
(telnet (append (cons pud-telnet-path pud-cli-arguments)
(list host port)))
(ssh (append (cons pud-cli-filepath-ssh pud-cli-arguments)
(list "-p" port host)))
(t (error "Unsupported connection type")))))
#+END_SRC
Some tests:
#+BEGIN_SRC emacs-lisp :tangle no
(ert-deftest pud-cli-command-test ()
(should (equal (pud-cli-command ["some-world" telnet "world.r.us" 4000])
'("telnet" "world.r.us" "4000")))
(should (equal (pud-cli-command ["nudder-world" ssh "world.r.us" 4004])
'("ssh" "-p" "4004" "world.r.us"))))
#+END_SRC
The empty and currently disused mode map for storing our custom keybindings inherits from =comint-mode-map=, so we get the same keys exposed in =comint-mode=.
#+BEGIN_SRC emacs-lisp
@ -317,7 +271,8 @@ The main entry point to the program is the =run-pud= function:
- username (can be overridden)
- password (should be overridden)"
(interactive (list (pud-get-world)))
(let* ((pud-cli (pud-cli-command world))
(let* ((pud-program pud-cli-file-path)
(pud-args (append pud-cli-arguments (pud-world-network world)))
(buffer (get-buffer-create (pud-buffer-name world)))
(proc-alive (comint-check-proc buffer))
(process (get-buffer-process buffer)))
@ -325,7 +280,7 @@ The main entry point to the program is the =run-pud= function:
;; mode.
(unless proc-alive
(with-current-buffer buffer
(apply 'make-comint-in-buffer "Pud" buffer (car pud-cli) nil (cdr pud-cli))
(apply 'make-comint-in-buffer "Pud" buffer pud-program nil pud-args)
(pud-mode)
(visual-line-mode 1)
(pud-reconnect world)))

View file

@ -1,4 +0,0 @@
# key: <ill
# name: ignore-line-length
# --
<!--- pyml disable-num-lines ${0} line-length -->

View file

@ -1,4 +0,0 @@
# key: <ils
# name: ignore-lines
# --
<!--- pyml disable-num-lines ${1} ${2:$$(yas-choose-value '("line-length" "no-bare-urls" "code-fence-style" ))} -->

View file

@ -1,4 +0,0 @@
# key: <inl
# name: ignore-next-line
# --
<!--- pyml disable-next-line ${1:$$(yas-choose-value '("line-length" "no-bare-urls" "code-fence-style" ))} -->

View file

@ -1,5 +0,0 @@
# -*- mode: snippet -*-
# name: ignore-lines
# key: <ils
# --
<!--- pyml disable-num-lines ${1} ${2:$$(yas-choose-value '("line-length" "no-bare-urls" "code-fence-style" ))} -->

View file

@ -1,6 +0,0 @@
# key: <sy
# name: yaml-code-block
# --
#+BEGIN_SRC yaml
$0
#+END_SRC