Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

267 lines
10 KiB

  1. ;;; better-shell.el --- Better shell management
  2. ;; Copyright (C) 2016 Russell Black
  3. ;; Author: Russell Black (killdash9@github)
  4. ;; Keywords: convenience
  5. ;; Package-Version: 1.2.1
  6. ;; Package-Commit: 70c787b981caeef8c5f8012b170eb7b9f167cd13
  7. ;; URL: https://github.com/killdash9/better-shell
  8. ;; Created: 1st Mar 2016
  9. ;; Version: 1.2.1
  10. ;; Package-Requires: ((emacs "24.4"))
  11. ;; This program is free software; you can redistribute it and/or modify
  12. ;; it under the terms of the GNU General Public License as published by
  13. ;; the Free Software Foundation, either version 3 of the License, or
  14. ;; (at your option) any later version.
  15. ;; This program is distributed in the hope that it will be useful,
  16. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. ;; GNU General Public License for more details.
  19. ;; You should have received a copy of the GNU General Public License
  20. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. ;;; Commentary:
  22. ;;
  23. ;; This package simplifies shell management and sudo access by providing
  24. ;; the following commands.
  25. ;;
  26. ;; `better-shell-shell'
  27. ;; --------------------
  28. ;;
  29. ;; Cycle through existing shell buffers, in order of recent use. If
  30. ;; there are no shells, one is created.
  31. ;;
  32. ;; With C-u prefix arg, invoke `better-shell-for-current-dir'.
  33. ;;
  34. ;; `better-shell-for-current-dir'
  35. ;; ------------------------------
  36. ;;
  37. ;; Bring up a shell on the same host and in the same directory as the
  38. ;; current buffer, choosing an existing shell if possible. The shell
  39. ;; chosen is guaranteed to be idle (not currently running a command). It
  40. ;; first looks for an idle shell that is already in the buffer's
  41. ;; directory. If none is found, it looks for another idle shell on the
  42. ;; same host as the buffer. If one is found, that shell is selected and
  43. ;; automatically placed into the buffer's directory with a `cd` command.
  44. ;; Otherwise, a new shell is created on the same host and in the same
  45. ;; directory as the buffer.
  46. ;;
  47. ;; `better-shell-remote-open'
  48. ;; --------------------------
  49. ;;
  50. ;; Open a shell on a remote server, allowing you to choose from any host
  51. ;; you've previously logged into (uses your ~/.ssh/known_hosts file) or
  52. ;; enter a new host. With C-u prefix arg, get sudo shell.
  53. ;;
  54. ;; `better-shell-sudo-here'
  55. ;; --------------------------
  56. ;;
  57. ;; Reopen the current file, directory, or shell as root.
  58. ;;; Code:
  59. (require 'cl-lib)
  60. (require 'tramp)
  61. (require 'shell)
  62. (defun better-shell-idle-p (buf)
  63. "Return t if the shell in BUF is not running something.
  64. When available, use process hierarchy information via pstree for
  65. local shells. Otherwise, we ask comint if the point is after a
  66. prompt."
  67. (with-current-buffer buf
  68. (let ((comint-says-idle (and
  69. (> (point) 1) ;; if point > 1
  70. ;; see if previous char has the prompt face
  71. (equal '(comint-highlight-prompt)
  72. (get-text-property
  73. (- (point) 1) 'font-lock-face)))))
  74. (if (file-remote-p default-directory)
  75. ;; for remote shells we have to rely on comint
  76. comint-says-idle
  77. ;; for local shells, we can potentially do better using pgrep
  78. (condition-case nil
  79. (case (call-process ;; look at the exit code of pgrep -P <pid>
  80. "pgrep" nil nil nil "-P"
  81. (number-to-string (process-id (get-buffer-process buf))))
  82. (0 nil) ;; child procxesses found, not idle
  83. (1 t) ;; not running any child processes, it's idle
  84. (t comint-says-idle)) ;; anything else, fall back on comint.
  85. (error comint-says-idle)))))) ;; comint fallback if execution failed
  86. (defun better-shell-shells ()
  87. "Return a list of buffers running shells."
  88. (cl-remove-if-not
  89. (lambda (buf)
  90. (and
  91. (get-buffer-process buf)
  92. (with-current-buffer buf
  93. (string-equal major-mode 'shell-mode))))
  94. (buffer-list)))
  95. (defun better-shell-idle-shells (remote-host)
  96. "Return all the buffers with idle shells on REMOTE-HOST.
  97. If REMOTE-HOST is nil, returns a list of idle local shells."
  98. (let ((current-buffer (current-buffer)))
  99. (cl-remove-if-not
  100. (lambda (buf)
  101. (with-current-buffer buf
  102. (and
  103. (string-equal (file-remote-p default-directory) remote-host)
  104. (better-shell-idle-p buf)
  105. (not (eq current-buffer buf)))))
  106. (better-shell-shells))))
  107. (defun better-shell-default-directory (buf)
  108. "Return the default directory for BUF."
  109. (with-current-buffer buf
  110. default-directory))
  111. (defun better-shell-for-current-dir ()
  112. "Find or create a shell in the buffer's directory.
  113. See `better-shell-for-dir' for details on how shells are found or created."
  114. (interactive)
  115. (better-shell-for-dir default-directory))
  116. (defun better-shell-for-dir (dir)
  117. "Find or create a shell in DIR.
  118. The shell chosen is guaranteed to be idle (not running another
  119. command). It first looks for an idle shell that is already in
  120. the buffer's directory. If none is found, it looks for another
  121. idle shell on the same host as the buffer. If one is found, that
  122. shell will be chosen, and automatically placed into the buffer's
  123. directory with a \"cd\" command. Otherwise, a new shell is
  124. created in the buffer's directory."
  125. (interactive "D")
  126. (let ((idle-shell
  127. (or
  128. ;; get currently idle shells, ones with matching directory
  129. ;; first.
  130. (car (sort
  131. (better-shell-idle-shells
  132. (file-remote-p default-directory))
  133. (lambda (s1 s2)
  134. (string-equal dir (better-shell-default-directory s1)))))
  135. ;; make a new shell if there are none
  136. (shell (generate-new-buffer-name
  137. (if (file-remote-p dir)
  138. (with-parsed-tramp-file-name dir nil
  139. (format "*shell/%s*" host))
  140. "*shell*"))))))
  141. ;; cd in the shell if needed
  142. (when (not (string-equal dir (better-shell-default-directory idle-shell)))
  143. (let ((localdir (if (file-remote-p dir)
  144. (with-parsed-tramp-file-name dir nil localname)
  145. (expand-file-name dir))))
  146. (with-current-buffer idle-shell
  147. (comint-delete-input)
  148. (goto-char (point-max))
  149. (insert (concat "cd \"" localdir "\""))
  150. (comint-send-input))))
  151. ;; now we have an idle shell in the correct directory. Pop to it.
  152. (pop-to-buffer idle-shell)))
  153. (defun better-shell-tramp-hosts ()
  154. "Ask tramp for a list of hosts that we can reach through ssh."
  155. (cl-reduce 'append
  156. (mapcar (lambda (x)
  157. (cl-remove nil (mapcar 'cadr (apply (car x) (cdr x)))))
  158. (tramp-get-completion-function "scp"))))
  159. ;;;###autoload
  160. (defun better-shell-remote-open (&optional arg)
  161. "Prompt for a remote host to connect to, and open a shell
  162. there. With prefix argument, get a sudo shell."
  163. (interactive "p")
  164. (let*
  165. ((hosts
  166. (cl-reduce 'append
  167. (mapcar
  168. (lambda (x)
  169. (cl-remove nil (mapcar 'cadr (apply (car x) (cdr x)))))
  170. (tramp-get-completion-function "ssh"))))
  171. (remote-host (completing-read "Remote host: " hosts)))
  172. (if (and arg (= 4 arg))
  173. ;; this means sudo
  174. (let ((tramp-default-proxies-alist nil))
  175. ;; so that you don't get method overrides. ssh is the only one that works for sudo.
  176. (with-temp-buffer
  177. (cd (concat "/ssh:" remote-host "|sudo:" remote-host ":"))
  178. (shell (format "*shell/sudo:%s*" remote-host))))
  179. ;; non-sudo
  180. (with-temp-buffer
  181. (cd (concat "/" (or tramp-default-method "ssh") ":" remote-host ":"))
  182. (shell (format "*shell/%s*" remote-host))))))
  183. ;;;###autoload
  184. (defun better-shell-sudo-here ()
  185. "Reopen the current file, directory, or shell as root.
  186. For files and dired buffers, the non-sudo buffer is replaced with
  187. a sudo buffer. For shells, a sudo shell is opened but the
  188. non-sudo shell is left in tact."
  189. (interactive)
  190. (let ((f (expand-file-name (or buffer-file-name default-directory))))
  191. (when (string-match-p "\\bsudo:" f) (user-error "Already sudo"))
  192. (let ((sudo-f (if (file-remote-p f)
  193. (with-parsed-tramp-file-name f nil
  194. (let ((user-string
  195. (when user (concat user "@"))))
  196. (concat "/ssh:" user-string host "|sudo:" host ":" localname)))
  197. (concat "/sudo:localhost:" f)))
  198. (tramp-default-proxies-alist nil)
  199. ;; so that you don't get method overrides. ssh is the only one that works for sudo.
  200. )
  201. (unless f (user-error "No file or default directory in this
  202. buffer. This command can only be used in file buffers,
  203. dired buffers, or shell buffers"))
  204. (cond ((or buffer-file-name (eq major-mode 'dired-mode))
  205. (find-alternate-file sudo-f))
  206. ((eq major-mode 'shell-mode)
  207. (with-temp-buffer
  208. (cd sudo-f)
  209. (shell (format "*shell/sudo:%s*"
  210. (with-parsed-tramp-file-name sudo-f nil host)))))
  211. (t (message "Can't sudo this buffer"))
  212. ))))
  213. (defun better-shell-existing-shell ()
  214. "Switch to the next existing shell in the stack."
  215. (interactive)
  216. ;; rotate through existing shells
  217. (let* ((shells (better-shell-shells))
  218. (buf (nth (mod (+ (or (cl-position (current-buffer) shells) -1) 1)
  219. (length shells))
  220. shells)))
  221. (switch-to-buffer buf t)
  222. (set-transient-map ; Read next key
  223. `(keymap (,(elt (this-command-keys-vector) 0) .
  224. better-shell-existing-shell))
  225. t (lambda () (switch-to-buffer (current-buffer))))))
  226. ;;;###autoload
  227. (defun better-shell-shell (&optional arg)
  228. "Pop to an appropriate shell.
  229. Cycle through all the shells, most recently used first. When
  230. called with a prefix ARG, finds or creates a shell in the current
  231. directory."
  232. (interactive "p")
  233. (let ((shells (better-shell-shells)))
  234. (if (or (null shells) (and arg (= 4 arg)))
  235. (better-shell-for-current-dir)
  236. (better-shell-existing-shell))))
  237. ;;;###autoload
  238. (defun better-shell-for-projectile-root ()
  239. "Find or create a shell in the projectile root.
  240. See `better-shell-for-dir' for details on how shells are found or created."
  241. (interactive)
  242. (unless (fboundp 'projectile-project-root)
  243. (error "Projectile does not appear to be installed"))
  244. (better-shell-for-dir (projectile-project-root)))
  245. (provide 'better-shell)
  246. ;;; better-shell.el ends here