├── .bashateconfig ├── .gitignore ├── .markdownlint.yaml ├── .olint.conf ├── LICENSE ├── README.md ├── power-zoom.tmux ├── pyproject.toml └── scripts ├── power_zoom.sh └── utils.sh /.bashateconfig: -------------------------------------------------------------------------------- 1 | max-line-length=92 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | \#* 3 | tests/ 4 | .DS_Store 5 | *.sublime-* 6 | 7 | # olint linter cache 8 | .cache.olint 9 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | MD013: false 4 | 5 | MD033: 6 | # Allowed elements 7 | allowed_elements: [br] 8 | -------------------------------------------------------------------------------- /.olint.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaclu/tmux-power-zoom/5916f2087c140e9fbac7f3bbf64765581646c042/.olint.conf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jacob Lundqvist 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tmux-Power-Zoom 2 | 3 | Zoom pane to separate window, and un-zoom back into the original location. 4 | 5 | This way you can open other panes whilst focusing on the zoomed pane, without 6 | risking getting a crowded mess of panes. 7 | 8 | You can also restore by triggering the power-zoom action on the place-holder 9 | pane. 10 | 11 | ## Recent changes 12 | 13 | - Ignore `@power_zoom_mouse_action` if tmux < 3.0 14 | - Had forgotten to abort if attempt to zoom only pane - fixed 15 | - Removed a previous config variable `power_zoom_mouse` 16 | - Recent changes resulted in compatibility with tmux 2.0 17 | - Repeated zooms of the same pane now works as expected 18 | 19 | ## Purpose 20 | 21 | Often when zooming a pane, working in it for a while and then figure out 22 | a new pane is needed to look something up, the zoomed state is forgotten. 23 | When opening a new pane this way, the new pane becomes squeezed in next 24 | to the original pane, often far to small to be practically usable. 25 | 26 | This plugin zooms panes into a new window and makes it convenient to open support 27 | panes. Hitting Zoom again unzooms and move the pane back to it's 28 | original location. The temp window closes, if no other panes are present. 29 | 30 | The temp windows name uses ID of the zoomed pane, so that if other 31 | panes were open and left running, there is a hint about 32 | the purpose of that window. 33 | 34 | ## Usage 35 | 36 | Hit `` + `@power_zoom_trigger` to toggle Power Zoom. 37 | If `@power_zoom_mouse_action` is defined, that mouse action also toggles 38 | Power Zoom. 39 | 40 | ## Install 41 | 42 | ### Dependencies 43 | 44 | `tmux 2.0` or higher. Needs select-pane -T option. Could be made to work on 45 | earlier version with a bit of rewrite, if anybody has such a need, 46 | create an Issue. 47 | 48 | ### Installation with [Tmux Plugin Manager](https://github.com/tmux-plugins/tpm) (recommended) 49 | 50 | Add plugin to the list of TPM plugins in `.tmux.conf`: 51 | 52 | ```tmux 53 | set -g @plugin 'jaclu/tmux-power-zoom' 54 | ``` 55 | 56 | Hit `prefix + I` to fetch the plugin and source it. 57 | 58 | ### Manual installation 59 | 60 | Clone the repository: 61 | 62 | ```bash 63 | git clone https://github.com/jaclu/tmux-power-zoom.git ~/clone/path 64 | ``` 65 | 66 | Add this line to the bottom of `.tmux.conf`: 67 | 68 | ```tmux 69 | run-shell ~/clone/path/power-zoom.tmux 70 | ``` 71 | 72 | Reload TMUX environment with `$ tmux source-file ~/.tmux.conf`, and that's it. 73 | 74 | ## Configuration options 75 | 76 | | Option | Default | Description | 77 | | ---------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 78 | | `@power_zoom_trigger` | Z | Key that triggers Power Zoom to toggle | 79 | | `@power_zoom_without_prefix` | 0 | If set to 1, trigger key is independent of `` | 80 | | `@power_zoom_mouse_action` | | Defines a mouse action trigger, supports modifiers
typically 1 is left button and 3 is right button
Examples:
DoubleClick3Pane
S-DoubleClick3Pane
M-DoubleClick3Pane
TrippleClick1Pane | 81 | 82 | ## Contributing 83 | 84 | Contributions are welcome, and they're appreciated. Every little bit 85 | helps, and credit is always given. 86 | 87 | The best way to send feedback is to file an issue at 88 | [tmux-power-zoom/issues](https://github.com/jaclu/tmux-power-zoom/issues) 89 | 90 | ### License 91 | 92 | [MIT](LICENSE.md) 93 | -------------------------------------------------------------------------------- /power-zoom.tmux: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Copyright (c) 2022,2025: Jacob.Lundqvist@gmail.com 4 | # License: MIT 5 | # 6 | # Part of https://github.com/jaclu/tmux-power-zoom 7 | # 8 | # Dependency: 2.6 - select-pane -T introduced with this version 9 | # 10 | 11 | # shellcheck disable=SC1007 12 | D_PLUGIN=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) 13 | 14 | D_SCRIPTS="$D_PLUGIN/scripts" 15 | 16 | # shellcheck source=scripts/utils.sh 17 | . "$D_SCRIPTS/utils.sh" 18 | 19 | # If logging is enabled, add spacer each time tmux starts up 20 | [[ -n "$log_file" ]] && log_it 21 | 22 | cmd_kb="bind-key" 23 | cmd_m="bind-key" 24 | 25 | # 26 | # Generic plugin setting I use to add Notes to plugin keys that are bound 27 | # This makes this key binding show up when doing ? 28 | # If not set to "Yes", no attempt at adding notes will happen. 29 | # bind-key Notes were added in tmux 3.1, so should not be used on older versions! 30 | # 31 | normalize_bool_param "$(get_tmux_option "@use_bind_key_notes_in_plugins" "No")" && { 32 | # set -- "$@" -N "plugin: $plugin_name" 33 | note="plugin: $plugin_name" 34 | cmd_kb+=" -N '$note'" 35 | cmd_m+=" -N '$note'" 36 | log_it "Using note: $note" 37 | } 38 | 39 | if normalize_bool_param "$(get_tmux_option "@power_zoom_without_prefix" "No")"; then 40 | # set -- "$@" -n 41 | cmd_kb+=" -n" 42 | log_it "Not using prefix" 43 | fi 44 | 45 | trigger_key=$(get_tmux_option "@power_zoom_trigger" "$default_key") 46 | cmd_kb+=" $trigger_key run-shell $D_SCRIPTS/power_zoom.sh" 47 | # 48 | # clear list of zoomed panes, should make it smarter so it leaves currently 49 | # present items, this will leave them hanging on a conf source, but that 50 | # is minor compared to the risk of having crash left-overs causing future 51 | # havoc, the zoomed pane is left intact and dead placeholders can easily be 52 | # killed. 53 | # 54 | $TMUX_BIN set-option -gu @power_zoom_state 55 | 56 | eval "$TMUX_BIN $cmd_kb" || { 57 | error_msg "Failed to bind plugin trigger" 58 | } 59 | log_it "using trigger: $trigger_key" 60 | 61 | 62 | # 63 | # If @power_zoom_mouse_action is defined, also bind a mouse action to this 64 | # 65 | mouse_action="$(get_tmux_option "@power_zoom_mouse_action")" 66 | [[ -z "$mouse_action" ]] && exit 0 # no mouse action defined, setup is done 67 | 68 | # 69 | # Simplistic check if tmux version is >= 3 70 | # 71 | tmux_vers_maj="$($TMUX_BIN -V | tr -dC '[:digit:]' | cut -c1)" 72 | if [[ "$tmux_vers_maj" -lt 3 ]]; then 73 | error_msg "tmux < 3 doesn't support pane selection via mouse" 74 | fi 75 | 76 | # Dummy bind, to verify mouse action is valid 77 | $TMUX_BIN bind-key -n "$mouse_action" info 2>/dev/null || { 78 | error_msg "Invalid mouse action: $mouse_action" 79 | } 80 | $TMUX_BIN unbind -n "$mouse_action" # remove dummy bind 81 | 82 | # 83 | # First select the mouse-over pane, then trigger zoom, otherwise the 84 | # focused pane would get zoomed, and not the clicked one. 85 | # 86 | mouse_cmd="resize-pane -Z -t= ; run-shell $D_SCRIPTS/power_zoom.sh" 87 | 88 | cmd_m+=" -n $mouse_action '$mouse_cmd'" 89 | eval "$TMUX_BIN $cmd_m" || { 90 | error_msg "Failed to bind plugin mouse action trigger" 91 | } 92 | log_it "using mouse trigger: $mouse_action" 93 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pymarkdown] 2 | plugins.md033.allowed_elements = "br" 3 | plugins.md013.enabled = false 4 | -------------------------------------------------------------------------------- /scripts/power_zoom.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Copyright (c) 2022,2025: Jacob.Lundqvist@gmail.com 4 | # License: MIT 5 | # 6 | # Part of https://github.com/jaclu/tmux-power-zoom 7 | # 8 | # Tracking the placeholder pane by its pane title, this works regardless 9 | # if pane titles are displayed or not. 10 | # 11 | # shellcheck disable=SC2154 12 | 13 | # shellcheck disable=SC1007 14 | D_SCRIPTS=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) 15 | 16 | # shellcheck source=/dev/null 17 | . "$D_SCRIPTS/utils.sh" 18 | 19 | is_zoomed="is_zoomed" 20 | get_placeholder="get_placeholder" 21 | get_zoomed="get_zoomed" 22 | 23 | set_pz_status() { 24 | local value="$1" 25 | log_it "set_pz_status($value)" 26 | $TMUX_BIN set-option @power_zoom_state "$value" 27 | } 28 | 29 | read_pz_status() { 30 | $TMUX_BIN show-option -qv @power_zoom_state 31 | } 32 | 33 | check_pz_status() { 34 | local this_id 35 | local updated_values 36 | local do_update 37 | local result 38 | local pow_zoomed_panes 39 | local placeholder 40 | local zoomed 41 | 42 | case $1 in 43 | 44 | "$is_zoomed" | "$get_placeholder" | "$get_zoomed") ;; 45 | 46 | *) 47 | error_msg "ERROR: check_pz_status - invalid param: [$1]" 48 | ;; 49 | esac 50 | 51 | this_id="$($TMUX_BIN display -p '#D')" 52 | updated_values="" 53 | do_update=false 54 | result="" 55 | 56 | # Split the status into an array 57 | IFS=', ' read -r -a pow_zoomed_panes <<<"$(read_pz_status)" 58 | 59 | for pzp in "${pow_zoomed_panes[@]}"; do 60 | placeholder="$(echo "$pzp" | cut -d= -f 1)" 61 | zoomed="$(echo "$pzp" | cut -d= -f 2)" 62 | if [[ $zoomed = "$this_id" ]]; then 63 | if [[ $1 = "$is_zoomed" ]]; then 64 | # Since this check won't update the list of zoomed panes 65 | # its ok to return early 66 | return # implicit true 67 | elif [[ $1 = "$get_placeholder" ]]; then 68 | result=$placeholder 69 | do_update=true 70 | # this will result in unzooming, so don't save current pair 71 | # in the update 72 | continue 73 | fi 74 | elif [[ $placeholder = "$this_id" ]] && [[ $1 = "$get_zoomed" ]]; then 75 | result=$zoomed 76 | # won't be doing updates this run, this will just trigger recursion 77 | # by the caller, when unzooming and list update will happen, 78 | # so no need to complete the loop 79 | break 80 | fi 81 | updated_values="$updated_values $placeholder=$zoomed" 82 | done 83 | $do_update && set_pz_status "$updated_values" 84 | if [[ -n "$result" ]]; then 85 | # In this case a string is expected, so the implicit true return 86 | # has no significance 87 | echo "$result" 88 | else 89 | false 90 | fi 91 | } 92 | 93 | power_zoom() { 94 | if check_pz_status "$is_zoomed"; then 95 | log_it "was zoomed" 96 | # 97 | # Is a zoomed pane, un-zoom it 98 | # 99 | placeholder="$(check_pz_status "$get_placeholder")" 100 | 101 | if [[ -z $placeholder ]]; then 102 | error_msg "Placeholder for pane is not listed" 103 | fi 104 | $TMUX_BIN join-pane -b -t "$placeholder" 105 | $TMUX_BIN kill-pane -t "$placeholder" 106 | return 107 | fi 108 | zoomed="$(check_pz_status "$get_zoomed")" 109 | if [[ -n "$zoomed" ]]; then 110 | log_it "was placeholder" 111 | if [[ -n "$1" ]]; then 112 | error_msg "Recursion detected when unzooming" 113 | exit 99 114 | fi 115 | # 116 | # Keep code simple, only use one unzoom procedure 117 | # 118 | $TMUX_BIN select-window -t "$zoomed" 119 | power_zoom recursion 120 | else 121 | # 122 | # Zoom it! 123 | # 124 | log_it "will zoom" 125 | if [[ "$($TMUX_BIN list-panes | wc -l)" -eq 1 ]]; then 126 | error_msg "Can't zoom only pane in a window" 127 | exit 0 128 | fi 129 | this_id="$($TMUX_BIN display -p '#D')" 130 | # 131 | # the place-holder pane will close when it's process is terminated, 132 | # so keep a long sleep going for ever in a loop. 133 | # Ctrl-C would exit script and pane would close in case the zoomed pane 134 | # is killed and the place-holder is left hanging. 135 | # 136 | # shellcheck disable=SC2154 137 | trigger_key=$(get_tmux_option "@power_zoom_trigger" "$default_key") 138 | 139 | # 140 | # What an unexpected pain, doing a while loop in a sub shell fails if 141 | # the shel is fish. Luckily in this case I could wrap it in /bin/sh -c "foo" 142 | # 143 | $TMUX_BIN split-window -b "echo; \ 144 | echo \" placeholder for zoomed pane ${this_id}\"; \ 145 | echo ; echo \" You can press $trigger_key\"; \ 146 | echo \" in this pane to restore it back here...\"; \ 147 | /bin/sh -c \"while true ; do sleep 300; done\"" 148 | $TMUX_BIN select-pane -T "$placeholder_title" 149 | placholder_pane_id="$($TMUX_BIN display -p '#D')" 150 | set_pz_status "$(read_pz_status) $placholder_pane_id=$this_id" 151 | $TMUX_BIN select-pane -t "$this_id" 152 | $TMUX_BIN break-pane # move it to new window 153 | $TMUX_BIN rename-window "ZOOMED $this_id" 154 | fi 155 | } 156 | 157 | power_zoom 158 | -------------------------------------------------------------------------------- /scripts/utils.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Copyright (c) 2022,2025: Jacob.Lundqvist@gmail.com 4 | # License: MIT 5 | # 6 | # Part of https://github.com/jaclu/tmux-power-zoom 7 | # 8 | # Common stuff 9 | # 10 | # Since this is a POSIX script, all variables are global. To ensure that 11 | # a function does not overwrite a value for a caller, it's good practice 12 | # to always use function related prefixes on all variable names. 13 | # 14 | 15 | # 16 | # Shorthand, to avoid manually typing package name on multiple 17 | # locations, easily getting out of sync. 18 | # 19 | plugin_name="tmux-power-zoom" 20 | 21 | # 22 | # By using Z as default we don't overwrite the default zoom binding (z) 23 | # unless the caller actually want this to happen. 24 | # 25 | # shellcheck disable=SC2034 26 | default_key="Z" 27 | 28 | # 29 | # I use an env var TMUX_BIN to point at the current tmux, defined in my 30 | # tmux.conf, in order to pick the version matching the server running. 31 | # If not found, it is set to whatever is in path, so should have no negative 32 | # impact. In all calls to tmux I use $TMUX_BIN instead in the rest of this 33 | # plugin. 34 | # 35 | [[ -z "$TMUX_BIN" ]] && TMUX_BIN="tmux" 36 | 37 | # 38 | # If log_file is empty or undefined, no logging will occur, 39 | # so comment it out for normal usage. 40 | # 41 | # log_file=~/tmp/"$plugin_name".log 42 | 43 | log_it() { 44 | # If $log_file is empty or undefined, no logging will occur. 45 | if [[ -z "$log_file" ]]; then 46 | return 47 | fi 48 | printf "[%s] %s\n" "$(date '+%H:%M:%S')" "$@" >>"$log_file" 49 | } 50 | 51 | error_msg() { 52 | # 53 | # Display $1 as an error message in log and as a tmux display-message 54 | # If no $2 or set to 0, process is not exited 55 | # 56 | msg="ERROR: $1" 57 | exit_code="${2:-1}" 58 | 59 | log_it "$msg" 60 | $TMUX_BIN display-message "$plugin_name $msg" 61 | sleep 1 # ensure message doesn't get overwritten immeditally 62 | [[ "$exit_code" -gt 0 ]] && exit "$exit_code" 63 | } 64 | 65 | get_tmux_option() { 66 | local option 67 | local default_value 68 | local value 69 | 70 | option=$1 71 | default_value=$2 72 | value=$($TMUX_BIN show-option -gqv "$option") 73 | if [[ -z "$value" ]]; then 74 | echo "$default_value" 75 | else 76 | echo "$value" 77 | fi 78 | } 79 | 80 | lowercase_it() { 81 | echo "$1" | tr '[:upper:]' '[:lower:]' 82 | } 83 | 84 | normalize_bool_param() { 85 | # 86 | # Take a boolean style text param and convert it into an actual boolean 87 | # that can be used in your code. Example of usage: 88 | # 89 | # normalize_normalize_bool_param "@menus_without_prefix" "$default_no_prefix" && 90 | # cfg_no_prefix=true || cfg_no_prefix=false 91 | # 92 | # $cfg_no_prefix && echo "Don't use prefix" 93 | # 94 | local nbp_param="$1" 95 | local nbp_default="$2" # only used for tmux options 96 | local nbp_variable_name="" 97 | local prefix 98 | 99 | # log_it "normalize_normalize_bool_param($nbp_param, $nbp_default) [$nbp_variable_name]" 100 | [[ "${nbp_param%"${nbp_param#?}"}" = "@" ]] && { 101 | # 102 | # If it starts with "@", assume it is a tmux option, thus 103 | # read its value from the tmux environment. 104 | # In this case $2 must be given as the default value! 105 | # 106 | [[ -z "$nbp_default" ]] && { 107 | error_msg "normalize_normalize_bool_param($nbp_param) - no default" 108 | } 109 | nbp_variable_name="$nbp_param" 110 | nbp_param="$(tmux_get_option "$nbp_param" "$nbp_default")" 111 | } 112 | 113 | nbp_param="$(lowercase_it "$nbp_param")" 114 | 115 | case "$nbp_param" in 116 | # 117 | # Handle the unfortunate tradition in the tmux community to use 118 | # 1 to indicate selected / active. 119 | # This means that as far as these booleans go 1 is 0 and 0 is 1, how Orwellian... 120 | # 121 | 1 | yes | true) 122 | # Be a nice guy and accept some common positive notations 123 | return 0 124 | ;; 125 | 126 | 0 | no | false) 127 | # Be a nice guy and accept some common false notations 128 | return 1 129 | ;; 130 | 131 | *) 132 | if [[ -n "$nbp_variable_name" ]]; then 133 | prefix="$nbp_variable_name=$nbp_param" 134 | else 135 | prefix="$nbp_param" 136 | fi 137 | error_msg "$prefix - should be yes/true or no/false" 138 | ;; 139 | 140 | esac 141 | 142 | # Should never get here... 143 | log_it "Invalid parameter normalize_bool_param($1)" 144 | error_msg "normalize_normalize_bool_param() - failed to evaluate $nbp_param" 145 | } 146 | --------------------------------------------------------------------------------