diff --git a/.config/tmux/plugins/resurrect/.gitattributes b/.config/tmux/plugins/resurrect/.gitattributes new file mode 100644 index 0000000..3772b5e --- /dev/null +++ b/.config/tmux/plugins/resurrect/.gitattributes @@ -0,0 +1,5 @@ +# Force text files to have unix eols, so Windows/Cygwin does not break them +*.* eol=lf + +# Except for images because then on checkout the files have been altered. +*.png binary diff --git a/.config/tmux/plugins/resurrect/.gitignore b/.config/tmux/plugins/resurrect/.gitignore new file mode 100644 index 0000000..72523e8 --- /dev/null +++ b/.config/tmux/plugins/resurrect/.gitignore @@ -0,0 +1,3 @@ +run_tests +tests/run_tests_in_isolation +tests/helpers/helpers.sh diff --git a/.config/tmux/plugins/resurrect/.gitmodules b/.config/tmux/plugins/resurrect/.gitmodules new file mode 100644 index 0000000..5e44e3c --- /dev/null +++ b/.config/tmux/plugins/resurrect/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/tmux-test"] + path = lib/tmux-test + url = https://github.com/tmux-plugins/tmux-test.git diff --git a/.config/tmux/plugins/resurrect/.travis.yml b/.config/tmux/plugins/resurrect/.travis.yml new file mode 100644 index 0000000..fea6850 --- /dev/null +++ b/.config/tmux/plugins/resurrect/.travis.yml @@ -0,0 +1,19 @@ +# generic packages and tmux +before_install: + - sudo apt-get update + - sudo apt-get install -y git-core expect + - sudo apt-get install -y python-software-properties software-properties-common + - sudo apt-get install -y libevent-dev libncurses-dev + - git clone https://github.com/tmux/tmux.git + - cd tmux + - git checkout 2.5 + - sh autogen.sh + - ./configure && make && sudo make install + +install: + - git fetch --unshallow --recurse-submodules || git fetch --recurse-submodules + # manual `git clone` required for testing `tmux-test` plugin itself + - git clone https://github.com/tmux-plugins/tmux-test lib/tmux-test; true + - lib/tmux-test/setup + +script: ./tests/run_tests_in_isolation diff --git a/.config/tmux/plugins/resurrect/CHANGELOG.md b/.config/tmux/plugins/resurrect/CHANGELOG.md new file mode 100644 index 0000000..2957f18 --- /dev/null +++ b/.config/tmux/plugins/resurrect/CHANGELOG.md @@ -0,0 +1,163 @@ +# Changelog + +### master +- Remove deprecated "restoring shell history" feature. + +### v4.0.0, 2022-04-10 +- Proper handling of `automatic-rename` window option. +- save and restore tmux pane title (breaking change: you have to re-save to be + able to properly restore!) + +### v3.0.0, 2021-08-30 +- save and restore tmux pane contents (@laomaiweng) +- update tmux-test to solve issue with recursing git submodules in that project +- set options quietly in `resurrect.tmux` script +- improve pane contents restoration: `cat ` is no longer shown in pane + content history +- refactoring: drop dependency on `paste` command +- bugfix for pane contents restoration +- expand tilde char `~` if used with `@resurrect-dir` +- do not save empty trailing lines when pane content is saved +- do not save pane contents if pane is empty (only for 'save pane contents' + feature) +- "save pane contents" feature saves files to a separate directory +- archive and compress pane contents file +- make archive & compress pane contents process more portable +- `mutt` added to the list of automatically restored programs +- added guide for migrating from tmuxinator +- fixed a bug for restoring commands on tmux 2.5 (and probably tmux 2.4) +- do not create another resurrect file if there are no changes (credit @vburdo) +- allow using '$HOSTNAME' in @resurrect-dir +- add zsh history saving and restoring +- delete resurrect files older than 30 days, but keep at least 5 files +- add save and restore hooks +- always use `-ao` flags for `ps` command to detect commands +- Deprecate restoring shell history feature. +- `view` added to the list of automatically restored programs +- Enable vim session strategy to work with custom session files, + e.g. `vim -S Session1.vim`. +- Enable restoring command arguments for inline strategies with `*` character. +- Kill session "0" if it wasn't restored. +- Add `@resurrect-delete-backup-after` option to specify how many days of + backups to keep - default is 30. + +### v2.4.0, 2015-02-23 +- add "tmux-test" +- add test for "resurrect save" feature +- add test for "resurrect restore" feature +- make the tests work and pass on travis +- add travis badge to the readme + +### v2.3.0, 2015-02-12 +- Improve fetching proper window_layout for zoomed windows. In order to fetch + proper value, window has to get unzoomed. This is now done faster so that + "unzoom,fetch value,zoom" cycle is almost unnoticable to the user. + +### v2.2.0, 2015-02-12 +- bugfix: zoomed windows related regression +- export save and restore script paths so that 'tmux-resurrect-save' plugin can + use them +- enable "quiet" saving (used by 'tmux-resurrect-save' plugin) + +### v2.1.0, 2015-02-12 +- if restore is started when there's only **1 pane in the whole tmux server**, + assume the users wants the "full restore" and overrwrite that pane. + +### v2.0.0, 2015-02-10 +- add link to the wiki page for "first pane/window issue" to the README as well + as other tweaks +- save and restore grouped sessions (used with multi-monitor workflow) +- save and restore active and alternate windows in grouped sessions +- if there are no grouped sessions, do not output empty line to "last" file +- restore active and alternate windows only if they are present in the "last" file +- refactoring: prefer using variable with tab character +- remove deprecated `M-s` and `M-r` key bindings (breaking change) + +### v1.5.0, 2014-11-09 +- add support for restoring neovim sessions + +### v1.4.0, 2014-10-25 +- plugin now uses strategies when fetching pane full command. Implemented + 'default' strategy. +- save command strategy: 'pgrep'. It's here only if fallback is needed. +- save command strategy: 'gdb' +- rename default strategy name to 'ps' +- create `expect` script that can fully restore tmux environment +- fix default save command strategy `ps` command flags. Flags are different for + FreeBSD. +- add bash history saving and restoring (@rburny) +- preserving layout of zoomed windows across restores (@Azrael3000) + +### v1.3.0, 2014-09-20 +- remove dependency on `pgrep` command. Use `ps` for fetching process names. + +### v1.2.1, 2014-09-02 +- tweak 'new_pane' creation strategy to fix #36 +- when running multiple tmux server and for a large number of panes (120 +) when + doing a restore, some panes might not be created. When that is the case also + don't restore programs for those panes. + +### v1.2.0, 2014-09-01 +- new feature: inline strategies when restoring a program + +### v1.1.0, 2014-08-31 +- bugfix: sourcing `variables.sh` file in save script +- add `Ctrl` key mappings, deprecate `Alt` keys mappings. + +### v1.0.0, 2014-08-30 +- show spinner during the save process +- add screencast script +- make default program running list even more conservative + +### v0.4.0, 2014-08-29 +- change plugin name to `tmux-resurrect`. Change all the variable names. + +### v0.3.0, 2014-08-29 +- bugfix: when top is running the pane `$PWD` can't be saved. This was causing + issues during the restore and is now fixed. +- restoring sessions multiple times messes up the whole environment - new panes + are all around. This is now fixed - pane restorations are now idempotent. +- if pane exists from before session restore - do not restore the process within + it. This makes the restoration process even more idempotent. +- more panes within a window can now be restored +- restore window zoom state + +### v0.2.0, 2014-08-29 +- bugfix: with vim 'session' strategy, if the session file does not exist - make + sure vim does not contain `-S` flag +- enable restoring programs with arguments (e.g. "rails console") and also + processes that contain program name +- improve `irb` restore strategy + +### v0.1.0, 2014-08-28 +- refactor checking if saved tmux session exists +- spinner while tmux sessions are restored + +### v0.0.5, 2014-08-28 +- restore pane processes +- user option for disabling pane process restoring +- enable whitelisting processes that will be restored +- expand readme with configuration options +- enable command strategies; enable restoring vim sessions +- update readme: explain restoring vim sessions + +### v0.0.4, 2014-08-26 +- restore pane layout for each window +- bugfix: correct pane ordering in a window + +### v0.0.3, 2014-08-26 +- save and restore current and alternate session +- fix a bug with non-existing window names +- restore active pane for each window that has multiple panes +- restore active and alternate window for each session + +### v0.0.2, 2014-08-26 +- saving a new session does not remove the previous one +- make the directory where sessions are stored configurable +- support only Tmux v1.9 or greater +- display a nice error message if saved session file does not exist +- added README + +### v0.0.1, 2014-08-26 +- started a project +- basic saving and restoring works diff --git a/.config/tmux/plugins/resurrect/CONTRIBUTING.md b/.config/tmux/plugins/resurrect/CONTRIBUTING.md new file mode 100644 index 0000000..444098c --- /dev/null +++ b/.config/tmux/plugins/resurrect/CONTRIBUTING.md @@ -0,0 +1,12 @@ +### Contributing + +Code contributions are welcome! + +### Reporting a bug + +If you find a bug please report it in the issues. When reporting a bug please +attach: +- a file symlinked to `~/.tmux/resurrect/last`. +- your `.tmux.conf` +- if you're getting an error paste it to a [gist](https://gist.github.com/) and + link it in the issue diff --git a/.config/tmux/plugins/resurrect/LICENSE.md b/.config/tmux/plugins/resurrect/LICENSE.md new file mode 100644 index 0000000..40f6ddd --- /dev/null +++ b/.config/tmux/plugins/resurrect/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (C) 2014 Bruno Sutic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/.config/tmux/plugins/resurrect/README.md b/.config/tmux/plugins/resurrect/README.md new file mode 100644 index 0000000..f137ad8 --- /dev/null +++ b/.config/tmux/plugins/resurrect/README.md @@ -0,0 +1,129 @@ +# Tmux Resurrect + +[![Build Status](https://travis-ci.org/tmux-plugins/tmux-resurrect.svg?branch=master)](https://travis-ci.org/tmux-plugins/tmux-resurrect) + +Restore `tmux` environment after system restart. + +Tmux is great, except when you have to restart the computer. You lose all the +running programs, working directories, pane layouts etc. +There are helpful management tools out there, but they require initial +configuration and continuous updates as your workflow evolves or you start new +projects. + +`tmux-resurrect` saves all the little details from your tmux environment so it +can be completely restored after a system restart (or when you feel like it). +No configuration is required. You should feel like you never quit tmux. + +It even (optionally) +[restores vim and neovim sessions](docs/restoring_vim_and_neovim_sessions.md)! + +Automatic restoring and continuous saving of tmux env is also possible with +[tmux-continuum](https://github.com/tmux-plugins/tmux-continuum) plugin. + +### Screencast + +[![screencast screenshot](/video/screencast_img.png)](https://vimeo.com/104763018) + +### Key bindings + +- `prefix + Ctrl-s` - save +- `prefix + Ctrl-r` - restore + +### About + +This plugin goes to great lengths to save and restore all the details from your +`tmux` environment. Here's what's been taken care of: + +- all sessions, windows, panes and their order +- current working directory for each pane +- **exact pane layouts** within windows (even when zoomed) +- active and alternative session +- active and alternative window for each session +- windows with focus +- active pane for each window +- "grouped sessions" (useful feature when using tmux with multiple monitors) +- programs running within a pane! More details in the + [restoring programs doc](docs/restoring_programs.md). + +Optional: + +- [restoring vim and neovim sessions](docs/restoring_vim_and_neovim_sessions.md) +- [restoring pane contents](docs/restoring_pane_contents.md) +- [restoring a previously saved environment](docs/restoring_previously_saved_environment.md) + +Requirements / dependencies: `tmux 1.9` or higher, `bash`. + +Tested and working on Linux, OSX and Cygwin. + +`tmux-resurrect` is idempotent! It will not try to restore panes or windows that +already exist.
+The single exception to this is when tmux is started with only 1 pane in order +to restore previous tmux env. Only in this case will this single pane be +overwritten. + +### Installation with [Tmux Plugin Manager](https://github.com/tmux-plugins/tpm) (recommended) + +Add plugin to the list of TPM plugins in `.tmux.conf`: + + set -g @plugin 'tmux-plugins/tmux-resurrect' + +Hit `prefix + I` to fetch the plugin and source it. You should now be able to +use the plugin. + +### Manual Installation + +Clone the repo: + + $ git clone https://github.com/tmux-plugins/tmux-resurrect ~/clone/path + +Add this line to the bottom of `.tmux.conf`: + + run-shell ~/clone/path/resurrect.tmux + +Reload TMUX environment with: `$ tmux source-file ~/.tmux.conf`. +You should now be able to use the plugin. + +### Docs + +- [Guide for migrating from tmuxinator](docs/migrating_from_tmuxinator.md) + +**Configuration** + +- [Changing the default key bindings](docs/custom_key_bindings.md). +- [Setting up hooks on save & restore](docs/hooks.md). +- Only a conservative list of programs is restored by default:
+ `vi vim nvim emacs man less more tail top htop irssi weechat mutt`.
+ [Restoring programs doc](docs/restoring_programs.md) explains how to restore + additional programs. +- [Change a directory](docs/save_dir.md) where `tmux-resurrect` saves tmux + environment. + +**Optional features** + +- [Restoring vim and neovim sessions](docs/restoring_vim_and_neovim_sessions.md) + is nice if you're a vim/neovim user. +- [Restoring pane contents](docs/restoring_pane_contents.md) feature. + +### Other goodies + +- [tmux-copycat](https://github.com/tmux-plugins/tmux-copycat) - a plugin for + regex searches in tmux and fast match selection +- [tmux-yank](https://github.com/tmux-plugins/tmux-yank) - enables copying + highlighted text to system clipboard +- [tmux-open](https://github.com/tmux-plugins/tmux-open) - a plugin for quickly + opening highlighted file or a url +- [tmux-continuum](https://github.com/tmux-plugins/tmux-continuum) - automatic + restoring and continuous saving of tmux env + +### Reporting bugs and contributing + +Both contributing and bug reports are welcome. Please check out +[contributing guidelines](CONTRIBUTING.md). + +### Credits + +[Mislav Marohnić](https://github.com/mislav) - the idea for the plugin came from his +[tmux-session script](https://github.com/mislav/dotfiles/blob/2036b5e03fb430bbcbc340689d63328abaa28876/bin/tmux-session). + +### License +[MIT](LICENSE.md) diff --git a/.config/tmux/plugins/resurrect/docs/custom_key_bindings.md b/.config/tmux/plugins/resurrect/docs/custom_key_bindings.md new file mode 100644 index 0000000..99bfc2c --- /dev/null +++ b/.config/tmux/plugins/resurrect/docs/custom_key_bindings.md @@ -0,0 +1,11 @@ +# Custom key bindings + +The default key bindings are: + +- `prefix + Ctrl-s` - save +- `prefix + Ctrl-r` - restore + +To change these, add to `.tmux.conf`: + + set -g @resurrect-save 'S' + set -g @resurrect-restore 'R' diff --git a/.config/tmux/plugins/resurrect/docs/hooks.md b/.config/tmux/plugins/resurrect/docs/hooks.md new file mode 100644 index 0000000..b373e50 --- /dev/null +++ b/.config/tmux/plugins/resurrect/docs/hooks.md @@ -0,0 +1,33 @@ +# Save & Restore Hooks + +Hooks allow to set custom commands that will be executed during session save +and restore. Most hooks are called with zero arguments, unless explicitly +stated otherwise. + +Currently the following hooks are supported: + +- `@resurrect-hook-post-save-layout` + + Called after all sessions, panes and windows have been saved. + + Passed single argument of the state file. + +- `@resurrect-hook-post-save-all` + + Called at end of save process right before the spinner is turned off. + +- `@resurrect-hook-pre-restore-all` + + Called before any tmux state is altered. + +- `@resurrect-hook-pre-restore-pane-processes` + + Called before running processes are restored. + +### Examples + +Here is an example how to save and restore window geometry for most terminals in X11. +Add this to `.tmux.conf`: + + set -g @resurrect-hook-post-save-all 'eval $(xdotool getwindowgeometry --shell $WINDOWID); echo 0,$X,$Y,$WIDTH,$HEIGHT > $HOME/.tmux/resurrect/geometry' + set -g @resurrect-hook-pre-restore-all 'wmctrl -i -r $WINDOWID -e $(cat $HOME/.tmux/resurrect/geometry)' diff --git a/.config/tmux/plugins/resurrect/docs/migrating_from_tmuxinator.md b/.config/tmux/plugins/resurrect/docs/migrating_from_tmuxinator.md new file mode 100644 index 0000000..f59f90f --- /dev/null +++ b/.config/tmux/plugins/resurrect/docs/migrating_from_tmuxinator.md @@ -0,0 +1,72 @@ +# Migrating from `tmuxinator` + +### Why migrate to `tmux-resurrect`? + +Here are some reasons why you'd want to migrate from `tmuxinator` to +`tmux-resurrect`: + +- **Less dependencies**
+ `tmuxinator` depends on `ruby` which can be a hassle to set up if you're not a + rubyist.
+ `tmux-resurrect` depends just on `bash` which is virtually + omnipresent. + +- **Simplicity**
+ `tmuxinator` has an executable, CLI interface with half dozen commands and + command completion.
+ `tmux-resurrect` defines just 2 tmux key bindings. + +- **No configuration**
+ `tmuxinator` is all about config files (and their constant updating).
+ `tmux-resurrect` requires no configuration to work. + +- **Better change handling**
+ When you make a change to any aspect of tmux layout, you also have to + update related `tmuxinator` project file (and test to make sure change is + ok).
+ With `tmux-resurrect` there's nothing to do: your change will be + remembered on the next save. + +### How to migrate? + +1. Install `tmux-resurrect`. +2. Open \*all* existing `tmuxinator` projects.
+ Verify all projects are open by pressing `prefix + s` and checking they are + all on the list. +3. Perform a `tmux-resurrect` save. + +That's it! You can continue using just `tmux-resurrect` should you choose so. + +Note: it probably makes no sense to use both tools at the same time as they do +the same thing (creating tmux environment for you to work in). +Technically however, there should be no issues. + +### Usage differences + +`tmuxinator` focuses on managing individual tmux sessions (projects). +`tmux-resurrect` keeps track of the \*whole* tmux environment: all sessions are +saved and restored together. + +A couple tips if you decide to switch to `tmux-resurrect`: + +- Keep all tmux sessions (projects) running all the time.
+ If you want to work on an existing project, you should be able to just + \*switch* to an already open session using `prefix + s`.
+ This is different from `tmuxinator` where you'd usually run `mux new [project]` + in order to start working on something. + +- No need to kill sessions with `tmux kill-session` (unless you really don't + want to work on it ever).
+ It's the recurring theme by now: just keep all the sessions running all the + time. This is convenient and also cheap in terms of resources. + +- The only 2 situations when you need `tmux-resurrect`:
+ 1) Save tmux environment just before restarting/shutting down your + computer.
+ 2) Restore tmux env after you turn the computer on. + +### Other questions? + +Still have questions? Feel free to open an +[issue](ihttps://github.com/tmux-plugins/tmux-resurrect/issues). We'll try to +answer it and also update this doc. diff --git a/.config/tmux/plugins/resurrect/docs/restoring_bash_history.md b/.config/tmux/plugins/resurrect/docs/restoring_bash_history.md new file mode 100644 index 0000000..2b6af17 --- /dev/null +++ b/.config/tmux/plugins/resurrect/docs/restoring_bash_history.md @@ -0,0 +1,39 @@ +tmux-ressurect no longer restores shell history for each pane, as of [this PR](https://github.com/tmux-plugins/tmux-resurrect/pull/308). + +As a workaround, you can use the `HISTFILE` environment variable to preserve history for each pane separately, and modify +`PROMPT_COMMAND` to make sure history gets saved with each new command. + +Unfortunately, we haven't found a perfect way of getting a unique identifier for each pane, as the `TMUX_PANE` variable +seems to occasionally change when resurrecting. As a workaround, the example below sets a unique ID in each pane's `title`. +The downside of this implementation is that pane titles must all be unique across sessions/windows, and also must use the `pane_id_prefix`. + +Any improvements/suggestions for getting a unique, persistent ID for each pane are welcome! + +```bash +pane_id_prefix="resurrect_" + +# Create history directory if it doesn't exist +HISTS_DIR=$HOME/.bash_history.d +mkdir -p "${HISTS_DIR}" + +if [ -n "${TMUX_PANE}" ]; then + + # Check if we've already set this pane title + pane_id=$(tmux display -pt "${TMUX_PANE:?}" "#{pane_title}") + if [[ $pane_id != "$pane_id_prefix"* ]]; then + + # if not, set it to a random ID + random_id=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 16) + printf "\033]2;$pane_id_prefix$random_id\033\\" + pane_id=$(tmux display -pt "${TMUX_PANE:?}" "#{pane_title}") + fi + + # use the pane's random ID for the HISTFILE + export HISTFILE="${HISTS_DIR}/bash_history_tmux_${pane_id}" +else + export HISTFILE="${HISTS_DIR}/bash_history_no_tmux" +fi + +# Stash the new history each time a command runs. +export PROMPT_COMMAND="$PROMPT_COMMAND;history -a" +``` diff --git a/.config/tmux/plugins/resurrect/docs/restoring_pane_contents.md b/.config/tmux/plugins/resurrect/docs/restoring_pane_contents.md new file mode 100644 index 0000000..2dff59a --- /dev/null +++ b/.config/tmux/plugins/resurrect/docs/restoring_pane_contents.md @@ -0,0 +1,31 @@ +# Restoring pane contents + +This plugin enables saving and restoring tmux pane contents. + +This feature can be enabled by adding this line to `.tmux.conf`: + + set -g @resurrect-capture-pane-contents 'on' + +##### Known issue + +When using this feature, please check the value of `default-command` +tmux option. That can be done with `$ tmux show -g default-command`. + +The value should NOT contain `&&` or `||` operators. If it does, simplify the +option so those operators are removed. + +Example: + +- this will cause issues (notice the `&&` and `||` operators): + + set -g default-command "which reattach-to-user-namespace > /dev/null && reattach-to-user-namespace -l $SHELL || $SHELL -l" + +- this is ok: + + set -g default-command "reattach-to-user-namespace -l $SHELL" + +Related [bug](https://github.com/tmux-plugins/tmux-resurrect/issues/98). + +Alternatively, you can let +[tmux-sensible](https://github.com/tmux-plugins/tmux-sensible) +handle this option in a cross-platform way and you'll have no problems. diff --git a/.config/tmux/plugins/resurrect/docs/restoring_previously_saved_environment.md b/.config/tmux/plugins/resurrect/docs/restoring_previously_saved_environment.md new file mode 100644 index 0000000..8e845ac --- /dev/null +++ b/.config/tmux/plugins/resurrect/docs/restoring_previously_saved_environment.md @@ -0,0 +1,14 @@ +# Restoring previously saved environment + +None of the previous saves are deleted (unless you explicitly do that). All save +files are kept in `~/.tmux/resurrect/` directory, or `~/.local/share/tmux/resurrect` +(unless `${XDG_DATA_HOME}` says otherwise).
+Here are the steps to restore to a previous point in time: + +- make sure you start this with a "fresh" tmux instance +- `$ cd ~/.tmux/resurrect/` +- locate the save file you'd like to use for restore (file names have a timestamp) +- symlink the `last` file to the desired save file: `$ ln -sf last` +- do a restore with `tmux-resurrect` key: `prefix + Ctrl-r` + +You should now be restored to the time when `` save happened. diff --git a/.config/tmux/plugins/resurrect/docs/restoring_programs.md b/.config/tmux/plugins/resurrect/docs/restoring_programs.md new file mode 100644 index 0000000..6d316d6 --- /dev/null +++ b/.config/tmux/plugins/resurrect/docs/restoring_programs.md @@ -0,0 +1,205 @@ +# Restoring programs + - [General instructions](#general-instructions) + - [Clarifications](#clarifications) + - [Working with NodeJS](#nodejs) + - [Restoring Mosh](#mosh) + +### General instructions +Only a conservative list of programs is restored by default:
+`vi vim nvim emacs man less more tail top htop irssi weechat mutt`. + +This can be configured with `@resurrect-processes` option in `.tmux.conf`. It +contains space-separated list of additional programs to restore. + +- Example restoring additional programs: + + set -g @resurrect-processes 'ssh psql mysql sqlite3' + +- Programs with arguments should be double quoted: + + set -g @resurrect-processes 'some_program "git log"' + +- Start with tilde to restore a program whose process contains target name: + + set -g @resurrect-processes 'irb pry "~rails server" "~rails console"' + +- Use `->` to specify a command to be used when restoring a program (useful if + the default restore command fails ): + + set -g @resurrect-processes 'some_program "grunt->grunt development"' + +- Use `*` to expand the arguments from the saved command when restoring: + + set -g @resurrect-processes 'some_program "~rails server->rails server *"' + +- Don't restore any programs: + + set -g @resurrect-processes 'false' + +- Restore **all** programs (dangerous!): + + set -g @resurrect-processes ':all:' + + Be *very careful* with this: tmux-resurrect can not know which programs take + which context, and a `sudo mkfs.vfat /dev/sdb` that was just formatting an + external USB stick could wipe your backup hard disk if that's what's attached + after rebooting. + + This option is primarily useful for experimentation (e.g., to find out which + program is recognized in a pane). + +### Clarifications + +> I don't understand tilde `~`, what is it and why is it used when restoring + programs? + +Let's say you use `rails server` command often. You want `tmux-resurrect` to +save and restore it automatically. You might try adding `rails server` to the +list of programs that will be restored: + + set -g @resurrect-processes '"rails server"' # will NOT work + +Upon save, `rails server` command will actually be saved as this command: +`/Users/user/.rbenv/versions/2.0.0-p481/bin/ruby script/rails server` +(if you wanna see how is any command saved, check it yourself in +`~/.tmux/resurrect/last` file). + +When programs are restored, the `rails server` command will NOT be restored +because it does not **strictly** match the long +`/Users/user/.rbenv/versions/2.0.0-p481/bin/ruby script/rails server` string. + +The tilde `~` at the start of the string relaxes process name matching. + + set -g @resurrect-processes '"~rails server"' # OK + +The above option says: "restore full process if `rails server` string is found +ANYWHERE in the process name". + +If you check long process string, there is in fact a `rails server` string at +the end, so now the process will be successfully restored. + +> What is arrow `->` and why is is used? + +(Please read the above clarification about tilde `~`). + +Continuing with our `rails server` example, when the process is finally restored +correctly it might not look pretty as you'll see the whole +`/Users/user/.rbenv/versions/2.0.0-p481/bin/ruby script/rails server` string in +the command line. + +Naturally, you'd rather want to see just `rails server` (what you initially +typed), but that information is now unfortunately lost. + +To aid this, you can use arrow `->`: (**note**: there is no space before and after `->`) + + set -g @resurrect-processes '"~rails server->rails server"' # OK + +This option says: "when this process is restored use `rails server` as the +command name". + +Full (long) process name is now ignored and you'll see just `rails server` in +the command line when the program is restored. + +> What is asterisk `*` and why is it used? + +(Please read the above clarifications about tilde `~` and arrow `->`). + +Continuing with the `rails server` example, you might have added flags for e.g. +verbose logging, but with the above configuration, the flags would be lost. + +To preserve the command arguments when restoring, use the asterisk `*`: (**note**: there **must** be a space before `*`) + + set -g @resurrect-processes '"~rails server->rails server *"' + +This option says: "when this process is restored use `rails server` as the +command name, but preserve its arguments". + +> Now I understand the tilde and the arrow, but things still don't work for me + +Here's the general workflow for figuring this out: + +- Set up your whole tmux environment manually.
+ In our example case, we'd type `rails server` in a pane where we want it to + run. +- Save tmux env (it will get saved to `~/.tmux/resurrect/last`). +- Open `~/.tmux/resurrect/last` file and try to find full process string for + your program.
+ Unfortunately this is a little vague but it should be easy. A smart + thing to do for our example is to search for string `rails` in the `last` + file. +- Now that you know the full and the desired process string use tilde `~` and + arrow `->` in `.tmux.conf` to make things work. + +### Working with NodeJS +If you are working with NodeJS, you may get some troubles with configuring restoring programs. + +Particularly, some programs like `gulp`, `grunt` or `npm` are not saved with parameters so tmux-resurrect cannot restore it. This is actually **not tmux-resurrect's issue** but more likely, those programs' issues. For example if you run `gulp watch` or `npm start` and then try to look at `ps` or `pgrep`, you will only see `gulp` or `npm`. + +To deal with these issues, one solution is to use [yarn](https://yarnpkg.com/en/docs/install) which a package manager for NodeJS and an alternative for `npm`. It's nearly identical to `npm` and very easy to use. Therefore you don't have to do any migration, you can simply use it immediately. For example: +- `npm test` is equivalent to `yarn test`, +- `npm run watch:dev` is equivalent to `yarn watch:dev` +- more interestingly, `gulp watch:dev` is equivalent to `yarn gulp watch:dev` + +Before continuing, please ensure that you understand the [clarifications](#clarifications) section about `~` and `->` + +#### yarn +It's fairly straight forward if you have been using `yarn` already. + + set -g @resurrect-processes '"~yarn watch"' + set -g @resurrect-processes '"~yarn watch->yarn watch"' + + +#### npm +Instead of + + set -g @resurrect-processes '"~npm run watch"' # will NOT work + +we use + + set -g @resurrect-processes '"~yarn watch"' # OK + + +#### gulp +Instead of + + set -g @resurrect-processes '"~gulp test"' # will NOT work + +we use + + set -g @resurrect-processes '"~yarn gulp test"' # OK + + +#### nvm +If you use `nvm` in your project, here is how you could config tmux-resurrect: + + set -g @resurrect-processes '"~yarn gulp test->nvm use && gulp test"' + +#### Another problem +Let take a look at this example + + set -g @resurrect-processes '\ + "~yarn gulp test->gulp test" \ + "~yarn gulp test-it->gulp test-it" \ + ' +**This will not work properly**, only `gulp test` is run, although you can see the command `node /path/to/yarn gulp test-it` is added correctly in `.tmux/resurrect/last` file. + +The reason is when restoring program, the **command part after the dash `-` is ignored** so instead of command `gulp test-it`, the command `gulp test` which will be run. + +A work around, for this problem until it's fixed, is: +- the config should be like this: + + set -g @resurrect-processes '\ + "~yarn gulp test->gulp test" \ + "~yarn gulp \"test-it\"->gulp test-it" \ + +- and in `.tmux/resurrect/last`, we should add quote to `test-it` word + + ... node:node /path/to/yarn gulp "test-it" + + +### Restoring Mosh +Mosh spawns a `mosh-client` process, so we need to specify that as the process to be resurrected. + + set -g @resurrect-processes 'mosh-client' + +Additionally a mosh-client strategy is provided to handle extracting the original arguments and re-run Mosh. diff --git a/.config/tmux/plugins/resurrect/docs/restoring_vim_and_neovim_sessions.md b/.config/tmux/plugins/resurrect/docs/restoring_vim_and_neovim_sessions.md new file mode 100644 index 0000000..f84442b --- /dev/null +++ b/.config/tmux/plugins/resurrect/docs/restoring_vim_and_neovim_sessions.md @@ -0,0 +1,19 @@ +# Restoring vim and neovim sessions + +- save vim/neovim sessions. I recommend + [tpope/vim-obsession](https://github.com/tpope/vim-obsession) (as almost every + plugin, it works for both vim and neovim). +- in `.tmux.conf`: + + # for vim + set -g @resurrect-strategy-vim 'session' + # for neovim + set -g @resurrect-strategy-nvim 'session' + +`tmux-resurrect` will now restore vim and neovim sessions if `Session.vim` file +is present. + +> If you're using the vim binary provided by MacVim.app then you'll need to set `@resurrect-processes`, for example: +> ``` +> set -g @resurrect-processes '~Vim -> vim' +> ``` diff --git a/.config/tmux/plugins/resurrect/docs/save_dir.md b/.config/tmux/plugins/resurrect/docs/save_dir.md new file mode 100644 index 0000000..bf724c6 --- /dev/null +++ b/.config/tmux/plugins/resurrect/docs/save_dir.md @@ -0,0 +1,15 @@ +# Resurrect save dir + +By default Tmux environment is saved to a file in `~/.tmux/resurrect` dir. +Change this with: + + set -g @resurrect-dir '/some/path' + +Using environment variables or shell interpolation in this option is not +allowed as the string is used literally. So the following won't do what is +expected: + + set -g @resurrect-dir '/path/$MY_VAR/$(some_executable)' + +Only the following variables and special chars are allowed: +`$HOME`, `$HOSTNAME`, and `~`. diff --git a/.config/tmux/plugins/resurrect/resurrect.tmux b/.config/tmux/plugins/resurrect/resurrect.tmux new file mode 100755 index 0000000..21fed7e --- /dev/null +++ b/.config/tmux/plugins/resurrect/resurrect.tmux @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$CURRENT_DIR/scripts/variables.sh" +source "$CURRENT_DIR/scripts/helpers.sh" + +set_save_bindings() { + local key_bindings=$(get_tmux_option "$save_option" "$default_save_key") + local key + for key in $key_bindings; do + tmux bind-key "$key" run-shell "$CURRENT_DIR/scripts/save.sh" + done +} + +set_restore_bindings() { + local key_bindings=$(get_tmux_option "$restore_option" "$default_restore_key") + local key + for key in $key_bindings; do + tmux bind-key "$key" run-shell "$CURRENT_DIR/scripts/restore.sh" + done +} + +set_default_strategies() { + tmux set-option -gq "${restore_process_strategy_option}irb" "default_strategy" + tmux set-option -gq "${restore_process_strategy_option}mosh-client" "default_strategy" +} + +set_script_path_options() { + tmux set-option -gq "$save_path_option" "$CURRENT_DIR/scripts/save.sh" + tmux set-option -gq "$restore_path_option" "$CURRENT_DIR/scripts/restore.sh" +} + +main() { + set_save_bindings + set_restore_bindings + set_default_strategies + set_script_path_options +} +main diff --git a/.config/tmux/plugins/resurrect/save_command_strategies/gdb.sh b/.config/tmux/plugins/resurrect/save_command_strategies/gdb.sh new file mode 100755 index 0000000..2f0ab56 --- /dev/null +++ b/.config/tmux/plugins/resurrect/save_command_strategies/gdb.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +PANE_PID="$1" + +exit_safely_if_empty_ppid() { + if [ -z "$PANE_PID" ]; then + exit 0 + fi +} + +full_command() { + gdb -batch --eval "attach $PANE_PID" --eval "call write_history(\"/tmp/bash_history-${PANE_PID}.txt\")" --eval 'detach' --eval 'q' >/dev/null 2>&1 + \tail -1 "/tmp/bash_history-${PANE_PID}.txt" +} + +main() { + exit_safely_if_empty_ppid + full_command +} +main diff --git a/.config/tmux/plugins/resurrect/save_command_strategies/linux_procfs.sh b/.config/tmux/plugins/resurrect/save_command_strategies/linux_procfs.sh new file mode 100755 index 0000000..6b64f7e --- /dev/null +++ b/.config/tmux/plugins/resurrect/save_command_strategies/linux_procfs.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +PANE_PID="$1" +COMMAND_PID=$(pgrep -P $PANE_PID) + +exit_safely_if_empty_ppid() { + if [ -z "$PANE_PID" ]; then + exit 0 + fi +} + +full_command() { + [[ -z "$COMMAND_PID" ]] && exit 0 + # See: https://unix.stackexchange.com/a/567021 + # Avoid complications with system printf by using bash subshell interpolation. + # This will properly escape sequences and null in cmdline. + cat /proc/${COMMAND_PID}/cmdline | xargs -0 bash -c 'printf "%q " "$0" "$@"' +} + +main() { + exit_safely_if_empty_ppid + full_command +} +main diff --git a/.config/tmux/plugins/resurrect/save_command_strategies/pgrep.sh b/.config/tmux/plugins/resurrect/save_command_strategies/pgrep.sh new file mode 100755 index 0000000..15d98b3 --- /dev/null +++ b/.config/tmux/plugins/resurrect/save_command_strategies/pgrep.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +PANE_PID="$1" + +exit_safely_if_empty_ppid() { + if [ -z "$PANE_PID" ]; then + exit 0 + fi +} + +full_command() { + \pgrep -lf -P "$PANE_PID" | + cut -d' ' -f2- +} + +main() { + exit_safely_if_empty_ppid + full_command +} +main diff --git a/.config/tmux/plugins/resurrect/save_command_strategies/ps.sh b/.config/tmux/plugins/resurrect/save_command_strategies/ps.sh new file mode 100755 index 0000000..15bb5aa --- /dev/null +++ b/.config/tmux/plugins/resurrect/save_command_strategies/ps.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +PANE_PID="$1" + +exit_safely_if_empty_ppid() { + if [ -z "$PANE_PID" ]; then + exit 0 + fi +} + +full_command() { + ps -ao "ppid,args" | + sed "s/^ *//" | + grep "^${PANE_PID}" | + cut -d' ' -f2- +} + +main() { + exit_safely_if_empty_ppid + full_command +} +main diff --git a/.config/tmux/plugins/resurrect/scripts/check_tmux_version.sh b/.config/tmux/plugins/resurrect/scripts/check_tmux_version.sh new file mode 100755 index 0000000..b0aedec --- /dev/null +++ b/.config/tmux/plugins/resurrect/scripts/check_tmux_version.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +VERSION="$1" +UNSUPPORTED_MSG="$2" + +get_tmux_option() { + local option=$1 + local default_value=$2 + local option_value=$(tmux show-option -gqv "$option") + if [ -z "$option_value" ]; then + echo "$default_value" + else + echo "$option_value" + fi +} + +# Ensures a message is displayed for 5 seconds in tmux prompt. +# Does not override the 'display-time' tmux option. +display_message() { + local message="$1" + + # display_duration defaults to 5 seconds, if not passed as an argument + if [ "$#" -eq 2 ]; then + local display_duration="$2" + else + local display_duration="5000" + fi + + # saves user-set 'display-time' option + local saved_display_time=$(get_tmux_option "display-time" "750") + + # sets message display time to 5 seconds + tmux set-option -gq display-time "$display_duration" + + # displays message + tmux display-message "$message" + + # restores original 'display-time' value + tmux set-option -gq display-time "$saved_display_time" +} + +# this is used to get "clean" integer version number. Examples: +# `tmux 1.9` => `19` +# `1.9a` => `19` +get_digits_from_string() { + local string="$1" + local only_digits="$(echo "$string" | tr -dC '[:digit:]')" + echo "$only_digits" +} + +tmux_version_int() { + local tmux_version_string=$(tmux -V) + echo "$(get_digits_from_string "$tmux_version_string")" +} + +unsupported_version_message() { + if [ -n "$UNSUPPORTED_MSG" ]; then + echo "$UNSUPPORTED_MSG" + else + echo "Error, Tmux version unsupported! Please install Tmux version $VERSION or greater!" + fi +} + +exit_if_unsupported_version() { + local current_version="$1" + local supported_version="$2" + if [ "$current_version" -lt "$supported_version" ]; then + display_message "$(unsupported_version_message)" + exit 1 + fi +} + +main() { + local supported_version_int="$(get_digits_from_string "$VERSION")" + local current_version_int="$(tmux_version_int)" + exit_if_unsupported_version "$current_version_int" "$supported_version_int" +} +main diff --git a/.config/tmux/plugins/resurrect/scripts/helpers.sh b/.config/tmux/plugins/resurrect/scripts/helpers.sh new file mode 100644 index 0000000..20d87dc --- /dev/null +++ b/.config/tmux/plugins/resurrect/scripts/helpers.sh @@ -0,0 +1,159 @@ +if [ -d "$HOME/.tmux/resurrect" ]; then + default_resurrect_dir="$HOME/.tmux/resurrect" +else + default_resurrect_dir="${XDG_DATA_HOME:-$HOME/.local/share}"/tmux/resurrect +fi +resurrect_dir_option="@resurrect-dir" + +SUPPORTED_VERSION="1.9" +RESURRECT_FILE_PREFIX="tmux_resurrect" +RESURRECT_FILE_EXTENSION="txt" +_RESURRECT_DIR="" +_RESURRECT_FILE_PATH="" + +d=$'\t' + +# helper functions +get_tmux_option() { + local option="$1" + local default_value="$2" + local option_value=$(tmux show-option -gqv "$option") + if [ -z "$option_value" ]; then + echo "$default_value" + else + echo "$option_value" + fi +} + +# Ensures a message is displayed for 5 seconds in tmux prompt. +# Does not override the 'display-time' tmux option. +display_message() { + local message="$1" + + # display_duration defaults to 5 seconds, if not passed as an argument + if [ "$#" -eq 2 ]; then + local display_duration="$2" + else + local display_duration="5000" + fi + + # saves user-set 'display-time' option + local saved_display_time=$(get_tmux_option "display-time" "750") + + # sets message display time to 5 seconds + tmux set-option -gq display-time "$display_duration" + + # displays message + tmux display-message "$message" + + # restores original 'display-time' value + tmux set-option -gq display-time "$saved_display_time" +} + + +supported_tmux_version_ok() { + $CURRENT_DIR/check_tmux_version.sh "$SUPPORTED_VERSION" +} + +remove_first_char() { + echo "$1" | cut -c2- +} + +capture_pane_contents_option_on() { + local option="$(get_tmux_option "$pane_contents_option" "off")" + [ "$option" == "on" ] +} + +files_differ() { + ! cmp -s "$1" "$2" +} + +get_grouped_sessions() { + local grouped_sessions_dump="$1" + export GROUPED_SESSIONS="${d}$(echo "$grouped_sessions_dump" | cut -f2 -d"$d" | tr "\\n" "$d")" +} + +is_session_grouped() { + local session_name="$1" + [[ "$GROUPED_SESSIONS" == *"${d}${session_name}${d}"* ]] +} + +# pane content file helpers + +pane_contents_create_archive() { + tar cf - -C "$(resurrect_dir)/save/" ./pane_contents/ | + gzip > "$(pane_contents_archive_file)" +} + +pane_content_files_restore_from_archive() { + local archive_file="$(pane_contents_archive_file)" + if [ -f "$archive_file" ]; then + mkdir -p "$(pane_contents_dir "restore")" + gzip -d < "$archive_file" | + tar xf - -C "$(resurrect_dir)/restore/" + fi +} + +# path helpers + +resurrect_dir() { + if [ -z "$_RESURRECT_DIR" ]; then + local path="$(get_tmux_option "$resurrect_dir_option" "$default_resurrect_dir")" + # expands tilde, $HOME and $HOSTNAME if used in @resurrect-dir + echo "$path" | sed "s,\$HOME,$HOME,g; s,\$HOSTNAME,$(hostname),g; s,\~,$HOME,g" + else + echo "$_RESURRECT_DIR" + fi +} +_RESURRECT_DIR="$(resurrect_dir)" + +resurrect_file_path() { + if [ -z "$_RESURRECT_FILE_PATH" ]; then + local timestamp="$(date +"%Y%m%dT%H%M%S")" + echo "$(resurrect_dir)/${RESURRECT_FILE_PREFIX}_${timestamp}.${RESURRECT_FILE_EXTENSION}" + else + echo "$_RESURRECT_FILE_PATH" + fi +} +_RESURRECT_FILE_PATH="$(resurrect_file_path)" + +last_resurrect_file() { + echo "$(resurrect_dir)/last" +} + +pane_contents_dir() { + echo "$(resurrect_dir)/$1/pane_contents/" +} + +pane_contents_file() { + local save_or_restore="$1" + local pane_id="$2" + echo "$(pane_contents_dir "$save_or_restore")/pane-${pane_id}" +} + +pane_contents_file_exists() { + local pane_id="$1" + [ -f "$(pane_contents_file "restore" "$pane_id")" ] +} + +pane_contents_archive_file() { + echo "$(resurrect_dir)/pane_contents.tar.gz" +} + +execute_hook() { + local kind="$1" + shift + local args="" hook="" + + hook=$(get_tmux_option "$hook_prefix$kind" "") + + # If there are any args, pass them to the hook (in a way that preserves/copes + # with spaces and unusual characters. + if [ "$#" -gt 0 ]; then + printf -v args "%q " "$@" + fi + + if [ -n "$hook" ]; then + eval "$hook $args" + fi +} diff --git a/.config/tmux/plugins/resurrect/scripts/process_restore_helpers.sh b/.config/tmux/plugins/resurrect/scripts/process_restore_helpers.sh new file mode 100644 index 0000000..546dfe1 --- /dev/null +++ b/.config/tmux/plugins/resurrect/scripts/process_restore_helpers.sh @@ -0,0 +1,198 @@ +restore_pane_processes_enabled() { + local restore_processes="$(get_tmux_option "$restore_processes_option" "$restore_processes")" + if [ "$restore_processes" == "false" ]; then + return 1 + else + return 0 + fi +} + +restore_pane_process() { + local pane_full_command="$1" + local session_name="$2" + local window_number="$3" + local pane_index="$4" + local dir="$5" + local command + if _process_should_be_restored "$pane_full_command" "$session_name" "$window_number" "$pane_index"; then + tmux switch-client -t "${session_name}:${window_number}" + tmux select-pane -t "$pane_index" + + local inline_strategy="$(_get_inline_strategy "$pane_full_command")" # might not be defined + if [ -n "$inline_strategy" ]; then + # inline strategy exists + # check for additional "expansion" of inline strategy, e.g. `vim` to `vim -S` + if _strategy_exists "$inline_strategy"; then + local strategy_file="$(_get_strategy_file "$inline_strategy")" + local inline_strategy="$($strategy_file "$pane_full_command" "$dir")" + fi + command="$inline_strategy" + elif _strategy_exists "$pane_full_command"; then + local strategy_file="$(_get_strategy_file "$pane_full_command")" + local strategy_command="$($strategy_file "$pane_full_command" "$dir")" + command="$strategy_command" + else + # just invoke the raw command + command="$pane_full_command" + fi + tmux send-keys -t "${session_name}:${window_number}.${pane_index}" "$command" "C-m" + fi +} + +# private functions below + +_process_should_be_restored() { + local pane_full_command="$1" + local session_name="$2" + local window_number="$3" + local pane_index="$4" + if is_pane_registered_as_existing "$session_name" "$window_number" "$pane_index"; then + # Scenario where pane existed before restoration, so we're not + # restoring the proces either. + return 1 + elif ! pane_exists "$session_name" "$window_number" "$pane_index"; then + # pane number limit exceeded, pane does not exist + return 1 + elif _restore_all_processes; then + return 0 + elif _process_on_the_restore_list "$pane_full_command"; then + return 0 + else + return 1 + fi +} + +_restore_all_processes() { + local restore_processes="$(get_tmux_option "$restore_processes_option" "$restore_processes")" + if [ "$restore_processes" == ":all:" ]; then + return 0 + else + return 1 + fi +} + +_process_on_the_restore_list() { + local pane_full_command="$1" + # TODO: make this work without eval + eval set $(_restore_list) + local proc + local match + for proc in "$@"; do + match="$(_get_proc_match_element "$proc")" + if _proc_matches_full_command "$pane_full_command" "$match"; then + return 0 + fi + done + return 1 +} + +_proc_matches_full_command() { + local pane_full_command="$1" + local match="$2" + if _proc_starts_with_tildae "$match"; then + match="$(remove_first_char "$match")" + # regex matching the command makes sure `$match` string is somewhere in the command string + if [[ "$pane_full_command" =~ ($match) ]]; then + return 0 + fi + else + # regex matching the command makes sure process is a "word" + if [[ "$pane_full_command" =~ (^${match} ) ]] || [[ "$pane_full_command" =~ (^${match}$) ]]; then + return 0 + fi + fi + return 1 +} + +_get_proc_match_element() { + echo "$1" | sed "s/${inline_strategy_token}.*//" +} + +_get_proc_restore_element() { + echo "$1" | sed "s/.*${inline_strategy_token}//" +} + +# given full command: 'ruby /Users/john/bin/my_program arg1 arg2' +# and inline strategy: '~bin/my_program->my_program *' +# returns: 'arg1 arg2' +_get_command_arguments() { + local pane_full_command="$1" + local match="$2" + if _proc_starts_with_tildae "$match"; then + match="$(remove_first_char "$match")" + fi + echo "$pane_full_command" | sed "s,^.*${match}[^ ]* *,," +} + +_get_proc_restore_command() { + local pane_full_command="$1" + local proc="$2" + local match="$3" + local restore_element="$(_get_proc_restore_element "$proc")" + if [[ "$restore_element" =~ " ${inline_strategy_arguments_token}" ]]; then + # replaces "%" with command arguments + local command_arguments="$(_get_command_arguments "$pane_full_command" "$match")" + echo "$restore_element" | sed "s,${inline_strategy_arguments_token},${command_arguments}," + else + echo "$restore_element" + fi +} + +_restore_list() { + local user_processes="$(get_tmux_option "$restore_processes_option" "$restore_processes")" + local default_processes="$(get_tmux_option "$default_proc_list_option" "$default_proc_list")" + if [ -z "$user_processes" ]; then + # user didn't define any processes + echo "$default_processes" + else + echo "$default_processes $user_processes" + fi +} + +_proc_starts_with_tildae() { + [[ "$1" =~ (^~) ]] +} + +_get_inline_strategy() { + local pane_full_command="$1" + # TODO: make this work without eval + eval set $(_restore_list) + local proc + local match + for proc in "$@"; do + if [[ "$proc" =~ "$inline_strategy_token" ]]; then + match="$(_get_proc_match_element "$proc")" + if _proc_matches_full_command "$pane_full_command" "$match"; then + echo "$(_get_proc_restore_command "$pane_full_command" "$proc" "$match")" + fi + fi + done +} + +_strategy_exists() { + local pane_full_command="$1" + local strategy="$(_get_command_strategy "$pane_full_command")" + if [ -n "$strategy" ]; then # strategy set? + local strategy_file="$(_get_strategy_file "$pane_full_command")" + [ -e "$strategy_file" ] # strategy file exists? + else + return 1 + fi +} + +_get_command_strategy() { + local pane_full_command="$1" + local command="$(_just_command "$pane_full_command")" + get_tmux_option "${restore_process_strategy_option}${command}" "" +} + +_just_command() { + echo "$1" | cut -d' ' -f1 +} + +_get_strategy_file() { + local pane_full_command="$1" + local strategy="$(_get_command_strategy "$pane_full_command")" + local command="$(_just_command "$pane_full_command")" + echo "$CURRENT_DIR/../strategies/${command}_${strategy}.sh" +} diff --git a/.config/tmux/plugins/resurrect/scripts/restore.exp b/.config/tmux/plugins/resurrect/scripts/restore.exp new file mode 100755 index 0000000..8664b1d --- /dev/null +++ b/.config/tmux/plugins/resurrect/scripts/restore.exp @@ -0,0 +1,14 @@ +#!/usr/bin/env expect + +# start tmux +spawn tmux -S/tmp/foo + +# delay with sleep to compensate for tmux starting time +sleep 2 + +# run restore script directly +send "~/.tmux/plugins/tmux-resurrect/scripts/restore.sh\r" + +# long wait until tmux restore is complete +# (things get messed up if expect client isn't attached) +sleep 100 diff --git a/.config/tmux/plugins/resurrect/scripts/restore.sh b/.config/tmux/plugins/resurrect/scripts/restore.sh new file mode 100755 index 0000000..1a5e3f9 --- /dev/null +++ b/.config/tmux/plugins/resurrect/scripts/restore.sh @@ -0,0 +1,387 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$CURRENT_DIR/variables.sh" +source "$CURRENT_DIR/helpers.sh" +source "$CURRENT_DIR/process_restore_helpers.sh" +source "$CURRENT_DIR/spinner_helpers.sh" + +# delimiter +d=$'\t' + +# Global variable. +# Used during the restore: if a pane already exists from before, it is +# saved in the array in this variable. Later, process running in existing pane +# is also not restored. That makes the restoration process more idempotent. +EXISTING_PANES_VAR="" + +RESTORING_FROM_SCRATCH="false" + +RESTORE_PANE_CONTENTS="false" + +is_line_type() { + local line_type="$1" + local line="$2" + echo "$line" | + \grep -q "^$line_type" +} + +check_saved_session_exists() { + local resurrect_file="$(last_resurrect_file)" + if [ ! -f $resurrect_file ]; then + display_message "Tmux resurrect file not found!" + return 1 + fi +} + +pane_exists() { + local session_name="$1" + local window_number="$2" + local pane_index="$3" + tmux list-panes -t "${session_name}:${window_number}" -F "#{pane_index}" 2>/dev/null | + \grep -q "^$pane_index$" +} + +register_existing_pane() { + local session_name="$1" + local window_number="$2" + local pane_index="$3" + local pane_custom_id="${session_name}:${window_number}:${pane_index}" + local delimiter=$'\t' + EXISTING_PANES_VAR="${EXISTING_PANES_VAR}${delimiter}${pane_custom_id}" +} + +is_pane_registered_as_existing() { + local session_name="$1" + local window_number="$2" + local pane_index="$3" + local pane_custom_id="${session_name}:${window_number}:${pane_index}" + [[ "$EXISTING_PANES_VAR" =~ "$pane_custom_id" ]] +} + +restore_from_scratch_true() { + RESTORING_FROM_SCRATCH="true" +} + +is_restoring_from_scratch() { + [ "$RESTORING_FROM_SCRATCH" == "true" ] +} + +restore_pane_contents_true() { + RESTORE_PANE_CONTENTS="true" +} + +is_restoring_pane_contents() { + [ "$RESTORE_PANE_CONTENTS" == "true" ] +} + +restored_session_0_true() { + RESTORED_SESSION_0="true" +} + +has_restored_session_0() { + [ "$RESTORED_SESSION_0" == "true" ] +} + +window_exists() { + local session_name="$1" + local window_number="$2" + tmux list-windows -t "$session_name" -F "#{window_index}" 2>/dev/null | + \grep -q "^$window_number$" +} + +session_exists() { + local session_name="$1" + tmux has-session -t "$session_name" 2>/dev/null +} + +first_window_num() { + tmux show -gv base-index +} + +tmux_socket() { + echo $TMUX | cut -d',' -f1 +} + +# Tmux option stored in a global variable so that we don't have to "ask" +# tmux server each time. +cache_tmux_default_command() { + local default_shell="$(get_tmux_option "default-shell" "")" + local opt="" + if [ "$(basename "$default_shell")" == "bash" ]; then + opt="-l " + fi + export TMUX_DEFAULT_COMMAND="$(get_tmux_option "default-command" "$opt$default_shell")" +} + +tmux_default_command() { + echo "$TMUX_DEFAULT_COMMAND" +} + +pane_creation_command() { + echo "cat '$(pane_contents_file "restore" "${1}:${2}.${3}")'; exec $(tmux_default_command)" +} + +new_window() { + local session_name="$1" + local window_number="$2" + local dir="$3" + local pane_index="$4" + local pane_id="${session_name}:${window_number}.${pane_index}" + dir="${dir/#\~/$HOME}" + if is_restoring_pane_contents && pane_contents_file_exists "$pane_id"; then + local pane_creation_command="$(pane_creation_command "$session_name" "$window_number" "$pane_index")" + tmux new-window -d -t "${session_name}:${window_number}" -c "$dir" "$pane_creation_command" + else + tmux new-window -d -t "${session_name}:${window_number}" -c "$dir" + fi +} + +new_session() { + local session_name="$1" + local window_number="$2" + local dir="$3" + local pane_index="$4" + local pane_id="${session_name}:${window_number}.${pane_index}" + if is_restoring_pane_contents && pane_contents_file_exists "$pane_id"; then + local pane_creation_command="$(pane_creation_command "$session_name" "$window_number" "$pane_index")" + TMUX="" tmux -S "$(tmux_socket)" new-session -d -s "$session_name" -c "$dir" "$pane_creation_command" + else + TMUX="" tmux -S "$(tmux_socket)" new-session -d -s "$session_name" -c "$dir" + fi + # change first window number if necessary + local created_window_num="$(first_window_num)" + if [ $created_window_num -ne $window_number ]; then + tmux move-window -s "${session_name}:${created_window_num}" -t "${session_name}:${window_number}" + fi +} + +new_pane() { + local session_name="$1" + local window_number="$2" + local dir="$3" + local pane_index="$4" + local pane_id="${session_name}:${window_number}.${pane_index}" + if is_restoring_pane_contents && pane_contents_file_exists "$pane_id"; then + local pane_creation_command="$(pane_creation_command "$session_name" "$window_number" "$pane_index")" + tmux split-window -t "${session_name}:${window_number}" -c "$dir" "$pane_creation_command" + else + tmux split-window -t "${session_name}:${window_number}" -c "$dir" + fi + # minimize window so more panes can fit + tmux resize-pane -t "${session_name}:${window_number}" -U "999" +} + +restore_pane() { + local pane="$1" + while IFS=$d read line_type session_name window_number window_active window_flags pane_index pane_title dir pane_active pane_command pane_full_command; do + dir="$(remove_first_char "$dir")" + pane_full_command="$(remove_first_char "$pane_full_command")" + if [ "$session_name" == "0" ]; then + restored_session_0_true + fi + if pane_exists "$session_name" "$window_number" "$pane_index"; then + if is_restoring_from_scratch; then + # overwrite the pane + # happens only for the first pane if it's the only registered pane for the whole tmux server + local pane_id="$(tmux display-message -p -F "#{pane_id}" -t "$session_name:$window_number")" + new_pane "$session_name" "$window_number" "$dir" "$pane_index" + tmux kill-pane -t "$pane_id" + else + # Pane exists, no need to create it! + # Pane existence is registered. Later, its process also won't be restored. + register_existing_pane "$session_name" "$window_number" "$pane_index" + fi + elif window_exists "$session_name" "$window_number"; then + new_pane "$session_name" "$window_number" "$dir" "$pane_index" + elif session_exists "$session_name"; then + new_window "$session_name" "$window_number" "$dir" "$pane_index" + else + new_session "$session_name" "$window_number" "$dir" "$pane_index" + fi + # set pane title + tmux select-pane -t "$session_name:$window_number.$pane_index" -T "$pane_title" + done < <(echo "$pane") +} + +restore_state() { + local state="$1" + echo "$state" | + while IFS=$d read line_type client_session client_last_session; do + tmux switch-client -t "$client_last_session" + tmux switch-client -t "$client_session" + done +} + +restore_grouped_session() { + local grouped_session="$1" + echo "$grouped_session" | + while IFS=$d read line_type grouped_session original_session alternate_window active_window; do + TMUX="" tmux -S "$(tmux_socket)" new-session -d -s "$grouped_session" -t "$original_session" + done +} + +restore_active_and_alternate_windows_for_grouped_sessions() { + local grouped_session="$1" + echo "$grouped_session" | + while IFS=$d read line_type grouped_session original_session alternate_window_index active_window_index; do + alternate_window_index="$(remove_first_char "$alternate_window_index")" + active_window_index="$(remove_first_char "$active_window_index")" + if [ -n "$alternate_window_index" ]; then + tmux switch-client -t "${grouped_session}:${alternate_window_index}" + fi + if [ -n "$active_window_index" ]; then + tmux switch-client -t "${grouped_session}:${active_window_index}" + fi + done +} + +never_ever_overwrite() { + local overwrite_option_value="$(get_tmux_option "$overwrite_option" "")" + [ -n "$overwrite_option_value" ] +} + +detect_if_restoring_from_scratch() { + if never_ever_overwrite; then + return + fi + local total_number_of_panes="$(tmux list-panes -a | wc -l | sed 's/ //g')" + if [ "$total_number_of_panes" -eq 1 ]; then + restore_from_scratch_true + fi +} + +detect_if_restoring_pane_contents() { + if capture_pane_contents_option_on; then + cache_tmux_default_command + restore_pane_contents_true + fi +} + +# functions called from main (ordered) + +restore_all_panes() { + detect_if_restoring_from_scratch # sets a global variable + detect_if_restoring_pane_contents # sets a global variable + if is_restoring_pane_contents; then + pane_content_files_restore_from_archive + fi + while read line; do + if is_line_type "pane" "$line"; then + restore_pane "$line" + fi + done < $(last_resurrect_file) +} + +handle_session_0() { + if is_restoring_from_scratch && ! has_restored_session_0; then + local current_session="$(tmux display -p "#{client_session}")" + if [ "$current_session" == "0" ]; then + tmux switch-client -n + fi + tmux kill-session -t "0" + fi +} + +restore_window_properties() { + local window_name + \grep '^window' $(last_resurrect_file) | + while IFS=$d read line_type session_name window_number window_name window_active window_flags window_layout automatic_rename; do + tmux select-layout -t "${session_name}:${window_number}" "$window_layout" + + # Below steps are properly handling window names and automatic-rename + # option. `rename-window` is an extra command in some scenarios, but we + # opted for always doing it to keep the code simple. + window_name="$(remove_first_char "$window_name")" + tmux rename-window -t "${session_name}:${window_number}" "$window_name" + if [ "${automatic_rename}" = ":" ]; then + tmux set-option -u -t "${session_name}:${window_number}" automatic-rename + else + tmux set-option -t "${session_name}:${window_number}" automatic-rename "$automatic_rename" + fi + done +} + +restore_all_pane_processes() { + if restore_pane_processes_enabled; then + local pane_full_command + awk 'BEGIN { FS="\t"; OFS="\t" } /^pane/ && $11 !~ "^:$" { print $2, $3, $6, $8, $11; }' $(last_resurrect_file) | + while IFS=$d read -r session_name window_number pane_index dir pane_full_command; do + dir="$(remove_first_char "$dir")" + pane_full_command="$(remove_first_char "$pane_full_command")" + restore_pane_process "$pane_full_command" "$session_name" "$window_number" "$pane_index" "$dir" + done + fi +} + +restore_active_pane_for_each_window() { + awk 'BEGIN { FS="\t"; OFS="\t" } /^pane/ && $9 == 1 { print $2, $3, $6; }' $(last_resurrect_file) | + while IFS=$d read session_name window_number active_pane; do + tmux switch-client -t "${session_name}:${window_number}" + tmux select-pane -t "$active_pane" + done +} + +restore_zoomed_windows() { + awk 'BEGIN { FS="\t"; OFS="\t" } /^pane/ && $5 ~ /Z/ && $9 == 1 { print $2, $3; }' $(last_resurrect_file) | + while IFS=$d read session_name window_number; do + tmux resize-pane -t "${session_name}:${window_number}" -Z + done +} + +restore_grouped_sessions() { + while read line; do + if is_line_type "grouped_session" "$line"; then + restore_grouped_session "$line" + restore_active_and_alternate_windows_for_grouped_sessions "$line" + fi + done < $(last_resurrect_file) +} + +restore_active_and_alternate_windows() { + awk 'BEGIN { FS="\t"; OFS="\t" } /^window/ && $6 ~ /[*-]/ { print $2, $5, $3; }' $(last_resurrect_file) | + sort -u | + while IFS=$d read session_name active_window window_number; do + tmux switch-client -t "${session_name}:${window_number}" + done +} + +restore_active_and_alternate_sessions() { + while read line; do + if is_line_type "state" "$line"; then + restore_state "$line" + fi + done < $(last_resurrect_file) +} + +# A cleanup that happens after 'restore_all_panes' seems to fix fish shell +# users' restore problems. +cleanup_restored_pane_contents() { + if is_restoring_pane_contents; then + rm "$(pane_contents_dir "restore")"/* + fi +} + +main() { + if supported_tmux_version_ok && check_saved_session_exists; then + start_spinner "Restoring..." "Tmux restore complete!" + execute_hook "pre-restore-all" + restore_all_panes + handle_session_0 + restore_window_properties >/dev/null 2>&1 + execute_hook "pre-restore-pane-processes" + restore_all_pane_processes + # below functions restore exact cursor positions + restore_active_pane_for_each_window + restore_zoomed_windows + restore_grouped_sessions # also restores active and alt windows for grouped sessions + restore_active_and_alternate_windows + restore_active_and_alternate_sessions + cleanup_restored_pane_contents + execute_hook "post-restore-all" + stop_spinner + display_message "Tmux restore complete!" + fi +} +main diff --git a/.config/tmux/plugins/resurrect/scripts/save.sh b/.config/tmux/plugins/resurrect/scripts/save.sh new file mode 100755 index 0000000..01edcde --- /dev/null +++ b/.config/tmux/plugins/resurrect/scripts/save.sh @@ -0,0 +1,278 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$CURRENT_DIR/variables.sh" +source "$CURRENT_DIR/helpers.sh" +source "$CURRENT_DIR/spinner_helpers.sh" + +# delimiters +d=$'\t' +delimiter=$'\t' + +# if "quiet" script produces no output +SCRIPT_OUTPUT="$1" + +grouped_sessions_format() { + local format + format+="#{session_grouped}" + format+="${delimiter}" + format+="#{session_group}" + format+="${delimiter}" + format+="#{session_id}" + format+="${delimiter}" + format+="#{session_name}" + echo "$format" +} + +pane_format() { + local format + format+="pane" + format+="${delimiter}" + format+="#{session_name}" + format+="${delimiter}" + format+="#{window_index}" + format+="${delimiter}" + format+="#{window_active}" + format+="${delimiter}" + format+=":#{window_flags}" + format+="${delimiter}" + format+="#{pane_index}" + format+="${delimiter}" + format+="#{pane_title}" + format+="${delimiter}" + format+=":#{pane_current_path}" + format+="${delimiter}" + format+="#{pane_active}" + format+="${delimiter}" + format+="#{pane_current_command}" + format+="${delimiter}" + format+="#{pane_pid}" + format+="${delimiter}" + format+="#{history_size}" + echo "$format" +} + +window_format() { + local format + format+="window" + format+="${delimiter}" + format+="#{session_name}" + format+="${delimiter}" + format+="#{window_index}" + format+="${delimiter}" + format+=":#{window_name}" + format+="${delimiter}" + format+="#{window_active}" + format+="${delimiter}" + format+=":#{window_flags}" + format+="${delimiter}" + format+="#{window_layout}" + echo "$format" +} + +state_format() { + local format + format+="state" + format+="${delimiter}" + format+="#{client_session}" + format+="${delimiter}" + format+="#{client_last_session}" + echo "$format" +} + +dump_panes_raw() { + tmux list-panes -a -F "$(pane_format)" +} + +dump_windows_raw(){ + tmux list-windows -a -F "$(window_format)" +} + +toggle_window_zoom() { + local target="$1" + tmux resize-pane -Z -t "$target" +} + +_save_command_strategy_file() { + local save_command_strategy="$(get_tmux_option "$save_command_strategy_option" "$default_save_command_strategy")" + local strategy_file="$CURRENT_DIR/../save_command_strategies/${save_command_strategy}.sh" + local default_strategy_file="$CURRENT_DIR/../save_command_strategies/${default_save_command_strategy}.sh" + if [ -e "$strategy_file" ]; then # strategy file exists? + echo "$strategy_file" + else + echo "$default_strategy_file" + fi +} + +pane_full_command() { + local pane_pid="$1" + local strategy_file="$(_save_command_strategy_file)" + # execute strategy script to get pane full command + $strategy_file "$pane_pid" +} + +number_nonempty_lines_on_screen() { + local pane_id="$1" + tmux capture-pane -pJ -t "$pane_id" | + sed '/^$/d' | + wc -l | + sed 's/ //g' +} + +# tests if there was any command output in the current pane +pane_has_any_content() { + local pane_id="$1" + local history_size="$(tmux display -p -t "$pane_id" -F "#{history_size}")" + local cursor_y="$(tmux display -p -t "$pane_id" -F "#{cursor_y}")" + # doing "cheap" tests first + [ "$history_size" -gt 0 ] || # history has any content? + [ "$cursor_y" -gt 0 ] || # cursor not in first line? + [ "$(number_nonempty_lines_on_screen "$pane_id")" -gt 1 ] +} + +capture_pane_contents() { + local pane_id="$1" + local start_line="-$2" + local pane_contents_area="$3" + if pane_has_any_content "$pane_id"; then + if [ "$pane_contents_area" = "visible" ]; then + start_line="0" + fi + # the printf hack below removes *trailing* empty lines + printf '%s\n' "$(tmux capture-pane -epJ -S "$start_line" -t "$pane_id")" > "$(pane_contents_file "save" "$pane_id")" + fi +} + +get_active_window_index() { + local session_name="$1" + tmux list-windows -t "$session_name" -F "#{window_flags} #{window_index}" | + awk '$1 ~ /\*/ { print $2; }' +} + +get_alternate_window_index() { + local session_name="$1" + tmux list-windows -t "$session_name" -F "#{window_flags} #{window_index}" | + awk '$1 ~ /-/ { print $2; }' +} + +dump_grouped_sessions() { + local current_session_group="" + local original_session + tmux list-sessions -F "$(grouped_sessions_format)" | + grep "^1" | + cut -c 3- | + sort | + while IFS=$d read session_group session_id session_name; do + if [ "$session_group" != "$current_session_group" ]; then + # this session is the original/first session in the group + original_session="$session_name" + current_session_group="$session_group" + else + # this session "points" to the original session + active_window_index="$(get_active_window_index "$session_name")" + alternate_window_index="$(get_alternate_window_index "$session_name")" + echo "grouped_session${d}${session_name}${d}${original_session}${d}:${alternate_window_index}${d}:${active_window_index}" + fi + done +} + +fetch_and_dump_grouped_sessions(){ + local grouped_sessions_dump="$(dump_grouped_sessions)" + get_grouped_sessions "$grouped_sessions_dump" + if [ -n "$grouped_sessions_dump" ]; then + echo "$grouped_sessions_dump" + fi +} + +# translates pane pid to process command running inside a pane +dump_panes() { + local full_command + dump_panes_raw | + while IFS=$d read line_type session_name window_number window_active window_flags pane_index pane_title dir pane_active pane_command pane_pid history_size; do + # not saving panes from grouped sessions + if is_session_grouped "$session_name"; then + continue + fi + full_command="$(pane_full_command $pane_pid)" + dir=$(echo $dir | sed 's/ /\\ /') # escape all spaces in directory path + echo "${line_type}${d}${session_name}${d}${window_number}${d}${window_active}${d}${window_flags}${d}${pane_index}${d}${pane_title}${d}${dir}${d}${pane_active}${d}${pane_command}${d}:${full_command}" + done +} + +dump_windows() { + dump_windows_raw | + while IFS=$d read line_type session_name window_index window_name window_active window_flags window_layout; do + # not saving windows from grouped sessions + if is_session_grouped "$session_name"; then + continue + fi + automatic_rename="$(tmux show-window-options -vt "${session_name}:${window_index}" automatic-rename)" + # If the option was unset, use ":" as a placeholder. + [ -z "${automatic_rename}" ] && automatic_rename=":" + echo "${line_type}${d}${session_name}${d}${window_index}${d}${window_name}${d}${window_active}${d}${window_flags}${d}${window_layout}${d}${automatic_rename}" + done +} + +dump_state() { + tmux display-message -p "$(state_format)" +} + +dump_pane_contents() { + local pane_contents_area="$(get_tmux_option "$pane_contents_area_option" "$default_pane_contents_area")" + dump_panes_raw | + while IFS=$d read line_type session_name window_number window_active window_flags pane_index pane_title dir pane_active pane_command pane_pid history_size; do + capture_pane_contents "${session_name}:${window_number}.${pane_index}" "$history_size" "$pane_contents_area" + done +} + +remove_old_backups() { + # remove resurrect files older than 30 days (default), but keep at least 5 copies of backup. + local delete_after="$(get_tmux_option "$delete_backup_after_option" "$default_delete_backup_after")" + local -a files + files=($(ls -t $(resurrect_dir)/${RESURRECT_FILE_PREFIX}_*.${RESURRECT_FILE_EXTENSION} | tail -n +6)) + [[ ${#files[@]} -eq 0 ]] || + find "${files[@]}" -type f -mtime "+${delete_after}" -exec rm -v "{}" \; > /dev/null +} + +save_all() { + local resurrect_file_path="$(resurrect_file_path)" + local last_resurrect_file="$(last_resurrect_file)" + mkdir -p "$(resurrect_dir)" + fetch_and_dump_grouped_sessions > "$resurrect_file_path" + dump_panes >> "$resurrect_file_path" + dump_windows >> "$resurrect_file_path" + dump_state >> "$resurrect_file_path" + execute_hook "post-save-layout" "$resurrect_file_path" + if files_differ "$resurrect_file_path" "$last_resurrect_file"; then + ln -fs "$(basename "$resurrect_file_path")" "$last_resurrect_file" + else + rm "$resurrect_file_path" + fi + if capture_pane_contents_option_on; then + mkdir -p "$(pane_contents_dir "save")" + dump_pane_contents + pane_contents_create_archive + rm "$(pane_contents_dir "save")"/* + fi + remove_old_backups + execute_hook "post-save-all" +} + +show_output() { + [ "$SCRIPT_OUTPUT" != "quiet" ] +} + +main() { + if supported_tmux_version_ok; then + if show_output; then + start_spinner "Saving..." "Tmux environment saved!" + fi + save_all + if show_output; then + stop_spinner + display_message "Tmux environment saved!" + fi + fi +} +main diff --git a/.config/tmux/plugins/resurrect/scripts/spinner_helpers.sh b/.config/tmux/plugins/resurrect/scripts/spinner_helpers.sh new file mode 100644 index 0000000..fe73cd7 --- /dev/null +++ b/.config/tmux/plugins/resurrect/scripts/spinner_helpers.sh @@ -0,0 +1,8 @@ +start_spinner() { + $CURRENT_DIR/tmux_spinner.sh "$1" "$2" & + export SPINNER_PID=$! +} + +stop_spinner() { + kill $SPINNER_PID +} diff --git a/.config/tmux/plugins/resurrect/scripts/tmux_spinner.sh b/.config/tmux/plugins/resurrect/scripts/tmux_spinner.sh new file mode 100755 index 0000000..9b1b979 --- /dev/null +++ b/.config/tmux/plugins/resurrect/scripts/tmux_spinner.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +# This script shows tmux spinner with a message. It is intended to be running +# as a background process which should be `kill`ed at the end. +# +# Example usage: +# +# ./tmux_spinner.sh "Working..." "End message!" & +# SPINNER_PID=$! +# .. +# .. execute commands here +# .. +# kill $SPINNER_PID # Stops spinner and displays 'End message!' + +MESSAGE="$1" +END_MESSAGE="$2" +SPIN='-\|/' + +trap "tmux display-message '$END_MESSAGE'; exit" SIGINT SIGTERM + +main() { + local i=0 + while true; do + i=$(( (i+1) %4 )) + tmux display-message " ${SPIN:$i:1} $MESSAGE" + sleep 0.1 + done +} +main diff --git a/.config/tmux/plugins/resurrect/scripts/variables.sh b/.config/tmux/plugins/resurrect/scripts/variables.sh new file mode 100644 index 0000000..9d42e02 --- /dev/null +++ b/.config/tmux/plugins/resurrect/scripts/variables.sh @@ -0,0 +1,48 @@ +# key bindings +default_save_key="C-s" +save_option="@resurrect-save" +save_path_option="@resurrect-save-script-path" + +default_restore_key="C-r" +restore_option="@resurrect-restore" +restore_path_option="@resurrect-restore-script-path" + +# default processes that are restored +default_proc_list_option="@resurrect-default-processes" +default_proc_list='vi vim view nvim emacs man less more tail top htop irssi weechat mutt' + +# User defined processes that are restored +# 'false' - nothing is restored +# ':all:' - all processes are restored +# +# user defined list of programs that are restored: +# 'my_program foo another_program' +restore_processes_option="@resurrect-processes" +restore_processes="" + +# Defines part of the user variable. Example usage: +# set -g @resurrect-strategy-vim "session" +restore_process_strategy_option="@resurrect-strategy-" + +inline_strategy_token="->" +inline_strategy_arguments_token="*" + +save_command_strategy_option="@resurrect-save-command-strategy" +default_save_command_strategy="ps" + +# Pane contents capture options. +# @resurrect-pane-contents-area option can be: +# 'visible' - capture only the visible pane area +# 'full' - capture the full pane contents +pane_contents_option="@resurrect-capture-pane-contents" +pane_contents_area_option="@resurrect-pane-contents-area" +default_pane_contents_area="full" + +# set to 'on' to ensure panes are never ever overwritten +overwrite_option="@resurrect-never-overwrite" + +# Hooks are set via ${hook_prefix}${name}, i.e. "@resurrect-hook-post-save-all" +hook_prefix="@resurrect-hook-" + +delete_backup_after_option="@resurrect-delete-backup-after" +default_delete_backup_after="30" # days diff --git a/.config/tmux/plugins/resurrect/strategies/irb_default_strategy.sh b/.config/tmux/plugins/resurrect/strategies/irb_default_strategy.sh new file mode 100755 index 0000000..897f5bb --- /dev/null +++ b/.config/tmux/plugins/resurrect/strategies/irb_default_strategy.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# "irb default strategy" +# +# Example irb process with junk variables: +# irb RBENV_VERSION=1.9.3-p429 GREP_COLOR=34;47 TERM_PROGRAM=Apple_Terminal +# +# When executed, the above will fail. This strategy handles that. + +ORIGINAL_COMMAND="$1" +DIRECTORY="$2" + +original_command_wo_junk_vars() { + echo "$ORIGINAL_COMMAND" | + sed 's/RBENV_VERSION[^ ]*//' | + sed 's/GREP_COLOR[^ ]*//' | + sed 's/TERM_PROGRAM[^ ]*//' +} + +main() { + echo "$(original_command_wo_junk_vars)" +} +main diff --git a/.config/tmux/plugins/resurrect/strategies/mosh-client_default_strategy.sh b/.config/tmux/plugins/resurrect/strategies/mosh-client_default_strategy.sh new file mode 100755 index 0000000..4d2f06b --- /dev/null +++ b/.config/tmux/plugins/resurrect/strategies/mosh-client_default_strategy.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# "mosh-client default strategy" +# +# Example mosh-client process: +# mosh-client -# charm tmux at | 198.199.104.142 60001 +# +# When executed, the above will fail. This strategy handles that. + +ORIGINAL_COMMAND="$1" +DIRECTORY="$2" + +mosh_command() { + local args="$ORIGINAL_COMMAND" + + args="${args#*-#}" + args="${args%|*}" + + echo "mosh $args" +} + +main() { + echo "$(mosh_command)" +} +main diff --git a/.config/tmux/plugins/resurrect/strategies/nvim_session.sh b/.config/tmux/plugins/resurrect/strategies/nvim_session.sh new file mode 100755 index 0000000..4987c68 --- /dev/null +++ b/.config/tmux/plugins/resurrect/strategies/nvim_session.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# "nvim session strategy" +# +# Same as vim strategy, see file 'vim_session.sh' + +ORIGINAL_COMMAND="$1" +DIRECTORY="$2" + +nvim_session_file_exists() { + [ -e "${DIRECTORY}/Session.vim" ] +} + +original_command_contains_session_flag() { + [[ "$ORIGINAL_COMMAND" =~ "-S" ]] +} + +main() { + if nvim_session_file_exists; then + echo "nvim -S" + elif original_command_contains_session_flag; then + # Session file does not exist, yet the original nvim command contains + # session flag `-S`. This will cause an error, so we're falling back to + # starting plain nvim. + echo "nvim" + else + echo "$ORIGINAL_COMMAND" + fi +} +main diff --git a/.config/tmux/plugins/resurrect/strategies/vim_session.sh b/.config/tmux/plugins/resurrect/strategies/vim_session.sh new file mode 100755 index 0000000..1b5293c --- /dev/null +++ b/.config/tmux/plugins/resurrect/strategies/vim_session.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# "vim session strategy" +# +# Restores a vim session from 'Session.vim' file, if it exists. +# If 'Session.vim' does not exist, it falls back to invoking the original +# command (without the `-S` flag). + +ORIGINAL_COMMAND="$1" +DIRECTORY="$2" + +vim_session_file_exists() { + [ -e "${DIRECTORY}/Session.vim" ] +} + +main() { + if vim_session_file_exists; then + echo "vim -S" + else + echo "$ORIGINAL_COMMAND" + fi +} +main diff --git a/.config/tmux/plugins/resurrect/tests/fixtures/restore_file.txt b/.config/tmux/plugins/resurrect/tests/fixtures/restore_file.txt new file mode 100644 index 0000000..dcf5779 --- /dev/null +++ b/.config/tmux/plugins/resurrect/tests/fixtures/restore_file.txt @@ -0,0 +1,21 @@ +pane 0 0 :bash 1 :* 0 :/tmp 1 bash : +pane blue 0 :vim 0 : 0 :/tmp 1 vim :vim foo.txt +pane blue 1 :man 0 :- 0 :/tmp 0 bash : +pane blue 1 :man 0 :- 1 :/usr/share/man 1 man :man echo +pane blue 2 :bash 1 :* 0 :/tmp 1 bash : +pane red 0 :bash 0 : 0 :/tmp 1 bash : +pane red 1 :bash 0 :-Z 0 :/tmp 0 bash : +pane red 1 :bash 0 :-Z 1 :/tmp 0 bash : +pane red 1 :bash 0 :-Z 2 :/tmp 1 bash : +pane red 2 :bash 1 :* 0 :/tmp 0 bash : +pane red 2 :bash 1 :* 1 :/tmp 1 bash : +pane yellow 0 :bash 1 :* 0 :/tmp/bar 1 bash : +window 0 0 1 :* ce9e,200x49,0,0,1 +window blue 0 0 : ce9f,200x49,0,0,2 +window blue 1 0 :- 178b,200x49,0,0{100x49,0,0,3,99x49,101,0,4} +window blue 2 1 :* cea2,200x49,0,0,5 +window red 0 0 : cea3,200x49,0,0,6 +window red 1 0 :-Z 135b,200x49,0,0[200x24,0,0,7,200x24,0,25{100x24,0,25,8,99x24,101,25,9}] +window red 2 1 :* db81,200x49,0,0[200x24,0,0,10,200x24,0,25,11] +window yellow 0 1 :* 6781,200x49,0,0,12 +state yellow blue diff --git a/.config/tmux/plugins/resurrect/tests/fixtures/save_file.txt b/.config/tmux/plugins/resurrect/tests/fixtures/save_file.txt new file mode 100644 index 0000000..0301f92 --- /dev/null +++ b/.config/tmux/plugins/resurrect/tests/fixtures/save_file.txt @@ -0,0 +1,21 @@ +pane 0 0 :bash 1 :* 0 :/tmp 1 bash : +pane blue 0 :vim 0 :! 0 :/tmp 1 vim :vim foo.txt +pane blue 1 :man 0 :!- 0 :/tmp 0 bash : +pane blue 1 :man 0 :!- 1 :/usr/share/man 1 man :man echo +pane blue 2 :bash 1 :* 0 :/tmp 1 bash : +pane red 0 :bash 0 : 0 :/tmp 1 bash : +pane red 1 :bash 0 :-Z 0 :/tmp 0 bash : +pane red 1 :bash 0 :-Z 1 :/tmp 0 bash : +pane red 1 :bash 0 :-Z 2 :/tmp 1 bash : +pane red 2 :bash 1 :* 0 :/tmp 0 bash : +pane red 2 :bash 1 :* 1 :/tmp 1 bash : +pane yellow 0 :bash 1 :* 0 :/tmp/bar 1 bash : +window 0 0 1 :* ce9d,200x49,0,0,0 +window blue 0 0 :! cea4,200x49,0,0,7 +window blue 1 0 :!- 9797,200x49,0,0{100x49,0,0,8,99x49,101,0,9} +window blue 2 1 :* 677f,200x49,0,0,10 +window red 0 0 : ce9e,200x49,0,0,1 +window red 1 0 :-Z 52b7,200x49,0,0[200x24,0,0,2,200x24,0,25{100x24,0,25,3,99x24,101,25,4}] +window red 2 1 :* bd68,200x49,0,0[200x24,0,0,5,200x24,0,25,6] +window yellow 0 1 :* 6780,200x49,0,0,11 +state yellow blue diff --git a/.config/tmux/plugins/resurrect/tests/helpers/create_and_save_tmux_test_environment.exp b/.config/tmux/plugins/resurrect/tests/helpers/create_and_save_tmux_test_environment.exp new file mode 100755 index 0000000..80cca2c --- /dev/null +++ b/.config/tmux/plugins/resurrect/tests/helpers/create_and_save_tmux_test_environment.exp @@ -0,0 +1,42 @@ +#!/usr/bin/env expect + +source "./tests/helpers/expect_helpers.exp" + +expect_setup + +spawn tmux +# delay with sleep to compensate for tmux starting time +sleep 1 + +run_shell_command "cd /tmp" + +# session red +new_tmux_session "red" + +new_tmux_window +horizontal_split +vertical_split +toggle_zoom_pane + +new_tmux_window +horizontal_split + +# session blue +new_tmux_session "blue" + +run_shell_command "touch foo.txt" +run_shell_command "vim foo.txt" + +new_tmux_window +vertical_split +run_shell_command "man echo" + +new_tmux_window + +# session yellow +new_tmux_session "yellow" +run_shell_command "cd /tmp/bar" + +start_resurrect_save + +run_shell_command "tmux kill-server" diff --git a/.config/tmux/plugins/resurrect/tests/helpers/expect_helpers.exp b/.config/tmux/plugins/resurrect/tests/helpers/expect_helpers.exp new file mode 100644 index 0000000..cbf4234 --- /dev/null +++ b/.config/tmux/plugins/resurrect/tests/helpers/expect_helpers.exp @@ -0,0 +1,70 @@ +# a set of expect helpers + +# basic setup for each script +proc expect_setup {} { + # disables script output + log_user 0 + # standard timeout + set timeout 5 +} + +proc new_tmux_window {} { + send "c" + send "cd /tmp\r" + sleep 0.2 +} + +proc rename_current_session {name} { + send "$" + # delete existing name with ctrl-u + send "" + send "$name\r" + sleep 0.2 +} + +proc new_tmux_session {name} { + send "TMUX='' tmux new -d -s $name\r" + sleep 1 + send "tmux switch-client -t $name\r" + send "cd /tmp\r" + sleep 0.5 +} + +proc horizontal_split {} { + send "\"" + sleep 0.2 + send "cd /tmp\r" + sleep 0.1 +} + +proc vertical_split {} { + send "%" + sleep 0.2 + send "cd /tmp\r" + sleep 0.1 +} + +proc toggle_zoom_pane {} { + send "z" + sleep 0.2 +} + +proc run_shell_command {command} { + send "$command\r" + sleep 1 +} + +proc start_resurrect_save {} { + send "" + sleep 5 +} + +proc start_resurrect_restore {} { + send "" + sleep 10 +} + +proc clear_screen_for_window {target} { + send "tmux send-keys -t $target C-l\r" + sleep 0.2 +} diff --git a/.config/tmux/plugins/resurrect/tests/helpers/restore_and_save_tmux_test_environment.exp b/.config/tmux/plugins/resurrect/tests/helpers/restore_and_save_tmux_test_environment.exp new file mode 100755 index 0000000..82da37f --- /dev/null +++ b/.config/tmux/plugins/resurrect/tests/helpers/restore_and_save_tmux_test_environment.exp @@ -0,0 +1,18 @@ +#!/usr/bin/env expect + +source "./tests/helpers/expect_helpers.exp" + +expect_setup + +spawn tmux +# delay with sleep to compensate for tmux starting time +sleep 1 + +start_resurrect_restore + +# delete all existing resurrect save files +run_shell_command "rm ~/.tmux/resurrect/*" + +start_resurrect_save + +run_shell_command "tmux kill-server" diff --git a/.config/tmux/plugins/resurrect/tests/helpers/resurrect_helpers.sh b/.config/tmux/plugins/resurrect/tests/helpers/resurrect_helpers.sh new file mode 100644 index 0000000..268aca5 --- /dev/null +++ b/.config/tmux/plugins/resurrect/tests/helpers/resurrect_helpers.sh @@ -0,0 +1,11 @@ +# we want "fixed" dimensions no matter the size of real display +set_screen_dimensions_helper() { + stty cols 200 + stty rows 50 +} + +last_save_file_differs_helper() { + local original_file="$1" + diff "$original_file" "${HOME}/.tmux/resurrect/last" + [ $? -ne 0 ] +} diff --git a/.config/tmux/plugins/resurrect/tests/test_resurrect_restore.sh b/.config/tmux/plugins/resurrect/tests/test_resurrect_restore.sh new file mode 100755 index 0000000..9cf4644 --- /dev/null +++ b/.config/tmux/plugins/resurrect/tests/test_resurrect_restore.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source $CURRENT_DIR/helpers/helpers.sh +source $CURRENT_DIR/helpers/resurrect_helpers.sh + +setup_before_restore() { + # setup restore file + mkdir -p ~/.tmux/resurrect/ + cp tests/fixtures/restore_file.txt "${HOME}/.tmux/resurrect/restore_file.txt" + ln -sf restore_file.txt "${HOME}/.tmux/resurrect/last" + + # directory used in restored tmux session + mkdir -p /tmp/bar +} + +restore_tmux_environment_and_save_again() { + set_screen_dimensions_helper + $CURRENT_DIR/helpers/restore_and_save_tmux_test_environment.exp +} + +main() { + install_tmux_plugin_under_test_helper + setup_before_restore + restore_tmux_environment_and_save_again + + if last_save_file_differs_helper "tests/fixtures/restore_file.txt"; then + fail_helper "Saved file not correct after restore" + fi + exit_helper +} +main diff --git a/.config/tmux/plugins/resurrect/tests/test_resurrect_save.sh b/.config/tmux/plugins/resurrect/tests/test_resurrect_save.sh new file mode 100755 index 0000000..99fb925 --- /dev/null +++ b/.config/tmux/plugins/resurrect/tests/test_resurrect_save.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source $CURRENT_DIR/helpers/helpers.sh +source $CURRENT_DIR/helpers/resurrect_helpers.sh + +create_tmux_test_environment_and_save() { + set_screen_dimensions_helper + $CURRENT_DIR/helpers/create_and_save_tmux_test_environment.exp +} + +main() { + install_tmux_plugin_under_test_helper + mkdir -p /tmp/bar # setup required dirs + create_tmux_test_environment_and_save + + if last_save_file_differs_helper "tests/fixtures/save_file.txt"; then + fail_helper "Saved file not correct (initial save)" + fi + exit_helper +} +main diff --git a/.config/tmux/plugins/resurrect/video/issue_vid.png b/.config/tmux/plugins/resurrect/video/issue_vid.png new file mode 100644 index 0000000..8ea5ea2 Binary files /dev/null and b/.config/tmux/plugins/resurrect/video/issue_vid.png differ diff --git a/.config/tmux/plugins/resurrect/video/screencast_img.png b/.config/tmux/plugins/resurrect/video/screencast_img.png new file mode 100644 index 0000000..f5f3d83 Binary files /dev/null and b/.config/tmux/plugins/resurrect/video/screencast_img.png differ diff --git a/.config/tmux/plugins/resurrect/video/script.md b/.config/tmux/plugins/resurrect/video/script.md new file mode 100644 index 0000000..ef38824 --- /dev/null +++ b/.config/tmux/plugins/resurrect/video/script.md @@ -0,0 +1,110 @@ +# Screencast script + +1. Intro +======== +Let's demo tmux resurrect plugin. + +Tmux resurrect enables persisting tmux sessions, so it can survive the dreaded +system restarts. + +The benefit is uninterrupted workflow with no configuration required. + +2. Working session +================== +Script +------ +Let me show you what I have in this tmux demo session. + +First of all, I have vim open and it has a couple files loaded. + +Then there's a tmux window with a couple splits in various directories across +the system. + +Next window contains tmux man page, + and then there's `htop` program. + +And this is just one of many projects I'm currently running. + +Actions +------- +- blank tmux window +- vim + - `ls` to show open files +- multiple pane windows (3) +- man tmux +- htop +- psql +- show a list of session + +3. Saving the environment +========================= +Script +------ +With vanilla tmux, when I restart the computer this whole environment will be +lost and I'll have to invest time to restore it. + +tmux resurrect gives you the ability to persist everything with +prefix plus alt-s. + +Now tmux environment is saved and I can safely shut down tmux with a +kill server command. + +Actions +------- +- prefix + M-s +- :kill-server + +4. Restoring the environment +============================ +Script +------ +At this point restoring everything back is easy. + +I'll fire up tmux again. Notice it's completely empty. + +Now, I'll press prefix plus alt-r and everything will restore. + +Let's see how things look now. +First of all, I'm back to the exact same window I was in when the environment +was saved. Second - you can see the `htop` program was restored. + +Going back there's tmux man page + a window with multiple panes with the exact same layout as before + and vim. + + +tmux resurrect takes special care of vim. By leveraging vim's sessions, it +preserves vim's split windows, open files, even the list of files edited before. + +Check out the project readme for more details about special treatment for vim. + +That was just one of the restored tmux sessions. If I open tmux session list you +can see all the other projects are restored as well. + + +When you see all these programs running you might be concerned that this plugin +started a lot of potentially destructive processes. + +For example, when you restore tmux you don't want to accidentally start backups, +resource intensive or sensitive programs. + +There's no need to be worried though. By default, this plugin starts only a +conservative list of programs like vim, less, tail, htop and similar. +This list of programs restored by default is in the project readme. Also, you +can easily add more programs to it. + +If you feel paranoid, there's an option that prevents restoring any program. + +Actions +------- +- tmux +- prefix + M-r + +- open previous windows +- in vim hit :ls + +- prefix + s for a list of panes + +5. Outro +======== +That's it for this demo. I hope you'll find tmux resurrect useful. diff --git a/.config/tmux/tmux.conf b/.config/tmux/tmux.conf index 0fef948..8f5a23f 100644 --- a/.config/tmux/tmux.conf +++ b/.config/tmux/tmux.conf @@ -55,3 +55,7 @@ set-environment -g COLORTERM "truecolor" # Splits start with $PWD from source pane bind '"' split-window -v -c "#{pane_current_path}" bind % split-window -h -c "#{pane_current_path}" + +# Restore tmux environment after system restart +#run-shell /Users/oliver.ladner/.config/tmux/plugins/resurrect/resurrect.tmux +run-shell ~/.config/tmux/plugins/resurrect/resurrect.tmux