├── LICENSE ├── README.rst ├── _wenv ├── completion.bash ├── examples ├── http-1 └── http-2 ├── extensions ├── c ├── edit ├── history ├── nvm ├── ssh-agent └── wd ├── template └── wenv /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 David Grisham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. default-role:: literal 2 | .. sectnum:: 3 | 4 | wenv: A Shell Workflow Tool 5 | =========================== 6 | 7 | .. contents:: Table of Contents 8 | 9 | Introduction 10 | ------------ 11 | 12 | Working in the terminal is fun, but it can also be tedious and messy. The working 13 | environment (wenv) project offloads the tedious work required to provide clean, 14 | project-specific environments that allow users to easily leverage the power of 15 | their shell. It acts as an extremely lightweight layer that connects tmux, Zsh, 16 | and your other favorite shell tools. 17 | 18 | The Story 19 | ~~~~~~~~~ 20 | 21 | My (@dgrisham's) motivation for this project came from a desire to make working 22 | in the terminal clean, easy, and fun. Zsh and tmux were ubiquitous in my 23 | workflow, and over time I started to see potential in those tools that I wanted 24 | to take advantage of. The wenv framework emerged from this potential. The actual 25 | conception of this project formed as I tried to solve relatively disparate 26 | problems. I'll start by describing some of those problems, then how they came 27 | together to shape a single solution. 28 | 29 | One of the issues I faced had to do with starting tmux with a given layout. Every 30 | time I started to work, I'd have to go through the steps of setting up the 31 | appropriate layout of panes and opening the relevant programs in those panes. 32 | This seemed silly, given that I was working in the terminal and should be able to 33 | automate such things. I saw advice like `this answer on Stack Overflow 34 | `_, which recommends running a 35 | sequence of tmux commands as a function that starts your desired development 36 | environment. However, as I worked on projects, I found that I'd want different 37 | tmux layouts depending on the project. Further, I wanted more than just a tmux 38 | layout -- I also wanted to automatically run project-specific commands in certain 39 | terminals in a given layout. This would require a bit more work than the shell 40 | function the SO post. 41 | 42 | At the same time, I'd set up a simple system for managing aliases with different 43 | domains. For example, I had a Python aliases file that defined useful functions 44 | for Python development (mainly creating and starting virtual environments). I 45 | also wrote alias files specific to certain projects, e.g. the `IPTB 46 | `_ development aliases defined the required 47 | `IPTB_ROOT` environment variable and additional functions to simplify IPTB and 48 | Docker startup, shutdown, and cleanup. Then I just had a few Zsh commands that 49 | would let me easily create, edit, and source these alias files. Organizing 50 | aliases in this way avoids putting a bunch of temporary or overly-specific 51 | aliases in my general Zsh aliases file, which I'd rather be accessible and 52 | general enough for people who might want to draw from it. 53 | 54 | Eventually, I decided to integrate a solution to my varied-tmux-startup conundrum 55 | with the alias files. That way, the entire definition for a project (both its 56 | layout and its Zsh variables/functions) could exist in one place. This approach 57 | also presented the possibility of having a project's aliases defined in every 58 | tmux pane/window in a given session, which would 59 | 60 | 1. be much nicer than having to manually source the aliases in every pane that 61 | I needed them, while 62 | 2. maintaining a clean Zsh namespace by restricting the project's aliases to a 63 | single tmux session. 64 | 65 | This emerging idea turned out to be more involved than I'd expected. Sourcing a 66 | given aliases file in every pane of a tmux session required work on both the Zsh 67 | end (to tell tmux which aliases file to source) and the tmux end (to actually 68 | source the aliases in each pane). 69 | 70 | Other useful features also quickly became apparent as I started working on this 71 | approach. For example, developing the aforementioned IPTB project required a 72 | running Docker daemon. I didn't want Docker to run unless I was working on the 73 | project, but I didn't want to have to think about starting processes like that 74 | when I was about to work. So, I thought it'd be nice if I could include something 75 | in the IPTB project file that would let me automatically run commands like `sudo systemctl start docker` 76 | when I started working on the project and `sudo systemctl stop docker` when I was finished. 77 | 78 | The wenv framework arose from this increasing complexity. However, it never left 79 | the realm of Zsh scripting. A project's wenv is defined by Zsh environment 80 | variables and functions, and the wenv 'framework' is just a bunch of Zsh 81 | functions. This is convenient because much of the code is just sequences of 82 | commands I'd run in the terminal anyway, and the rest maintain state in the Zsh 83 | and tmux environments. 84 | 85 | The advantages that stem from this tool being 'lightweight' go beyond the usual 86 | ideas of load time/etc. The art of shell scripting is slowly being lost to more 87 | integrated and high-level tools. While those tools might have their place 88 | (debatable), there are disadvantages to the lower resolution [*]_. The lightweight 89 | nature of the wenv project aims to achieve many of the advantages that higher level 90 | tools offer while staying at the level of abstraction of the shell. This means that 91 | wenvs require minimal understand on the user's part beyond shell scripting -- if 92 | you understand how shells work, then you're only a few steps from understanding 93 | how wenvs work. And if you don't understand how shells work, using wenvs can help 94 | you do so. 95 | 96 | .. [*] For more on this topic, see the talk `Preventing the Collapse of Civilation 97 | by Jonathon Blow `_. 98 | 99 | Installation 100 | ------------ 101 | 102 | Dependencies 103 | ~~~~~~~~~~~~ 104 | 105 | - Zsh 106 | - tmux 107 | 108 | Steps 109 | ~~~~~ 110 | 111 | For now, the installation is manual -- fortunately, it's also relatively 112 | painless. The following steps (or variations on them) should get the job done 113 | (note that you may want to store this repo somewhere permanent and symlink to 114 | its contents instead of copying to make updates easier): 115 | 116 | 1. Clone this repository. 117 | 2. Create the directory `$XDG_CONFIG_HOME/wenv` (or `$HOME/.config/wenv`) and 118 | put both the `template` file and `extensions` directory there. Also, create 119 | a directory inside of that `wenv` directory called `wenvs`, which will store 120 | the wenv files for all of your projects. If you're in this repository, you 121 | can run the following lines to complete this step: 122 | 123 | .. code-block:: zsh 124 | 125 | export wenv_cfg="${XDG_CONFIG_HOME:-$HOME/.config}/wenv" 126 | mkdir -p "$wenv_cfg/wenvs" 127 | ln -s /{template,extensions} "$wenv_cfg" 128 | 129 | 3. Put the `wenv` and `_wenv` files wherever you like, and add the following lines to your `zshrc`: 130 | 131 | .. code-block:: zsh 132 | 133 | # source wenv file 134 | source 135 | 136 | 4. To load the completions, you can move or symlink the `_wenv` file to a directory in your `fpath`. 137 | For example, if the completion file is at `~/src/wenv/_wenv` and you store completions in 138 | `$XDG_DATA_HOME/zsh/completions/`, you would run: 139 | 140 | .. code-block:: zsh 141 | 142 | ln -s `~/src/wenv/_wenv` `$XDG_DATA_HOME/zsh/completions/` 143 | 144 | Then ensure the path is in your `fpath` by adding this to your `zshrc`: 145 | 146 | .. code-block:: zsh 147 | 148 | fpath=($XDG_DATA_HOME/zsh/completions $fpath) 149 | 150 | 5. In order for wenvs to work with `tmux`, the following line should be added 151 | to your `zshrc`: 152 | 153 | .. code-block:: zsh 154 | 155 | [[ -n "$WENV" ]] && wenv_source -c "$WENV" 156 | 157 | This makes it so that the wenv associated with a given tmux session can be 158 | loaded whenever a new pane or window is opened within that session. 159 | 160 | Recommended 161 | ~~~~~~~~~~~ 162 | 163 | **Wenv name in prompt** 164 | 165 | It's useful to have the name of the wenv in your prompt, as both an easy reference for which wenv you're in and 166 | sometimes as a debugging tool to verify whether a wenv properly loaded. This used to be the default, but for better 167 | flexibility it's now up to the user to configure this. 168 | 169 | A simple way to do this would be to add the following lines to your `zshrc`: 170 | 171 | .. code-block:: zsh 172 | 173 | wenv_prompt() { 174 | [[ -n "$WENV" ]] && echo "($WENV) " 175 | } 176 | 177 | setopt prompt_subst 178 | PS1="\$(wenv_prompt)$PS1" 179 | 180 | This prepends the name of the active wenv in parentheses, followed by a space, before your prompt. This may be 181 | added before or after the code added in step 4. For more information on the `prompt_subst` option in Zsh, see 182 | https://zsh.sourceforge.io/Doc/Release/Prompt-Expansion.html. 183 | 184 | **Clean wenv startup history** 185 | 186 | When you run the `wenv start` command, you'll get the following command in your shell's history: 187 | 188 | .. code-block:: zsh 189 | 190 | source $tmp_start_file && rm -f $tmp_start_file 191 | 192 | This command is prefixed with space -- this means that if you have the `HIST_IGNORE_SPACE` Zsh option set, that command 193 | won't be saved in your shell history. To set this option, add the following to your `zshrc`: 194 | 195 | .. code-block:: zsh 196 | 197 | setopt HIST_IGNORE_SPACE 198 | 199 | Usage 200 | ----- 201 | 202 | :: 203 | 204 | USAGE 205 | wenv [-h] 206 | 207 | OPTIONS 208 | -h Display this help message. 209 | 210 | SUBCOMMANDS 211 | start Start the working environment . 212 | stop Stop the current working environment. 213 | new Create a new working environment. 214 | list List all wenvs 215 | edit Edit the wenv file for . 216 | rename Rename wenv to . 217 | remove Delete the wenv file for . 218 | source Source the wenv file for . 219 | cd Change to 's base directory. 220 | extension Interact with wenv extensions. 221 | bootstrap Run 's bootstrap function. 222 | 223 | Run `wenv -h` for more information on a given subcommand . 224 | 225 | Wenv Environment Summary 226 | ~~~~~~~~~~~~~~~~~~~~~~~~ 227 | 228 | See the Walkthrough_ for further elaboration and examples. 229 | 230 | **Variables** 231 | 232 | - `wenv_dir`: The path to the base directory of this project. 233 | - `wenv_deps`: An array containing the names of the wenvs that this wenv is 234 | dependent on. 235 | - `wenv_extensions`: An array containing the names of the extensions to load 236 | for the wenv. 237 | 238 | **Functions** 239 | 240 | - `startup_wenv()` is run whenever you start the wenv. This function is good 241 | for starting up any necessary daemons, setting up a tmux layout, opening 242 | programs (e.g. a text editor), etc. It will run inside `"$wenv_dir"`. 243 | - `shutdown_wenv()` is run when you stop the wenv. This can be used to stop 244 | daemons started by `startup_wenv()`, and do any other cleanup. 245 | - `bootstrap_wenv()` sets up the environment that the wenv expects to exist. 246 | For example, this function might pull down a git repository for development 247 | or check to ensure that all packages required by this wenv are installed. 248 | You can run this function on a wenv `` by running 249 | `wenv bootstrap `. 250 | 251 | Walkthrough 252 | ----------- 253 | 254 | The utility of wenvs takes a bit of time to explain. This walkthrough gives the 255 | basic configuration/commands for getting started while also explaining what I've 256 | found them to be useful for. If you're experienced with shell scripting, you'll 257 | see that much of the value of wenvs comes from allowing the user to leverage the 258 | tools provided by shells. This project is less focused on forcing a specific 259 | workflow for users and more focused on giving users a convenient environment in 260 | which to define their own workflow unrestricted by the limitations of a single 261 | terminal. 262 | 263 | The example wenvs in the `examples`__ directory give concrete examples of wenv 264 | definitions for general projects. Each example includes a comprehensive 265 | description of the wenv's definition and features that are used to create a clean 266 | and useful environment. I recommend going through these examples, as they 267 | compliment this walkthrough. 268 | 269 | __ examples/ 270 | 271 | Creating a wenv 272 | ~~~~~~~~~~~~~~~ 273 | 274 | Here's an example that creates a wenv for a project called 'hello-world': 275 | 276 | .. code-block:: zsh 277 | 278 | $ mkdir hello-world 279 | $ cd hello-world 280 | $ wenv new hello-world 281 | 282 | The `wenv new` command will copy the wenv `template` file into a new wenv 283 | file called `hello-world`. The template file provides a base structure for a new 284 | wenv. On my machine, the above wenv command creates a new wenv file that starts 285 | with the following lines: 286 | 287 | .. code-block:: zsh 288 | 289 | wenv_dir="" 290 | wenv_deps=() 291 | wenv_extensions=('wd') 292 | 293 | startup_wenv() {} 294 | shutdown_wenv() {} 295 | bootstrap_wenv() {} 296 | 297 | ((only_load_wenv_vars == 1)) && return 0 298 | 299 | # define all desired aliases/functions/etc. here 300 | 301 | Each project's wenv start with a set of variables and functions that make it a wenv, 302 | which are all of the variables and functions defined above. Below the above block 303 | is where any shell aliases/functions/etc. for the project should be defined. 304 | 305 | A shell running a wenv will have environment variables exported that correspond to the 306 | variables defined in the wenv file: 307 | 308 | - `WENV_DIR` will contain the value of `wenv_dir`. 309 | - `WENV_DEPS` will contain the value of `wenv_deps`. 310 | - `WENV_EXTENSIONS` will contain the value of `wenv_extensions`. 311 | 312 | The most important of these is `wenv_dir`, which we'll focus on first. 313 | 314 | `wenv_dir` 315 | ~~~~~~~~~~ 316 | 317 | The `wenv_dir` value represents the base directory of the project. When we 318 | start a wenv with e.g. `wenv start hello-world`, we'll automatically `cd` into 319 | the project's `wenv_dir`. Further, whenever a wenv is active, we can run `wenv 320 | cd` (without an argument) to `cd` into its base directory from anywhere. If we 321 | want to `cd` into an inactive wenv's `wenv_dir`, we can do so by passing the 322 | wenv name as an argument -- e.g. `wenv cd hello-world`. 323 | 324 | In the example in the previous section, `wenv_dir`'s value was automatically populated 325 | with our current working directory (this may be overidden with the `-d` flag). 326 | 327 | `startup_wenv()` 328 | ~~~~~~~~~~~~~~~~ 329 | 330 | Now let's talk about what you can do when starting a wenv. The `startup_wenv()` 331 | function is run whenever you activate a wenv with `wenv start `. This can 332 | be useful for running startup commands, e.g. 333 | 334 | .. code-block:: zsh 335 | 336 | startup_wenv() { 337 | sudo systemctl start docker 338 | } 339 | 340 | Or opening programs like text editors: 341 | 342 | .. code-block:: zsh 343 | 344 | startup_wenv() { 345 | $EDITOR main.cpp 346 | } 347 | 348 | Additionally the `startup_wenv()` function can be used to automatically create 349 | Tmux layouts for the project. 350 | 351 | So, we can start our wenv with a horizontal split with the startup function: 352 | 353 | .. code-block:: zsh 354 | 355 | startup_wenv() { 356 | tmux split -h 357 | } 358 | 359 | We can also open a file in our text editor in the new pane: 360 | 361 | .. code-block:: zsh 362 | 363 | startup_wenv() { 364 | tmux split -h "$EDITOR main.cpp" 365 | } 366 | 367 | Other tmux commands can be useful in specifying a layout as well. For example, if 368 | we wanted to create a small vertical pane under the initial pane then refocus 369 | on the larger pane: 370 | 371 | .. code-block:: zsh 372 | 373 | startup_wenv() { 374 | tmux split 375 | tmux resize-pane -y 7 376 | tmux select-pane -U 377 | } 378 | 379 | Note that `wenv start` will `cd` into `"$wenv_dir"` before 380 | `startup_wenv()` is run, so you can assume you'll be in the wenv's base 381 | directory when writing your `startup_wenv()` functions. Additionally, your wenv 382 | aliases will be sourced once `startup_wenv()` is called, so can take advantage 383 | of any environment variables/functions defined outside of `wenv_def()`. 384 | 385 | `shutdown_wenv()` 386 | ~~~~~~~~~~~~~~~~ 387 | 388 | This is essentially the opposite of `startup_wenv()` -- it runs whenver you 389 | deactivate the current wenv with `wenv stop`. So, if we have a wenv whose 390 | `startup_wenv()` function runs `sudo systemctl start docker`, our 391 | `shutdown_wenv()` might be: 392 | 393 | .. code-block:: zsh 394 | 395 | shutdown_wenv() { 396 | sudo systemctl stop docker 397 | } 398 | 399 | Note, however, that the `wenv stop` command doesn't deactivate the wenv if 400 | `shutdown_wenv()` returns a non-zero exit code. You can always pass the `-f` 401 | flag to `wenv stop` to close the wenv even if `shutdown_wenv()` fails. 402 | 403 | `wenv_deps` 404 | ~~~~~~~~~~~ 405 | 406 | `wenv_deps` is an array of wenvs that this wenv is dependent on. Essentially, 407 | every wenv in `wenv_deps` is sourced when starting the wenv. Let's take the 408 | example of a wenv for IPTB (which we'll call `iptb`): 409 | 410 | .. code-block:: zsh 411 | 412 | # ... 413 | 414 | export IPTB_ROOT="$HOME/.iptb" 415 | 416 | Let's say we wanted to create another wenv that also used IPTB, and therefore 417 | also needs to set the `IPTB_ROOT` variable. We *could* initialize the new wenv 418 | with the `iptb` wenv as a base using `wenv new -i iptb `, so our new 419 | wenv would have the same `export` command. However, this approach isn't 420 | particularly maintainable -- e.g. if the IPTB developers decide to rename the 421 | `IPTB_ROOT` variable, all wenvs that use IPTB would have to update that 422 | variable's value. Alternatively, we could just source the `iptb` wenv and get 423 | all of its environment variables every time we start any wenv that uses IPTB. To 424 | do this, we'd add `iptb` to our `wenv_deps`: 425 | 426 | .. code-block:: zsh 427 | 428 | wenv_deps=('iptb') 429 | 430 | Extensions 431 | ~~~~~~~~~~ 432 | 433 | Wenv extensions define shell code that may be reused across multiple wenvs. A 434 | wenv extension is nothing more than a shell file that you want to source in every 435 | shell of a wenv. Extensions are stored in `"$WENV_CFG/extensions"`. To load an 436 | extension, add its name to the `wenv_extensions` array. For example, if we 437 | wanted to load the `wd` and `edit` extensions, we'd write: 438 | 439 | .. code-block:: zsh 440 | 441 | wenv_extensions=('wd' 'edit') 442 | 443 | Then the files `"$WENV_CFG/extensions/wd"` and `"$WENV_CFG/extensions/edit"` would 444 | be sourced in every shell of our wenv. See the `wd` and `edit` extension files for more 445 | information on their usage. 446 | -------------------------------------------------------------------------------- /_wenv: -------------------------------------------------------------------------------- 1 | #compdef wenv 2 | 3 | local state 4 | integer ret=1 5 | 6 | list_wenvs() { 7 | find "$WENV_CFG/wenvs" ! -name ".gitignore" ! -path '*/\.git/*' ! -type d | perl -pe "s|$WENV_CFG/wenvs/||" 8 | } 9 | 10 | list_extensions() { 11 | find "$WENV_CFG/extensions/" ! -type d | perl -pe "s|$WENV_CFG/extensions/||" 12 | } 13 | 14 | _arguments -C \ 15 | ':command:(start stop cd new edit rm mv source extension bootstrap)' \ 16 | '*::arg:->args' && ret=0 17 | 18 | case "$line[1]" in 19 | start) 20 | _arguments "-i[Do not run wenv's startup function]" "-d[Do not attach to wenv's tmux session]" "-h[Show help message]" "*:wenvs:($(list_wenvs))" && ret=0 21 | ;; 22 | stop) 23 | _arguments "-f[Force shutdown even if shutdown_wenv fails]" "-s[Do not run shutdown function]" "-h[Show help message]" && ret=0 24 | ;; 25 | cd) 26 | _arguments "-h[Show help message]" "*:wenvs:($(list_wenvs))" && ret=0 27 | ;; 28 | new) 29 | _arguments "-d[Wenv's base directory]:directory" "-i[Initial wenv to copy]:initial_wenv:->wenv" "-h[Show help message]" "1: :->wenv" && ret=0 30 | case $state in 31 | wenv) _describe 'command' "($(list_wenvs))" ;; 32 | esac 33 | ;; 34 | edit) 35 | _arguments "-h[Show help message]" "1: :->wenv" && ret=0 36 | case $state in 37 | wenv) _describe 'command' "($(list_wenvs))" ;; 38 | esac 39 | ;; 40 | ls|list) 41 | _arguments "-h[Show help message]" "1: :->wenv" && ret=0 42 | case $state in 43 | wenv) _describe 'command' "($(list_wenvs))" ;; 44 | esac 45 | ;; 46 | rm) 47 | _arguments "-h[Show help message]" "1: :->wenv" && ret=0 48 | case $state in 49 | wenv) _describe 'command' "($(list_wenvs))" ;; 50 | esac 51 | ;; 52 | mv) 53 | _arguments "-h[Show help message]" "1:src:->wenv" "2:dst:->wenv" && ret=0 54 | case $state in 55 | wenv) _describe 'command' "($(list_wenvs))" ;; 56 | esac 57 | ;; 58 | source) 59 | _arguments "-h[Show help message]" "-c[Stay in WENV_DIR after running]" "*:wenvs:($(list_wenvs))" && ret=0 60 | ;; 61 | extension) 62 | _arguments -C \ 63 | "-h[Show help message]" \ 64 | ':command:(edit load rm)' \ 65 | '*::arg:->args' && ret=0 66 | 67 | (($#line == 2)) && case "$line[1]" in 68 | edit|load|rm) _describe 'command' "($(list_extensions))" ;; 69 | esac 70 | ;; 71 | bootstrap) 72 | _arguments "-h[Show help message]" && ret=0 73 | ;; 74 | esac 75 | 76 | return $ret 77 | -------------------------------------------------------------------------------- /completion.bash: -------------------------------------------------------------------------------- 1 | _wenv_comp() { 2 | COMPREPLY=( $(compgen -W "$1" -- ${word}) ) 3 | if [[ ${#COMPREPLY[@]} == 1 && ${COMPREPLY[0]} == "--"*"=" ]]; then 4 | # If there's only one option, with =, then discard space 5 | complete -o nospace 6 | fi 7 | } 8 | 9 | _show_wenvs() { 10 | find "$WENV_CFG/wenvs" ! -name ".gitignore" ! -path '*/\.git/*' ! -type d | perl -pe "s|$WENV_CFG/wenvs/||" 11 | } 12 | 13 | _wenv_start() { 14 | if [[ $word == -* ]]; then 15 | _wenv_comp "-i -d -h" 16 | else 17 | _show_wenvs 18 | fi 19 | } 20 | 21 | _wenv_stop() { 22 | _wenv_comp "-s -h -f" 23 | } 24 | 25 | _wenv_cd() { 26 | if [[ $word == -* ]]; then 27 | _wenv_comp "-r -h" 28 | else 29 | _show_wenvs 30 | fi 31 | } 32 | 33 | _wenv_new() { 34 | if [[ ${prev} == "-i" ]]; then 35 | _show_wenvs 36 | elif [[ ${word} == -* ]]; then 37 | _wenv_comp "-i -d -h" 38 | fi 39 | } 40 | 41 | _wenv_edit() { 42 | _show_wenvs 43 | } 44 | 45 | _wenv_rm() { 46 | _wenv_remove 47 | } 48 | 49 | _wenv_remove() { 50 | if [[ $word == -* ]]; then 51 | _wenv_comp "-h" 52 | else 53 | _show_wenvs 54 | fi 55 | } 56 | 57 | _wenv_rename() { 58 | if [[ $word == -* ]]; then 59 | _wenv_comp "-h" 60 | else 61 | _show_wenvs 62 | fi 63 | } 64 | 65 | _wenv_mv() { 66 | _wenv_rename 67 | } 68 | 69 | _wenv_source() { 70 | if [[ $word == -* ]]; then 71 | _wenv_comp "-h" 72 | else 73 | _wenv_comp "load edit" 74 | fi 75 | } 76 | 77 | _wenv_extension() { 78 | if [[ $word == -* ]]; then 79 | _wenv_comp "-h" 80 | else 81 | _wenv_comp "edit load rm" 82 | fi 83 | } 84 | 85 | _wenv_extension_load() { 86 | if [[ $word == -* ]]; then 87 | _wenv_comp "-h" 88 | else 89 | ls "$WENV_CFG/extensions" 90 | fi 91 | } 92 | 93 | _wenv_extension_edit() { 94 | if [[ $word == -* ]]; then 95 | _wenv_comp "-h" 96 | else 97 | ls "$WENV_CFG/extensions" 98 | fi 99 | } 100 | 101 | _wenv_extension_remove() { 102 | if [[ $word == -* ]]; then 103 | _wenv_comp "-h" 104 | else 105 | ls "$WENV_CFG/extensions" 106 | fi 107 | } 108 | 109 | _wenv_bootstrap() { 110 | if [[ $word == -* ]]; then 111 | _wenv_comp "-h" 112 | else 113 | _show_wenvs 114 | fi 115 | } 116 | 117 | _wenv() { 118 | COMPREPLY=() 119 | complete +o default # Disable default to not deny completion, see: http://stackoverflow.com/a/19062943/1216348 120 | 121 | local word="${COMP_WORDS[COMP_CWORD]}" 122 | local prev="${COMP_WORDS[COMP_CWORD-1]}" 123 | 124 | case "${COMP_CWORD}" in 125 | 1) 126 | if [[ $word == -* ]]; then 127 | _wenv_comp '-h' 128 | else 129 | local opts="start stop cd new edit rm mv source extension bootstrap" 130 | COMPREPLY=( $(compgen -W "${opts}" -- ${word}) ) 131 | fi 132 | ;; 133 | 2) 134 | local command="${COMP_WORDS[1]}" 135 | eval "_wenv_$command" 2> /dev/null 136 | ;; 137 | *) 138 | local command="${COMP_WORDS[1]}" 139 | local subcommand="${COMP_WORDS[2]}" 140 | eval "_wenv_${command}_${subcommand}" 2> /dev/null && return 141 | eval "_wenv_$command" 2> /dev/null 142 | ;; 143 | esac 144 | } 145 | complete -F _wenv __wenv 146 | -------------------------------------------------------------------------------- /examples/http-1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # This is a simple example of a wenv that starts an environment for a toy web 4 | # development environment. The WENV_DIR is just set to the base directory of my 5 | # toy website. The diretory tree for this project looks like: 6 | # 7 | # $ tree 8 | # . 9 | # ├── css 10 | # │   └── styles.css 11 | # ├── index.html 12 | # └── js 13 | # └── main.js 14 | # 15 | # Now look at the startup_wenv(). This starts by using wenv_tmux_split() to 16 | # open a new window, named 'http', and run the command 'python -m http.server' 17 | # (to run a simple HTTP server in the base directory of the wenv). After the 18 | # first command runs, we'll be in the second tmux window. To get back to the 19 | # first, we use the `tmux select-window` command. Then we use edit() to open 20 | # the Javascript file main.js, which works because 'js' maps to 'main.js' in 21 | # wenv_files. We pass the -r flag to edit() to rename the current tmux window 22 | # to 'js'. The result of all of this is: two tmux windows, the first named 'js' 23 | # with main.js opened in your editor, and the second named 'http' that contains 24 | # a running python HTTP server. 25 | 26 | wenv_dir="$HOME/scratch/webdev-practice" 27 | wenv_deps=() 28 | wenv_extensions=('c' 'edit') 29 | 30 | startup_wenv() { 31 | wenv_tmux_split -n http window 'python -m http.server' 32 | tmux select-window -t 1 33 | edit -r js 34 | } 35 | shutdown_wenv() {} 36 | bootstrap_wenv() {} 37 | 38 | ((only_load_wenv_vars == 1)) && return 0 39 | 40 | declare -Ag wenv_dirs 41 | wenv_dirs[js]='js' 42 | wenv_dirs[css]='css' 43 | 44 | declare -Ag wenv_files 45 | wenv_files[html]='index.html' 46 | wenv_files[js]="js/main.js" 47 | wenv_files[css]="css/styles.css" 48 | -------------------------------------------------------------------------------- /examples/http-2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # This example builds off of http-1. Relative to that example, this wenv's 4 | # initial layout is more elaborate. Additionally, the values in some of the 5 | # entries of wenv_files are defined a more maintainable way. 6 | # 7 | # The directory tree for this project looks like: 8 | # 9 | # $ tree 10 | # . 11 | # ├── css 12 | # │   └── styles.css 13 | # ├── index.html 14 | # └── js 15 | # └── main.js 16 | # 17 | # Take a look at startup_wenv(). Here are the steps it executes 18 | # 19 | # 1. Rename the current window to 'js', to signify that it will primarily be 20 | # used for editing Javascript files. 21 | # 2. Create a new pane below the current one, set its height to 7, and start a 22 | # Python HTTP server in this new pane. 23 | # 3. Select the original pane (which is above the current one). 24 | # 4. Create a new window called 'html-css', and use edit() to open the HTML and 25 | # CSS files for the project. 26 | # 5. Select the first window by its name, 'js'. 27 | # 6. Use edit() to open the Javascript file. 28 | # 29 | # After startup_wenv() runs, we'll have two windows: the first named 'js' with 30 | # the Javascript file open in the larger top pane and HTTP server in the bottom 31 | # pane, and the second named 'html-css' with the HTML and CSS files open. 32 | 33 | wenv_dir="$HOME/scratch/webdev-practice" 34 | wenv_deps=() 35 | wenv_extensions=('c' 'edit') 36 | 37 | startup_wenv() { 38 | tmux rename-window js 39 | tmux split 40 | tmux resize-pane -y 7 41 | tmux send-keys 'python -m http.server' 'Enter' 42 | tmux select-pane -U 43 | 44 | tmux new -n html-css 'edit html css' 45 | tmux select-window -t js 46 | edit js 47 | } 48 | shutdown_wenv() {} 49 | bootstrap_wenv() {} 50 | 51 | ((only_load_wenv_vars == 1)) && return 0 52 | 53 | declare -Ag wenv_dirs 54 | wenv_dirs[js]='src/js' 55 | wenv_dirs[css]='src/css' 56 | 57 | declare -Ag wenv_files 58 | wenv_files[html]='index.html' 59 | wenv_files[js]="${wenv_dirs[js]}/scripts.js" 60 | wenv_files[css]="${wenv_dirs[css]}/styles.css" 61 | -------------------------------------------------------------------------------- /extensions/c: -------------------------------------------------------------------------------- 1 | wd -------------------------------------------------------------------------------- /extensions/edit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | edit() { 4 | local usage="\ 5 | USAGE 6 | edit [-r] [-h] [ ...] 7 | 8 | OPTIONS 9 | -r Rename the current tmux window to the first passed . 10 | -h Display this help message. 11 | 12 | REQUIREMENTS 13 | - EDITOR environment variable must be set. 14 | - Define the global associative array \`wenv_files\` in your wenv file (see 15 | examples below). 16 | 17 | DESCRIPTION 18 | This extension provides an easy way to edit files related to a wenv. To use 19 | this extension, a wenv must define an associative array \`wenv_files\` with 20 | entries for the files. For example, if the base directory of your wenv has a 21 | file main.cpp that you want to edit by running \`edit main\`, you'd define 22 | \`wenv_files\` as 23 | 24 | declare -Ag wenv_files=( 25 | ['main']='main.cpp' 26 | ) 27 | 28 | Then you can run \`edit main\` to open main.cpp in your editor. Note that 29 | non-absolute paths will be treated as relative to WENV_DIR. 30 | 31 | You can also use Zsh globs/expansions/etc. when defining sets of files to 32 | open: 33 | 34 | declare -Ag wenv_files=( 35 | # open class.h and class.cpp 36 | ['class']='class.{h,cpp}' 37 | # open all .go files in WENV_DIR and all subdirectories 38 | ['go']='\$(find . -name \*.go)' 39 | ) 40 | 41 | Finally, note that you can tab-complete among all of the keys in \`wenv_files\` 42 | when passing the arguments. 43 | " 44 | 45 | local flag_r=0 46 | while getopts ":r:h" opt; do 47 | case $opt in 48 | r) 49 | flag_r=1 50 | shift 51 | ;; 52 | h) 53 | echo "$usage" 54 | return 0 55 | ;; 56 | \?) 57 | echo "unknown option: -$OPTARG" >&2 58 | ;; 59 | esac 60 | done 61 | 62 | (($# == 0)) && { echo "$usage" >&2 ; return 1 } 63 | 64 | local files=() 65 | for arg in $@; do 66 | [ "${wenv_files[$arg]+0}" ] || { echo "no entry '$arg'" >&2 ; return 1 } 67 | files+="${wenv_files[$arg]}" 68 | done 69 | 70 | local abs=() 71 | for file in $files; do 72 | if [[ $file != /* ]]; then 73 | abs+=($(realpath "$WENV_DIR/$file")) 74 | else 75 | abs+=($file) 76 | fi 77 | done 78 | 79 | (( flag_r == 1 )) && tmux rename-window "$1" 80 | eval "$EDITOR $abs" 81 | } 82 | _edit() { 83 | COMPREPLY=() 84 | complete +o default 85 | 86 | local word="${COMP_WORDS[COMP_CWORD]}" 87 | local prev="${COMP_WORDS[COMP_CWORD-1]}" 88 | 89 | if [[ ${word} == -* ]]; then 90 | COMPREPLY=($(compgen -W "-r -h" -- ${word})) 91 | else 92 | local opts="${(k)wenv_files[@]}" 93 | COMPREPLY=($(compgen -W "${opts}" -- ${word})) 94 | fi 95 | } 96 | complete -F _edit edit 97 | -------------------------------------------------------------------------------- /extensions/history: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 4 | # !This extension might be broken + delete your history if you use it. WIP ! 5 | # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 6 | 7 | export WENV_HISTDIR="${XDG_CACHE_HOME:-$HOME/.cache}/wenv/history" 8 | export WENV_HISTFILE=$WENV_HISTDIR/$(sed 's/\//-/g' <<< $WENV) 9 | 10 | # ensure history file (+ directory) exists 11 | if [[ ! -f $WENV_HISTFILE ]]; then 12 | mkdir -p $WENV_HISTDIR 13 | touch $WENV_HISTFILE 14 | fi 15 | 16 | # hook to update the wenv-specific history file every time the general history is updated 17 | _update_wenv_history() { 18 | print -sr -- ${1%%$'\n'} 19 | fc -pa $WENV_HISTFILE 20 | } 21 | # add-zsh-hook zshaddhistory _update_wenv_history 22 | 23 | # zsh widget to search the wenv-specific history 24 | _search_wenv_history() { 25 | fc -pa $WENV_HISTFILE 26 | # fc -R 27 | bindkey "^T" history-incremental-search-backward 28 | zle history-incremental-search-backward 29 | bindkey "^T" _search_wenv_history 30 | # fc -P 31 | } 32 | zle -N _search_wenv_history 33 | bindkey "^T" _search_wenv_history 34 | -------------------------------------------------------------------------------- /extensions/nvm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | export NVM_DIR="$HOME/.nvm" 4 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm 5 | [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion 6 | -------------------------------------------------------------------------------- /extensions/ssh-agent: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | export SSH_AUTH_SOCK="${XDG_RUNTIME_DIR}/ssh-agent.socket" 4 | 5 | ssh_agent() { 6 | case "$1" in 7 | 'start') 8 | systemctl --user start ssh-agent 9 | ;; 10 | 'stop') 11 | systemctl --user stop ssh-agent 12 | ;; 13 | 'status') 14 | systemctl --user status ssh-agent 15 | ;; 16 | *) 17 | echo "invalid argument: $1" >2 18 | ;; 19 | esac 20 | } 21 | -------------------------------------------------------------------------------- /extensions/wd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # `wd` is a function for easier access to the `wenv_dirs` associative array defined in your wenvs. 4 | # It takes one argument, a key to `wenv_dirs`, and outputs the corresponding value. 5 | # 6 | # For example, you might define `wenv_dirs` as: 7 | # ``` 8 | # define -Ag wenv_dirs 9 | # wenv_dirs[tmp]="/tmp" 10 | # ``` 11 | # 12 | # Then `wd` would output the following: 13 | # ``` 14 | # $ wd tmp 15 | # /tmp 16 | # ``` 17 | # 18 | # If you don't pass an argument, `wd` will output `WENV_DIR`. 19 | # 20 | # The `_wd` completion function lets you tab-complete the keys of wenv_dirs. 21 | # 22 | # `wd` and `_wd` can easily be wrapped so that any function can leverage your `wenv_dirs` directories. 23 | # The quintessential example is `c`, which is an easy way to `cd` into any of the directories by their key. 24 | # In the above example, running `c tmp` would change to the /tmp directory. 25 | # 26 | # See the bottom of this file for additional `wd` wrapper function examples. 27 | 28 | wd() { 29 | (($# != 1)) && { echo "$WENV_DIR" ; return 0 } 30 | local input="$1" 31 | shift 32 | 33 | [ "${wenv_dirs[$input]+0}" ] || { echo "no entry '$input'" >&2 ; return 1 } 34 | dir="${wenv_dirs[$input]}" 35 | 36 | local abs 37 | if [[ $dir != /* ]]; then 38 | abs=$(realpath "$WENV_DIR/$dir") 39 | else 40 | abs=$dir 41 | fi 42 | echo "$abs" 43 | } 44 | _wd() { 45 | (( ${#wenv_dirs} == 0 )) && return 0 46 | if ((CURRENT == 2)); then 47 | compadd -- "${(k)wenv_dirs[@]}" 48 | fi 49 | } 50 | compdef _wd wd 51 | 52 | c() { 53 | dir=$(wd $@) || return $? 54 | cd $dir 55 | } 56 | compdef _wd c 57 | 58 | lfwd() { 59 | dir=$(wd $@) || return $? 60 | lf $dir 61 | } 62 | compdef _wd lfwd 63 | 64 | lfc() { 65 | dir=$(wd $@) || return $? 66 | lfcd $dir 67 | } 68 | compdef _wd lfc 69 | -------------------------------------------------------------------------------- /template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | wenv_dir="" 4 | wenv_deps=() 5 | wenv_extensions=('wd') 6 | 7 | startup_wenv() {} 8 | shutdown_wenv() {} 9 | bootstrap_wenv() {} 10 | 11 | ((only_load_wenv_vars == 1)) && return 0 12 | 13 | # for c extension 14 | # declare -Ag wenv_dirs 15 | 16 | # for edit extension 17 | # declare -Ag wenv_files 18 | -------------------------------------------------------------------------------- /wenv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/zsh 2 | 3 | wenv_dir="$SRC/wenv" 4 | wenv_deps=() 5 | wenv_extensions=('c') 6 | 7 | bootstrap_wenv() {} 8 | startup_wenv() {} 9 | shutdown_wenv() {} 10 | 11 | export WENV_CFG="${XDG_CONFIG_HOME:-$HOME/.config}/wenv" 12 | 13 | __wenv() { 14 | local usage="\ 15 | USAGE 16 | wenv [-h] 17 | 18 | OPTIONS 19 | -h Display this help message. 20 | 21 | SUBCOMMANDS 22 | start Start the working environment . 23 | stop Stop the current working environment. 24 | new Create a new working environment. 25 | list List all wenvs 26 | edit Edit the wenv file for . 27 | rename Rename wenv to . 28 | remove Delete the wenv file for . 29 | source Source the wenv file for . 30 | cd Change to 's base directory. 31 | extension Interact with wenv extensions. 32 | bootstrap Run 's bootstrap function. 33 | 34 | Run \`wenv -h\` for more information on a given subcommand . 35 | " 36 | 37 | [[ $# == 0 ]] && { echo "$usage" >&2 ; return 1 } 38 | 39 | local opt OPTIND 40 | while getopts ":h" opt; do 41 | case "$opt" in 42 | h) 43 | echo "$usage" 44 | return 0 45 | ;; 46 | \?) 47 | echo "unknown option: -$OPTARG" >&2 48 | return 1 49 | ;; 50 | esac 51 | done 52 | 53 | local wenv_command="$1" 54 | shift 55 | case "$wenv_command" in 56 | start) 57 | wenv_start $@ 58 | ;; 59 | stop) 60 | wenv_stop $@ 61 | ;; 62 | 'cd') 63 | wenv_cd $@ 64 | ;; 65 | new) 66 | wenv_new $@ 67 | ;; 68 | ls|list) 69 | wenv_list $@ 70 | ;; 71 | edit) 72 | wenv_edit $@ 73 | ;; 74 | rm|remove) 75 | wenv_remove $@ 76 | ;; 77 | mv|rename) 78 | wenv_rename $@ 79 | ;; 80 | 'source') 81 | wenv_source $@ 82 | ;; 83 | extension) 84 | wenv_extension $@ 85 | ;; 86 | bootstrap) 87 | wenv_bootstrap $@ 88 | ;; 89 | *) 90 | echo "invalid command '$wenv_command'" >&2 91 | return 1 92 | ;; 93 | esac 94 | } 95 | alias wenv='noglob __wenv' 96 | # for some reason the above alias breaks completions sometimes. this seems to fix it 97 | compdef _wenv __wenv 98 | 99 | wenv_start() { 100 | local usage="\ 101 | USAGE 102 | wenv start [-i] [-d] [-h] - Start working environemnt . If is already running, attach to it. 103 | 104 | OPTIONS 105 | -i Run 's startup function. Default: true. 106 | -d Don't attach to the wenv's tmux session after starting. Default: false. 107 | -h Display this help message. 108 | " 109 | 110 | local flag_i=1 111 | local flag_d=0 112 | 113 | local opt OPTIND 114 | while getopts ":qidh" opt; do 115 | case $opt in 116 | i) 117 | flag_i=0 118 | ;; 119 | d) 120 | flag_d=1 121 | ;; 122 | h) 123 | echo "$usage" 124 | return 0 125 | ;; 126 | \?) 127 | echo "unknown option: -$OPTARG" >&2 128 | return 1 129 | ;; 130 | esac 131 | done 132 | shift $((OPTIND-1)) 133 | 134 | (($# == 0)) && return 1 135 | 136 | # expand the $@ wenv args into an array here, handling globs/multiple wenvs/etc. 137 | # the `perl` command prefixes every wenv with $WENV_CFG/wenvs, while the `eval echo` handles glob expansions 138 | local wenv_files 139 | wenv_files=($(eval echo $(perl -pe "s|([^ ]+)|$WENV_CFG/wenvs/\1|g" <<< $@))) 140 | (($(wc -w <<< $wenv_files) > 1)) && flag_d=1 # if multiple wenvs were provided, don't attach to tmux session 141 | 142 | for wenv_file in $wenv_files; do 143 | 144 | local wenv=${wenv_file#"$WENV_CFG/wenvs/"} 145 | 146 | if [[ ! -f $wenv_file ]]; then 147 | echo "wenv '$wenv' does not exist" >&2 148 | continue 149 | fi 150 | 151 | if ! tmux list-sessions | grep "^$wenv:" >/dev/null; then # wenv isn't running yet, start it 152 | # create tmux session for wenv 153 | tmux new-session -d -s "$wenv" 154 | 155 | mkdir -p /tmp/wenv 156 | # replace / with - for wenvs in subdirectories 157 | local tmp_start_file="/tmp/wenv/start-$(perl -pe 's|/|-|g' <<< $wenv)" 158 | [[ -f "$tmp_start_file" ]] && rm -f "$tmp_start_file" 159 | echo -e ' 160 | export WENV='"$wenv"' 161 | wenv_source -c '"$wenv"' || { echo "failed to source wenv '$wenv'" >&2 ; return 1 } 162 | tmux set-environment WENV "$WENV" 163 | (('"$flag_i"' == 1)) && startup_wenv 164 | clear 165 | '> "$tmp_start_file" 166 | 167 | tmux send -t "$wenv" " source $tmp_start_file && rm -f $tmp_start_file" ENTER 168 | 169 | ((flag_d == 1)) && { echo "started wenv '$wenv' " ; continue } 170 | else # wenv is already running 171 | ((flag_d == 1)) && { echo "wenv '$wenv' already running" ; continue } 172 | fi 173 | 174 | if [[ -n $TMUX ]]; then # we're in tmux, switch to specified wenv's session 175 | tmux switch -t "$wenv" 176 | else # not in tmux, attach to specified wenv's session 177 | tmux attach-session -t "$wenv" 178 | fi 179 | done 180 | 181 | return 0 182 | } 183 | 184 | wenv_exec() { 185 | echo "wenv_exec has been deprecated! change the call \`wenv_exec\` in your zshrc/zsh startup to \`wenv_source -c\` instead" >&2 186 | return 1 187 | } 188 | 189 | wenv_source() { 190 | local usage="\ 191 | USAGE 192 | wenv source [-h] - Source active wenv + its dependencies. 193 | 194 | OPTIONS 195 | -h Display this help message. 196 | -c Stay in the wenv's base directory after loading the wenv, rather than changing back to the caller's original working directory. Default: false. 197 | 198 | DESCRIPTION 199 | \`wenv source\` sources the active wenv's aliases and its dependencies. 200 | " 201 | 202 | # -c means "don't change back to the original working directory after cd'ing into the wenv's base directory" 203 | local stay_in_wenv_dir_after_running=0 204 | 205 | local opt OPTIND 206 | while getopts ":hc" opt; do 207 | case $opt in 208 | c) stay_in_wenv_dir_after_running=1 209 | ;; 210 | h) echo "$usage" 211 | return 0 212 | ;; 213 | \?) echo "unknown option: -$OPTARG" >&2 214 | return 1 215 | ;; 216 | esac 217 | done 218 | shift $((OPTIND-1)) 219 | 220 | local wenv="${1:-${WENV}}" 221 | [[ -z "$wenv" ]] && { echo "error sourcing wenv: no wenv arg provided and \$WENV is empty" >&2 ; return 1 } 222 | 223 | only_load_wenv_vars=1 source $WENV_CFG/wenvs/$wenv 224 | 225 | export WENV_DIR=$wenv_dir 226 | export WENV_DEPS=("$wenv_deps[@]") 227 | export WENV_EXTENSIONS=("${wenv_extensions[@]}") 228 | 229 | ((${#WENV_EXTENSIONS[@]} != 0)) && wenv_extension_load "${WENV_EXTENSIONS[@]}" 230 | ((${#WENV_DEPS[@]} != 0)) && source_wenv_dependencies_recursively "${WENV_DEPS[@]}" 231 | 232 | if ((stay_in_wenv_dir_after_running == 1)); then 233 | cd "$WENV_DIR" &> /dev/null 234 | source $WENV_CFG/wenvs/$wenv 235 | else 236 | pushd "$WENV_DIR" &> /dev/null 237 | source $WENV_CFG/wenvs/$wenv 238 | popd &> /dev/null 239 | fi 240 | 241 | return 0 242 | } 243 | 244 | source_wenv_dependencies_recursively() { 245 | local wenv 246 | for wenv in $@; do 247 | unset_quiet wenv_deps 248 | 249 | only_load_wenv_vars=1 source "$WENV_CFG/wenvs/$wenv" 250 | eval source_wenv_dependencies_recursively $wenv_deps 251 | 252 | # source this wenv now that its dependencies are loaded 253 | source $WENV_CFG/wenvs/$wenv 254 | done 255 | } 256 | 257 | wenv_stop() { 258 | local usage="\ 259 | USAGE 260 | wenv stop [-h] [-f] [-s] - Shutdown the active working environment. 261 | 262 | OPTIONS 263 | -f Force shutdown even if \`shutdown_wenv()\` fails. Default: false. 264 | -s Run the active wenv's shutdown function. Default: true. 265 | -h Display this help message. 266 | " 267 | 268 | [[ -z "$WENV" ]] && return 1 269 | 270 | local flag_f=0 271 | local flag_s=1 272 | 273 | local opt OPTIND 274 | while getopts ":fsh" opt; do 275 | case $opt in 276 | f) 277 | flag_f=1 278 | ;; 279 | s) 280 | flag_s=0 281 | ;; 282 | h) 283 | echo "$usage" 284 | return 0 285 | ;; 286 | \?) 287 | echo "unknown option: -$OPTARG" >&2 288 | return 1 289 | ;; 290 | esac 291 | done 292 | shift $((OPTIND-1)) 293 | 294 | wenv_cd 295 | 296 | ((flag_s == 1)) && shutdown_wenv 297 | [[ $? -ne 0 ]] && ((flag_f != 1)) && { echo "error: shutdown_wenv failed" ; return 1 } 298 | wenv_clean_up 299 | } 300 | 301 | wenv_clean_up() { 302 | [[ -z "$WENV" ]] && return 1 303 | 304 | unset_quiet WENV 305 | unset_quiet WENV_{DIR,DEPS,EXTENSIONS} 306 | unset_quiet -f {bootstrap,startup,shutdown}_wenv 307 | 308 | if [[ -n "$TMUX" ]]; then 309 | tmux set-environment WENV '' 310 | tmux rename-session $(tmux display-message -p '#{session_id}' | tr -d '$') 311 | fi 312 | } 313 | 314 | wenv_new() { 315 | local usage="\ 316 | USAGE 317 | wenv new [-d ] [-i ] [-h] - Create a new wenv called . 318 | 319 | OPTIONS 320 | -d Initialize wenv with as the new wenv's base directory rather than the current working directory. 321 | -i Use 's wenv file as the initial definition for the new wenv. 322 | -h Display this help message. 323 | " 324 | 325 | local template="$WENV_CFG/template" 326 | local wenv_dir=$(pwd) 327 | 328 | local opt OPTIND 329 | while getopts ":d:i:h" opt; do 330 | case $opt in 331 | d) 332 | wenv_dir="$OPTARG" 333 | ;; 334 | i) 335 | template="$WENV_CFG/wenvs/${OPTARG}" 336 | ;; 337 | h) 338 | echo "$usage" 339 | return 0 340 | ;; 341 | :) 342 | echo "unknown option: $OPTARG requires an argument" >&2 343 | return 1 344 | ;; 345 | \?) 346 | echo "unknown option: -$OPTARG" >&2 347 | return 1 348 | ;; 349 | esac 350 | done 351 | shift $((OPTIND-1)) 352 | 353 | [[ -z "$1" ]] && return 1 354 | local wenv="$1" 355 | 356 | if is_wenv "$1"; then 357 | echo "wenv '$wenv' already exists" >&2 358 | printf "overwrite? [yN] " >&2 359 | [[ ! $(read -e) =~ [yY] ]] && return 1 360 | rm -f "$WENV_CFG/wenvs/$wenv" 361 | fi 362 | 363 | mkdir -p $WENV_CFG/wenvs/$(dirname $wenv) 364 | cat =(perl -pe "s|wenv_dir=.*?$|wenv_dir=\"$wenv_dir\"|" "$template") > "$WENV_CFG/wenvs/$wenv" 365 | 366 | wenv_edit "$wenv" 367 | } 368 | 369 | wenv_edit() { 370 | local usage="\ 371 | USAGE 372 | wenv edit [-h] - Open 's wenv file in \$EDITOR. 373 | 374 | OPTIONS 375 | -h Display this help message. 376 | " 377 | 378 | local opt OPTIND 379 | while getopts ":h" opt; do 380 | case "$opt" in 381 | h) 382 | echo "$usage" 383 | return 0 384 | ;; 385 | \?) 386 | echo "unknown option: -$OPTARG" >&2 387 | return 1 388 | ;; 389 | esac 390 | done 391 | 392 | # nice bash syntax for setting var to first non-empty variable 393 | local wenv="${1:-${WENV}}" 394 | [[ -f "$WENV_CFG/wenvs/$wenv" ]] && $EDITOR "$WENV_CFG/wenvs/$wenv" 395 | } 396 | 397 | wenv_list() { 398 | local usage="\ 399 | USAGE 400 | wenv list [-h] - List wenvs. 401 | 402 | OPTIONS 403 | -h Display this help message. 404 | " 405 | 406 | local opt OPTIND 407 | while getopts ":h" opt; do 408 | case "$opt" in 409 | h) 410 | echo "$usage" 411 | return 0 412 | ;; 413 | \?) 414 | echo "unknown option: -$OPTARG" >&2 415 | return 1 416 | ;; 417 | esac 418 | done 419 | 420 | find "$WENV_CFG/wenvs/$1" ! -type d | perl -pe "s|$WENV_CFG/wenvs/||" | sort 421 | } 422 | 423 | wenv_remove() { 424 | local usage="\ 425 | USAGE 426 | wenv remove [-h] [] - Delete 's wenv file. 427 | 428 | OPTIONS 429 | -h Display this help message. 430 | List of options to forward to \`rm\`. 431 | " 432 | 433 | local opt OPTIND 434 | while getopts ":h" opt; do 435 | case "$opt" in 436 | h) 437 | echo "$usage" 438 | return 0 439 | ;; 440 | *) # forward the rest of the opts to rm 441 | break 442 | ;; 443 | esac 444 | done 445 | 446 | # last passed argument is wenv to remove (usually quoted string) 447 | local wenv="${@[$#]}" 448 | # all other opts are passed to rm 449 | local rm_opts=${@:1:$(($# - 1))} 450 | 451 | if ! is_wenv "$wenv"; then 452 | echo "WENV '$wenv' does not exist." 453 | return 1 454 | fi 455 | 456 | local wenv_file="$WENV_CFG/wenvs/$wenv" 457 | eval "rm $rm_opts $wenv_file" 458 | } 459 | 460 | wenv_cd() { 461 | local usage="\ 462 | USAGE 463 | wenv cd [-h] - cd into 's base directory. 464 | 465 | OPTIONS 466 | -h Display this help message. 467 | 468 | DESCRIPTION 469 | Calling \`wenv cd\` with no arguments will \`cd\` into the active wenv's base 470 | directory. 471 | 472 | Calling as \`wenv cd \` will \`cd\` into 's base directory. 473 | 474 | The base directory of a wenv is defined by its \`wenv_dir\` value. 475 | " 476 | 477 | local opt OPTIND 478 | while getopts ":h" opt; do 479 | case $opt in 480 | h) 481 | echo "$usage" 482 | return 0 483 | ;; 484 | \?) 485 | echo "unknown option: -$OPTARG" >&2 486 | return 1 487 | ;; 488 | esac 489 | done 490 | shift $((OPTIND-1)) 491 | 492 | if [[ -z "$1" ]]; then 493 | [[ -n "$WENV_DIR" ]] && cd "$WENV_DIR" &> /dev/null 494 | return 0 495 | fi 496 | 497 | if ! is_wenv "$1"; then 498 | echo "wenv '$1' doesn't exist" >&2 499 | return 1 500 | fi 501 | local wenv="$1" 502 | 503 | unset wenv_dir 504 | only_load_wenv_vars=1 source $WENV_CFG/wenvs/$wenv 505 | 506 | [[ -z "$wenv_dir" ]] && { echo "wenv_dir not defined for wenv '$wenv'" >&2 ; return 1 } 507 | 508 | cd $wenv_dir 509 | 510 | return 0 511 | } 512 | 513 | wenv_rename() { 514 | local usage="\ 515 | USAGE 516 | wenv rename [-h] - Rename wenv to . 517 | 518 | OPTIONS 519 | -h Display this help message. 520 | " 521 | 522 | local opt OPTIND 523 | while getopts ":h" opt; do 524 | case "$opt" in 525 | h) 526 | echo "$usage" 527 | return 0 528 | ;; 529 | \?) 530 | echo "unknown option: -$OPTARG" >&2 531 | return 1 532 | ;; 533 | esac 534 | done 535 | 536 | [[ $# != 2 ]] && { echo "wenv rename requires two arguments" >&2 ; return 1 } 537 | local old="$1" 538 | local new="$2" 539 | 540 | if [[ "$new" == *"/"* ]]; then 541 | local new_dir="$WENV_CFG/wenvs/$(dirname $new)" 542 | if [[ ! -d "$new_dir" ]]; then 543 | mkdir -p $new_dir 544 | fi 545 | fi 546 | 547 | mv "$WENV_CFG/wenvs/$old" "$WENV_CFG/wenvs/$new" 548 | } 549 | 550 | wenv_bootstrap() { 551 | local usage="\ 552 | USAGE 553 | wenv bootstrap [-h] - Run the bootstrap function for a wenv. 554 | 555 | OPTIONS 556 | -h Display this help message. 557 | 558 | DESCRIPTION 559 | A wenv's bootstrap function is used to set up the wenv's environment before 560 | the first time it's used. A wenv bootstrap function might pull down relevant 561 | git repos, install relevant packages, create config files, etc. 562 | " 563 | 564 | local opt OPTIND 565 | while getopts ":h" opt; do 566 | case "$opt" in 567 | h) 568 | echo "$usage" 569 | return 0 570 | ;; 571 | \?) 572 | echo "unknown option: -$OPTARG" >&2 573 | return 1 574 | ;; 575 | esac 576 | done 577 | 578 | if ! is_wenv "$1"; then 579 | echo "wenv '$1' doesn't exist" >&2 580 | return 1 581 | fi 582 | local wenv="$1" 583 | 584 | # clear out bootstrap_wenv if we're in an active wenv 585 | [[ -n "$WENV" ]] && unset_quiet -f bootstrap_wenv 586 | 587 | only_load_wenv_vars=1 source $WENV_CFG/wenvs/$wenv 588 | if ! function_exists bootstrap_wenv; then 589 | echo "bootstrap_wenv not defined for wenv '$1'" >&2 590 | return 1 591 | fi 592 | 593 | bootstrap_wenv 594 | unset_quiet -f bootstrap_wenv 595 | } 596 | 597 | wenv_extension() { 598 | local usage="\ 599 | USAGE 600 | wenv extension [-h] [] - Interact with wenv extensions. 601 | 602 | OPTIONS 603 | -h Display this help message. 604 | 605 | SUBCOMMANDS 606 | edit Create or edit existing extensions. 607 | load Load an extension. 608 | rm Remove an extension. 609 | 610 | DESCRIPTION 611 | Use \`wenv extension -h\` for more information on a given subcommand . 612 | " 613 | 614 | local opt OPTIND 615 | while getopts ":h" opt; do 616 | case $opt in 617 | h) 618 | echo "$usage" 619 | return 0 620 | ;; 621 | \?) 622 | echo "unknown option: -$OPTARG" >&2 623 | return 1 624 | ;; 625 | esac 626 | done 627 | shift $((OPTIND-1)) 628 | 629 | local arg="$1" 630 | shift 2> /dev/null 631 | case "$arg" in 632 | load) 633 | wenv_extension_load $@ 634 | ;; 635 | edit) 636 | wenv_extension_edit $@ 637 | ;; 638 | rm|remove) 639 | wenv_extension_remove $@ 640 | ;; 641 | *) 642 | echo "$usage" >&2 643 | return 1 644 | ;; 645 | esac 646 | } 647 | 648 | wenv_extension_load() { 649 | local usage="\ 650 | USAGE 651 | wenv extension load [-h] [ ...] - Source wenv extensions. 652 | 653 | OPTIONS 654 | -h Display this help message. 655 | 656 | ARGUMENTS 657 | [ ...] The names of the extension files to load. 658 | 659 | DESCRIPTION 660 | This function sources the corresponding extension file at \$WENV_CFG/. 661 | This will only source the extension file in the current shell. See the 662 | documentation on the \`wenv_extensions\` configuration variable for loading an 663 | extension in every shell of a wenv. 664 | " 665 | 666 | local opt OPTIND 667 | while getopts ":h" opt; do 668 | case $opt in 669 | h) 670 | echo "$usage" 671 | return 0 672 | ;; 673 | \?) 674 | echo "unknown option: -$OPTARG" >&2 675 | return 1 676 | ;; 677 | esac 678 | done 679 | shift $((OPTIND-1)) 680 | 681 | [[ -z "$@" ]] && { echo "$usage" >&2 ; return 1 } 682 | local extensions="$WENV_CFG/extensions" 683 | for extension in "$@"; do 684 | if [[ ! -f "$extensions/$extension" ]]; then 685 | echo "'$extension' not found in $extensions" >&2 686 | return 1 687 | fi 688 | source "$extensions/$extension" 689 | done 690 | } 691 | 692 | wenv_extension_edit() { 693 | local usage="\ 694 | USAGE 695 | wenv extension new [-h] [ ...] - Create or edit existing extensions. 696 | 697 | OPTIONS 698 | -h Display this help message. 699 | 700 | ARGUMENTS 701 | [ ...] The names of the extension files to create/edit. 702 | 703 | DESCRIPTION 704 | Opens one or more extension files in \$EDITOR. When editing a extension file 705 | that doesn't already exist, the file will be created with a default shebang. 706 | " 707 | 708 | local opt OPTIND 709 | while getopts ":h" opt; do 710 | case $opt in 711 | h) 712 | echo "$usage" 713 | return 0 714 | ;; 715 | \?) 716 | echo "unknown option: -$OPTARG" >&2 717 | return 1 718 | ;; 719 | esac 720 | done 721 | shift $((OPTIND-1)) 722 | 723 | [[ -z "$@" ]] && { echo "$usage" >&2 ; return 1 } 724 | 725 | local abs=() 726 | for ext in $@; do 727 | file="$WENV_CFG/extensions/$ext" 728 | [[ ! -f "$file" ]] && echo '#!/usr/bin/env zsh' > "$file" 729 | abs+=($file) 730 | done 731 | $EDITOR "$abs[@]" 732 | } 733 | 734 | wenv_extension_remove() { 735 | local usage="\ 736 | USAGE 737 | wenv extension remove [-h] [-f] [ ...] - Delete extension files. 738 | 739 | OPTIONS 740 | -h Display this help message. 741 | -f Force removal (passes \`-f\` flag to \`rm\`). 742 | 743 | DESCRIPTION 744 | Removes extensions from \$WENV_CFG/extensions. If any of the passed extension 745 | files don't exist, nothing will be removed. 746 | " 747 | 748 | local flag_f=0 749 | 750 | local opt OPTIND 751 | while getopts ":fh" opt; do 752 | case "$opt" in 753 | f) 754 | flag_f=1 755 | ;; 756 | h) 757 | echo "$usage" 758 | return 0 759 | ;; 760 | \?) 761 | echo "unknown option: -$OPTARG" >&2 762 | return 1 763 | ;; 764 | esac 765 | done 766 | shift $((OPTIND-1)) 767 | 768 | declare -A abs 769 | for ext in $@; do 770 | local file="$WENV_CFG/extensions/$ext" 771 | [[ ! -f "$file" ]] && { echo "Extension '$ext' does not exist" >&2 ; return 1 } 772 | abs[$ext]="$file" 773 | done 774 | 775 | if ((flag_f == 1)); then 776 | rm -f "${abs[@]}" 777 | else 778 | for ext in $@; do 779 | rm "${abs[$ext]}" 780 | done 781 | fi 782 | return 0 783 | } 784 | 785 | is_wenv() { 786 | if [[ -z "$1" ]]; then 787 | return 1 788 | fi 789 | local wenv_file="$WENV_CFG/wenvs/$1" 790 | if [[ ! -f "$wenv_file" ]]; then 791 | return 1 792 | fi 793 | return 0 794 | } 795 | 796 | unset_quiet() { 797 | unset $@ 2>/dev/null 798 | } 799 | 800 | function_exists() { 801 | declare -f "$1" >/dev/null 802 | } 803 | --------------------------------------------------------------------------------