From b1d77b0f5d7f96c200fcf5429fbf5960a3e73a69 Mon Sep 17 00:00:00 2001 From: Alexander Rosenberg Date: Mon, 17 Feb 2025 21:21:52 -0800 Subject: [PATCH] Better distrobox and sudo tramp support --- elisp/arch-ros2.el | 133 --------------------------- elisp/eshell-starship.el | 191 ++++++++++++++++++++++++++++----------- init.el | 165 +++++++++++++++++++++++++++++---- 3 files changed, 282 insertions(+), 207 deletions(-) delete mode 100644 elisp/arch-ros2.el diff --git a/elisp/arch-ros2.el b/elisp/arch-ros2.el deleted file mode 100644 index 48d70fc..0000000 --- a/elisp/arch-ros2.el +++ /dev/null @@ -1,133 +0,0 @@ -;;; arch-ros2.el --- Activate and deactivate ROS2 dev environment on ArchLinux -*- lexical-binding: t -*- -;;; Commentary: -;;; Code: -(require 'cl-lib) - -(defcustom arch-ros2-root "/opt/ros/humble/" - "Root directory of the ROS2 install." - :type 'directory - :group 'arch-ros2) - -(defcustom arch-ros2-distro "humble" - "Version name of ROS2." - :type 'string - :group 'arch-ros2) - -(defcustom arch-ros2-version 2 - "Version number of ROS2 (probably 2)." - :type 'integer - :group 'arch-ros2) - -(defcustom arch-ros2-python-version "3.13" - "Python version of ROS2." - :type 'string - :group 'arch-ros2) - -(defvar arch-ros2-active nil - "Weather of not the ROS2 development environment is active.") - -(defconst arch-ros2-mode-line-format `(arch-ros2-active - ,(propertize "[ROS2]" - 'face 'mode-line-emphasis)) - "Mode line element for ROS2.") - -(defvar arch-ros2--saved-env-vars (make-hash-table :test 'equal) - "Hash table of saved environment variables. -The key of each entry is the variable name. The value is a cons. The car is -either the symbol \\='value or \\='files. If it is \\='value, the cons is a -list of the old value and the value we installed. If the cdr is \\='files, the -value is a list of files to be removed from the variable.") - -(defun arch-ros2--set-env-var (var value) - "Set the environment variable VAR to VALUE, saving its old value." - (puthash var (list 'value (getenv var) value) arch-ros2--saved-env-vars) - (setenv var value)) - -(defun arch-ros2--add-file-to-var (var &rest values) - "Add each of VALUES to the file list environment variable VAR. -This will prepend the values to VAR." - (let* ((cur-val (split-string (or (getenv var) "") ":" t)) - (to-set)) - (dolist (value values) - (unless (cl-find value cur-val :test 'equal) - (push value to-set))) - (let ((cache (gethash var arch-ros2--saved-env-vars))) - (puthash var (cons 'files (seq-uniq (append to-set (cdr cache)))) - arch-ros2--saved-env-vars)) - (setenv var (string-join (append to-set cur-val) ":")))) - -(defun arch-ros2--add-to-path (&rest values) - "Add each of VALUES to the variable `exec-path'." - (let ((to-check (butlast exec-path)) - (did-add nil)) - (dolist (value values) - (unless (cl-find value to-check :test 'equal) - (push value exec-path) - (push value did-add))) - (puthash 'exec-path (append did-add - (gethash 'exec-path arch-ros2--saved-env-vars)) - arch-ros2--saved-env-vars))) - -(defun arch-ros2--restore-env-var (var) - "Restore the value of VAR set with `arch-ros2--set-env-var'." - (let ((entry (gethash var arch-ros2--saved-env-vars))) - (cl-case (car entry) - (value - (cl-destructuring-bind (&optional old-val our-val) (cdr entry) - ;; don't restore values that have been changed - (when (equal our-val (getenv var)) - (setenv var old-val)))) - (files - (when-let ((cur-val (getenv var)) - (parts (split-string cur-val ":" t))) - (setenv var (string-join (seq-difference parts (cdr entry)) ":"))))) - (remhash var arch-ros2--saved-env-vars))) - -(defun arch-ros2-activate () - "Activate a ROS2 development environment." - (interactive) - (setq arch-ros2-active t) - (add-to-list 'mode-line-misc-info arch-ros2-mode-line-format) - (arch-ros2--add-to-path "/opt/ros/humble/bin/") - (arch-ros2--set-env-var "AMENT_PREFIX_PATH" arch-ros2-root) - (arch-ros2--set-env-var "CMAKE_PREFIX_PATH" arch-ros2-root) - (arch-ros2--set-env-var "COLCON_PREFIX_PATH" arch-ros2-root) - (arch-ros2--set-env-var "ROS_DISTRO" arch-ros2-distro) - (arch-ros2--set-env-var "ROS_LOCALHOST_ONLY" "0") - (arch-ros2--set-env-var "ROS_PYTHON_VERSION" - (car (split-string arch-ros2-python-version "\\."))) - (arch-ros2--set-env-var "ROS_VERSION" - (number-to-string arch-ros2-version)) - (arch-ros2--add-file-to-var - "LD_LIBRARY_PATH" - (expand-file-name "opt/rviz_ogre_vendor/lib" - arch-ros2-root) - (expand-file-name "lib" - arch-ros2-root)) - (arch-ros2--add-file-to-var - "PKG_CONFIG_PATH" (expand-file-name "lib/pkgconfig" arch-ros2-root)) - (let ((python-dir (expand-file-name - (concat "lib/python" arch-ros2-python-version) - arch-ros2-root))) - (arch-ros2--add-file-to-var "PYTHONPATH" - (expand-file-name "dist-packages" python-dir) - (expand-file-name "site-packages" python-dir)))) - -(defun arch-ros2-deactivate () - "Deactivate the ROS2 development environment." - (interactive) - (setq arch-ros2-active nil - mode-line-misc-info (cl-remove arch-ros2-mode-line-format - mode-line-misc-info - :test 'equal)) - (maphash (lambda (k v) - (cond - ((stringp k) - (arch-ros2--restore-env-var k)) - ((eq k 'exec-path) - (setq exec-path (seq-difference exec-path v)) - (remhash 'exec-path arch-ros2--saved-env-vars)))) - arch-ros2--saved-env-vars)) - -(provide 'arch-ros2) -;;; arch-ros2.el ends here diff --git a/elisp/eshell-starship.el b/elisp/eshell-starship.el index 67877a9..add521f 100644 --- a/elisp/eshell-starship.el +++ b/elisp/eshell-starship.el @@ -5,6 +5,8 @@ (require 'vc-git) (require 'eshell) (require 'cl-lib) +(require 'tramp) +(eval-when-compile (require 'rx)) ;;; Configuration options (defgroup eshell-starship nil @@ -35,7 +37,7 @@ This will also update all eshell-starship explain buffers that need updating." (revert-buffer))))))) (defcustom eshell-starship-module-order - '("remote" "cwd" "git" "vc" t "cmd-time" "arrow") + '("remote" "root" "cwd" "git" "vc" t "cmd-time" "newline" "container" "arrow") "The order of modules for eshell-starship. This is a list with each element being a module name. The special value t can appear at most once to denote \"all remaining modules\"." @@ -58,6 +60,22 @@ appear at most once to denote \"all remaining modules\"." :tag "Suppress eshell-starship explore refresh messages" :type 'boolean) +(defcustom eshell-starship-overridden-remote-methods + '("docker" "podman" "kubernetes" "doas" "su" "sudo" "sudoedit") + "List of `file-remote-p' mwthods that should NOT be considered remote. +Any eshell buffer with a `default-directory' managed by one of these methods +will not be considered remote and all modules that would be disabled because of +the remote directory will work as usual." + :group 'eshell-starship + :tag "Overridden Remote Methods" + :type '(repeat (string :tag "Method"))) + +(defcustom eshell-starship-verbose-tramp 1 + "Tramp verbosity level when rendering the prompt." + :group 'eshell-starship + :tag "Tramp Verbosity Level" + :type 'integer) + (defface eshell-starship-icon-face '((t :inherit default)) "Face to use when drawing module icons. Note that the foreground color will be overridden by the module." @@ -162,7 +180,7 @@ be nil.") :documentation "Weather the module should be run if `default-directory' is a `file-remote-p'.") (action :initarg :action - :initform 'ignore + :initform 'string :accessor eshell-starship-module-action :type function :documentation "A function that produces the main text for the @@ -304,12 +322,16 @@ Example: (defun eshell-starship--get-current-dir () "Get dir for `eshell-starship--prompt-function'." (concat - (propertize (eshell-starship--limit-path-parts - 3 (if-let ((worktree (vc-root-dir)) - (parent (file-name-parent-directory worktree))) - (file-relative-name default-directory parent) - (eshell-starship--replace-home-with-tilda default-directory))) - 'face '(:foreground "dark turquoise")) + (propertize + (eshell-starship--limit-path-parts + 3 (let ((cwd (or (file-remote-p default-directory 'localname) + default-directory))) + (if-let ((worktree (vc-root-dir)) + (parent (file-name-parent-directory worktree))) + (file-relative-name cwd (or (file-remote-p parent 'localname) + parent)) + (eshell-starship--replace-home-with-tilda cwd)))) + 'face '(:foreground "dark turquoise")) (unless (file-writable-p default-directory) " "))) @@ -721,24 +743,56 @@ This does not mean anything if pyenv-mode is not installed.") ;;; Misc modules (eshell-starship-defmodule remote - :icon "🌐" + :icon "🌐 " :color "light blue" - :predicate (lambda () - (file-remote-p default-directory)) + :predicate + (lambda () + (eshell-starship--remote-for-modules-p default-directory)) + :action + (lambda () + (or (file-remote-p default-directory 'host) "")) + :reload-on 'cwd :doc "A small icon if the working directory is remote.") +(eshell-starship-defmodule root + :predicate + (lambda () + (member (file-remote-p default-directory 'method) + '("doas" "sudo" "su" "sudoedit"))) + :action + (lambda () + (format "%s in" + (propertize (file-remote-p default-directory 'user) + 'face '(:weight bold :foreground "red")))) + :reload-on 'cwd + :doc "Show the current sudo or doas user.") + +(eshell-starship-defmodule newline + :predicate 'always + :action (lambda () (propertize "\n" 'read-only t 'rear-nonsticky t)) + :doc "A newline in the prompt.") + +(eshell-starship-defmodule container + :icon "⬢ " + :color "firebrick" + :predicate (lambda () + (member (file-remote-p default-directory 'method) + '("docker" "podman" "kubernetes"))) + :action (lambda () + (format "[%s]" (file-remote-p default-directory 'host))) + :reload-on 'cwd + :doc "The name of the current container.") + (eshell-starship-defmodule arrow :predicate 'always :reload-on 'always :action (lambda () - (concat - (propertize "\n" 'read-only t 'rear-nonsticky t) - (propertize - "❯ " 'face `(:foreground - ,(if (= eshell-last-command-status 0) - "lime green" - "red")) - 'rear-nonsticky t))) + (propertize + "❯ " 'face `(:foreground + ,(if (= eshell-last-command-status 0) + "lime green" + "red")) + 'rear-nonsticky t)) :doc "An arrow that appears next to where you type.") @@ -773,36 +827,52 @@ That is, if EXT is \"pkg.tar.gz\", this will return (substring name (1+ idx)) "")) +(defun eshell-starship--remote-for-modules-p (file) + "Return non-nil if FILE is remote for the purpose of running modules." + (let ((method (file-remote-p file 'method))) + (and method + (not (member method eshell-starship-overridden-remote-methods))))) + (defun eshell-starship--modules-for-dir (dir) "Return a list of modules that are applicable to DIR." - (seq-uniq - (nconc - (mapcan - (lambda (entry) - (let ((name (car entry)) - (is-dir (eq t (file-attribute-type (cdr entry))))) - (if is-dir - (copy-sequence (eshell-starship--module-by :dirs name)) - (apply 'nconc - (eshell-starship--module-by :files name) - (mapcar (lambda (ext) - (copy-sequence (eshell-starship--module-by - :extensions ext))) - (eshell-starship--permute-extension - (eshell-starship--file-name-extension name))))))) - (directory-files-and-attributes dir nil nil t)) - (let ((default-directory dir)) - (cl-loop for (name is-dir module) in eshell-starship--extra-module-files - when (and is-dir (file-directory-p name)) - collect module - when (and (not is-dir) (file-exists-p name)) - collect module)) - (let ((default-directory dir)) - (cl-loop for module being the hash-values of eshell-starship-modules - for predicate = (eshell-starship-module-predicate module) - when (funcall predicate) - collect module))) - 'eq)) + (let ((is-remote (eshell-starship--remote-for-modules-p dir))) + (seq-uniq + (nconc + (cl-delete-if + (lambda (module) + (and is-remote (not (eshell-starship-module-allow-remote-p module)))) + (mapcan + (lambda (entry) + (let ((name (car entry)) + (is-dir (eq t (file-attribute-type (cdr entry))))) + (if is-dir + (copy-sequence (eshell-starship--module-by :dirs name)) + (apply 'nconc + (eshell-starship--module-by :files name) + (mapcar (lambda (ext) + (copy-sequence (eshell-starship--module-by + :extensions ext))) + (eshell-starship--permute-extension + (eshell-starship--file-name-extension name))))))) + (directory-files-and-attributes dir nil nil t))) + (let ((default-directory dir)) + (cl-loop for (name is-dir module) in eshell-starship--extra-module-files + when (and (or (not is-remote) + (eshell-starship-module-allow-remote-p module)) + is-dir (file-directory-p name)) + collect module + when (and (or (not is-remote) + (eshell-starship-module-allow-remote-p module)) + (not is-dir) (file-exists-p name)) + collect module)) + (let ((default-directory dir)) + (cl-loop for module being the hash-values of eshell-starship-modules + for predicate = (eshell-starship-module-predicate module) + when (and (or (not is-remote) + (eshell-starship-module-allow-remote-p module)) + (funcall predicate)) + collect module))) + 'eq))) (defun eshell-starship--propertize-face (str append &rest faces) "Copy STR and add FACES to its text properties. @@ -902,9 +972,17 @@ Return a hash table mapping module names to their output." (t (push (gethash cur-name output) pre) (remhash cur-name output)))) - (mapconcat 'identity - (nconc (nreverse pre) (hash-table-values output) (nreverse post)) - " "))) + (cl-loop for (part . rest) = (nconc (nreverse pre) + (hash-table-values output) + (nreverse post)) + then rest + while part + concat part + unless (or (string-suffix-p "\n" part) + (string-empty-p part) + (not (car rest)) + (string-prefix-p "\n" (car rest))) + concat " "))) (defun eshell-starship--render-prompt () "Actually produce the prompt." @@ -918,7 +996,8 @@ Return a hash table mapping module names to their output." (defun eshell-starship--prompt-function () "Function for `eshell-prompt-function'." - (let (start-time prompt end-time) + (let ((tramp-verbose eshell-starship-verbose-tramp) + start-time prompt end-time) (setq start-time (float-time) prompt (eshell-starship--render-prompt) end-time (float-time) @@ -940,10 +1019,12 @@ Return a hash table mapping module names to their output." (defun eshell-starship--enable () "Enable eshell-starship." (setq-local eshell-starship--restore-state - (buffer-local-set-state eshell-prompt-function - 'eshell-starship--prompt-function - eshell-prompt-regexp "^❯ " - eshell-highlight-prompt nil) + (buffer-local-set-state + eshell-prompt-function + 'eshell-starship--prompt-function + ;; temporary fix until the next version where eshell uses fields + eshell-prompt-regexp (rx bol (? "⬢ [" (+ any) "] ") "❯ ") + eshell-highlight-prompt nil) eshell-starship--module-cache (make-hash-table :test 'equal)) (add-hook 'eshell-pre-command-hook #'eshell-starship--run-module-precmd-actions nil t) diff --git a/init.el b/init.el index e78f6e6..35c76b0 100644 --- a/init.el +++ b/init.el @@ -227,12 +227,32 @@ Interactively, force the recompile if called with a prefix." (use-package tramp :ensure nil :config + (add-to-list 'tramp-connection-properties + (list (rx bos "/" (or "podman" "docker") ":") + "direct-async-process" t)) + (add-to-list 'tramp-connection-properties + (list (rx bos "/" (or "ssh" "sshx") ":") + "direct-async-process" t)) + (add-to-list 'tramp-connection-properties + (list (rx bos "/" (or "sudo" "su" "doas" + "sudoedit") + ":") + "direct-async-process" t)) (connection-local-set-profile-variables - 'remote-direct-async-process - '((tramp-direct-async-process . t))) + 'error-only + '((tramp-verbose . 1))) (connection-local-set-profiles - '(:protocol "ssh") - 'remote-direct-async-process)) + '(:method "sudo") + 'error-only) + (connection-local-set-profiles + '(:method "doas") + 'error-only) + (connection-local-set-profiles + '(:method "su") + 'error-only) + (connection-local-set-profiles + '(:method "sudoedit") + 'error-only)) (use-package midnight :ensure nil @@ -1762,9 +1782,6 @@ otherwise, call `bibtex-find-text'." (use-package pyvenv) (use-package pyenv-mode) -;; My dev environment for ROS2 -(require 'arch-ros2) - ;; java-ts-mode (use-package java-ts-mode :hook ((java-ts-mode . trusted-files-eglot-ensure-if-safe) @@ -2464,7 +2481,7 @@ argument." :hook ((eshell-load . eat-eshell-visual-command-mode) (eshell-mode . eat-eshell-mode) (eshell-mode . my/-eshell-mode-setup) - (eshell-directory-change . my/-eshell-maybe-setup-remote-aliases)) + (eshell-directory-change . my/-eshell-maybe-setup-remote)) :bind (:map eshell-mode-map ("TAB" . completion-at-point) ("" . completion-at-point)) @@ -2479,14 +2496,73 @@ argument." (or " " eos)) (cl-second elt)))) eshell-command-aliases-list)) - (defun my/-eshell-maybe-setup-remote-aliases () - (if (file-remote-p default-directory) - (setq-local eshell-command-aliases-list (my/-eshell-filter-alias-list)) - (kill-local-variable 'eshell-command-aliases-list))) + + (defun my/-eshell-container-p (&optional dir) + (member (file-remote-p default-directory 'method) + '("docker" "podman"))) + (defun my/-eshell-distrobox-p (&optional dir) + (and (my/-eshell-container-p dir) + (let ((default-directory (or dir default-directory))) + (executable-find "distrobox-host-exec" t)))) + (defun my/-eshell-sudo-p (&optional dir) + (member (file-remote-p (or dir default-directory) 'method) + '("sudo" "doas" "su" "sudoedit"))) + (defun my/-eshell-really-remote-p (&optional dir) + (and (file-remote-p (or dir default-directory)) + (not (my/-eshell-distrobox-p dir)) + (not (my/-eshell-sudo-p dir)))) + + (defun eshell/-captive-cd (&optional dir &rest _) + (cond + ((not dir) + (eshell/-captive-cd "~")) + ((or (not (file-remote-p default-directory)) + (file-remote-p dir)) + (eshell/cd dir)) + ((file-name-absolute-p dir) + (eshell/cd (concat (file-remote-p default-directory) dir))) + (t + (eshell/cd dir)))) + + (defvar-local my/-eshell-last-remote-system nil) + (defun my/-eshell-maybe-setup-remote (&optional force) + (when (or force (not (equal my/-eshell-last-remote-system + (file-remote-p default-directory)))) + (kill-local-variable 'eshell-syntax-highlighting-highlight-in-remote-dirs) + (if (my/-eshell-really-remote-p) + (setq-local eshell-command-aliases-list (my/-eshell-filter-alias-list)) + (setq-local eshell-command-aliases-list + (default-toplevel-value 'eshell-command-aliases-list))) + (setq-local eshell-command-aliases-list + (copy-tree eshell-command-aliases-list)) + (when (file-remote-p default-directory) + (add-to-list 'eshell-command-aliases-list '("cd" "-captive-cd $1") t)) + (when (or (my/-eshell-distrobox-p) + (my/-eshell-sudo-p)) + (setq-local eshell-syntax-highlighting-highlight-in-remote-dirs t) + (setf (alist-get "pwd" eshell-command-aliases-list nil nil 'equal) + '("(directory-file-name (file-remote-p default-directory 'localname))"))) + (when (my/-eshell-distrobox-p) + (unless (executable-find "eza" t) + (if (executable-find "exa" t) + (setf (alist-get "ls" eshell-command-aliases-list nil nil 'equal) + '("exa -F $*")) + (setf (alist-get "ls" eshell-command-aliases-list nil t 'equal) + nil))) + (unless (executable-find "trash-put" t) + (setf (alist-get "tp" eshell-command-aliases-list nil t 'equal) nil + (alist-get "trr" eshell-command-aliases-list nil t 'equal) nil + (alist-get "tre" eshell-command-aliases-list nil t 'equal) nil + (alist-get "trm" eshell-command-aliases-list nil t 'equal) nil + (alist-get "rm" eshell-command-aliases-list nil t 'equal) nil)) + (setf (alist-get "ldg" eshell-command-aliases-list nil t 'equal) nil))) + (setq-local my/-eshell-last-remote-system + (file-remote-p default-directory))) + (defun my/-eshell-mode-setup () "Setup function run from `eshell-mode-hook'" (setq-local corfu-auto nil) - (my/-eshell-maybe-setup-remote-aliases)) + (my/-eshell-maybe-setup-remote t)) (setq-default eshell-command-aliases-list '(("clear" "clear t") ("e" "find-file $1") @@ -2507,10 +2583,10 @@ argument." ("tp" "trash-put $*") ("trr" "trash-restore $*") ("tre" "trash-empty $*") - ("tre" "trash-empty $*") ("trm" "trash-rm $*") ("rm" "echo 'rm: I''m unsafe! Don''t use me.'; false") ("\\rm" "eshell/rm"))) + (defvar my/eshell-bm-auto-ls t "Weather or not to run ls after `eshell/bm'") (defun eshell/bm (&optional name) @@ -2518,10 +2594,61 @@ argument." If no name is given, list all bookmarks instead." (if name (progn - (eshell/cd (bookmark-get-filename name)) - (when my/eshell-bm-auto-ls - (eshell/ls))) - (eshell-print (string-join (bookmark-all-names) " "))))) + (string-match (rx bos (group (* (not "/"))) (* "/") (group (* any))) + name) + (let* ((bm-name (match-string 1 name)) + (after-path (match-string 2 name)) + (bm-path (bookmark-get-filename bm-name)) + (full-path (expand-file-name after-path bm-path))) + (when (my/-eshell-distrobox-p) + (setq full-path (concat (file-remote-p default-directory) + full-path))) + (if (not (file-directory-p full-path)) + (progn + (find-file full-path) + (goto-char (bookmark-get-position bm-name))) + (eshell/cd full-path) + (when my/eshell-bm-auto-ls + (eshell/ls))))) + (bookmark-maybe-load-default-file) + (eshell-print + (mapconcat (lambda (record) + (let ((name (bookmark-name-from-full-record record)) + (file (bookmark-get-filename record))) + (format "%s => %s" + (propertize name 'face '(:foreground "deep sky blue" + :weight bold)) + (if (file-directory-p file) + (file-name-as-directory file) + (directory-file-name file))))) + bookmark-alist + "\n")))) + (defun pcomplete/bm () + "Completions for `bm'." + (let ((arg (pcomplete-arg))) + (if (not (cl-find ?/ arg)) + (pcomplete-here (mapcar (##concat % "/") (bookmark-all-names))) + (when (string-match (rx bos (group (+ (not "/"))) (+ "/") (group (* any))) + arg) + (let ((bm-name (match-string 1 arg)) + (after-path (match-string 2 arg))) + (when-let ((base (ignore-errors (bookmark-get-filename bm-name))) + ((file-directory-p base)) + (abs-path (expand-file-name after-path base)) + (dir-path (if (string-empty-p after-path) + abs-path + (file-name-directory abs-path))) + (path-end (if (string-empty-p after-path) + "" + (file-name-nondirectory abs-path)))) + (pcomplete-here + (mapcan (lambda (entry) + (unless (member (car entry) '(".." ".")) + (if (eq t (file-attribute-type (cdr entry))) + (list (concat (car entry) "/")) + (list (car entry))))) + (directory-files-and-attributes dir-path)) + path-end)))))))) (use-package esh-help :hook (eshell-mode . my/-setup-eshell-help-func) :init @@ -2547,7 +2674,7 @@ If `default-directory' is remote, call `my/project-eat-or-default'. Otherwise, call `my/project-eshell-or-default'. ARG is the same as for either of the above functions (only eshell uses it at the time of writing)." (interactive "P") - (if (file-remote-p default-directory) + (if (my/-eshell-really-remote-p) (my/project-eat-or-default) (my/project-eshell-or-default arg))) (keymap-global-set "C-c v" #'my/open-shell-dwim)