├── .gitignore ├── README.org └── project-shells.el /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Project Shells 2 | 3 | ** Why bother? 4 | 5 | This is to manage multiple shell (or terminal, eshell) buffers for 6 | each project. For example, to develop for Linux kernel, I usually use 7 | one shell buffer to configure and build kernel, one shell buffer to 8 | run some git command not supported by magit, one shell buffer to run 9 | qemu for built kernel, one shell buffer to ssh into guest system to 10 | test. Different set of commands is used by the shell in each buffer, 11 | so each shell should have different command history configuration, and 12 | for some shell, I may need different setup. And I have several 13 | projects to work on. In addition to project specific shell buffers, I 14 | want some global shell buffers, so that I can use them whichever 15 | project I am working on. Project shells is an Emacs package to let my 16 | life easier via helping me to manage all these shell/terminal/eshell 17 | buffers. 18 | 19 | ** Install 20 | 21 | The preferred install method is through package manager: package.el. 22 | project-shells is available in [[http://melpa.org/][MELPA]] package repository. 23 | 24 | Or you can download the project-shells.el, add its path into emacs 25 | load path, and load the file. For example, add 26 | 27 | #+BEGIN_SRC emacs-lisp 28 | (add-to-list 'load-path "") 29 | (require 'project-shells) 30 | #+END_SRC 31 | 32 | in your Emacs init file. 33 | 34 | ** Usage 35 | 36 | To enable the project-shells minor mode globally, 37 | ~global-project-shells-mode~ can be used, or it can enabled in a 38 | buffer with ~project-shells-mode~. The default prefix key is "C-c s", 39 | which can be changed via customizing ~project-shells-keymap-prefix~. 40 | 41 | The package could also be setup with enabling minor mode via call 42 | ~project-shells-setup~, the parameter is the keymap to add project 43 | shells key binding. For example, to use project shells with 44 | projectile you can put the following forms in your init file, 45 | 46 | #+BEGIN_SRC emacs-lisp 47 | (project-shells-setup projectile-mode-map) 48 | #+END_SRC 49 | 50 | You can add project-shells key binding in other keymap too, or you can 51 | create your own keymap. 52 | 53 | After enabling the project-shells minor mode or binding the key, you 54 | can create or switch to the shell buffers via, 55 | 56 | 57 | 58 | The default s are "1", "2", "3", "4", "5", "6", "7", "8", 59 | "9", "0", "-", "=", so you can have 12 shell/terminal/eshell buffers 60 | for each project. You can change the number of buffers and keys to 61 | activate them via customizing ~project-shells-keys~. By default "1" - 62 | "0" will create shell buffers, "-" will create terminal buffers, "=" 63 | will create eshell buffers. You can customize which keys to create 64 | terminal, eshell buffers via customize the following variables, 65 | 66 | #+BEGIN_SRC emacs-lisp 67 | project-shells-term-keys 68 | project-shells-eshell-keys 69 | project-shells-vterm-keys 70 | #+END_SRC 71 | 72 | To create or switch to the global shells, you need to use position 73 | parameter for the . For example, 74 | 75 | C-u 76 | 77 | Use ~customize-group~ project-shell to change the configuration. 78 | 79 | ** Work with project management package 80 | 81 | You need a project management package enabled, so that project shells 82 | knows what the current project is and what the project root directory 83 | is. The default configuration works with projectile. To work with 84 | other project management package, you need to customize 85 | ~project-shells-project-name-func~ and 86 | ~project-shells-project-root-func~. 87 | 88 | ** Shell configuration 89 | 90 | In the default configuration, each shell program and eshell runs by 91 | project-shells will has own history file. The default history file 92 | name is "~/.sessions///.shell_history". The 93 | default configuration works for bash, to support other shell, the 94 | ~project-shells-histfile-env~ may need to be customized. 95 | 96 | If some special shell configuration is needed, the shell initialize 97 | file could be added into the session directory, the default initialize 98 | file name is "~/.sessions///.shellrc". The default 99 | configuration works for bash, to support other shell, the 100 | ~project-shells-default-init-func~ may need to be customized. 101 | 102 | ** SSH 103 | 104 | The shell buffer could be a remote shell buffer, that is, a ssh 105 | session accessing a remote machine. To make a shell buffer remote, 106 | use something like ~/ssh:@:~ as the default directory 107 | when configure shell buffers via ~project-shells-setup~. Or you can 108 | use ~ask~ (symbol ask) as default directory, and you will be prompted 109 | for destination user@host. 110 | 111 | The ssh support code is based on Ian Eure's nssh. Thanks Ian! 112 | 113 | ** Example configuration 114 | 115 | One of the most important configuration for project-shells is 116 | ~project-shells-setup~. It could be set via customizing or setting in 117 | Emacs lisp directly. The following is the example for the Linux 118 | kernel development described at the begin of the document. 119 | 120 | #+BEGIN_SRC emacs-lisp 121 | (setf project-shells-setup 122 | `(("linux" . 123 | (("0" . 124 | ("build" "~/projects/linux/obj")) 125 | ("9" . 126 | ("qemu" "~/projects/linux/qemu")) 127 | ("8" . 128 | ("guest" "/ssh:root@qemu:")) 129 | ("1" . 130 | ("git")))))) 131 | #+END_SRC 132 | 133 | Where 4 shell buffers are configured for project "linux", they can be 134 | activated via key "0", "9", "8", and "1". The name in configuration 135 | reflects the intended usage. Different initial directory is specified 136 | for "0", and "9", because that is more convenient for the 137 | corresponding intended usage, for example, there may be some qemu 138 | scripts in ~/projects/linux/qemu. "8" is a ssh shell buffer, which is 139 | used to ssh into the testing guest system running in qemu. The usage 140 | is quite simple, just start the qemu in "9", then when you activate 141 | "8", ssh will be run in the buffer. 142 | -------------------------------------------------------------------------------- /project-shells.el: -------------------------------------------------------------------------------- 1 | ;;; project-shells.el --- Manage the shell buffers of each project -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2017 "Huang, Ying" 4 | 5 | ;; Author: "Huang, Ying" 6 | ;; Maintainer: "Huang, Ying" 7 | ;; URL: https://github.com/hying-caritas/project-shells 8 | ;; Version: 20170311 9 | ;; Package-Version: 20171107.851 10 | ;; Package-X-Original-Version: 20170311 11 | ;; Package-Type: simple 12 | ;; Keywords: processes, terminals 13 | ;; Package-Requires: ((emacs "24.3") (seq "2.19")) 14 | 15 | ;; This file is NOT part of GNU Emacs. 16 | 17 | ;; This program is free software; you can redistribute it and/or modify 18 | ;; it under the terms of the GNU General Public License as published by 19 | ;; the Free Software Foundation; either version 3, or (at your option) 20 | ;; any later version. 21 | ;; 22 | ;; This program is distributed in the hope that it will be useful, 23 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 24 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 | ;; GNU General Public License for more details. 26 | ;; 27 | ;; You should have received a copy of the GNU General Public License 28 | ;; along with GNU Emacs; see the file COPYING. If not, write to the 29 | ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 30 | ;; Boston, MA 02110-1301, USA. 31 | 32 | ;;; Commentary: 33 | 34 | ;; Manage multiple shell/terminal buffers for each project. For 35 | ;; example, to develop for Linux kernel, I usually use one shell 36 | ;; buffer to configure and build kernel, one shell buffer to run some 37 | ;; git command not supported by magit, one shell buffer to run qemu 38 | ;; for built kernel, one shell buffer to ssh into guest system to 39 | ;; test. Different set of commands is used by the shell in each 40 | ;; buffer, so each shell should have different command history 41 | ;; configuration, and for some shell, I may need different setup. And 42 | ;; I have several projects to work on. In addition to project 43 | ;; specific shell buffers, I want some global shell buffers, so that I 44 | ;; can use them whichever project I am working on. Project shells is 45 | ;; an Emacs program to let my life easier via helping me to manage all 46 | ;; these shell/terminal buffers. 47 | 48 | ;; The ssh support code is based on Ian Eure's nssh. Thanks Ian! 49 | 50 | ;;; Code: 51 | 52 | (require 'cl-lib) 53 | (require 'shell) 54 | (require 'term) 55 | (require 'eshell) 56 | (require 'seq) 57 | 58 | (defvar-local project-shells-project-name nil) 59 | (defvar-local project-shells-project-root nil) 60 | 61 | (defvar project-shells--dest-history nil) 62 | 63 | ;;; Customization 64 | (defgroup project-shells nil 65 | "Manage shell buffers of each project" 66 | :group 'tools 67 | :link '(url-link :tag "Github" "https://github.com/hying-caritas/project-shells")) 68 | 69 | (defcustom project-shells-default-shell-name "sh" 70 | "Default shell buffer name." 71 | :group 'project-shells 72 | :type 'string) 73 | 74 | (defcustom project-shells-empty-project "-" 75 | "Name of the empty project. 76 | 77 | This is used to create non-project specific shells." 78 | :group 'project-shells 79 | :type 'string) 80 | 81 | (defcustom project-shells-setup `((,project-shells-empty-project . 82 | (("1" . 83 | (,project-shells-default-shell-name 84 | "~/" shell nil))))) 85 | "Configration form for shells of each project. 86 | 87 | The format of the variable is an alist which maps the project 88 | name (string) to the project shells configuration. Which is an 89 | alist which maps the key (string) to the shell configuration. 90 | Which is a list of shell name (string), initial 91 | directory (string), type ('shell, 'term, or 'vterm), and intialization 92 | function (symbol or lambda)." 93 | :group 'project-shells 94 | :type '(alist :key-type (string :tag "Project") :value-type 95 | (alist :tag "Project setup" 96 | :key-type (string :tag "Key") 97 | :value-type (list :tag "Shell setup" 98 | (string :tag "Name") 99 | (choice :tag "Directory" string (const ask)) 100 | (choice :tag "Type" (const term) (const shell) (const eshell) (const vterm)) 101 | (choice :tag "Function" (const nil) function))))) 102 | 103 | (defcustom project-shells-default-init-func 'project-shells-init-sh 104 | "Default function to initialize the shell buffer") 105 | 106 | (defcustom project-shells-keys '("1" "2" "3" "4" "5" "6" "7" "8" "9" "0" "-" "=") 107 | "Keys used to create shell buffers. 108 | 109 | One shell will be created for each key. Usually these key will 110 | be bound in a non-global keymap." 111 | :group 'project-shells 112 | :type '(repeat string)) 113 | 114 | (defcustom project-shells-vterm-keys nil 115 | "Keys used to create vterm buffers. 116 | 117 | One vterm will be created for each key. Usually these key will 118 | be bound in a non-global keymap." 119 | :group 'project-shells 120 | :type '(repeat string)) 121 | 122 | (defcustom project-shells-term-keys '("-") 123 | "Keys used to create terminal buffers. 124 | 125 | By default shell mode will be used, but for keys in 126 | ‘project-shells-term-keys’, ansi terminal mode will be used. This 127 | should be a subset of poject-shells-keys." 128 | :group 'project-shells 129 | :type '(repeat string)) 130 | 131 | (defcustom project-shells-eshell-keys '("=") 132 | "Keys used to create eshell buffers. 133 | 134 | By default shell mode will be used, but for keys in 135 | ‘project-shells-eshell-keys’, eshell mode will be used. This 136 | should be a subset of poject-shells-keys." 137 | :group 'project-shells 138 | :type '(repeat string)) 139 | 140 | (defcustom project-shells-session-root "~/.sessions" 141 | "The root directory for the shell sessions." 142 | :group 'project-shells 143 | :type 'string) 144 | 145 | (defcustom project-shells-project-name-func 'projectile-project-name 146 | "Function to get project name." 147 | :group 'project-shells 148 | :type 'function) 149 | 150 | (defcustom project-shells-project-root-func 'projectile-project-root 151 | "Function to get project root directory." 152 | :group 'project-shells 153 | :type 'function) 154 | 155 | (defcustom project-shells-histfile-env "HISTFILE" 156 | "Environment variable to set shell history file." 157 | :group 'project-shells 158 | :type 'string) 159 | 160 | (defcustom project-shells-histfile-name ".shell_history" 161 | "Shell history file name used to set environment variable." 162 | :group 'project-shells 163 | :type 'string) 164 | 165 | (defcustom project-shells-init-file-name ".shellrc" 166 | "Shell initialize file name to load at startup" 167 | :group 'project-shells 168 | :type 'string) 169 | 170 | (defcustom project-shells-term-args nil 171 | "Shell arguments used in terminal." 172 | :group 'project-shells 173 | :type '(repeat string)) 174 | 175 | (defcustom project-shells-keymap-prefix "C-c s" 176 | "project-shells keymap prefix." 177 | :group 'project-shells 178 | :type 'string) 179 | 180 | (let ((saved-shell-buffer-list nil) 181 | (last-shell-name nil)) 182 | (cl-defun project-shells--buffer-list () 183 | (setf saved-shell-buffer-list 184 | (cl-remove-if-not #'buffer-live-p saved-shell-buffer-list))) 185 | 186 | (cl-defun project-shells--switch (&optional name to-create) 187 | (let* ((name (or name last-shell-name)) 188 | (buffer-list (project-shells--buffer-list)) 189 | (buf (when name 190 | (cl-find-if (lambda (b) (string= name (buffer-name b))) 191 | buffer-list)))) 192 | (when (and (or buf to-create) 193 | (cl-find (current-buffer) buffer-list)) 194 | (setf last-shell-name (buffer-name (current-buffer)))) 195 | (if buf 196 | (progn 197 | (select-window (display-buffer buf)) 198 | buf) 199 | (unless to-create 200 | (message "No such shell: %s" name) 201 | nil)))) 202 | 203 | (cl-defun project-shells-switch-to-last () 204 | "Switch to the last shell buffer." 205 | (interactive) 206 | (let ((name (or (and last-shell-name (get-buffer last-shell-name) 207 | last-shell-name) 208 | (and (project-shells--buffer-list) 209 | (buffer-name (cl-first (project-shells--buffer-list))))))) 210 | (if name 211 | (project-shells--switch name) 212 | (message "No more shell buffers!")))) 213 | 214 | (cl-defun project-shells--create (name dir &optional (type 'shell)) 215 | (let ((default-directory (expand-file-name (or dir "~/")))) 216 | (cl-ecase type 217 | (vterm (vterm) 218 | (rename-buffer name)) 219 | (term (ansi-term "/bin/sh") 220 | (rename-buffer name)) 221 | (shell (pop-to-buffer name) 222 | (unless (comint-check-proc (current-buffer)) 223 | (setf comint-prompt-read-only t) 224 | (cd dir) 225 | (shell (current-buffer)))) 226 | (eshell (let ((eshell-buffer-name name)) 227 | (eshell)))) 228 | (push (current-buffer) saved-shell-buffer-list)))) 229 | 230 | (cl-defun project-shells-send-shell-command (cmdline) 231 | "Send the command line to the current (shell) buffer. Can be 232 | used in shell initialized function." 233 | (insert cmdline) 234 | (comint-send-input)) 235 | 236 | (cl-defun project-shells-init-sh (session-dir type) 237 | "Initialize the shell via loading initialize file" 238 | (let* ((init-file (concat session-dir "/" project-shells-init-file-name)) 239 | (cmdline (concat ". " init-file))) 240 | (when (and (not (eq type 'eshell)) (file-exists-p init-file)) 241 | (cl-ecase type 242 | (shell (project-shells-send-shell-command cmdline)) 243 | (term (term-send-raw-string (concat cmdline "\n"))))))) 244 | 245 | (cl-defun project-shells--project-name () 246 | (or project-shells-project-name 247 | (and (symbol-function project-shells-project-name-func) 248 | (funcall project-shells-project-name-func)) 249 | project-shells-empty-project)) 250 | 251 | (cl-defun project-shells--project-root (proj-name) 252 | (if (string= proj-name project-shells-empty-project) 253 | "~/" 254 | (or project-shells-project-root 255 | (and (symbol-function project-shells-project-root-func) 256 | (funcall project-shells-project-root-func)) 257 | "~/"))) 258 | 259 | (cl-defun project-shells--histfile-name (session-dir) 260 | (when project-shells-histfile-name 261 | (expand-file-name project-shells-histfile-name session-dir))) 262 | 263 | (cl-defun project-shells--command-string (args) 264 | (mapconcat 265 | #'identity 266 | (cl-loop 267 | for arg in args 268 | collect (concat "\"" (shell-quote-argument arg) "\"")) 269 | " ")) 270 | 271 | (cl-defun project-shells--term-command-string () 272 | (let* ((prog (or explicit-shell-file-name 273 | (getenv "ESHELL") shell-file-name))) 274 | (concat "exec " (project-shells--command-string 275 | (cons prog project-shells-term-args)) "\n"))) 276 | 277 | ;;;###autoload 278 | (cl-defun project-shells-activate-for-key (key &optional proj proj-root) 279 | "Create or switch to the shell buffer for the key, the project 280 | name, and the project root directory." 281 | (let* ((key (replace-regexp-in-string "/" "slash" key)) 282 | (proj (or proj (project-shells--project-name))) 283 | (proj-shells (cdr (assoc proj project-shells-setup))) 284 | (shell-info (cdr (assoc key proj-shells))) 285 | (name (or (cl-first shell-info) project-shells-default-shell-name)) 286 | (shell-name (format "*%s.%s.%s*" key name proj))) 287 | (unless (project-shells--switch shell-name t) 288 | (with-temp-buffer 289 | (let* ((proj-root (or proj-root (project-shells--project-root proj))) 290 | (type (cond 291 | ((cl-third shell-info)) 292 | ((member key project-shells-term-keys) 'term) 293 | ((member key project-shells-eshell-keys) 'eshell) 294 | ((member key project-shells-vterm-keys) 'vterm) 295 | (t 'shell))) 296 | (dir (or (cl-second shell-info) proj-root)) 297 | (func (cl-fourth shell-info)) 298 | (session-dir (expand-file-name (format "%s/%s" proj key) 299 | project-shells-session-root))) 300 | (when (eq dir 'ask) 301 | (let* ((dest (completing-read 302 | "Destination: " 303 | project-shells--dest-history 304 | nil nil nil 'project-shells--dest-history))) 305 | (setf dir (if (or (string-prefix-p "/" dest) 306 | (string-prefix-p "~" dest)) 307 | dest 308 | (format "/ssh:%s:" dest))))) 309 | (with-environment-variables 310 | ((project-shells-histfile-env 311 | (when project-shells-histfile-name 312 | (project-shells--histfile-name session-dir)))) 313 | (mkdir session-dir t) 314 | (project-shells--create shell-name dir type) 315 | (cl-case type 316 | (term 317 | (term-send-raw-string (project-shells--term-command-string))) 318 | (eshell 319 | (setq-local eshell-history-file-name 320 | (project-shells--histfile-name session-dir)) 321 | (eshell-read-history))) 322 | (when (or (string-prefix-p "/ssh:" dir) 323 | (string-prefix-p "/sudo:" dir)) 324 | (set-process-sentinel (get-buffer-process (current-buffer)) 325 | #'shell-write-history-on-exit)) 326 | (setf project-shells-project-name proj 327 | project-shells-project-root proj-root) 328 | (when project-shells-default-init-func 329 | (funcall project-shells-default-init-func session-dir type)) 330 | (project-shells-mode) 331 | (when func 332 | (funcall func session-dir)))))))) 333 | 334 | ;;;###autoload 335 | (cl-defun project-shells-activate (p) 336 | "Create or switch to the shell buffer for the key just typed" 337 | (interactive "p") 338 | (let* ((keys (this-command-keys-vector)) 339 | (key (seq-subseq keys (1- (seq-length keys)))) 340 | (key-desc (key-description key))) 341 | (project-shells-activate-for-key 342 | key-desc (and (/= p 1) project-shells-empty-project)))) 343 | 344 | ;;;###autoload 345 | (cl-defun project-shells-setup (map &optional setup) 346 | "Configure the project shells with the prefix keymap and the 347 | setup, for format of setup, please refer to document of 348 | project-shells-setup." 349 | (when setup 350 | (setf project-shells-setup setup)) 351 | (cl-loop 352 | for key in project-shells-keys 353 | do (define-key map (kbd key) 'project-shells-activate))) 354 | 355 | ;;; Minor mode 356 | 357 | (defvar project-shells-map 358 | (let ((map (make-sparse-keymap))) 359 | (project-shells-setup map) 360 | (define-key map (kbd "s") 'project-shells-switch-to-last) 361 | map) 362 | "Sub-keymap for project-shells mode.") 363 | (fset 'project-shells-map project-shells-map) 364 | 365 | (defvar project-shells-mode-map 366 | (let ((sub-map (make-sparse-keymap)) 367 | (map (make-sparse-keymap))) 368 | (project-shells-setup sub-map) 369 | (define-key map (kbd project-shells-keymap-prefix) 370 | 'project-shells-map) 371 | map) 372 | "Keymap for project-shells mode.") 373 | 374 | ;;;###autoload 375 | (define-minor-mode project-shells-mode 376 | nil 377 | :keymap project-shells-mode-map 378 | :group project-shells) 379 | 380 | ;;;###autoload 381 | (define-globalized-minor-mode global-project-shells-mode 382 | project-shells-mode project-shells-mode) 383 | 384 | (provide 'project-shells) 385 | 386 | ;;; project-shells.el ends here 387 | --------------------------------------------------------------------------------