All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
2 readonly XP_SHELL="/usr/bin/env bash"
4 # @Author Yamada, Yasuhiro
8 readonly XP_VERSION="4.1.3"
10 ## trap might be updated in 'xpns_pre_execution' function
11 trap 'rm -f "${XP_CACHE_HOME}"/__xpns_*$$; xpns_clean_session' EXIT
13 ## --------------------------------
15 ## --------------------------------
16 # Invalid option/argument
25 # Impossible layout: Small pane
26 readonly XP_ESMLPANE=7
28 # Log related exit status is 2x.
29 ## Could not create a directory.
30 readonly XP_ELOGDIR=20
32 ## Could not directory to store logs is not writable.
33 readonly XP_ELOGWRITE=21
35 # User's intentional exit is 3x
36 ## User exit the process intentionally by following warning message.
37 readonly XP_EINTENT=30
39 ## All the panes are closed before processing due to user's options/command.
40 readonly XP_ENOPANE=31
42 # Necessary commands are not found
43 readonly XP_ENOCMD=127
47 # XP_THIS_FILE_NAME is supposed to be "xpanes".
48 readonly XP_THIS_FILE_NAME="${0##*/}"
49 readonly XP_THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
50 readonly XP_ABS_THIS_FILE_NAME="${XP_THIS_DIR}/${XP_THIS_FILE_NAME}"
52 # Prevent cache directory being created under root / directory in any case.
53 # This is quite rare case (but it can be happened).
54 readonly XP_USER_HOME="${HOME:-/tmp}"
56 # Basically xpanes follows XDG Base Direcotry Specification.
57 # https://specifications.freedesktop.org/basedir-spec/basedir-spec-0.6.html
58 XDG_CACHE_HOME="${XDG_CACHE_HOME:-${XP_USER_HOME}/.cache}"
59 readonly XP_CACHE_HOME="${XDG_CACHE_HOME}/xpanes"
61 # This is supposed to be xpanes-12345(PID)
62 readonly XP_SESSION_NAME="${XP_THIS_FILE_NAME}-$$"
63 # Temporary window name is tmp-12345(PID)
64 readonly XP_TMP_WIN_NAME="tmp-$$"
65 readonly XP_EMPTY_STR="EMPTY"
67 readonly XP_SUPPORT_TMUX_VERSION_LOWER="1.8"
69 # Check dependencies just in case.
70 # Even POSIX compliant commands are only used in this program.
71 # `xargs`, `sleep`, `mkfifo` are omitted because minimum functions can work without them.
72 readonly XP_DEPENDENCIES="${XP_DEPENDENCIES:-tmux grep sed tr od echo touch printf cat sort pwd cd mkfifo}"
74 ## --------------------------------
75 # User customizable shell variables
76 ## --------------------------------
77 TMUX_XPANES_EXEC=${TMUX_XPANES_EXEC:-tmux}
78 TMUX_XPANES_PANE_BORDER_FORMAT="${TMUX_XPANES_PANE_BORDER_FORMAT:-#[bg=green,fg=black] #T #[default]}"
79 TMUX_XPANES_PANE_BORDER_STATUS="${TMUX_XPANES_PANE_BORDER_STATUS:-bottom}"
80 TMUX_XPANES_PANE_DEAD_MESSAGE=${TMUX_XPANES_PANE_DEAD_MESSAGE:-'\033[41m\033[4m\033[30m Pane is dead: Press [Enter] to exit... \033[0m\033[39m\033[49m'}
81 XP_DEFAULT_TMUX_XPANES_LOG_FORMAT="[:ARG:].log.%Y-%m-%d_%H-%M-%S"
82 TMUX_XPANES_LOG_FORMAT="${TMUX_XPANES_LOG_FORMAT:-${XP_DEFAULT_TMUX_XPANES_LOG_FORMAT}}"
83 XP_DEFAULT_TMUX_XPANES_LOG_DIRECTORY="${XP_CACHE_HOME}/logs"
84 TMUX_XPANES_LOG_DIRECTORY="${TMUX_XPANES_LOG_DIRECTORY:-${XP_DEFAULT_TMUX_XPANES_LOG_DIRECTORY}}"
86 ## --------------------------------
88 ## --------------------------------
89 # options which work individually.
90 # readonly XP_FLAG_OPTIONS="[hVdetxs]"
91 # options which need arguments.
92 readonly XP_ARG_OPTIONS="[ISclnCRB]"
93 readonly XP_DEFAULT_LAYOUT="tiled"
94 readonly XP_DEFAULT_REPSTR="{}"
95 readonly XP_DEFAULT_CMD_UTILITY="echo {} "
96 readonly XP_SSH_CMD_UTILITY="ssh -o StrictHostKeyChecking=no {} "
97 readonly XP_OFS="${XP_OFS:- }"
108 XP_DEFAULT_SOCKET_PATH_BASE="${XP_CACHE_HOME}/socket"
109 XP_DEFAULT_SOCKET_PATH="${XP_DEFAULT_SOCKET_PATH_BASE}.$$"
110 XP_SOCKET_PATH="${XP_SOCKET_PATH:-${XP_DEFAULT_SOCKET_PATH}}"
114 XP_LAYOUT="${XP_DEFAULT_LAYOUT}"
117 XP_OPT_CHANGE_BORDER=0
120 XP_OPT_SPEEDY_AWAIT=0
121 XP_OPT_USE_PRESET_LAYOUT=0
122 XP_OPT_CUSTOM_SIZE_COLS=
123 XP_OPT_CUSTOM_SIZE_ROWS=
130 XP_OPT_IGNORE_SIZE_LIMIT=0
132 ## --------------------------------
134 # $1 -- Log level (i.e Warning, Error)
137 # xpanes:Error: invalid option.
139 # This log format is created with reference to openssl's one.
140 # $ echo | openssl -a
141 # openssl:Error: '-a' is an invalid command.
142 ## --------------------------------
146 local _msg="${XP_THIS_FILE_NAME}:${_loglevel}: ${_msgbody}"
147 printf "%s\\n" "${_msg}" >&2
155 xpns_msg "Warning" "$1"
159 if [[ $XP_OPT_DEBUG -eq 1 ]]; then
160 xpns_msg "Debug" "$(date "+[%F_%T]"):${FUNCNAME[1]}:$1"
165 xpns_msg "Error" "$1"
170 echo "Try '${XP_THIS_FILE_NAME} --help' for more information." >&2
175 Usage: ${XP_THIS_FILE_NAME} [OPTIONS] [argument ...]
176 Usage(Pipe mode): command ... | ${XP_THIS_FILE_NAME} [OPTIONS] [<command> ...]
183 ${XP_THIS_FILE_NAME} [OPTIONS] [argument ...]
186 command ... | ${XP_THIS_FILE_NAME} [OPTIONS] [<command> ...]
189 -h,--help Display this help and exit.
190 -V,--version Output version information and exit.
191 -B <begin-command> Run <begin-command> before processing <command> in each pane. Multiple options are allowed.
192 -c <command> Set <command> to be executed in each pane. Default is \`echo {}\`.
193 -d,--desync Make synchronize-panes option off in new window.
194 -e Execute given arguments as is. Same as \`-c '{}'\`
195 -I <repstr> Replacing one or more occurrences of <repstr> in command provided by -c or -B. Default is \`{}\`.
196 -C NUM,--cols=NUM Number of columns of window layout.
197 -R NUM,--rows=NUM Number of rows of window layout.
198 -l <layout> Set the preset of window layout. Recognized layout arguments are:
204 -n <number> Set the maximum number of <argument> taken for each pane.
205 -s Speedy mode: Run command without opening an interactive shell.
206 -ss Speedy mode AND close a pane automatically at the same time as process exiting.
207 -S <socket-path> Set a full alternative path to the server socket.
208 -t Display each argument on the each pane's border as their title.
209 -x Create extra panes in the current active window.
210 --log[=<directory>] Enable logging and store log files to ~/.cache/xpanes/logs or <directory>.
211 --log-format=<FORMAT> Make name of log files follow <FORMAT>. Default is \`${XP_DEFAULT_TMUX_XPANES_LOG_FORMAT}\`.
212 --ssh Same as \`-t -s -c 'ssh -o StrictHostKeyChecking=no {}'\`.
213 --stay Do not switch to new window.
214 --bulk-cols=NUM1[,NUM2 ...] Set number of columns on multiple rows (i.e, "2,2,2" represents 2 cols x 3 rows).
215 --debug Print debug message.
217 Copyright (c) 2021 Yamada, Yasuhiro
218 Released under the MIT License.
219 https://github.com/greymd/tmux-xpanes
223 # Show version number
225 echo "${XP_THIS_FILE_NAME} ${XP_VERSION}"
228 # Get version number for tmux
229 xpns_get_tmux_version() {
230 local _tmux_version=""
231 if ! ${TMUX_XPANES_EXEC} -V &> /dev/null; then
232 # From tmux 0.9 to 1.3, there is no -V option.
233 _tmux_version="tmux 0.9-1.3"
235 _tmux_version="$( ${TMUX_XPANES_EXEC} -V)"
237 read -r _ _ver <<< "${_tmux_version}"
238 # Strip the leading "next-" part that is present in tmux versions that are
239 # in development. Eg: next-3.3
240 echo "${_ver//next-/}"
243 # Check whether the prefered tmux version is greater than host's tmux version.
244 # $1 ... Prefered version.
245 # $2 ... Host tmux version(optional).
246 # In case of tmux version is 1.7, the result will be like this.
247 # 0 is true, 1 is false.
256 xpns_tmux_is_greater_equals() {
257 local _check_version="$1"
258 local _tmux_version="${2:-$(xpns_get_tmux_version)}"
259 # Simple numerical comparison does not work because there is the version like "1.9a".
260 if [[ "$( printf "%s\\n%s" "${_tmux_version}" "${_check_version}" | sort -n | head -n 1)" != "${_check_version}" ]]; then
267 xpns_get_local_tmux_conf() {
268 local _conf_name="$1"
269 local _session="${2-}"
271 if [[ -z "${_session-}" ]]; then
272 ${TMUX_XPANES_EXEC} show-window-options
274 ${TMUX_XPANES_EXEC} -S "${_session}" show-window-options
276 } | grep "^${_conf_name}" |
279 printf "%s\\n" "${_v}"
283 xpns_get_global_tmux_conf() {
284 local _conf_name="$1"
285 local _session="${2-}"
287 if [[ -z "${_session-}" ]]; then
288 ${TMUX_XPANES_EXEC} show-window-options -g
290 ${TMUX_XPANES_EXEC} -S "${_session}" show-window-options -g
292 } | grep "^${_conf_name}" |
295 printf "%s\\n" "${_v}"
299 # Disable allow-rename because
300 # window separation does not work correctly
301 # if "allow-rename" option is on
302 xpns_suppress_allow_rename() {
303 local _default_allow_rename="$1"
304 local _session="${2-}"
305 if [[ "${_default_allow_rename-}" == "on" ]]; then
306 ## Temporary, disable "allow-rename"
307 xpns_msg_debug "'allow-rename' option is 'off' temporarily."
308 if [[ -z "${_session-}" ]]; then
309 ${TMUX_XPANES_EXEC} set-window-option -g allow-rename off
311 ${TMUX_XPANES_EXEC} -S "${_session}" set-window-option -g allow-rename off
316 # Restore default "allow-rename"
317 # Do not write like 'xpns_restore_allow_rename "some value" "some value" > /dev/null'
318 # In tmux 1.6, 'tmux set-window-option' might be stopped in case of redirection.
319 xpns_restore_allow_rename() {
320 local _default_allow_rename="$1"
321 local _session="${2-}"
322 if [[ "${_default_allow_rename-}" == "on" ]]; then
323 xpns_msg_debug "Restore original value of 'allow-rename' option."
324 if [[ -z "${_session-}" ]]; then
325 ${TMUX_XPANES_EXEC} set-window-option -g allow-rename on
327 ${TMUX_XPANES_EXEC} -S "${_session}" set-window-option -g allow-rename on
334 # 11 / 2 = 5.5 => ceiling => 6
339 printf "%s\\n" $(((_divide + _by - 1) / _by))
344 # Divide 10 into 3 parts as equally as possible.
345 xpns_divide_equally() {
349 local _upper _lower _upper_count _lower_count
350 _upper="$(xpns_ceiling "$_number" "$_count")"
351 _lower=$((_upper - 1))
352 _lower_count=$((_upper * _count - _number))
353 _upper_count=$((_count - _lower_count))
354 eval "printf '${_upper} %.0s' {1..$_upper_count}"
355 ((_lower_count > 0)) && eval "printf '${_lower} %.0s' {1..$_lower_count}"
358 # echo 3 3 3 3 | func
360 xpns_nums_accumulate_sum() {
365 done < <( cat | tr ' ' '\n')
371 # For example, "3 2 2 2" represents following cell positions
373 # 1 [] [] [] => 3 rows
378 # After the transposition, it must be "4 4 1" which represents below
380 # 1 [] [] [] [] => 4 rows
381 # 2 [] [] [] [] => 4 rows
383 xpns_nums_transpose() {
387 xpns_msg_debug "column num = $_colnum, input = $*"
388 _spaces="$(for i in "$@"; do
392 # 'for' statement does not work somehow
393 _result="$(while read -r i; do
394 ## This part is depending on the following 'cut' behavior
395 ## $ echo 1234 | cut -c 5
396 ## => result is supposed to be empty
397 printf "%s\\n" "$_spaces" | cut -c "$i" | grep -c ' '
398 done < <(xpns_seq 1 "${_colnum}") | xpns_newline2space)"
399 xpns_msg_debug "result = $_result"
400 printf "%s\\n" "$_result"
403 # Adjust size of columns and rows in accordance with given N
404 # func <col> <row> <N>
410 xpns_adjust_col_row() {
419 ((col != 0)) && fix_col_flg=1 || fix_col_flg=0
420 ((row != 0)) && fix_row_flg=1 || fix_row_flg=0
422 # This is just a author (@greymd)'s preference.
423 if ((fix_col_flg == 0)) && ((fix_row_flg == 0)) && ((N == 2)); then
426 printf "%d %d\\n" "${col}" "${row}"
430 # If both values are provided, col is used.
431 if ((fix_col_flg == 1)) && ((fix_row_flg == 1)); then
435 # This algorhythm is almost same as tmux default
436 # https://github.com/tmux/tmux/blob/2.8/layout-set.c#L436
437 while ((col * row < N)); do
438 ((fix_row_flg != 1)) && ((row = row + 1))
439 if ((col * row < N)); then
440 ((fix_col_flg != 1)) && ((col = col + 1))
443 printf "%d %d\\n" "${col}" "${row}"
446 # Make each line unique by adding index number
447 # echo aaa bbb ccc aaa ccc ccc | xargs -n 1 | xpns_unique_line
455 # Eval is used because associative array is not supported before bash 4.2
458 while read -r line; do
459 _val_name="__xpns_hash_$(printf "%s" "${line}" | xpns_value2key)"
460 # initialize variable
461 eval "${_val_name}=\${${_val_name}:-0}"
463 eval "${_val_name}=\$(( ++${_val_name} ))"
464 printf "%s\\n" "${line}-$(eval printf "%s" "\$${_val_name}")"
469 # Generate log file names from given arguments.
471 # echo <arg1> <arg2> ... | xpns_log_filenames <FORMAT>
475 # $ echo aaa bbb ccc aaa ccc ccc | xargs -n 1 | xpns_log_filenames '[:ARG:]_[:PID:]_%Y%m%d.log'
476 # aaa-1_1234_20160101.log
477 # bbb-1_1234_20160101.log
478 # ccc-1_1234_20160101.log
479 # aaa-2_1234_20160101.log
480 # ccc-2_1234_20160101.log
481 # ccc-3_1234_20160101.log
483 xpns_log_filenames() {
486 _full_fmt="$(date "+${_arg_fmt}")"
488 # 1st argument + '-' + unique number (avoid same argument has same name)
490 while read -r _arg; do
491 cat <<< "${_full_fmt}" |
492 sed "s/\\[:ARG:\\]/${_arg}/g" |
493 sed "s/\\[:PID:\\]/$$/g"
497 ## --------------------------------
498 # Normalize directory by making following conversion.
500 # * Remove the slash '/' at the end of the dirname.
502 # xpns_normalize_directory <direname>
504 # Normalized <dirname>
505 ## --------------------------------
506 xpns_normalize_directory() {
508 # Remove end of slash '/'
511 _dir="${_dir/#~/${HOME}}"
512 printf "%s\\n" "${_dir}"
515 ## --------------------------------
516 # Ensure existence of given directory
518 # xpns_is_valid_directory <direname>
520 # Absolute path of the <dirname>
521 ## --------------------------------
522 xpns_is_valid_directory() {
524 local _checkfile="${XP_THIS_FILE_NAME}.$$"
526 if [[ ! -d "${_dir}" ]]; then
528 if mkdir "${_dir}"; then
529 xpns_msg_info "${_dir} is created."
531 xpns_msg_error "Failed to create ${_dir}"
535 # Try to create file.
536 # Not only checking directory permission,
537 # but also i-node and other misc situations.
538 if ! touch "${_dir}/${_checkfile}"; then
539 xpns_msg_error "${_dir} is not writable."
540 rm -f "${_dir}/${_checkfile}"
543 rm -f "${_dir}/${_checkfile}"
546 # Convert array to string which is can be used as command line argument.
548 # xpns_arr2args <array object>
550 # array=(aaa bbb "ccc ddd" eee "f'f")
551 # xpns_arr2args "${array[@]}"
552 # @returns "'aaa' 'bbb' 'ccc ddd' 'eee' 'f\'f'"
556 # If there is no argument, usage will be shown.
557 if [[ $# -lt 1 ]]; then
562 # Use 'cat <<<"input"' command instead of 'echo',
563 # because such the command recognizes option like '-e'.
565 # Escaping single quotations.
566 sed "s/'/'\"'\"'/g" |
567 # Surround argument with single quotations.
568 sed "s/^/'/;s/$/' /" |
574 # Extract first field to generate window name.
575 # ex, $2 = 'aaa bbb ccc'
576 # return = aaa-12345(PID)
577 xpns_generate_window_name() {
578 local _unprintable_str="${1-}"
580 # Leave first 200 characters to prevent
581 # the name exceed the maximum length of tmux window name (2000 byte).
582 printf "%s\\n" "${1:-${_unprintable_str}}" |
583 ( read -r _name _ && printf "%s\\n" "${_name:0:200}-$$" )
586 # Convert any string (including multi-byte chars) to another string
587 # which can be handled as tmux window name.
589 od -v -tx1 -An | tr -dc 'a-zA-Z0-9' | tr -d '\n'
592 # Restore string encoded by xpns_value2key function.
595 # shellcheck disable=SC2059
596 printf "$(printf "%s" "$_key" | sed 's/../\\x&/g')"
600 # This function behaves like `awk NF`
601 xpns_rm_empty_line() {
605 } | while IFS= read -r line; do
606 # shellcheck disable=SC2086
608 if [[ $# != 0 ]]; then
609 printf "%s\\n" "${line}"
614 # Enable logging feature to the all the panes in the target window.
615 xpns_enable_logging() {
616 local _window_name="$1"
618 local _index_offset="$1"
622 local _log_format="$1"
624 local _unprintable_str="$1"
627 local _args_num=$(($# - 1))
628 # Generate log files from arguments.
630 while read -r _logfile; do
632 xpns_msg_debug "Start logging pipe-pane(cat >> '${_log_dir}/${_logfile}')"
633 ${TMUX_XPANES_EXEC} \
634 pipe-pane -t "${_window_name}.$((_idx + _index_offset))" \
635 "cat >> '${_log_dir}/${_logfile}'" # Tilde expansion does not work here.
638 for i in $(xpns_seq 0 "${_args_num}"); do
639 # Replace empty string.
640 printf "%s\\n" "${_args[i]:-${_unprintable_str}}"
641 done | xpns_log_filenames "${_log_format}"
645 ## Print "1" on the particular named pipe
650 _fifo="${XP_CACHE_HOME}/__xpns_${_wait_id}"
651 xpns_msg_debug "Notify to $_fifo"
652 printf "%s\\n" 1 > "$_fifo" &
655 xpns_notify_logging() {
656 local _window_name="$1"
658 local _args_num=$(($# - 1))
659 for i in $(xpns_seq 0 "${_args_num}"); do
660 xpns_notify "log_${_window_name}-${i}-$$"
665 local _window_name="$1"
667 local _args_num=$(($# - 1))
668 for i in $(xpns_seq 0 "${_args_num}"); do
669 xpns_notify "sync_${_window_name}-${i}-$$" &
673 xpns_is_window_alive() {
674 local _window_name="$1"
676 local _speedy_await_flag="$1"
678 local _def_allow_rename="$1"
680 if ! ${TMUX_XPANES_EXEC} display-message -t "$_window_name" -p > /dev/null 2>&1; then
681 xpns_msg_info "All the panes are closed before displaying the result."
682 if [[ "${_speedy_await_flag}" -eq 0 ]]; then
683 xpns_msg_info "Use '-s' option instead of '-ss' option to avoid this behavior."
685 xpns_restore_allow_rename "${_def_allow_rename-}"
690 xpns_inject_title() {
691 local _target_pane="$1"
696 _pane_tty="$( ${TMUX_XPANES_EXEC} display-message -t "${_target_pane}" -p "#{pane_tty}")"
697 printf "\\033]2;%s\\033\\\\" "${_message}" > "${_pane_tty}"
698 xpns_msg_debug "target_pane=${_target_pane} pane_title=${_message} pane_tty=${_pane_tty}"
701 xpns_is_pane_title_required() {
702 local _title_flag="$1"
704 local _extra_flag="$1"
706 local _pane_border_status=
707 _pane_border_status=$(xpns_get_local_tmux_conf "pane-border-status")
708 if [[ $_title_flag -eq 1 ]]; then
710 elif [[ ${_extra_flag} -eq 1 ]] &&
711 [[ "${_pane_border_status}" != "off" ]] &&
712 [[ -n "${_pane_border_status}" ]]; then
714 # Even the -t option is not specified, it is required to inject pane title here.
715 # Because user expects the title is displayed on the pane if the original window is
716 # generated from tmux-xpanes with -t option.
722 # Set pane titles for each pane for -t option
724 local _window_name="$1"
726 local _index_offset="$1"
731 _pane_index=$((_index + _index_offset))
732 xpns_inject_title "${_window_name}.${_pane_index}" "${arg}"
733 _index=$((_index + 1))
737 # Send command to the all the panes in the target window.
738 xpns_send_commands() {
739 local _window_name="$1"
741 local _index_offset="$1"
751 _exec_cmd="${_cmd//${_repstr}/${arg}}"
752 _pane_index=$((_index + _index_offset))
753 ${TMUX_XPANES_EXEC} send-keys -t "${_window_name}.${_pane_index}" "${_exec_cmd}" C-m
754 _index=$((_index + 1))
758 # Separate window vertically, when the number of panes is 1 or 2.
759 xpns_organize_panes() {
760 local _window_name="$1"
766 if [[ "${_args_num}" -eq 1 ]]; then
767 ${TMUX_XPANES_EXEC} select-layout -t "${_window_name}" even-horizontal
768 elif [[ "${_args_num}" -gt 1 ]]; then
769 ${TMUX_XPANES_EXEC} select-layout -t "${_window_name}" tiled
774 if [[ "${XP_LAYOUT}" != "${XP_DEFAULT_LAYOUT}" ]]; then
775 ${TMUX_XPANES_EXEC} select-layout -t "${_window_name}" "${XP_LAYOUT}"
780 # Generate sequential number descending order.
781 # seq is not used because old version of
782 # seq does not generate descending order.
792 eval "printf \"%d\\n\" {$_num1..$_num2}"
797 local _fifo="${XP_CACHE_HOME}/__xpns_${_wait_id}"
798 local _arr=("$_fifo")
800 _fifo_arg=$(xpns_arr2args "${_arr[@]}")
801 xpns_msg_debug "mkfifo $_fifo"
803 xpns_msg_debug "grep -q 1 ${_fifo_arg}"
804 printf "%s\\n" "grep -q 1 ${_fifo_arg}"
807 # Split a new window into multiple panes.
809 xpns_split_window() {
810 local _window_name="$1"
814 local _title_flag="$1"
816 local _speedy_flag="$1"
818 local _await_flag="$1"
820 local _pane_base_index="$1"
824 local _cmd_template="$1"
829 _last_idx=$((${#args[@]} - 1))
831 for i in $(xpns_seq $_last_idx 0); do
832 xpns_msg_debug "Index:${i} Argument:${args[i]}"
833 _sep_count=$((_sep_count + 1))
834 _exec_cmd="${_cmd_template//${_repstr}/${args[i]}}"
837 if [[ $_speedy_flag -eq 1 ]]; then
839 _exec_cmd=$(xpns_inject_wait_command "${_log_flag}" "${_title_flag}" "${_speedy_flag}" "${_await_flag}" "$i" "${_exec_cmd}")
840 # Execute command as a child process of default-shell.
841 ${TMUX_XPANES_EXEC} split-window -t "${_window_name}" -h -d "${_exec_cmd}"
843 # Open login shell and execute command on the interactive screen.
844 ${TMUX_XPANES_EXEC} split-window -t "${_window_name}" -h -d
846 # Restraining that size of pane's width becomes
847 # less than the minimum size which is defined by tmux.
848 if [[ ${_sep_count} -gt 2 ]]; then
849 ${TMUX_XPANES_EXEC} select-layout -t "${_window_name}" tiled
855 # Create new panes on existing window.
857 # func <window name> <offset of index> <number of pane>
859 xpns_prepare_extra_panes() {
860 local _window_name="$1"
862 local _pane_base_index="$1"
866 local _title_flag="$1"
868 local _speedy_flg="$1"
870 local _await_flg="$1"
872 # specify a pane which has the biggest index number.
873 # Because pane_id may not be immutable.
874 # If the small number of index is specified here, correspondance between pane_title and command can be slip off.
875 ${TMUX_XPANES_EXEC} select-pane -t "${_window_name}.${_pane_base_index}"
877 # split window into multiple panes
884 "${_pane_base_index}" \
888 xpns_get_joined_begin_commands() {
890 if [[ "${#XP_BEGIN_ARGS[*]}" -lt 1 ]]; then
891 printf "%s" "${_commands}"
894 printf "%s\\n" "${XP_BEGIN_ARGS[@]}" "${_commands}"
897 xpns_inject_wait_command() {
900 local _title_flag="$1"
902 local _speedy_flg="$1"
904 local _await_flg="$1"
911 ## Speedy mode + logging
912 if [[ "${_log_flag}" -eq 1 ]] && [[ "${_speedy_flg}" -eq 1 ]]; then
913 # Wait for start of logging
914 # Without this part, logging thread may start after new process is finished.
915 # Execute function to wait for logging start.
916 _exec_cmd="$(xpns_wait_func "log_${_window_name}-${_idx}-$$")"$'\n'"${_exec_cmd}"
919 ## Speedy mode (Do not allow to close panes before the separation is finished).
920 if [[ "${_speedy_flg}" -eq 1 ]]; then
921 _exec_cmd="$(xpns_wait_func "sync_${_window_name}-${_idx}-$$")"$'\n'${_exec_cmd}
924 ## -s: Speedy mode (Not -ss: Speedy mode + nowait)
925 if [[ "${_await_flg}" -eq 1 ]]; then
927 _msg="$(xpns_arr2args "${TMUX_XPANES_PANE_DEAD_MESSAGE}" | sed 's/"/\\"/g')"
928 _exec_cmd="${_exec_cmd}"$'\n'"${XP_SHELL} -c \"printf -- ${_msg} >&2 && read\""
930 printf "%s" "${_exec_cmd}"
934 local _window_name="$1"
936 local _attach_flg="$1"
938 local _speedy_flg="$1"
945 if [[ "${_attach_flg}" -eq 1 ]]; then
946 if [[ "${_speedy_flg}" -eq 1 ]]; then
947 _window_id=$(${TMUX_XPANES_EXEC} new-window -n "${_window_name}" -F '#{window_id}' -P "${_exec_cmd}")
949 _window_id=$(${TMUX_XPANES_EXEC} new-window -n "${_window_name}" -F '#{window_id}' -P)
953 if [[ "${_speedy_flg}" -eq 1 ]]; then
954 _window_id=$(${TMUX_XPANES_EXEC} new-window -n "${_window_name}" -F '#{window_id}' -P -d "${_exec_cmd}")
956 _window_id=$(${TMUX_XPANES_EXEC} new-window -n "${_window_name}" -F '#{window_id}' -P -d)
959 printf "%s" "${_window_id}"
962 xpns_new_pane_vertical() {
963 local _window_id="$1"
965 local _cell_height="$1"
967 local _speedy_flg="$1"
972 if [[ "${_speedy_flg}" -eq 1 ]]; then
973 _pane_id="$(${TMUX_XPANES_EXEC} split-window -t "$_window_id" -v -d -l "${_cell_height}" -F '#{pane_id}' -P "${_exec_cmd}")"
975 _pane_id="$(${TMUX_XPANES_EXEC} split-window -t "$_window_id" -v -d -l "${_cell_height}" -F '#{pane_id}' -P)"
977 printf "%s\\n" "${_pane_id}"
980 xpns_split_pane_horizontal() {
981 local _target_pane_id="$1"
983 local _cell_width="$1"
985 local _speedy_flg="$1"
989 if [[ "${_speedy_flg}" -eq 1 ]]; then
990 ${TMUX_XPANES_EXEC} split-window -t "$_target_pane_id" -h -d -l "$_cell_width" "${_exec_cmd}"
992 ${TMUX_XPANES_EXEC} split-window -t "$_target_pane_id" -h -d -l "$_cell_width"
996 xpns_prepare_window() {
997 local _window_name="$1"
1001 local _title_flag="$1"
1003 local _attach_flg="$1"
1005 local _speedy_flg="$1"
1007 local _await_flg="$1"
1011 local _cmd_template="$1"
1014 local _window_height="$XP_WINDOW_HEIGHT"
1015 local _window_width="$XP_WINDOW_WIDTH"
1016 local _col="$XP_OPT_CUSTOM_SIZE_COLS"
1017 local _row="$XP_OPT_CUSTOM_SIZE_ROWS"
1018 local _cols=("${XP_COLS[@]}")
1019 local _cols_offset=("${XP_COLS_OFFSETS[@]}")
1022 local _first_pane_id=
1026 local _top_pane_height=
1027 local _current_pane_width=
1034 _cell_height=$(((_window_height - _row + 1) / _row))
1035 ## Insert first element
1036 _exec_cmd="${_cmd_template//${_repstr}/${_args[0]}}"
1037 _exec_cmd="$(xpns_inject_wait_command "${_log_flag}" "${_title_flag}" "${_speedy_flg}" "${_await_flg}" 0 "${_exec_cmd}")"
1038 _window_id=$(xpns_new_window "${_window_name}" "${_attach_flg}" "${_speedy_flg}" "${_exec_cmd}")
1039 _first_pane_id=$(${TMUX_XPANES_EXEC} display-message -t "$_window_id" -p -F '#{pane_id}' | head -n 1)
1041 ## Start from last row
1042 for ((i = _row - 1; i > 0; i--)); do
1044 _cell_width=$(((_window_width - _col + 1) / _col))
1045 xpns_msg_debug "_col=$_col"
1046 ((_offset = _cols_offset[i]))
1047 for ((j = 0; j < _col; j++)); do
1049 ((idx = _offset - _col))
1051 # Insert first element of the row first
1052 _exec_cmd="${_cmd_template//${_repstr}/${_args[idx]}}"
1053 _exec_cmd="$(xpns_inject_wait_command "${_log_flag}" "${_title_flag}" "${_speedy_flg}" "${_await_flg}" "${idx}" "${_exec_cmd}")"
1054 _pane_id=$(xpns_new_pane_vertical "${_window_name}" "${_cell_height}" "${_speedy_flg}" "${_exec_cmd}")
1056 # Separate row into columns
1058 ((idx = _offset - j))
1059 _exec_cmd="${_cmd_template//${_repstr}/${_args[idx]}}"
1060 _exec_cmd="$(xpns_inject_wait_command "${_log_flag}" "${_title_flag}" "${_speedy_flg}" "${_await_flg}" "${idx}" "${_exec_cmd}")"
1061 ## Separate row into columns
1062 _current_pane_width=$(${TMUX_XPANES_EXEC} display-message -t "$_pane_id" -p '#{pane_width}' | head -n 1)
1063 _rest_col=$((_col - j + 1))
1064 _cell_width=$(((_current_pane_width - _rest_col + 1) / _rest_col))
1065 xpns_split_pane_horizontal "$_pane_id" "$_cell_width" "${_speedy_flg}" "${_exec_cmd}"
1070 _top_pane_height=$(${TMUX_XPANES_EXEC} display-message -t "$_window_id" -p '#{pane_height}' | head -n 1)
1072 xpns_msg_debug "_top_pane_height=$_top_pane_height _rest_row=$_rest_row"
1073 _cell_height=$(((_top_pane_height - _rest_row + 1) / _rest_row))
1076 # Split first row into columns
1078 _cell_width=$(((_window_width - _col + 1) / _col))
1079 for ((j = 1; j < _col; j++)); do
1080 idx=$((_cols_offset[0] - j))
1082 _current_pane_width=$(${TMUX_XPANES_EXEC} display-message -t "$_first_pane_id" -p '#{pane_width}' | head -n 1)
1083 _rest_col=$((_col - j + 1))
1084 _cell_width=$(((_current_pane_width - _rest_col + 1) / _rest_col))
1085 ## Split top row into columns
1086 _exec_cmd="${_cmd_template//${_repstr}/${_args[idx]}}"
1087 _exec_cmd="$(xpns_inject_wait_command "${_log_flag}" "${_title_flag}" "${_speedy_flg}" "${_await_flg}" "${idx}" "${_exec_cmd}")"
1088 xpns_split_pane_horizontal "${_first_pane_id}" "${_cell_width}" "${_speedy_flg}" "${_exec_cmd}"
1092 xpns_is_session_running() {
1094 ${TMUX_XPANES_EXEC} -S "${_socket}" list-session > /dev/null 2>&1
1097 # Remove unnecessary session files as much as possible
1098 # to make xpanes avoid loading old .tmux.conf.
1099 xpns_clean_session() {
1100 if [[ "${XP_SOCKET_PATH}" != "${XP_DEFAULT_SOCKET_PATH}" ]]; then
1103 # Delete old socket file (xpanes v3.1.0 or before).
1104 if [[ -e "${XP_DEFAULT_SOCKET_PATH_BASE}" ]]; then
1105 if ! xpns_is_session_running "${XP_DEFAULT_SOCKET_PATH_BASE}"; then
1106 xpns_msg_debug "socket(${XP_DEFAULT_SOCKET_PATH_BASE}) is not running. Remove it"
1107 rm -f "${XP_DEFAULT_SOCKET_PATH_BASE}"
1110 for _socket in "${XP_CACHE_HOME}"/socket.*; do
1111 xpns_msg_debug "file = ${_socket}"
1112 if ! xpns_is_session_running "${_socket}"; then
1113 xpns_msg_debug "socket(${_socket}) is not running. Remove it"
1116 xpns_msg_debug "socket(${_socket}) is running. Keep ${_socket}"
1122 # Split a new window into multiple panes.
1124 # xpns_prepare_preset_layout_window <window name> <offset of index> <number of pane> <attach or not>
1126 xpns_prepare_preset_layout_window() {
1127 local _window_name="$1"
1129 local _pane_base_index="$1"
1131 local _log_flag="$1"
1133 local _title_flag="$1"
1135 local _attach_flg="$1"
1137 local _speedy_flg="$1"
1139 local _await_flg="$1"
1141 # Create new window.
1142 if [[ "${_attach_flg}" -eq 1 ]]; then
1143 ${TMUX_XPANES_EXEC} new-window -n "${_window_name}"
1146 ${TMUX_XPANES_EXEC} new-window -n "${_window_name}" -d
1149 # specify a pane which has the youngest number of index.
1150 ${TMUX_XPANES_EXEC} select-pane -t "${_window_name}.${_pane_base_index}"
1152 # split window into multiple panes
1159 "${_pane_base_index}" \
1162 ### If the first pane is still remaining,
1163 ### panes cannot be organized well.
1164 # Delete the first pane
1165 ${TMUX_XPANES_EXEC} kill-pane -t "${_window_name}.${_pane_base_index}"
1167 # Select second pane here.
1168 # If the command gets error, it would most likely be caused by user (XP_ENOPANE).
1169 # Suppress error message here and announce it in xpns_execution.
1170 ${TMUX_XPANES_EXEC} select-pane -t "${_window_name}.${_pane_base_index}" > /dev/null 2>&1
1173 # Check whether given command is in the PATH or not.
1176 while read -r cmd; do
1177 if ! type "${cmd}" > /dev/null 2>&1; then
1178 if [[ "${cmd}" == "tmux" ]] && [[ "${TMUX_XPANES_EXEC}" == "tmux" ]]; then
1179 xpns_msg_error "${cmd} is required. Install ${cmd} or set TMUX_XPANES_EXEC variable."
1181 elif [[ "${cmd}" != "tmux" ]]; then
1182 xpns_msg_error "${cmd} is required."
1186 done < <(echo "${_cmds}" | tr ' ' '\n')
1188 if ! mkdir -p "${XP_CACHE_HOME}"; then
1189 xpns_msg_warning "failed to create cache directory '${XP_CACHE_HOME}'."
1192 # Do not omit this part, this is used by testing.
1193 TMUX_XPANES_TMUX_VERSION="${TMUX_XPANES_TMUX_VERSION:-$(xpns_get_tmux_version)}"
1194 if ( xpns_tmux_is_greater_equals \
1195 "${XP_SUPPORT_TMUX_VERSION_LOWER}" \
1196 "${TMUX_XPANES_TMUX_VERSION}" ); then
1197 : "Supported tmux version"
1200 "'${XP_THIS_FILE_NAME}' may not work correctly! Please check followings.
1201 * tmux is installed correctly.
1202 * Supported tmux version is installed.
1203 Version ${XP_SUPPORT_TMUX_VERSION_LOWER} and over is officially supported."
1209 xpns_pipe_filter() {
1210 local _number="${1-}"
1211 if [[ -z "${_number-}" ]]; then
1214 xargs -n "${_number}"
1218 # Merge array's element by combining with space.
1222 # => array is going to be ("1 2 3" "4 5")
1223 xpns_merge_array_elements() {
1225 local _pane_num="$1"
1227 local _arr_name="$1"
1229 local _filtered_args=()
1230 eval 'set -- "${'"$_arr_name"'[@]}"'
1232 for ((i = 1; i <= _num; i++)); do
1233 if [[ -z "$_line" ]]; then
1236 _line="${_line}${XP_OFS}$1"
1239 if ((i % _pane_num == 0)); then
1240 _filtered_args+=("${_line}")
1244 if [[ -n "$_line" ]]; then
1245 _filtered_args+=("${_line}")
1247 eval "$_arr_name"'=("${_filtered_args[@]}")'
1250 xpns_newline2space() {
1252 while read -r _line; do
1253 if [[ -z "$_result" ]]; then
1256 _result="${_result}${XP_OFS}${_line}"
1259 printf "%s\\n" "${_result}"
1262 xpns_get_window_height_width() {
1267 local _pattern='^([0-9]+)[ \t]+([0-9]+)$'
1269 if ! type stty > /dev/null 2>&1; then
1270 xpns_msg_debug "'stty' does not exist: Failed to get window height and size. Skip checking"
1274 ## This condition is used for unit testing
1275 if [[ -z "${XP_IS_PIPE_MODE-}" ]]; then
1276 if [[ ! -t 0 ]]; then
1280 if [[ $XP_IS_PIPE_MODE -eq 0 ]]; then
1281 if _result=$(stty size 2> /dev/null) && [[ "$_result" =~ $_pattern ]]; then
1282 _height="${BASH_REMATCH[1]}"
1283 _width="${BASH_REMATCH[2]}"
1284 xpns_msg_debug "window height: $_height, width: $_width"
1285 printf "%s\\n" "$_height $_width"
1289 if ! type ps > /dev/null 2>&1; then
1290 xpns_msg_debug "'ps' does not exist: Failed to get window height and size. Skip checking"
1294 read -r # Remove first line
1296 } < <(ps -o tty -p $$ 2> /dev/null)
1297 ## If it's Linux, -F option is used
1298 if _result=$(stty -F "/dev/${_dev}" size 2> /dev/null) && [[ "$_result" =~ $_pattern ]]; then
1299 _height="${BASH_REMATCH[1]}"
1300 _width="${BASH_REMATCH[2]}"
1301 xpns_msg_debug "window height: $_height, width: $_width"
1302 printf "%s\\n" "$_height $_width"
1305 ## If it's BSD, macOS, -F option is used
1306 if _result=$(stty -f "/dev/${_dev}" size 2> /dev/null) && [[ "$_result" =~ $_pattern ]]; then
1307 _height="${BASH_REMATCH[1]}"
1308 _width="${BASH_REMATCH[2]}"
1309 xpns_msg_debug "window height: $_height, width: $_width"
1310 printf "%s\\n" "$_height $_width"
1318 xpns_check_cell_size_bulk() {
1319 local _cell_num="$1"
1321 local _bulk_cols="$1"
1323 local _win_height="$1"
1325 local _win_width="$1"
1327 local _ignore_flag="$1"
1330 # shellcheck disable=SC2178
1334 IFS="," read -r -a _all_cols <<< "${_bulk_cols}"
1335 _rows="${#_all_cols[@]}"
1336 for i in "${_all_cols[@]}"; do
1337 ((i >= _cols)) && ((_cols = i))
1338 ((_sum_cell = _sum_cell + i))
1340 if ((_sum_cell != _cell_num)); then
1341 xpns_msg_error "Number of cols does not equals to the number of arguments."
1342 xpns_msg_error "Expected (# of args) : $_cell_num, Actual (--bulk-cols) : $_sum_cell)."
1343 return ${XP_ELAYOUT:-6}
1345 local cell_height=$(((_win_height - _rows + 1) / _rows))
1346 local cell_width=$(((_win_width - _cols + 1) / _cols))
1348 ## Display basic information
1349 xpns_msg_debug "Window: { Height: $_win_height, Width: $_win_width }"
1350 xpns_msg_debug "Cell: { Height: $cell_height, Width: $cell_width }"
1351 xpns_msg_debug "# Of Panes: ${_cell_num}"
1352 xpns_msg_debug " | Row[0] --...--> Row[MAX]"
1353 xpns_msg_debug " -----+------------------------..."
1354 xpns_msg_debug " Col[]| ${_all_cols[*]}"
1355 xpns_msg_debug " -----+------------------------..."
1357 if [[ "$_ignore_flag" -ne 1 ]] && ( ((cell_height < 2)) || ((cell_width < 2)) ); then
1358 xpns_msg_error "Expected pane size is too small (height: $cell_height lines, width: $cell_width chars)"
1359 return ${XP_ESMLPANE:-7}
1361 printf "%s\\n" "${_cols} ${_rows} ${_all_cols[*]}"
1364 xpns_check_cell_size() {
1365 local _cell_num="$1"
1371 local _win_height="$1"
1373 local _win_width="$1"
1375 local _ignore_flag="$1"
1377 local _all_cols_num=
1380 if [[ -n "${_cols-}" ]] && [[ -n "${_rows-}" ]]; then
1381 xpns_msg_warning "Both col size and row size are provided. Col size is preferentially going to be applied."
1383 ## if col is only defined
1384 if [[ -n "${_cols-}" ]]; then
1385 read -r _cols _rows < <(xpns_adjust_col_row "${_cols-}" 0 "${_cell_num}")
1386 IFS=" " read -r -a _all_rows <<< "$(xpns_divide_equally "${_cell_num}" "${_cols}")"
1387 _all_cols_num="$(xpns_nums_transpose "${_all_rows[@]}")"
1389 ## if row is only defined
1390 elif [[ -n "${_rows-}" ]]; then
1391 read -r _cols _rows < <(xpns_adjust_col_row 0 "${_rows-}" "${_cell_num}")
1392 _all_cols_num="$(xpns_divide_equally "${_cell_num}" "${_rows}")"
1394 ## if both are undefined
1396 read -r _cols _rows < <(xpns_adjust_col_row 0 0 "${_cell_num}")
1397 _all_cols_num="$(xpns_divide_equally "${_cell_num}" "${_rows}")"
1400 local cell_height=$(((_win_height - _rows + 1) / _rows))
1401 local cell_width=$(((_win_width - _cols + 1) / _cols))
1403 ## Display basic information
1404 xpns_msg_debug "Window: { Height: $_win_height, Width: $_win_width }"
1405 xpns_msg_debug "Cell: { Height: $cell_height, Width: $cell_width }"
1406 xpns_msg_debug "# Of Panes: ${_cell_num}"
1407 xpns_msg_debug " | Row[0] --...--> Row[MAX]"
1408 xpns_msg_debug " -----+------------------------..."
1409 xpns_msg_debug " Col[]| ${_all_cols_num}"
1410 xpns_msg_debug " -----+------------------------..."
1412 if [[ "$_ignore_flag" -ne 1 ]] && ( ((cell_height < 2)) || ((cell_width < 2)) ); then
1413 xpns_msg_error "Expected pane size is too small (height: $cell_height lines, width: $cell_width chars)"
1414 return "${XP_ESMLPANE:-7}"
1416 printf "%s\\n" "${_cols} ${_rows} ${_all_cols_num}"
1419 # Execute from Normal mode1
1420 xpns_pre_execution() {
1424 if [[ ${XP_OPT_EXTRA} -eq 1 ]]; then
1425 xpns_msg_error "'-x' must be used within the running tmux session."
1429 # Run as best effort.
1430 # Because after the tmux session is created, cols and rows would be provided by tmux.
1431 IFS=" " read -r XP_WINDOW_HEIGHT XP_WINDOW_WIDTH < <(xpns_get_window_height_width) && {
1432 local _arg_num="${#XP_ARGS[@]}"
1433 local _cell_num _tmp_col_row_cols _tmp_cols
1434 if [[ -n "$XP_MAX_PANE_ARGS" ]] && ((XP_MAX_PANE_ARGS > 1)); then
1435 _cell_num=$((_arg_num / XP_MAX_PANE_ARGS))
1437 _cell_num="${_arg_num}"
1439 if [[ -n "${XP_OPT_BULK_COLS}" ]]; then
1440 _tmp_col_row_cols="$(xpns_check_cell_size_bulk \
1442 "${XP_OPT_BULK_COLS}" \
1443 "${XP_WINDOW_HEIGHT}" \
1444 "${XP_WINDOW_WIDTH}" \
1445 "${XP_OPT_IGNORE_SIZE_LIMIT:-0}")"
1446 local _exit_status="$?"
1447 [[ $_exit_status -eq ${XP_ELAYOUT} ]] && exit ${XP_ELAYOUT}
1448 [[ $_exit_status -eq ${XP_ESMLPANE} ]] && exit ${XP_ESMLPANE}
1450 _tmp_col_row_cols="$(xpns_check_cell_size \
1452 "${XP_OPT_CUSTOM_SIZE_COLS-}" \
1453 "${XP_OPT_CUSTOM_SIZE_ROWS-}" \
1454 "${XP_WINDOW_HEIGHT}" \
1455 "${XP_WINDOW_WIDTH}" \
1456 "${XP_OPT_IGNORE_SIZE_LIMIT:-0}")"
1457 [[ $? -eq ${XP_ESMLPANE} ]] && exit ${XP_ESMLPANE}
1460 IFS=" " read -r XP_OPT_CUSTOM_SIZE_COLS XP_OPT_CUSTOM_SIZE_ROWS _tmp_cols <<< "$_tmp_col_row_cols"
1461 IFS=" " read -r -a XP_COLS <<< "${_tmp_cols}"
1462 IFS=" " read -r -a XP_COLS_OFFSETS <<< "$(printf "%s\\n" "${XP_COLS[*]}" | xpns_nums_accumulate_sum)"
1463 xpns_msg_debug "Options: $(xpns_arr2args "${XP_OPTIONS[@]}")"
1464 xpns_msg_debug "Arguments: $(xpns_arr2args "${XP_ARGS[@]}")"
1468 # Because any arguments may have `-`
1469 if [[ ${XP_NO_OPT} -eq 1 ]]; then
1470 XP_ARGS=("--" "${XP_ARGS[@]}")
1473 # If there is any options, escape them.
1474 if [[ -n "${XP_OPTIONS[*]-}" ]]; then
1475 _opts4args=$(xpns_arr2args "${XP_OPTIONS[@]}")
1477 _args4args=$(xpns_arr2args "${XP_ARGS[@]}")
1479 # Run as best effort
1480 xpns_clean_session || true
1482 # Create new session.
1483 ${TMUX_XPANES_EXEC} -S "${XP_SOCKET_PATH}" new-session \
1484 -s "${XP_SESSION_NAME}" \
1485 -n "${XP_TMP_WIN_NAME}" \
1486 -d "${XP_ABS_THIS_FILE_NAME} ${_opts4args} ${_args4args}"
1488 # Avoid attaching (for unit testing).
1489 if [[ ${XP_OPT_ATTACH} -eq 1 ]]; then
1490 if ! ${TMUX_XPANES_EXEC} -S "${XP_SOCKET_PATH}" attach-session -t "${XP_SESSION_NAME}" &&
1491 [[ ${XP_IS_PIPE_MODE} -eq 1 ]]; then
1492 ## In recovery case, overwrite trap to keep socket file
1493 trap 'rm -f "${XP_CACHE_HOME}"/__xpns_*$$;' EXIT
1495 xpns_msg "Recovery" \
1496 "Execute below command line to re-attach the new session.
1498 ${TMUX_XPANES_EXEC} -S ${XP_SOCKET_PATH} attach-session -t ${XP_SESSION_NAME}
1506 # Execute from inside of tmux session
1508 local _pane_base_index=
1510 local _last_args_idx=
1511 local _def_allow_rename=
1514 if [[ ${XP_IS_PIPE_MODE} -eq 0 ]] && [[ -n "${XP_MAX_PANE_ARGS-}" ]]; then
1515 xpns_merge_array_elements "${XP_MAX_PANE_ARGS}" 'XP_ARGS'
1518 ## Fix window size and define pane size
1520 local _tmp_col_row_cols _tmp_cols
1521 IFS=" " read -r XP_WINDOW_HEIGHT XP_WINDOW_WIDTH < \
1522 <(${TMUX_XPANES_EXEC} display-message -p '#{window_height} #{window_width}')
1523 if [[ -n "${XP_OPT_BULK_COLS}" ]]; then
1524 _tmp_col_row_cols="$(xpns_check_cell_size_bulk \
1526 "${XP_OPT_BULK_COLS}" \
1527 "${XP_WINDOW_HEIGHT}" \
1528 "${XP_WINDOW_WIDTH}" \
1529 "${XP_OPT_IGNORE_SIZE_LIMIT:-0}")"
1530 local _exit_status="$?"
1531 [[ $_exit_status -eq ${XP_ELAYOUT} ]] && exit ${XP_ELAYOUT}
1532 [[ $_exit_status -eq ${XP_ESMLPANE} ]] && exit ${XP_ESMLPANE}
1534 _tmp_col_row_cols="$(xpns_check_cell_size \
1536 "${XP_OPT_CUSTOM_SIZE_COLS-}" \
1537 "${XP_OPT_CUSTOM_SIZE_ROWS-}" \
1538 "${XP_WINDOW_HEIGHT}" \
1539 "${XP_WINDOW_WIDTH}" \
1540 "${XP_OPT_IGNORE_SIZE_LIMIT:-0}")"
1541 [[ $? -eq ${XP_ESMLPANE} ]] && exit ${XP_ESMLPANE}
1543 IFS=" " read -r XP_OPT_CUSTOM_SIZE_COLS XP_OPT_CUSTOM_SIZE_ROWS _tmp_cols <<< "$_tmp_col_row_cols"
1544 IFS=" " read -r -a XP_COLS <<< "${_tmp_cols}"
1545 IFS=" " read -r -a XP_COLS_OFFSETS <<< "$(printf "%s\\n" "${XP_COLS[*]}" | xpns_nums_accumulate_sum)"
1546 xpns_msg_debug "Options: $(xpns_arr2args "${XP_OPTIONS[@]}")"
1547 xpns_msg_debug "Arguments: $(xpns_arr2args "${XP_ARGS[@]}")"
1550 _pane_base_index=$(xpns_get_global_tmux_conf 'pane-base-index')
1551 _last_args_idx=$((${#XP_ARGS[@]} - 1))
1552 _def_allow_rename="$(xpns_get_global_tmux_conf 'allow-rename')"
1554 xpns_suppress_allow_rename "${_def_allow_rename-}"
1555 XP_CMD_UTILITY="$(xpns_get_joined_begin_commands "${XP_CMD_UTILITY}")"
1557 if [[ ${XP_OPT_EXTRA} -eq 1 ]]; then
1558 # Reuse existing window name
1559 # tmux 1.6 does not support -F option
1560 _window_name="$( ${TMUX_XPANES_EXEC} display -p -F "#{window_id}")"
1561 _pane_count="$( ${TMUX_XPANES_EXEC} list-panes | grep -c .)"
1562 _pane_base_index=$((_pane_base_index + _pane_count - 1))
1563 _pane_active_pane_id=$(${TMUX_XPANES_EXEC} display -p -F "#{pane_id}")
1566 xpns_generate_window_name \
1573 ## --------------------
1574 # Prepare window and panes
1575 ## --------------------
1576 if [[ ${XP_OPT_EXTRA} -eq 1 ]]; then
1577 xpns_prepare_extra_panes \
1579 "${_pane_base_index}" \
1580 "${XP_OPT_LOG_STORE}" \
1581 "${XP_OPT_SET_TITLE}" \
1582 "${XP_OPT_SPEEDY}" \
1583 "${XP_OPT_SPEEDY_AWAIT}" \
1585 "${XP_CMD_UTILITY}" \
1587 elif [[ ${XP_OPT_USE_PRESET_LAYOUT} -eq 1 ]]; then
1588 xpns_prepare_preset_layout_window \
1590 "${_pane_base_index}" \
1591 "${XP_OPT_LOG_STORE}" \
1592 "${XP_OPT_SET_TITLE}" \
1593 "${XP_OPT_ATTACH}" \
1594 "${XP_OPT_SPEEDY}" \
1595 "${XP_OPT_SPEEDY_AWAIT}" \
1597 "${XP_CMD_UTILITY}" \
1599 elif [[ ${XP_OPT_USE_PRESET_LAYOUT} -eq 0 ]]; then
1600 xpns_prepare_window \
1602 "${XP_OPT_LOG_STORE}" \
1603 "${XP_OPT_SET_TITLE}" \
1604 "${XP_OPT_ATTACH}" \
1605 "${XP_OPT_SPEEDY}" \
1606 "${XP_OPT_SPEEDY_AWAIT}" \
1608 "${XP_CMD_UTILITY}" \
1612 ## With -ss option, it is possible to close all the panes as of here.
1613 ## Check status of the window. If no window exists, there is nothing to do any more and just exit.
1614 xpns_msg_debug "xpns_is_window_alive:1: After window separation"
1615 xpns_is_window_alive "${_window_name}" "${XP_OPT_SPEEDY_AWAIT}" "${_def_allow_rename-}"
1617 if [[ ${XP_OPT_EXTRA} -eq 1 ]]; then
1618 # Set offset to avoid sending command to the original pane.
1619 _pane_base_index=$((_pane_base_index + 1))
1620 # Avoid to make layout even-horizontal even if there are many panes.
1621 # in xpns_organize_panes
1622 _last_args_idx=$((_last_args_idx + _pane_count))
1623 # Re-select the windown that was active before.
1624 ${TMUX_XPANES_EXEC} select-pane -t "${_window_name}.${_pane_active_pane_id}"
1627 if [[ ${XP_OPT_LOG_STORE} -eq 1 ]]; then
1628 xpns_enable_logging \
1630 "${_pane_base_index}" \
1631 "${TMUX_XPANES_LOG_DIRECTORY}" \
1632 "${TMUX_XPANES_LOG_FORMAT}" \
1636 if [[ $XP_OPT_SPEEDY -eq 1 ]]; then
1637 xpns_notify_logging \
1643 xpns_msg_debug "xpns_is_window_alive:2: After logging"
1644 xpns_is_window_alive "${_window_name}" "${XP_OPT_SPEEDY_AWAIT}" "${_def_allow_rename-}"
1646 # Set pane titles for each pane.
1647 if xpns_is_pane_title_required "${XP_OPT_SET_TITLE}" "${XP_OPT_EXTRA}"; then
1650 "${_pane_base_index}" \
1654 if [[ $XP_OPT_SPEEDY -eq 1 ]]; then
1660 xpns_msg_debug "xpns_is_window_alive:3: After setting title"
1661 xpns_is_window_alive "${_window_name}" "${XP_OPT_SPEEDY_AWAIT}" "${_def_allow_rename-}"
1663 # Sending operations for each pane.
1664 # With -s option, command is already sent.
1665 if [[ $XP_OPT_SPEEDY -eq 0 ]]; then
1666 xpns_send_commands \
1668 "${_pane_base_index}" \
1670 "${XP_CMD_UTILITY}" \
1674 xpns_msg_debug "xpns_is_window_alive:4: After sending commands"
1675 xpns_is_window_alive "${_window_name}" "${XP_OPT_SPEEDY_AWAIT}" "${_def_allow_rename-}"
1677 ## With -l <layout>, panes are organized.
1678 ## As well as -x, they are re-organized.
1679 if [[ $XP_OPT_USE_PRESET_LAYOUT -eq 1 ]] || [[ ${XP_OPT_EXTRA} -eq 1 ]]; then
1680 xpns_organize_panes \
1685 # Enable broadcasting
1686 if [[ ${XP_OPT_IS_SYNC} -eq 1 ]] && [[ ${XP_OPT_EXTRA} -eq 0 ]]; then
1687 ${TMUX_XPANES_EXEC} \
1688 set-window-option -t "${_window_name}" \
1689 synchronize-panes on
1692 ## In case of -t option
1693 if [[ ${XP_OPT_SET_TITLE} -eq 1 ]] && [[ ${XP_OPT_CHANGE_BORDER} -eq 1 ]]; then
1695 ${TMUX_XPANES_EXEC} \
1696 set-window-option -t "${_window_name}" \
1697 pane-border-format "${TMUX_XPANES_PANE_BORDER_FORMAT}"
1698 # Show border status
1699 ${TMUX_XPANES_EXEC} \
1700 set-window-option -t "${_window_name}" \
1701 pane-border-status "${TMUX_XPANES_PANE_BORDER_STATUS}"
1704 # In case of -x, this statement is skipped to keep the original window name
1705 if [[ ${XP_OPT_EXTRA} -eq 0 ]]; then
1706 # Restore original window name.
1707 ${TMUX_XPANES_EXEC} \
1708 rename-window -t "${_window_name}" \
1709 -- "$(printf "%s\\n" "${_window_name}" | xpns_key2value)"
1712 xpns_restore_allow_rename "${_def_allow_rename-}"
1716 # Arrange options for pipe mode
1717 # * argument -> command
1718 # * stdin -> argument
1720 xpns_switch_pipe_mode() {
1721 local _pane_num4new_term=""
1722 if [[ -n "${XP_ARGS[*]-}" ]] && [[ -n "${XP_CMD_UTILITY-}" ]]; then
1723 xpns_msg_error "Both arguments and other options (like '-c', '-e') which updates <command> are given."
1727 if [[ -z "${TMUX-}" ]]; then
1728 xpns_msg_warning "Attached session is required for 'Pipe mode'."
1729 # This condition is used when the following situations.
1730 # * Enter from outside of tmux session(Normal mode1)
1735 # (Normal mode1)$ echo {a..g} | ./xpanes -n 2
1736 # => This will once create the new window like this.
1737 # (inside of tmux session)$ ./xpanes '-n' '2' 'a' 'b' 'c' 'd' 'e' 'f' 'g'
1738 # => After the window is closed, following panes would be left.
1739 # (pane 1)$ echo a b
1740 # (pane 2)$ echo c d
1741 # (pane 3)$ echo e f
1743 # In order to create such the query,
1744 # separate all the argument into minimum tokens
1746 if [[ -n "${XP_MAX_PANE_ARGS-}" ]]; then
1747 _pane_num4new_term=1
1751 while read -r line; do
1752 XP_STDIN+=("${line}")
1753 done < <(cat | xpns_rm_empty_line |
1754 xpns_pipe_filter "${_pane_num4new_term:-${XP_MAX_PANE_ARGS}}")
1756 # Merge them into command.
1757 if [[ -n "${XP_ARGS[*]-}" ]]; then
1758 # Attention: It might be wrong result if IFS is changed.
1759 XP_CMD_UTILITY="${XP_ARGS[*]}"
1762 # If there is empty -I option or user does not assign the <repstr>,
1763 # Append the space and <repstr> at the end of the <command>
1764 # This is same as the xargs command of GNU.
1766 # $ echo 10 | xargs seq
1769 # $ echo 10 | xargs -I@ seq @
1771 if [[ -z "${XP_REPSTR}" ]]; then
1772 XP_REPSTR="${XP_DEFAULT_REPSTR}"
1773 if [[ -n "${XP_ARGS[*]-}" ]]; then
1774 XP_CMD_UTILITY="${XP_ARGS[*]-} ${XP_REPSTR}"
1778 # Deal with stdin as arguments.
1779 XP_ARGS=("${XP_STDIN[@]-}")
1782 xpns_layout_short2long() {
1785 -e 's/^eh$/even-horizontal/' \
1786 -e 's/^ev$/even-vertical/' \
1787 -e 's/^mh$/main-horizontal/' \
1788 -e 's/^mv$/main-vertical/' \
1792 xpns_is_valid_layout() {
1793 local _layout="${1-}"
1794 local _pat='^(tiled|even-horizontal|even-vertical|main-horizontal|main-vertical)$'
1795 if ! [[ $_layout =~ $_pat ]]; then
1796 xpns_msg_error "Invalid layout '${_layout}'."
1801 xpns_warning_before_extra() {
1803 local _synchronized=
1804 _synchronized="$(xpns_get_local_tmux_conf "synchronize-panes")"
1805 if [[ "on" == "${_synchronized}" ]]; then
1806 xpns_msg_warning "Panes are now synchronized.
1807 '-x' option may cause unexpected behavior on the synchronized panes."
1808 printf "Are you really sure? [y/n]: "
1810 if ! [[ "${_ans-}" =~ ^[yY] ]]; then
1816 xpns_opt_check_num() {
1820 if [[ -n "$_arg" ]] && [[ -z "${_arg//[0-9]/}" ]]; then
1823 xpns_msg_error "Invalid argument '$_arg' for $_option option"
1827 xpns_opt_check_str() {
1831 if [[ -n "$_arg" ]]; then
1834 xpns_msg_error "Invalid argument '$_arg' for $_option option"
1838 xpns_opt_check_num_csv() {
1842 if [[ "$1" =~ ^([0-9][0-9]*,?)+$ ]]; then
1845 xpns_msg_error "Invalid argument '$_arg' for $_option option"
1849 xpns_parse_options() {
1850 while (($# > 0)); do
1853 if [[ ${XP_NO_OPT} -eq 1 ]]; then
1858 ## Skip regularization if the arg is empty or --log= option
1859 if [[ -n "$opt" ]] && [[ -n "${opt##--log=*}" ]]; then
1860 ## -ovalue → -o value
1861 if [[ -z "${opt##-${XP_ARG_OPTIONS}?*}" ]]; then
1862 set -- "${opt#??}" ${1+"$@"}
1865 elif [[ -z "${opt##-[!-]?*}" ]]; then
1866 set -- "-${opt#??}" ${1+"$@"}
1868 ## --option=value → --option value
1869 elif [[ -z "${opt##--*=*}" ]]; then
1870 set -- "${opt#--*=}" ${1+"$@"}
1877 # Disable any more options
1892 XP_OPTIONS+=("$opt")
1896 xpns_opt_check_str "$opt" "$1"
1897 XP_OPTIONS+=("$opt")
1899 TMUX_XPANES_LOG_FORMAT="$1"
1905 XP_OPTIONS+=("$opt")
1906 TMUX_XPANES_LOG_DIRECTORY="${opt#--log=}"
1907 xpns_opt_check_str "${opt%=}" "$TMUX_XPANES_LOG_DIRECTORY"
1911 XP_OPTIONS+=("$opt")
1914 XP_OPTIONS+=("$opt")
1915 XP_CMD_UTILITY="${XP_SSH_CMD_UTILITY}"
1918 XP_OPT_CHANGE_BORDER=1
1921 XP_OPT_SPEEDY_AWAIT=1
1924 XP_OPTIONS+=("$opt")
1928 xpns_opt_check_num "$opt" "$1"
1929 XP_OPTIONS+=("$opt")
1930 XP_OPT_CUSTOM_SIZE_COLS="$1"
1935 xpns_opt_check_num "$opt" "$1"
1936 XP_OPTIONS+=("$opt")
1937 XP_OPT_CUSTOM_SIZE_ROWS="$1"
1942 xpns_opt_check_num_csv "$opt" "$1"
1943 XP_OPTIONS+=("$opt")
1944 XP_OPT_BULK_COLS="$1"
1949 XP_OPTIONS+=("$opt")
1953 XP_OPTIONS+=("$opt")
1956 --ignore-size-limit)
1957 XP_OPTIONS+=("$opt")
1958 XP_OPT_IGNORE_SIZE_LIMIT=1
1961 # Short options without argument
1972 XP_OPTIONS+=("$opt")
1974 XP_OPT_USE_PRESET_LAYOUT=1 ## Layout presets must be used with -x
1975 if ! xpns_warning_before_extra; then
1980 XP_OPTIONS+=("$opt")
1984 XP_OPTIONS+=("$opt")
1989 XP_OPTIONS+=("$opt")
1990 if ( xpns_tmux_is_greater_equals 2.3 ); then
1992 XP_OPT_CHANGE_BORDER=1
1994 xpns_msg_warning "-t option cannot be used by tmux version less than 2.3. Disabled."
1999 XP_OPTIONS+=("$opt")
2001 XP_OPT_SPEEDY_AWAIT=1
2002 if [[ -z "${1#-s}" ]]; then
2003 XP_OPT_SPEEDY_AWAIT=0
2009 # Short options with argument
2012 xpns_opt_check_str "$opt" "$1"
2013 XP_OPTIONS+=("$opt")
2019 xpns_opt_check_str "$opt" "$1"
2020 XP_OPTIONS+=("$opt")
2021 XP_OPT_USE_PRESET_LAYOUT=1
2022 XP_LAYOUT="$(printf '%s\n' "$1" | xpns_layout_short2long)"
2023 xpns_is_valid_layout "${XP_LAYOUT}"
2029 # xpns_opt_check_str "$opt" "$1"
2030 XP_OPTIONS+=("$opt")
2032 XP_OPT_CMD_UTILITY=1
2037 xpns_opt_check_num "$opt" "$1"
2038 XP_OPTIONS+=("$opt")
2039 XP_MAX_PANE_ARGS="$1"
2044 xpns_opt_check_str "$opt" "$1"
2045 XP_OPTIONS+=("$opt")
2051 xpns_opt_check_num "$opt" "$1"
2052 XP_OPTIONS+=("$opt")
2053 XP_OPT_CUSTOM_SIZE_COLS="$1"
2058 xpns_opt_check_num "$opt" "$1"
2059 XP_OPTIONS+=("$opt")
2060 XP_OPT_CUSTOM_SIZE_ROWS="$1"
2066 # xpns_opt_check_str "$opt" "$1"
2067 XP_OPTIONS+=("$opt")
2068 XP_BEGIN_ARGS+=("$1")
2073 xpns_msg_error "Invalid option -- '${opt}'"
2086 # If there is any standard input from pipe,
2087 # 1 line handled as 1 argument.
2088 if [[ ! -t 0 ]]; then
2090 xpns_switch_pipe_mode
2093 # When no argument is given, exit.
2094 if [[ -z "${XP_ARGS[*]-}" ]]; then
2095 xpns_msg_error "No arguments."
2100 if [[ -n "${XP_OPT_CUSTOM_SIZE_COLS-}" ]] || [[ -n "${XP_OPT_CUSTOM_SIZE_ROWS-}" ]]; then
2101 if [[ "$XP_OPT_EXTRA" -eq 1 ]]; then
2102 xpns_msg_warning "The columns/rows options (-C, --cols, -R, --rows) cannot be used with -x option. Ignored."
2103 elif [[ "$XP_OPT_EXTRA" -eq 0 ]] && [[ "${XP_OPT_USE_PRESET_LAYOUT}" -eq 1 ]]; then
2104 # This part is required to keep backward compatibility.
2105 ## Users can simulate xpanes v3.x to set : alias xpanes="xpanes -lt"
2106 xpns_msg_info "Columns/rows option (-C, --cols, -R, --rows) and -l option are provided. Disable -l. "
2107 XP_OPT_USE_PRESET_LAYOUT=0
2111 # Set default value in case of empty.
2112 XP_CMD_UTILITY="${XP_CMD_UTILITY:-${XP_DEFAULT_CMD_UTILITY}}"
2113 XP_REPSTR="${XP_REPSTR:-${XP_DEFAULT_REPSTR}}"
2115 # To set command on pre_execution, set -c option manually.
2116 if [[ ${XP_OPT_CMD_UTILITY} -eq 0 ]]; then
2117 XP_OPTIONS+=("-c" "${XP_CMD_UTILITY}")
2121 ## --------------------------------
2123 ## --------------------------------
2125 xpns_parse_options ${1+"$@"}
2126 xpns_check_env "${XP_DEPENDENCIES}"
2127 ## --------------------------------
2128 # Parameter validation
2129 ## --------------------------------
2130 # When do dry-run flag is enabled, skip running (this is used to execute unit test of itself).
2131 if [[ ${XP_OPT_DRY_RUN} -eq 1 ]]; then
2134 # Validate log directory.
2135 if [[ ${XP_OPT_LOG_STORE} -eq 1 ]]; then
2136 TMUX_XPANES_LOG_DIRECTORY=$(xpns_normalize_directory "${TMUX_XPANES_LOG_DIRECTORY}")
2137 xpns_is_valid_directory "${TMUX_XPANES_LOG_DIRECTORY}" &&
2138 TMUX_XPANES_LOG_DIRECTORY=$(cd "${TMUX_XPANES_LOG_DIRECTORY}" && pwd)
2140 ## --------------------------------
2141 # If current shell is outside of tmux session.
2142 ## --------------------------------
2143 if [[ -z "${TMUX-}" ]]; then
2145 ## --------------------------------
2146 # If current shell is already inside of tmux session.
2147 ## --------------------------------
2154 ## --------------------------------
2156 ## --------------------------------