+#!/usr/bin/env bash
+readonly XP_SHELL="/usr/bin/env bash"
+
+# @Author Yamada, Yasuhiro
+# @Filename xpanes
+
+set -u
+readonly XP_VERSION="4.1.1"
+
+## trap might be updated in 'xpns_pre_execution' function
+trap 'rm -f "${XP_CACHE_HOME}"/__xpns_*$$; rm -f "${XP_DEFAULT_SOCKET_PATH}"' EXIT
+
+## --------------------------------
+# Error constants
+## --------------------------------
+# Undefined or General errors
+readonly XP_EUNDEF=1
+
+# Invalid option/argument
+readonly XP_EINVAL=4
+
+# Could not open tty.
+readonly XP_ETTY=5
+
+# Invalid layout.
+readonly XP_ELAYOUT=6
+
+# Impossible layout: Small pane
+readonly XP_ESMLPANE=7
+
+# Log related exit status is 2x.
+## Could not create a directory.
+readonly XP_ELOGDIR=20
+
+## Could not directory to store logs is not writable.
+readonly XP_ELOGWRITE=21
+
+# User's intentional exit is 3x
+## User exit the process intentionally by following warning message.
+readonly XP_EINTENT=30
+
+## All the panes are closed before processing due to user's options/command.
+readonly XP_ENOPANE=31
+
+# Necessary commands are not found
+readonly XP_ENOCMD=127
+
+# ===============
+
+# XP_THIS_FILE_NAME is supposed to be "xpanes".
+readonly XP_THIS_FILE_NAME="${0##*/}"
+readonly XP_THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-${(%):-%N}}")" && pwd)"
+readonly XP_ABS_THIS_FILE_NAME="${XP_THIS_DIR}/${XP_THIS_FILE_NAME}"
+
+# Prevent cache directory being created under root / directory in any case.
+# This is quite rare case (but it can be happened).
+readonly XP_USER_HOME="${HOME:-/tmp}"
+
+# Basically xpanes follows XDG Base Direcotry Specification.
+# https://specifications.freedesktop.org/basedir-spec/basedir-spec-0.6.html
+XDG_CACHE_HOME="${XDG_CACHE_HOME:-${XP_USER_HOME}/.cache}"
+readonly XP_CACHE_HOME="${XDG_CACHE_HOME}/xpanes"
+
+# This is supposed to be xpanes-12345(PID)
+readonly XP_SESSION_NAME="${XP_THIS_FILE_NAME}-$$"
+# Temporary window name is tmp-12345(PID)
+readonly XP_TMP_WIN_NAME="tmp-$$"
+readonly XP_EMPTY_STR="EMPTY"
+
+readonly XP_SUPPORT_TMUX_VERSION_LOWER="1.8"
+
+# Check dependencies just in case.
+# Even POSIX compliant commands are only used in this program.
+# `xargs`, `sleep`, `mkfifo` are omitted because minimum functions can work without them.
+readonly XP_DEPENDENCIES="${XP_DEPENDENCIES:-tmux grep sed tr od echo touch printf cat sort pwd cd mkfifo}"
+
+## --------------------------------
+# User customizable shell variables
+## --------------------------------
+TMUX_XPANES_EXEC=${TMUX_XPANES_EXEC:-tmux}
+TMUX_XPANES_PANE_BORDER_FORMAT="${TMUX_XPANES_PANE_BORDER_FORMAT:-#[bg=green,fg=black] #T #[default]}"
+TMUX_XPANES_PANE_BORDER_STATUS="${TMUX_XPANES_PANE_BORDER_STATUS:-bottom}"
+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'}
+XP_DEFAULT_TMUX_XPANES_LOG_FORMAT="[:ARG:].log.%Y-%m-%d_%H-%M-%S"
+TMUX_XPANES_LOG_FORMAT="${TMUX_XPANES_LOG_FORMAT:-${XP_DEFAULT_TMUX_XPANES_LOG_FORMAT}}"
+XP_DEFAULT_TMUX_XPANES_LOG_DIRECTORY="${XP_CACHE_HOME}/logs"
+TMUX_XPANES_LOG_DIRECTORY="${TMUX_XPANES_LOG_DIRECTORY:-${XP_DEFAULT_TMUX_XPANES_LOG_DIRECTORY}}"
+
+## --------------------------------
+# Initialize Options
+## --------------------------------
+# options which work individually.
+readonly XP_FLAG_OPTIONS="[hVdetxs]"
+# options which need arguments.
+readonly XP_ARG_OPTIONS="[ISclnCRB]"
+readonly XP_DEFAULT_LAYOUT="tiled"
+readonly XP_DEFAULT_REPSTR="{}"
+readonly XP_DEFAULT_CMD_UTILITY="echo {} "
+readonly XP_SSH_CMD_UTILITY="ssh -o StrictHostKeyChecking=no {} "
+XP_OPTIONS=()
+XP_ARGS=()
+XP_STDIN=()
+XP_BEGIN_ARGS=()
+XP_IS_PIPE_MODE=0
+XP_OPT_IS_SYNC=1
+XP_OPT_DRY_RUN=0
+XP_OPT_ATTACH=1
+XP_OPT_LOG_STORE=0
+XP_REPSTR=""
+XP_DEFAULT_SOCKET_PATH_BASE="${XP_CACHE_HOME}/socket"
+XP_DEFAULT_SOCKET_PATH="${XP_DEFAULT_SOCKET_PATH_BASE}.$$"
+XP_SOCKET_PATH="${XP_SOCKET_PATH:-${XP_DEFAULT_SOCKET_PATH}}"
+XP_NO_OPT=0
+XP_OPT_CMD_UTILITY=0
+XP_CMD_UTILITY=""
+XP_LAYOUT="${XP_DEFAULT_LAYOUT}"
+XP_MAX_PANE_ARGS=""
+XP_OPT_SET_TITLE=0
+XP_OPT_CHANGE_BORDER=0
+XP_OPT_EXTRA=0
+XP_OPT_SPEEDY=0
+XP_OPT_SPEEDY_AWAIT=0
+XP_OPT_USE_PRESET_LAYOUT=0
+XP_OPT_CUSTOM_SIZE_COLS=
+XP_OPT_CUSTOM_SIZE_ROWS=
+XP_OPT_BULK_COLS=
+XP_WINDOW_WIDTH=
+XP_WINDOW_HEIGHT=
+XP_COLS=
+XP_COLS_OFFSETS=
+XP_OPT_DEBUG=0
+XP_OPT_IGNORE_SIZE_LIMIT=0
+
+## --------------------------------
+# Logger
+# $1 -- Log level (i.e Warning, Error)
+# $2 -- Message
+# i.e
+# xpanes:Error: invalid option.
+#
+# This log format is created with reference to openssl's one.
+# $ echo | openssl -a
+# openssl:Error: '-a' is an invalid command.
+## --------------------------------
+xpns_msg() {
+ local _loglevel="$1"
+ local _msgbody="$2"
+ local _msg="${XP_THIS_FILE_NAME}:${_loglevel}: ${_msgbody}"
+ printf "%s\\n" "${_msg}" >&2
+}
+
+xpns_msg_info() {
+ xpns_msg "Info" "$1"
+}
+
+xpns_msg_warning() {
+ xpns_msg "Warning" "$1"
+}
+
+xpns_msg_debug() {
+ if [[ $XP_OPT_DEBUG -eq 1 ]];then
+ xpns_msg "Debug" "$(date "+[%F_%T]"):${FUNCNAME[1]}:$1"
+ fi
+}
+
+xpns_msg_error() {
+ xpns_msg "Error" "$1"
+}
+
+xpns_usage_warn() {
+ xpns_usage_short >&2
+ echo "Try '${XP_THIS_FILE_NAME} --help' for more information." >&2
+}
+
+xpns_usage_short() {
+ cat << _EOS_
+Usage: ${XP_THIS_FILE_NAME} [OPTIONS] [argument ...]
+Usage(Pipe mode): command ... | ${XP_THIS_FILE_NAME} [OPTIONS] [<command> ...]
+_EOS_
+}
+
+xpns_usage() {
+ cat <<USAGE
+Usage:
+ ${XP_THIS_FILE_NAME} [OPTIONS] [argument ...]
+
+Usage(Pipe mode):
+ command ... | ${XP_THIS_FILE_NAME} [OPTIONS] [<command> ...]
+
+OPTIONS:
+ -h,--help Display this help and exit.
+ -V,--version Output version information and exit.
+ -B <begin-command> Run <begin-command> before processing <command> in each pane. Multiple options are allowed.
+ -c <command> Set <command> to be executed in each pane. Default is \`echo {}\`.
+ -d,--desync Make synchronize-panes option off in new window.
+ -e Execute given arguments as is. Same as \`-c '{}'\`
+ -I <repstr> Replacing one or more occurrences of <repstr> in command provided by -c or -B. Default is \`{}\`.
+ -C NUM,--cols=NUM Number of columns of window layout.
+ -R NUM,--rows=NUM Number of rows of window layout.
+ -l <layout> Set the preset of window layout. Recognized layout arguments are:
+ t tiled
+ eh even-horizontal
+ ev even-vertical
+ mh main-horizontal
+ mv main-vertical
+ -n <number> Set the maximum number of <argument> taken for each pane.
+ -s Speedy mode: Run command without opening an interactive shell.
+ -ss Speedy mode AND close a pane automatically at the same time as process exiting.
+ -S <socket-path> Set a full alternative path to the server socket.
+ -t Display each argument on the each pane's border as their title.
+ -x Create extra panes in the current active window.
+ --log[=<directory>] Enable logging and store log files to ~/.cache/xpanes/logs or <directory>.
+ --log-format=<FORMAT> Make name of log files follow <FORMAT>. Default is \`${XP_DEFAULT_TMUX_XPANES_LOG_FORMAT}\`.
+ --ssh Same as \`-t -s -c 'ssh -o StrictHostKeyChecking=no {}'\`.
+ --stay Do not switch to new window.
+ --bulk-cols=NUM1[,NUM2 ...] Set number of columns on multiple rows (i.e, "2,2,2" represents 2 cols x 3 rows).
+ --debug Print debug message.
+
+Copyright (c) 2019 Yamada, Yasuhiro
+Released under the MIT License.
+https://github.com/greymd/tmux-xpanes
+USAGE
+}
+
+# Show version number
+xpns_version() {
+ echo "${XP_THIS_FILE_NAME} ${XP_VERSION}"
+}
+
+# Get version number for tmux
+xpns_get_tmux_version() {
+ local _tmux_version=""
+ if ! ${TMUX_XPANES_EXEC} -V &> /dev/null; then
+ # From tmux 0.9 to 1.3, there is no -V option.
+ _tmux_version="tmux 0.9-1.3"
+ else
+ _tmux_version="$( ${TMUX_XPANES_EXEC} -V )"
+ fi
+ ( read -r _ _ver; echo "${_ver}" ) <<<"${_tmux_version}"
+}
+
+# Check whether the prefered tmux version is greater than host's tmux version.
+# $1 ... Prefered version.
+# $2 ... Host tmux version(optional).
+# In case of tmux version is 1.7, the result will be like this.
+# 0 is true, 1 is false.
+## arg -> result
+# func 1.5 1.7 -> 0
+# func 1.6 1.7 -> 0
+# func 1.7 1.7 -> 0
+# func 1.8 1.7 -> 1
+# func 1.9 1.7 -> 1
+# func 1.9a 1.7 -> 1
+# func 2.0 1.7 -> 1
+xpns_tmux_is_greater_equals() {
+ local _check_version="$1"
+ local _tmux_version="${2:-$(xpns_get_tmux_version)}"
+ # Simple numerical comparison does not work because there is the version like "1.9a".
+ if [[ "$( (echo "${_tmux_version}"; echo "${_check_version}") | sort -n | head -n 1)" != "${_check_version}" ]];then
+ return 1
+ else
+ return 0
+ fi
+}
+
+xpns_get_local_tmux_conf() {
+ local _conf_name="$1"
+ local _session="${2-}"
+ {
+ if [[ -z "${_session-}" ]];then
+ ${TMUX_XPANES_EXEC} show-window-options
+ else
+ ${TMUX_XPANES_EXEC} -S "${_session}" show-window-options
+ fi
+ } | grep "^${_conf_name}" \
+ | ( read -r _ _v; printf "%s\\n" "${_v}" )
+}
+
+xpns_get_global_tmux_conf() {
+ local _conf_name="$1"
+ local _session="${2-}"
+ {
+ if [[ -z "${_session-}" ]];then
+ ${TMUX_XPANES_EXEC} show-window-options -g
+ else
+ ${TMUX_XPANES_EXEC} -S "${_session}" show-window-options -g
+ fi
+ } | grep "^${_conf_name}" \
+ | ( read -r _ _v; printf "%s\\n" "${_v}" )
+}
+
+# Disable allow-rename because
+# window separation does not work correctly
+# if "allow-rename" option is on
+xpns_suppress_allow_rename () {
+ local _default_allow_rename="$1"
+ local _session="${2-}"
+ if [[ "${_default_allow_rename-}" == "on" ]]; then
+ ## Temporary, disable "allow-rename"
+ xpns_msg_debug "'allow-rename' option is 'off' temporarily."
+ if [[ -z "${_session-}" ]];then
+ ${TMUX_XPANES_EXEC} set-window-option -g allow-rename off
+ else
+ ${TMUX_XPANES_EXEC} -S "${_session}" set-window-option -g allow-rename off
+ fi
+ fi
+}
+
+# Restore default "allow-rename"
+# Do not write like 'xpns_restore_allow_rename "some value" "some value" > /dev/null'
+# In tmux 1.6, 'tmux set-window-option' might be stopped in case of redirection.
+xpns_restore_allow_rename () {
+ local _default_allow_rename="$1"
+ local _session="${2-}"
+ if [[ "${_default_allow_rename-}" == "on" ]]; then
+ xpns_msg_debug "Restore original value of 'allow-rename' option."
+ if [[ -z "${_session-}" ]];then
+ ${TMUX_XPANES_EXEC} set-window-option -g allow-rename on
+ else
+ ${TMUX_XPANES_EXEC} -S "${_session}" set-window-option -g allow-rename on
+ fi
+ fi
+}
+
+# func "11" "2"
+# => 6
+# 11 / 2 = 5.5 => ceiling => 6
+xpns_ceiling () {
+ local _divide="$1";shift
+ local _by="$1"
+ printf "%s\\n" $(( ( _divide + _by - 1 ) / _by ))
+}
+
+# func "10" "3"
+# => 4 3 3
+# Divide 10 into 3 parts as equally as possible.
+xpns_divide_equally () {
+ local _number="$1";shift
+ local _count="$1"
+ local _upper _lower _upper_count _lower_count
+ _upper="$(xpns_ceiling "$_number" "$_count")"
+ _lower=$(( _upper - 1 ))
+ _lower_count=$(( _upper * _count - _number ))
+ _upper_count=$(( _count - _lower_count ))
+ eval "printf '${_upper} %.0s' {1..$_upper_count}"
+ (( _lower_count > 0 )) && eval "printf '${_lower} %.0s' {1..$_lower_count}"
+}
+
+# echo 3 3 3 3 | func
+# => 3 6 9 12
+xpns_nums_accumulate_sum () {
+ local s=0
+ while read -r n; do
+ ((s = s + n ))
+ printf "%s " "$s"
+ done < <( cat | tr ' ' '\n')
+}
+
+# func 3 2 2 2
+# => 4 4 1
+#
+# For example, "3 2 2 2" represents following cell positions
+# 1 2 3
+# 1 [] [] [] => 3 rows
+# 2 [] [] => 2 rows
+# 3 [] [] => 2 rows
+# 4 [] [] => 2 rows
+#
+# After the transposition, it must be "4 4 1" which represents below
+# 1 2 3 4
+# 1 [] [] [] [] => 4 rows
+# 2 [] [] [] [] => 4 rows
+# 3 [] => 1 rows
+xpns_nums_transpose () {
+ local _colnum="$1"
+ local _spaces=
+ local _result=
+ xpns_msg_debug "column num = $_colnum, input = $*"
+ _spaces="$(for i in "$@";do
+ printf "%${i}s\\n"
+ done)"
+
+ # 'for' statement does not work somehow
+ _result="$(while read -r i; do
+ ## This part is depending on the following 'cut' behavior
+ ## $ echo 1234 | cut -c 5
+ ## => result is supposed to be empty
+ printf "%s\\n" "$_spaces" | cut -c "$i" | grep -c ' '
+ done < <(xpns_seq 1 "${_colnum}") | xargs)"
+ xpns_msg_debug "result = $_result"
+ printf "%s\\n" "$_result"
+}
+
+# Adjust size of columns and rows in accordance with given N
+# func <col> <row> <N>
+# i.e:
+# func "" "" 20
+# => returns 4 5
+# func "6" 0 20
+# => returns 6 4
+xpns_adjust_col_row() {
+ local col="${1:-0}" ;shift
+ local row="${1:-0}" ;shift
+ local N="$1" ;shift
+ local fix_col_flg
+ local fix_row_flg
+ (( col != 0 )) && fix_col_flg=1 || fix_col_flg=0
+ (( row != 0 )) && fix_row_flg=1 || fix_row_flg=0
+
+ # This is just a author (@greymd)'s preference.
+ if (( fix_col_flg == 0 )) && (( fix_row_flg == 0 )) && (( N == 2)) ;then
+ col=2
+ row=1
+ printf "%d %d\\n" "${col}" "${row}"
+ return
+ fi
+
+ # If both valures are provided, col is used.
+ if (( fix_col_flg == 1 )) && (( fix_row_flg == 1 ));then
+ row=0
+ fix_row_flg=0
+ fi
+ # This algorhythm is almost same as tmux default
+ # https://github.com/tmux/tmux/blob/2.8/layout-set.c#L436
+ while (( col * row < N )) ;do
+ (( fix_row_flg != 1 )) && (( row = row + 1 ))
+ if (( col * row < N ));then
+ (( fix_col_flg != 1 )) && (( col = col + 1 ))
+ fi
+ done
+ printf "%d %d\\n" "${col}" "${row}"
+}
+
+# Make each line unique by adding index number
+# echo aaa bbb ccc aaa ccc ccc | xargs -n 1 | xpns_unique_line
+# aaa-1
+# bbb-1
+# ccc-1
+# aaa-2
+# ccc-2
+# ccc-3
+#
+# Eval is used because associative array is not supported before bash 4.2
+xpns_unique_line () {
+ local _val_name
+ while read -r line; do
+ _val_name="__xpns_hash_$(printf "%s" "${line}" | xpns_value2key)"
+ # initialize variable
+ eval "${_val_name}=\${${_val_name}:-0}"
+ # increment variable
+ eval "${_val_name}=\$(( ++${_val_name} ))"
+ printf "%s\\n" "${line}-$(eval printf "%s" "\$${_val_name}")"
+ done
+}
+
+#
+# Generate log file names from given arguments.
+# Usage:
+# echo <arg1> <arg2> ... | xpns_log_filenames <FORMAT>
+# Return:
+# File names.
+# Example:
+# $ echo aaa bbb ccc aaa ccc ccc | xargs -n 1 | xpns_log_filenames '[:ARG:]_[:PID:]_%Y%m%d.log'
+# aaa-1_1234_20160101.log
+# bbb-1_1234_20160101.log
+# ccc-1_1234_20160101.log
+# aaa-2_1234_20160101.log
+# ccc-2_1234_20160101.log
+# ccc-3_1234_20160101.log
+#
+xpns_log_filenames () {
+ local _arg_fmt="$1"
+ local _full_fmt=
+ _full_fmt="$(date "+${_arg_fmt}")"
+ cat \
+ | \
+ # 1st argument + '-' + unique number (avoid same argument has same name)
+ xpns_unique_line \
+ | while read -r _arg
+ do
+ cat <<<"${_full_fmt}" \
+ | sed "s/\\[:ARG:\\]/${_arg}/g" \
+ | sed "s/\\[:PID:\\]/$$/g"
+ done
+}
+
+## --------------------------------
+# Normalize directory by making following conversion.
+# * Tilde expansion.
+# * Remove the slash '/' at the end of the dirname.
+# Usage:
+# xpns_normalize_directory <direname>
+# Return:
+# Normalized <dirname>
+## --------------------------------
+xpns_normalize_directory() {
+ local _dir="$1"
+ # Remove end of slash '/'
+ _dir="${_dir%/}"
+ # tilde expansion
+ _dir="${_dir/#~/${HOME}}"
+ printf "%s\\n" "${_dir}"
+}
+
+## --------------------------------
+# Ensure existence of given directory
+# Usage:
+# xpns_is_valid_directory <direname>
+# Return:
+# Absolute path of the <dirname>
+## --------------------------------
+xpns_is_valid_directory() {
+ local _dir="$1"
+ local _checkfile="${XP_THIS_FILE_NAME}.$$"
+ # Check directory.
+ if [[ ! -d "${_dir}" ]]; then
+ # Create directory
+ if mkdir "${_dir}"; then
+ xpns_msg_info "${_dir} is created."
+ else
+ xpns_msg_error "Failed to create ${_dir}"
+ exit ${XP_ELOGDIR}
+ fi
+ fi
+ # Try to create file.
+ # Not only checking directory permission,
+ # but also i-node and other misc situations.
+ if ! touch "${_dir}/${_checkfile}"; then
+ xpns_msg_error "${_dir} is not writable."
+ rm -f "${_dir}/${_checkfile}"
+ exit ${XP_ELOGWRITE}
+ fi
+ rm -f "${_dir}/${_checkfile}"
+}
+
+# Convert array to string which is can be used as command line argument.
+# Usage:
+# xpns_arr2args <array object>
+# Example:
+# array=(aaa bbb "ccc ddd" eee "f'f")
+# xpns_arr2args "${array[@]}"
+# @returns "'aaa' 'bbb' 'ccc ddd' 'eee' 'f\'f'"
+# Result:
+xpns_arr2args() {
+ local _arg=""
+ # If there is no argument, usage will be shown.
+ if [[ $# -lt 1 ]]; then
+ return 0
+ fi
+ for i in "$@" ;do
+ _arg="${i}"
+ # Use 'cat <<<"input"' command instead of 'echo',
+ # because such the command recognizes option like '-e'.
+ cat <<<"${_arg}" \
+ | \
+ # Escaping single quotations.
+ sed "s/'/'\"'\"'/g" \
+ | \
+ # Surround argument with single quotations.
+ sed "s/^/'/;s/$/' /" \
+ | \
+ # Remove new lines
+ tr -d '\n'
+ done
+}
+
+# Extract first field to generate window name.
+# ex, $2 = 'aaa bbb ccc'
+# return = aaa-12345(PID)
+xpns_generate_window_name() {
+ local _unprintable_str="${1-}"; shift
+ # Leave first 200 characters to prevent
+ # the name exceed the maximum length of tmux window name (2000 byte).
+ printf "%s\\n" "${1:-${_unprintable_str}}" \
+ | ( read -r _name _ && printf "%s\\n" "${_name:0:200}-$$" )
+}
+
+# Convert string to another string which can be handled as tmux window name.
+xpns_value2key() {
+ od -v -tx1 -An | tr -dc 'a-zA-Z0-9' | tr -d '\n'
+}
+
+# Restore string encoded by xpns_value2key function.
+xpns_key2value() {
+ read -r _key
+ # shellcheck disable=SC2059
+ printf "$(printf "%s" "$_key" | sed 's/../\\x&/g')"
+}
+
+# Remove empty lines
+# This function behaves like `awk NF`
+xpns_rm_empty_line() {
+ { cat; printf "\\n";} | while IFS= read -r line;do
+ # shellcheck disable=SC2086
+ set -- ${line-}
+ if [[ $# != 0 ]]; then
+ printf "%s\\n" "${line}"
+ fi
+ done
+}
+
+# Extract matched patterns from string
+# $ xpns_extract_matched "aaa123bbb" "[0-9]{3}"
+# => "123"
+xpns_extract_matched() {
+ local _args="$1" ;shift
+ local _regex="($1)"
+ if [[ $_args =~ $_regex ]];then
+ printf "%s" "${BASH_REMATCH[0]}"
+ fi
+}
+
+# Enable logging feature to the all the panes in the target window.
+xpns_enable_logging() {
+ local _window_name="$1" ; shift
+ local _index_offset="$1" ; shift
+ local _log_dir="$1" ; shift
+ local _log_format="$1" ; shift
+ local _unprintable_str="$1" ; shift
+ local _args=("$@")
+ local _args_num=$(($# - 1))
+ # Generate log files from arguments.
+ local _idx=0
+ while read -r _logfile ; do
+ # Start logging
+ xpns_msg_debug "Start logging pipe-pane(cat >> '${_log_dir}/${_logfile}')"
+ ${TMUX_XPANES_EXEC} \
+ pipe-pane -t "${_window_name}.$(( _idx + _index_offset ))" \
+ "cat >> '${_log_dir}/${_logfile}'" # Tilde expansion does not work here.
+ _idx=$(( _idx + 1 ))
+ done < <(
+ for i in $(xpns_seq 0 "${_args_num}")
+ do
+ # Replace empty string.
+ printf "%s\\n" "${_args[i]:-${_unprintable_str}}"
+ done | xpns_log_filenames "${_log_format}"
+ )
+}
+
+## Print "1" on the particular named pipe
+xpns_notify() {
+ local _wait_id="$1" ; shift
+ local _fifo=
+ _fifo="${XP_CACHE_HOME}/__xpns_${_wait_id}"
+ xpns_msg_debug "Notify to $_fifo"
+ printf "%s\\n" 1 > "$_fifo" &
+}
+
+xpns_notify_logging() {
+ local _window_name="$1" ; shift
+ local _args_num=$(($# - 1))
+ for i in $(xpns_seq 0 "${_args_num}"); do
+ xpns_notify "log_${_window_name}-${i}-$$"
+ done
+}
+
+xpns_notify_sync() {
+ local _window_name="$1" ; shift
+ local _args_num=$(($# - 1))
+ for i in $(xpns_seq 0 "${_args_num}"); do
+ xpns_notify "sync_${_window_name}-${i}-$$" &
+ done
+}
+
+xpns_is_window_alive() {
+ local _window_name="$1" ;shift
+ local _speedy_await_flag="$1" ;shift
+ local _def_allow_rename="$1" ;shift
+ if ! ${TMUX_XPANES_EXEC} display-message -t "$_window_name" -p > /dev/null 2>&1 ;then
+ xpns_msg_info "All the panes are closed before displaying the result."
+ if [[ "${_speedy_await_flag}" -eq 0 ]] ;then
+ xpns_msg_info "Use '-s' option instead of '-ss' option to avoid this behavior."
+ fi
+ xpns_restore_allow_rename "${_def_allow_rename-}"
+ exit ${XP_ENOPANE}
+ fi
+}
+
+xpns_inject_title() {
+ local _target_pane="$1" ;shift
+ local _message="$1" ;shift
+ local _pane_tty=
+ _pane_tty="$( ${TMUX_XPANES_EXEC} display-message -t "${_target_pane}" -p "#{pane_tty}" )"
+ printf "\\033]2;%s\\033\\\\" "${_message}" > "${_pane_tty}"
+ xpns_msg_debug "target_pane=${_target_pane} pane_title=${_message} pane_tty=${_pane_tty}"
+}
+
+xpns_is_pane_title_required() {
+ local _title_flag="$1" ; shift
+ local _extra_flag="$1" ; shift
+ local _pane_border_status=
+ _pane_border_status=$(xpns_get_local_tmux_conf "pane-border-status")
+ if [[ $_title_flag -eq 1 ]]; then
+ return 0
+ elif [[ ${_extra_flag} -eq 1 ]] && \
+ [[ "${_pane_border_status}" != "off" ]] && \
+ [[ -n "${_pane_border_status}" ]] ;then
+ ## For -x option
+ # Even the -t option is not specified, it is required to inject pane title here.
+ # Because user expects the title is displayed on the pane if the original window is
+ # generated from tmux-xpanes with -t option.
+ return 0
+ fi
+ return 1
+}
+
+# Set pane titles for each pane for -t option
+xpns_set_titles() {
+ local _window_name="$1" ; shift
+ local _index_offset="$1" ; shift
+ local _index=0
+ local _pane_index=
+ for arg in "$@"
+ do
+ _pane_index=$(( _index + _index_offset ))
+ xpns_inject_title "${_window_name}.${_pane_index}" "${arg}"
+ _index=$(( _index + 1 ))
+ done
+}
+
+# Send command to the all the panes in the target window.
+xpns_send_commands() {
+ local _window_name="$1" ; shift
+ local _index_offset="$1" ; shift
+ local _repstr="$1" ; shift
+ local _cmd="$1" ; shift
+ local _index=0
+ local _pane_index=
+ local _exec_cmd=
+ for arg in "$@"
+ do
+ _exec_cmd="${_cmd//${_repstr}/${arg}}"
+ _pane_index=$(( _index + _index_offset ))
+ ${TMUX_XPANES_EXEC} send-keys -t "${_window_name}.${_pane_index}" "${_exec_cmd}" C-m
+ _index=$(( _index + 1 ))
+ done
+}
+
+# Separate window vertically, when the number of panes is 1 or 2.
+xpns_organize_panes() {
+ local _window_name="$1" ; shift
+ local _args_num="$1"
+ ## ----------------
+ # Default behavior
+ ## ----------------
+ if [[ "${_args_num}" -eq 1 ]]; then
+ ${TMUX_XPANES_EXEC} select-layout -t "${_window_name}" even-horizontal
+ elif [[ "${_args_num}" -gt 1 ]]; then
+ ${TMUX_XPANES_EXEC} select-layout -t "${_window_name}" tiled
+ fi
+ ## ----------------
+ # Update layout
+ ## ----------------
+ if [[ "${XP_LAYOUT}" != "${XP_DEFAULT_LAYOUT}" ]]; then
+ ${TMUX_XPANES_EXEC} select-layout -t "${_window_name}" "${XP_LAYOUT}"
+ fi
+}
+
+#
+# Generate sequential number descending order.
+# seq is not used because old version of
+# seq does not generate descending oorder.
+# $ xpns_seq 3 0
+# 3
+# 2
+# 1
+# 0
+#
+xpns_seq () {
+ local _num1="$1"
+ local _num2="$2"
+ eval "printf \"%d\\n\" {$_num1..$_num2}"
+}
+
+xpns_wait_func() {
+ local _wait_id="$1"
+ local _fifo="${XP_CACHE_HOME}/__xpns_${_wait_id}"
+ local _arr=("$_fifo")
+ local _fifo_arg=
+ _fifo_arg=$(xpns_arr2args "${_arr[@]}")
+ xpns_msg_debug "mkfifo $_fifo"
+ mkfifo "${_fifo}"
+ xpns_msg_debug "grep -q 1 ${_fifo_arg}"
+ printf "%s\\n" "grep -q 1 ${_fifo_arg}"
+}
+
+# Split a new window into multiple panes.
+#
+xpns_split_window() {
+ local _window_name="$1" ; shift
+ local _log_flag="$1" ; shift
+ local _title_flag="$1" ; shift
+ local _speedy_flag="$1" ; shift
+ local _await_flag="$1" ; shift
+ local _pane_base_index="$1" ; shift
+ local _repstr="$1" ; shift
+ local _cmd_template="$1" ; shift
+ local _exec_cmd=
+ local _sep_count=0
+ local args=("$@")
+ _last_idx=$(( ${#args[@]} - 1 ))
+
+ for i in $(xpns_seq $_last_idx 0)
+ do
+ xpns_msg_debug "Index:${i} Argument:${args[i]}"
+ _sep_count=$((_sep_count + 1))
+ _exec_cmd="${_cmd_template//${_repstr}/${args[i]}}"
+
+ ## Speedy mode
+ if [[ $_speedy_flag -eq 1 ]]; then
+
+ _exec_cmd=$(xpns_inject_wait_command "${_log_flag}" "${_title_flag}" "${_speedy_flag}" "${_await_flag}" "$i" "${_exec_cmd}")
+ # Execute command as a child process of default-shell.
+ ${TMUX_XPANES_EXEC} split-window -t "${_window_name}" -h -d "${_exec_cmd}"
+ else
+ # Open login shell and execute command on the interactive screen.
+ ${TMUX_XPANES_EXEC} split-window -t "${_window_name}" -h -d
+ fi
+ # Restraining that size of pane's width becomes
+ # less than the minimum size which is defined by tmux.
+ if [[ ${_sep_count} -gt 2 ]]; then
+ ${TMUX_XPANES_EXEC} select-layout -t "${_window_name}" tiled
+ fi
+ done
+}
+
+#
+# Create new panes to the existing window.
+# Usage:
+# func <window name> <offset of index> <number of pane>
+#
+xpns_prepare_extra_panes() {
+ local _window_name="$1" ; shift
+ local _pane_base_index="$1" ; shift
+ local _log_flag="$1" ; shift
+ local _title_flag="$1" ; shift
+ local _speedy_flg="$1" ; shift
+ local _await_flg="$1" ; shift
+ # specify a pane which has the biggest index number.
+ # Because pane_id may not be immutable.
+ # If the small number of index is specified here, correspondance between pane_title and command can be slip off.
+ ${TMUX_XPANES_EXEC} select-pane -t "${_window_name}.${_pane_base_index}"
+
+ # split window into multiple panes
+ xpns_split_window \
+ "${_window_name}" \
+ "${_log_flag}" \
+ "${_title_flag}" \
+ "${_speedy_flg}" \
+ "${_await_flg}" \
+ "${_pane_base_index}" \
+ "$@"
+}
+
+xpns_get_joined_begin_commands () {
+ local _commands="$1"
+ if [[ "${#XP_BEGIN_ARGS[*]}" -lt 1 ]]; then
+ printf "%s" "${_commands}"
+ return
+ fi
+ printf "%s\\n" "${XP_BEGIN_ARGS[@]}" "${_commands}"
+}
+
+xpns_inject_wait_command () {
+ local _log_flag="$1" ; shift
+ local _title_flag="$1" ; shift
+ local _speedy_flg="$1" ; shift
+ local _await_flg="$1" ; shift
+ local _idx="$1" ; shift
+ local _exec_cmd="$1" ; shift
+
+ ## Speedy mode + logging
+ if [[ "${_log_flag}" -eq 1 ]] && [[ "${_speedy_flg}" -eq 1 ]]; then
+ # Wait for start of logging
+ # Without this part, logging thread may start after new process is finished.
+ # Execute function to wait for logging start.
+ _exec_cmd="$(xpns_wait_func "log_${_window_name}-${_idx}-$$")"$'\n'"${_exec_cmd}"
+ fi
+
+ ## Speedy mode (Do not allow to close panes before the separation is finished).
+ if [[ "${_speedy_flg}" -eq 1 ]]; then
+ _exec_cmd="$(xpns_wait_func "sync_${_window_name}-${_idx}-$$")"$'\n'${_exec_cmd}
+ fi
+
+ ## -s: Speedy mode (Not -ss: Speedy mode + nowait)
+ if [[ "${_await_flg}" -eq 1 ]]; then
+ local _msg
+ _msg="$(xpns_arr2args "${TMUX_XPANES_PANE_DEAD_MESSAGE}" | sed 's/"/\\"/g')"
+ _exec_cmd="${_exec_cmd}"$'\n'"${XP_SHELL} -c \"printf -- ${_msg} >&2 && read\""
+ fi
+ printf "%s" "${_exec_cmd}"
+}
+
+xpns_new_window () {
+ local _window_name="$1" ; shift
+ local _attach_flg="$1" ; shift
+ local _speedy_flg="$1" ; shift
+ local _exec_cmd="$1" ; shift
+ local _window_id=
+
+ # Create new window.
+ if [[ "${_attach_flg}" -eq 1 ]]; then
+ if [[ "${_speedy_flg}" -eq 1 ]]; then
+ _window_id=$(${TMUX_XPANES_EXEC} new-window -n "${_window_name}" -F '#{window_id}' -P "${_exec_cmd}")
+ else
+ _window_id=$(${TMUX_XPANES_EXEC} new-window -n "${_window_name}" -F '#{window_id}' -P )
+ fi
+ else
+ # Keep background
+ if [[ "${_speedy_flg}" -eq 1 ]]; then
+ _window_id=$(${TMUX_XPANES_EXEC} new-window -n "${_window_name}" -F '#{window_id}' -P -d "${_exec_cmd}")
+ else
+ _window_id=$(${TMUX_XPANES_EXEC} new-window -n "${_window_name}" -F '#{window_id}' -P -d)
+ fi
+ fi
+ printf "%s" "${_window_id}"
+}
+
+xpns_new_pane_vertical () {
+ local _window_id="$1" ; shift
+ local _cell_height="$1" ; shift
+ local _speedy_flg="$1" ; shift
+ local _exec_cmd="$1" ; shift
+ local _pane_id=
+ if [[ "${_speedy_flg}" -eq 1 ]]; then
+ _pane_id="$(${TMUX_XPANES_EXEC} split-window -t "$_window_id" -v -d -l "${_cell_height}" -F '#{pane_id}' -P "${_exec_cmd}")"
+ else
+ _pane_id="$(${TMUX_XPANES_EXEC} split-window -t "$_window_id" -v -d -l "${_cell_height}" -F '#{pane_id}' -P)"
+ fi
+ printf "%s\\n" "${_pane_id}"
+}
+
+xpns_split_pane_horizontal () {
+ local _target_pane_id="$1" ; shift
+ local _cell_width="$1" ; shift
+ local _speedy_flg="$1" ; shift
+ local _exec_cmd="$1" ; shift
+ if [[ "${_speedy_flg}" -eq 1 ]]; then
+ ${TMUX_XPANES_EXEC} split-window -t "$_target_pane_id" -h -d -l "$_cell_width" "${_exec_cmd}"
+ else
+ ${TMUX_XPANES_EXEC} split-window -t "$_target_pane_id" -h -d -l "$_cell_width"
+ fi
+}
+
+xpns_prepare_window () {
+ local _window_name="$1" ; shift
+ local _log_flag="$1" ; shift
+ local _title_flag="$1" ; shift
+ local _attach_flg="$1" ; shift
+ local _speedy_flg="$1" ; shift
+ local _await_flg="$1" ; shift
+ local _repstr="$1" ; shift
+ local _cmd_template="$1" ; shift
+ local _args=("$@")
+ local _window_height="$XP_WINDOW_HEIGHT"
+ local _window_width="$XP_WINDOW_WIDTH"
+ local _col="$XP_OPT_CUSTOM_SIZE_COLS"
+ local _row="$XP_OPT_CUSTOM_SIZE_ROWS"
+ local _cols=("${XP_COLS[@]}")
+ local _cols_offset=("${XP_COLS_OFFSETS[@]}")
+ local _exec_cmd=
+ local _pane_id=
+ local _first_pane_id=
+ local _window_id=
+ local _cell_height=
+ local _cell_width=
+ local _top_pane_height=
+ local _current_pane_width=
+ local i=
+ local j=
+ local _rest_col=
+ local _rest_row=
+ local _offset=
+
+ _cell_height=$(( ( _window_height - _row + 1 ) / _row ))
+ ## Insert first element
+ _exec_cmd="${_cmd_template//${_repstr}/${_args[0]}}"
+ _exec_cmd="$(xpns_inject_wait_command "${_log_flag}" "${_title_flag}" "${_speedy_flg}" "${_await_flg}" 0 "${_exec_cmd}")"
+ _window_id=$(xpns_new_window "${_window_name}" "${_attach_flg}" "${_speedy_flg}" "${_exec_cmd}")
+ _first_pane_id=$(${TMUX_XPANES_EXEC} display-message -t "$_window_id" -p -F '#{pane_id}' | head -n 1)
+
+ ## Start from last row
+ for (( i = _row - 1 ; i > 0 ; i-- ));do
+ _col="${_cols[i]}"
+ _cell_width=$(( ( _window_width - _col + 1 ) / _col ))
+ xpns_msg_debug "_col=$_col"
+ (( _offset = _cols_offset[i] ))
+ for (( j = 0 ; j < _col ; j++ ));do
+ if (( j == 0 )) ;then
+ (( idx = _offset - _col ))
+ # Create new row
+ # Insert first element of the row first
+ _exec_cmd="${_cmd_template//${_repstr}/${_args[idx]}}"
+ _exec_cmd="$(xpns_inject_wait_command "${_log_flag}" "${_title_flag}" "${_speedy_flg}" "${_await_flg}" "${idx}" "${_exec_cmd}")"
+ _pane_id=$(xpns_new_pane_vertical "${_window_name}" "${_cell_height}" "${_speedy_flg}" "${_exec_cmd}")
+ fi
+ # Separate row into columns
+ if (( j != 0 )) ;then
+ (( idx = _offset - j ))
+ _exec_cmd="${_cmd_template//${_repstr}/${_args[idx]}}"
+ _exec_cmd="$(xpns_inject_wait_command "${_log_flag}" "${_title_flag}" "${_speedy_flg}" "${_await_flg}" "${idx}" "${_exec_cmd}")"
+ ## Separate row into columns
+ _current_pane_width=$(${TMUX_XPANES_EXEC} display-message -t "$_pane_id" -p '#{pane_width}' | head -n 1)
+ _rest_col=$(( _col - j + 1 ))
+ _cell_width=$(( ( _current_pane_width - _rest_col + 1 ) / _rest_col ))
+ xpns_split_pane_horizontal "$_pane_id" "$_cell_width" "${_speedy_flg}" "${_exec_cmd}"
+ fi
+ done
+
+ # Adjust height
+ _top_pane_height=$(${TMUX_XPANES_EXEC} display-message -t "$_window_id" -p '#{pane_height}' | head -n 1)
+ _rest_row=$(( i ))
+ xpns_msg_debug "_top_pane_height=$_top_pane_height _rest_row=$_rest_row"
+ _cell_height=$(( ( _top_pane_height - _rest_row + 1 ) / _rest_row ))
+ done
+
+ # Split first row into columns
+ _col="${_cols[0]}"
+ _cell_width=$(( ( _window_width - _col + 1 ) / _col ))
+ for (( j = 1 ; j < _col ; j++ ));do
+ idx=$(( _cols_offset[0] - j ))
+ # Adjust width
+ _current_pane_width=$(${TMUX_XPANES_EXEC} display-message -t "$_first_pane_id" -p '#{pane_width}' | head -n 1)
+ _rest_col=$(( _col - j + 1 ))
+ _cell_width=$(( ( _current_pane_width - _rest_col + 1 ) / _rest_col ))
+ ## Split top row into columns
+ _exec_cmd="${_cmd_template//${_repstr}/${_args[idx]}}"
+ _exec_cmd="$(xpns_inject_wait_command "${_log_flag}" "${_title_flag}" "${_speedy_flg}" "${_await_flg}" "${idx}" "${_exec_cmd}")"
+ xpns_split_pane_horizontal "${_first_pane_id}" "${_cell_width}" "${_speedy_flg}" "${_exec_cmd}"
+ done
+}
+
+xpns_is_session_running() {
+ local _socket="$1"
+ ${TMUX_XPANES_EXEC} -S "${_socket}" list-session > /dev/null 2>&1
+}
+
+# Remove unnecessary session files as much as possible
+# to let xpanes avoids to load old .tmux.conf.
+xpns_clean_session() {
+ if [[ "${XP_SOCKET_PATH}" != "${XP_DEFAULT_SOCKET_PATH}" ]]; then
+ return
+ fi
+ # Delete old socket file (xpanes v3.1.0 or before).
+ if [[ -e "${XP_DEFAULT_SOCKET_PATH_BASE}" ]]; then
+ if ! xpns_is_session_running "${XP_DEFAULT_SOCKET_PATH_BASE}" ;then
+ xpns_msg_debug "socket(${XP_DEFAULT_SOCKET_PATH_BASE}) is not running. Remove it"
+ rm -f "${XP_DEFAULT_SOCKET_PATH_BASE}"
+ fi
+ fi
+ for _socket in "${XP_CACHE_HOME}"/socket.* ;do
+ xpns_msg_debug "file = ${_socket}"
+ if ! xpns_is_session_running "${_socket}" ;then
+ xpns_msg_debug "socket(${_socket}) is not running. Remove it"
+ rm -f "${_socket}"
+ else
+ xpns_msg_debug "socket(${_socket}) is running. Keep ${_socket}"
+ fi
+ done
+}
+
+#
+# Split a new window which was created by tmux into multiple panes.
+# Usage:
+# xpns_prepare_preset_layout_window <window name> <offset of index> <number of pane> <attach or not>
+#
+xpns_prepare_preset_layout_window() {
+ local _window_name="$1" ; shift
+ local _pane_base_index="$1" ; shift
+ local _log_flag="$1" ; shift
+ local _title_flag="$1" ; shift
+ local _attach_flg="$1" ; shift
+ local _speedy_flg="$1" ; shift
+ local _await_flg="$1" ; shift
+ # Create new window.
+ if [[ "${_attach_flg}" -eq 1 ]]; then
+ ${TMUX_XPANES_EXEC} new-window -n "${_window_name}"
+ else
+ # Keep background
+ ${TMUX_XPANES_EXEC} new-window -n "${_window_name}" -d
+ fi
+
+ # specify a pane which has the youngest number of index.
+ ${TMUX_XPANES_EXEC} select-pane -t "${_window_name}.${_pane_base_index}"
+
+ # split window into multiple panes
+ xpns_split_window \
+ "${_window_name}" \
+ "${_log_flag}" \
+ "${_title_flag}" \
+ "${_speedy_flg}" \
+ "${_await_flg}" \
+ "${_pane_base_index}" \
+ "$@"
+
+ ### If the first pane is still remaining,
+ ### panes cannot be organized well.
+ # Delete the first pane
+ ${TMUX_XPANES_EXEC} kill-pane -t "${_window_name}.${_pane_base_index}"
+
+ # Select second pane here.
+ # If the command gets error, it would most likely be caused by user (XP_ENOPANE).
+ # Suppress error message here and announce it in xpns_execution.
+ ${TMUX_XPANES_EXEC} select-pane -t "${_window_name}.${_pane_base_index}" > /dev/null 2>&1
+}
+
+# Check whether given command is in the PATH or not.
+xpns_check_env() {
+ local _cmds="$1"
+ while read -r cmd ; do
+ if ! type "${cmd}" > /dev/null 2>&1; then
+ if [[ "${cmd}" == "tmux" ]] && [[ "${TMUX_XPANES_EXEC}" == "tmux" ]]; then
+ xpns_msg_error "${cmd} is required. Install ${cmd} or set TMUX_XPANES_EXEC variable."
+ exit ${XP_ENOCMD}
+ elif [[ "${cmd}" != "tmux" ]]; then
+ xpns_msg_error "${cmd} is required."
+ exit ${XP_ENOCMD}
+ fi
+ fi
+ done < <(echo "${_cmds}" | tr ' ' '\n')
+
+ if ! mkdir -p "${XP_CACHE_HOME}";then
+ xpns_msg_warning "failed to create cache directory '${XP_CACHE_HOME}'."
+ fi
+
+ # Do not omit this part, this is used by testing.
+ TMUX_XPANES_TMUX_VERSION="${TMUX_XPANES_TMUX_VERSION:-$(xpns_get_tmux_version)}"
+ if ( xpns_tmux_is_greater_equals \
+ "${XP_SUPPORT_TMUX_VERSION_LOWER}" \
+ "${TMUX_XPANES_TMUX_VERSION}" ) ;then
+ : "Supported tmux version"
+ else
+ xpns_msg_warning \
+"'${XP_THIS_FILE_NAME}' may not work correctly! Please check followings.
+* tmux is installed correctly.
+* Supported tmux version is installed.
+ Version ${XP_SUPPORT_TMUX_VERSION_LOWER} and over is officially supported."
+ fi
+
+ return 0
+}
+
+xpns_pipe_filter() {
+ local _number="${1-}"
+ if [[ -z "${_number-}" ]]; then
+ cat
+ else
+ xargs -n "${_number}"
+ fi
+}
+
+xpns_set_args_per_pane() {
+ local _pane_num="$1"; shift
+ local _filtered_args=()
+ while read -r _line; do
+ _filtered_args+=("${_line}")
+ done < <(xargs -n "${_pane_num}" <<<"$(xpns_arr2args "${XP_ARGS[@]}")")
+ XP_ARGS=("${_filtered_args[@]}")
+}
+
+xpns_get_window_height_width() {
+ local _height=
+ local _width=
+ local _result=
+ local _dev=
+ local _pattern='^([0-9]+)[ \t]+([0-9]+)$'
+
+ if ! type stty > /dev/null 2>&1; then
+ xpns_msg_debug "'stty' does not exist: Failed to get window height and size. Skip checking"
+ return 1
+ fi
+
+ ## This condition is used for unit testing
+ if [[ -z "${XP_IS_PIPE_MODE-}" ]]; then
+ if [[ ! -t 0 ]]; then
+ XP_IS_PIPE_MODE=1
+ fi
+ fi
+ if [[ $XP_IS_PIPE_MODE -eq 0 ]]; then
+ if _result=$(stty size 2> /dev/null) && [[ "$_result" =~ $_pattern ]];then
+ _height="${BASH_REMATCH[1]}"
+ _width="${BASH_REMATCH[2]}"
+ xpns_msg_debug "window height: $_height, width: $_width"
+ printf "%s\\n" "$_height $_width"
+ return 0
+ fi
+ else
+ if ! type ps > /dev/null 2>&1 ;then
+ xpns_msg_debug "'ps' does not exist: Failed to get window height and size. Skip checking"
+ return 1
+ fi
+ { read -r; read -r _dev; } < <(ps -o tty -p $$)
+ ## If it's Linux, -F option is used
+ if _result=$(stty -F "/dev/${_dev}" size 2> /dev/null) && [[ "$_result" =~ $_pattern ]];then
+ _height="${BASH_REMATCH[1]}"
+ _width="${BASH_REMATCH[2]}"
+ xpns_msg_debug "window height: $_height, width: $_width"
+ printf "%s\\n" "$_height $_width"
+ return 0
+ fi
+ ## If it's BSD, macOS, -F option is used
+ if _result=$(stty -f "/dev/${_dev}" size 2> /dev/null) && [[ "$_result" =~ $_pattern ]];then
+ _height="${BASH_REMATCH[1]}"
+ _width="${BASH_REMATCH[2]}"
+ xpns_msg_debug "window height: $_height, width: $_width"
+ printf "%s\\n" "$_height $_width"
+ return 0
+ fi
+ return 1
+ fi
+ return 1
+}
+
+xpns_check_cell_size_bulk() {
+ local _cell_num="$1" ; shift
+ local _bulk_cols="$1" ; shift
+ local _win_height="$1" ; shift
+ local _win_width="$1" ; shift
+ local _ignore_flag="$1" ; shift
+ local _all_cols=()
+ # shellcheck disable=SC2178
+ local _cols=0
+ local _rows=0
+ local _sum_cell=0
+ IFS="," read -r -a _all_cols <<< "${_bulk_cols}"
+ _rows="${#_all_cols[@]}"
+ for i in "${_all_cols[@]}"; do
+ (( i >= _cols )) && (( _cols = i ))
+ (( _sum_cell = _sum_cell + i ))
+ done
+ if (( _sum_cell != _cell_num )) ;then
+ xpns_msg_error "Number of cols does not equals to the number of arguments."
+ xpns_msg_error "Expected (# of args) : $_cell_num, Actual (--bulk-cols) : $_sum_cell)."
+ return ${XP_ELAYOUT:-6}
+ fi
+ local cell_height=$(( ( _win_height - _rows + 1 ) / _rows ))
+ local cell_width=$(( ( _win_width - _cols + 1 ) / _cols ))
+
+ ## Display basic information
+ xpns_msg_debug "Window: { Height: $_win_height, Width: $_win_width }"
+ xpns_msg_debug "Cell: { Height: $cell_height, Width: $cell_width }"
+ xpns_msg_debug "# Of Panes: ${_cell_num}"
+ xpns_msg_debug " | Row[0] --...--> Row[MAX]"
+ xpns_msg_debug " -----+------------------------..."
+ xpns_msg_debug " Col[]| ${_all_cols[*]}"
+ xpns_msg_debug " -----+------------------------..."
+
+ if [[ "$_ignore_flag" -ne 1 ]] && ( (( cell_height < 2 )) || (( cell_width < 2 )) ); then
+ xpns_msg_error "Expected pane size is too small (height: $cell_height lines, width: $cell_width chars)"
+ return ${XP_ESMLPANE:-7}
+ fi
+ printf "%s\\n" "${_cols} ${_rows} ${_all_cols[*]}"
+}
+
+xpns_check_cell_size() {
+ local _cell_num="$1" ; shift
+ local _cols="$1" ; shift
+ local _rows="$1" ; shift
+ local _win_height="$1" ; shift
+ local _win_width="$1" ; shift
+ local _ignore_flag="$1" ; shift
+ local _all_cols_num=
+ local _all_rows=()
+
+ if [[ -n "${_cols-}" ]] && [[ -n "${_rows-}" ]];then
+ xpns_msg_warning "Both col size and row size are provided. Col size is preferentially going to be applied."
+ fi
+ ## if col is only defined
+ if [[ -n "${_cols-}" ]] ;then
+ read -r _cols _rows < <(xpns_adjust_col_row "${_cols-}" 0 "${_cell_num}")
+ IFS=" " read -r -a _all_rows <<< "$(xpns_divide_equally "${_cell_num}" "${_cols}")"
+ _all_cols_num="$(xpns_nums_transpose "${_all_rows[@]}")"
+
+ ## if row is only defined
+ elif [[ -n "${_rows-}" ]] ;then
+ read -r _cols _rows < <(xpns_adjust_col_row 0 "${_rows-}" "${_cell_num}")
+ _all_cols_num="$(xpns_divide_equally "${_cell_num}" "${_rows}")"
+
+ ## if both are undefined
+ else
+ read -r _cols _rows < <(xpns_adjust_col_row 0 0 "${_cell_num}")
+ _all_cols_num="$(xpns_divide_equally "${_cell_num}" "${_rows}")"
+ fi
+
+ local cell_height=$(( ( _win_height - _rows + 1 ) / _rows ))
+ local cell_width=$(( ( _win_width - _cols + 1 ) / _cols ))
+
+ ## Display basic information
+ xpns_msg_debug "Window: { Height: $_win_height, Width: $_win_width }"
+ xpns_msg_debug "Cell: { Height: $cell_height, Width: $cell_width }"
+ xpns_msg_debug "# Of Panes: ${_cell_num}"
+ xpns_msg_debug " | Row[0] --...--> Row[MAX]"
+ xpns_msg_debug " -----+------------------------..."
+ xpns_msg_debug " Col[]| ${_all_cols_num}"
+ xpns_msg_debug " -----+------------------------..."
+
+ if [[ "$_ignore_flag" -ne 1 ]] && ( (( cell_height < 2 )) || (( cell_width < 2 )) ); then
+ xpns_msg_error "Expected pane size is too small (height: $cell_height lines, width: $cell_width chars)"
+ return "${XP_ESMLPANE:-7}"
+ fi
+ printf "%s\\n" "${_cols} ${_rows} ${_all_cols_num}"
+}
+
+# Execute from Normal mode1
+xpns_pre_execution() {
+ local _opts4args=""
+ local _args4args=""
+
+ if [[ ${XP_OPT_EXTRA} -eq 1 ]];then
+ xpns_msg_error "'-x' must be used within the running tmux session."
+ exit ${XP_EINVAL}
+ fi
+
+ # Run as best effort.
+ # Because after the tmux session is created, cols and rows would be provided by tmux.
+ IFS=" " read -r XP_WINDOW_HEIGHT XP_WINDOW_WIDTH < <(xpns_get_window_height_width) && {
+ local _arg_num="${#XP_ARGS[@]}"
+ local _cell_num _tmp_col_row_cols _tmp_cols
+ if [[ -n "$XP_MAX_PANE_ARGS" ]] && (( XP_MAX_PANE_ARGS > 1 ));then
+ _cell_num=$(( _arg_num / XP_MAX_PANE_ARGS ))
+ else
+ _cell_num="${_arg_num}"
+ fi
+ if [[ -n "${XP_OPT_BULK_COLS}" ]]; then
+ _tmp_col_row_cols="$(xpns_check_cell_size_bulk \
+ "${_cell_num}" \
+ "${XP_OPT_BULK_COLS}" \
+ "${XP_WINDOW_HEIGHT}" \
+ "${XP_WINDOW_WIDTH}" \
+ "${XP_OPT_IGNORE_SIZE_LIMIT:-0}")"
+ local _exit_status="$?"
+ [[ $_exit_status -eq ${XP_ELAYOUT} ]] && exit ${XP_ELAYOUT}
+ [[ $_exit_status -eq ${XP_ESMLPANE} ]] && exit ${XP_ESMLPANE}
+ else
+ _tmp_col_row_cols="$(xpns_check_cell_size \
+ "${_cell_num}" \
+ "${XP_OPT_CUSTOM_SIZE_COLS-}" \
+ "${XP_OPT_CUSTOM_SIZE_ROWS-}" \
+ "${XP_WINDOW_HEIGHT}" \
+ "${XP_WINDOW_WIDTH}" \
+ "${XP_OPT_IGNORE_SIZE_LIMIT:-0}")"
+ [[ $? -eq ${XP_ESMLPANE} ]] && exit ${XP_ESMLPANE}
+ fi
+
+ IFS=" " read -r XP_OPT_CUSTOM_SIZE_COLS XP_OPT_CUSTOM_SIZE_ROWS _tmp_cols <<< "$_tmp_col_row_cols"
+ IFS=" " read -r -a XP_COLS <<< "${_tmp_cols}"
+ IFS=" " read -r -a XP_COLS_OFFSETS <<< "$(printf "%s\\n" "${XP_COLS[*]}" | xpns_nums_accumulate_sum)"
+ xpns_msg_debug "Options: $(xpns_arr2args "${XP_OPTIONS[@]}")"
+ xpns_msg_debug "Arguments: $(xpns_arr2args "${XP_ARGS[@]}")"
+ }
+
+ # Append -- flag.
+ # Because any arguments may have `-`
+ if [[ ${XP_NO_OPT} -eq 1 ]]; then
+ XP_ARGS=("--" "${XP_ARGS[@]}")
+ fi
+
+ # If there is any options, escape them.
+ if [[ -n "${XP_OPTIONS[*]-}" ]]; then
+ _opts4args=$(xpns_arr2args "${XP_OPTIONS[@]}")
+ fi
+ _args4args=$(xpns_arr2args "${XP_ARGS[@]}")
+
+ # Run as best effort
+ xpns_clean_session || true
+
+ # Create new session.
+ ${TMUX_XPANES_EXEC} -S "${XP_SOCKET_PATH}" new-session \
+ -s "${XP_SESSION_NAME}" \
+ -n "${XP_TMP_WIN_NAME}" \
+ -d "${XP_ABS_THIS_FILE_NAME} ${_opts4args} ${_args4args}"
+
+ # Avoid attaching (for unit testing).
+ if [[ ${XP_OPT_ATTACH} -eq 1 ]]; then
+ if ! ${TMUX_XPANES_EXEC} -S "${XP_SOCKET_PATH}" attach-session -t "${XP_SESSION_NAME}" \
+ && [[ ${XP_IS_PIPE_MODE} -eq 1 ]]; then
+ ## In recovery case, overwrite trap to keep socket file
+ trap 'rm -f "${XP_CACHE_HOME}"/__xpns_*$$;' EXIT
+
+ xpns_msg "Recovery" \
+"Execute below command line to re-attach the new session.
+
+${TMUX_XPANES_EXEC} -S ${XP_SOCKET_PATH} attach-session -t ${XP_SESSION_NAME}
+
+"
+ exit ${XP_ETTY}
+ fi
+ fi
+}
+
+# Execute from inside of tmux session
+xpns_execution() {
+ local _pane_base_index=
+ local _window_name=
+ local _last_args_idx=
+ local _def_allow_rename=
+ local _pane_count=0
+
+ if [[ ${XP_IS_PIPE_MODE} -eq 0 ]] && [[ -n "${XP_MAX_PANE_ARGS-}" ]];then
+ xpns_set_args_per_pane "${XP_MAX_PANE_ARGS}"
+ fi
+
+ ## Fix window size and define pane size
+ {
+ local _tmp_col_row_cols _tmp_cols
+ IFS=" " read -r XP_WINDOW_HEIGHT XP_WINDOW_WIDTH < \
+ <(${TMUX_XPANES_EXEC} display-message -p '#{window_height} #{window_width}')
+ if [[ -n "${XP_OPT_BULK_COLS}" ]]; then
+ _tmp_col_row_cols="$(xpns_check_cell_size_bulk \
+ "${#XP_ARGS[@]}" \
+ "${XP_OPT_BULK_COLS}" \
+ "${XP_WINDOW_HEIGHT}" \
+ "${XP_WINDOW_WIDTH}" \
+ "${XP_OPT_IGNORE_SIZE_LIMIT:-0}")"
+ local _exit_status="$?"
+ [[ $_exit_status -eq ${XP_ELAYOUT} ]] && exit ${XP_ELAYOUT}
+ [[ $_exit_status -eq ${XP_ESMLPANE} ]] && exit ${XP_ESMLPANE}
+ else
+ _tmp_col_row_cols="$(xpns_check_cell_size \
+ "${#XP_ARGS[@]}" \
+ "${XP_OPT_CUSTOM_SIZE_COLS-}" \
+ "${XP_OPT_CUSTOM_SIZE_ROWS-}" \
+ "${XP_WINDOW_HEIGHT}" \
+ "${XP_WINDOW_WIDTH}" \
+ "${XP_OPT_IGNORE_SIZE_LIMIT:-0}")"
+ [[ $? -eq ${XP_ESMLPANE} ]] && exit ${XP_ESMLPANE}
+ fi
+ IFS=" " read -r XP_OPT_CUSTOM_SIZE_COLS XP_OPT_CUSTOM_SIZE_ROWS _tmp_cols <<< "$_tmp_col_row_cols"
+ IFS=" " read -r -a XP_COLS <<< "${_tmp_cols}"
+ IFS=" " read -r -a XP_COLS_OFFSETS <<< "$(printf "%s\\n" "${XP_COLS[*]}" | xpns_nums_accumulate_sum)"
+ xpns_msg_debug "Options: $(xpns_arr2args "${XP_OPTIONS[@]}")"
+ xpns_msg_debug "Arguments: $(xpns_arr2args "${XP_ARGS[@]}")"
+ }
+
+ _pane_base_index=$(xpns_get_global_tmux_conf 'pane-base-index')
+ _last_args_idx=$((${#XP_ARGS[@]} - 1))
+ _def_allow_rename="$(xpns_get_global_tmux_conf 'allow-rename')"
+
+ xpns_suppress_allow_rename "${_def_allow_rename-}"
+ XP_CMD_UTILITY="$(xpns_get_joined_begin_commands "${XP_CMD_UTILITY}")"
+
+ if [[ ${XP_OPT_EXTRA} -eq 1 ]];then
+ # Reuse existing window name
+ # tmux 1.6 does not support -F option
+ _window_name="$( ${TMUX_XPANES_EXEC} display -p -F "#{window_id}" )"
+ _pane_count="$( ${TMUX_XPANES_EXEC} list-panes | grep -c . )"
+ _pane_base_index=$(( _pane_base_index + _pane_count - 1 ))
+ _pane_active_pane_id=$(${TMUX_XPANES_EXEC} display -p -F "#{pane_id}")
+ else
+ _window_name=$(
+ xpns_generate_window_name \
+ "${XP_EMPTY_STR}" \
+ "${XP_ARGS[0]}" \
+ | xpns_value2key)
+ fi
+
+ ## --------------------
+ # Prepare window and panes
+ ## --------------------
+ if [[ ${XP_OPT_EXTRA} -eq 1 ]];then
+ xpns_prepare_extra_panes \
+ "${_window_name}" \
+ "${_pane_base_index}" \
+ "${XP_OPT_LOG_STORE}" \
+ "${XP_OPT_SET_TITLE}" \
+ "${XP_OPT_SPEEDY}" \
+ "${XP_OPT_SPEEDY_AWAIT}" \
+ "${XP_REPSTR}" \
+ "${XP_CMD_UTILITY}" \
+ "${XP_ARGS[@]}"
+ elif [[ ${XP_OPT_USE_PRESET_LAYOUT} -eq 1 ]];then
+ xpns_prepare_preset_layout_window \
+ "${_window_name}" \
+ "${_pane_base_index}" \
+ "${XP_OPT_LOG_STORE}" \
+ "${XP_OPT_SET_TITLE}" \
+ "${XP_OPT_ATTACH}" \
+ "${XP_OPT_SPEEDY}" \
+ "${XP_OPT_SPEEDY_AWAIT}" \
+ "${XP_REPSTR}" \
+ "${XP_CMD_UTILITY}" \
+ "${XP_ARGS[@]}"
+ elif [[ ${XP_OPT_USE_PRESET_LAYOUT} -eq 0 ]];then
+ xpns_prepare_window \
+ "${_window_name}" \
+ "${XP_OPT_LOG_STORE}" \
+ "${XP_OPT_SET_TITLE}" \
+ "${XP_OPT_ATTACH}" \
+ "${XP_OPT_SPEEDY}" \
+ "${XP_OPT_SPEEDY_AWAIT}" \
+ "${XP_REPSTR}" \
+ "${XP_CMD_UTILITY}" \
+ "${XP_ARGS[@]}"
+ fi
+
+ ## With -ss option, it is possible to close all the panes as of here.
+ ## Check status of the window. If no window exists, there is nothing to do execpt to exit.
+ xpns_msg_debug "xpns_is_window_alive:1: After window separation"
+ xpns_is_window_alive "${_window_name}" "${XP_OPT_SPEEDY_AWAIT}" "${_def_allow_rename-}"
+
+ if [[ ${XP_OPT_EXTRA} -eq 1 ]];then
+ # Set offset to avoid sending command to the original pane.
+ _pane_base_index=$((_pane_base_index + 1))
+ # Avoid to make layout even-horizontal even if there are many panes.
+ # in xpns_organize_panes
+ _last_args_idx=$((_last_args_idx + _pane_count))
+ # Re-select the windown that was active before.
+ ${TMUX_XPANES_EXEC} select-pane -t "${_window_name}.${_pane_active_pane_id}"
+ fi
+
+ if [[ ${XP_OPT_LOG_STORE} -eq 1 ]]; then
+ xpns_enable_logging \
+ "${_window_name}" \
+ "${_pane_base_index}" \
+ "${TMUX_XPANES_LOG_DIRECTORY}" \
+ "${TMUX_XPANES_LOG_FORMAT}" \
+ "${XP_EMPTY_STR}" \
+ "${XP_ARGS[@]}"
+
+ if [[ $XP_OPT_SPEEDY -eq 1 ]]; then
+ xpns_notify_logging \
+ "${_window_name}" \
+ "${XP_ARGS[@]}"
+ fi
+ fi
+
+ xpns_msg_debug "xpns_is_window_alive:2: After logging"
+ xpns_is_window_alive "${_window_name}" "${XP_OPT_SPEEDY_AWAIT}" "${_def_allow_rename-}"
+
+ # Set pane titles for each pane.
+ if xpns_is_pane_title_required "${XP_OPT_SET_TITLE}" "${XP_OPT_EXTRA}" ;then
+ xpns_set_titles \
+ "${_window_name}" \
+ "${_pane_base_index}" \
+ "${XP_ARGS[@]}"
+ fi
+
+ if [[ $XP_OPT_SPEEDY -eq 1 ]];then
+ xpns_notify_sync \
+ "${_window_name}" \
+ "${XP_ARGS[@]}"
+ fi
+
+ xpns_msg_debug "xpns_is_window_alive:3: After setting title"
+ xpns_is_window_alive "${_window_name}" "${XP_OPT_SPEEDY_AWAIT}" "${_def_allow_rename-}"
+
+ # Sending operations for each pane.
+ # With -s option, command is already sent.
+ if [[ $XP_OPT_SPEEDY -eq 0 ]]; then
+ xpns_send_commands \
+ "${_window_name}" \
+ "${_pane_base_index}" \
+ "${XP_REPSTR}" \
+ "${XP_CMD_UTILITY}" \
+ "${XP_ARGS[@]}"
+ fi
+
+ xpns_msg_debug "xpns_is_window_alive:4: After sending commands"
+ xpns_is_window_alive "${_window_name}" "${XP_OPT_SPEEDY_AWAIT}" "${_def_allow_rename-}"
+
+ ## With -l <layout>, panes are organized.
+ ## As well as -x, they are re-organized.
+ if [[ $XP_OPT_USE_PRESET_LAYOUT -eq 1 ]] || [[ ${XP_OPT_EXTRA} -eq 1 ]]; then
+ xpns_organize_panes \
+ "${_window_name}" \
+ "${_last_args_idx}"
+ fi
+
+ # Enable broadcasting
+ if [[ ${XP_OPT_IS_SYNC} -eq 1 ]] && [[ ${XP_OPT_EXTRA} -eq 0 ]]; then
+ ${TMUX_XPANES_EXEC} \
+ set-window-option -t "${_window_name}" \
+ synchronize-panes on
+ fi
+
+ ## In case of -t option
+ if [[ ${XP_OPT_SET_TITLE} -eq 1 ]] && [[ ${XP_OPT_CHANGE_BORDER} -eq 1 ]]; then
+ # Set border format
+ ${TMUX_XPANES_EXEC} \
+ set-window-option -t "${_window_name}" \
+ pane-border-format "${TMUX_XPANES_PANE_BORDER_FORMAT}"
+ # Show border status
+ ${TMUX_XPANES_EXEC} \
+ set-window-option -t "${_window_name}" \
+ pane-border-status "${TMUX_XPANES_PANE_BORDER_STATUS}"
+ fi
+
+ # In case of -x, this statement is skipped to keep the original window name
+ if [[ ${XP_OPT_EXTRA} -eq 0 ]];then
+ # Restore original window name.
+ ${TMUX_XPANES_EXEC} \
+ rename-window -t "${_window_name}" \
+ -- "$(printf "%s\\n" "${_window_name}" | xpns_key2value)"
+ fi
+
+ xpns_restore_allow_rename "${_def_allow_rename-}"
+}
+
+## ----------------
+# Arrange options for pipe mode
+# * argument -> command
+# * stdin -> argument
+## ----------------
+xpns_switch_pipe_mode() {
+ local _pane_num4new_term=""
+ if [[ -n "${XP_ARGS[*]-}" ]] && [[ -n "${XP_CMD_UTILITY-}" ]]; then
+ xpns_msg_error "Both arguments and other options (like '-c', '-e') which updates <command> are given."
+ exit ${XP_EINVAL}
+ fi
+
+ if [[ -z "${TMUX-}" ]]; then
+ xpns_msg_warning "Attached session is required for 'Pipe mode'."
+ # This condition is used when the following situations.
+ # * Enter from outside of tmux session(Normal mode1)
+ # * Pipe mode.
+ # * -n option.
+ #
+ # For example:
+ # (Normal mode1)$ echo {a..g} | ./xpanes -n 2
+ # => This will once create the new window like this.
+ # (inside of tmux session)$ ./xpanes '-n' '2' 'a' 'b' 'c' 'd' 'e' 'f' 'g'
+ # => After the window is closed, following panes would be left.
+ # (pane 1)$ echo a b
+ # (pane 2)$ echo c d
+ # (pane 3)$ echo e f
+ # (pane 4)$ echo g
+ # In order to create such the query,
+ # separate all the argument into minimum tokens
+ # with xargs -n 1
+ if [[ -n "${XP_MAX_PANE_ARGS-}" ]]; then
+ _pane_num4new_term=1
+ fi
+ fi
+
+ while read -r line;
+ do
+ XP_STDIN+=("${line}")
+ done < <(cat | xpns_rm_empty_line | \
+ xpns_pipe_filter "${_pane_num4new_term:-${XP_MAX_PANE_ARGS}}")
+
+
+ # Merge them into command.
+ if [[ -n "${XP_ARGS[*]-}" ]]; then
+ # Attention: It might be wrong result if IFS is changed.
+ XP_CMD_UTILITY="${XP_ARGS[*]}"
+ fi
+
+ # If there is empty -I option or user does not assign the <repstr>,
+ # Append the space and <repstr> at the end of the <command>
+ # This is same as the xargs command of GNU.
+ # i.e,
+ # $ echo 10 | xargs seq
+ # => seq 10
+ # Whith is same as
+ # $ echo 10 | xargs -I@ seq @
+ # => seq 10
+ if [[ -z "${XP_REPSTR}" ]]; then
+ XP_REPSTR="${XP_DEFAULT_REPSTR}"
+ if [[ -n "${XP_ARGS[*]-}" ]]; then
+ XP_CMD_UTILITY="${XP_ARGS[*]-} ${XP_REPSTR}"
+ fi
+ fi
+
+ # Deal with stdin as arguments.
+ XP_ARGS=("${XP_STDIN[@]-}")
+}
+
+xpns_layout_short2long() {
+ sed \
+ -e 's/^t$/tiled/' \
+ -e 's/^eh$/even-horizontal/' \
+ -e 's/^ev$/even-vertical/' \
+ -e 's/^mh$/main-horizontal/' \
+ -e 's/^mv$/main-vertical/' \
+ -e ';'
+}
+
+xpns_is_valid_layout() {
+ local _layout="${1-}"
+ local _pat='^(tiled|even-horizontal|even-vertical|main-horizontal|main-vertical)$'
+ if ! [[ $_layout =~ $_pat ]] ; then
+ xpns_msg_error "Invalid layout '${_layout}'."
+ exit ${XP_ELAYOUT}
+ fi
+}
+
+xpns_warning_before_extra() {
+ local _ans=
+ local _synchronized=
+ _synchronized="$(xpns_get_local_tmux_conf "synchronize-panes")"
+ if [[ "on" == "${_synchronized}" ]];then
+ xpns_msg_warning "Panes are now synchronized.
+'-x' option may cause unexpected behavior on the synchronized panes."
+ printf "Are you really sure? [y/n]: "
+ read -r _ans
+ if ! [[ "${_ans-}" =~ ^[yY] ]]; then
+ return 1
+ fi
+ fi
+}
+
+xpns_load_flag_options() {
+ if [[ "$1" =~ h ]]; then
+ xpns_usage
+ exit 0
+ fi
+ if [[ "$1" =~ V ]]; then
+ xpns_version
+ exit 0
+ fi
+ if [[ "$1" =~ x ]]; then
+ XP_OPT_EXTRA=1
+ XP_OPT_USE_PRESET_LAYOUT=1 ## Layout presets must be used with -x
+ if ! xpns_warning_before_extra; then
+ exit ${XP_EINTENT}
+ fi
+ fi
+ if [[ "$1" =~ d ]]; then
+ XP_OPT_IS_SYNC=0
+ fi
+ if [[ "$1" =~ e ]]; then
+ XP_REPSTR="{}"
+ XP_CMD_UTILITY="{}"
+ fi
+ if [[ "$1" =~ t ]]; then
+ if ( xpns_tmux_is_greater_equals 2.3 ) ; then
+ XP_OPT_SET_TITLE=1
+ XP_OPT_CHANGE_BORDER=1
+ else
+ xpns_msg_warning "-t option cannot be used by tmux version less than 2.3. Disabled."
+ sleep 1
+ fi
+ fi
+ if [[ "$1" =~ s ]]; then
+ XP_OPT_SPEEDY=1
+ XP_OPT_SPEEDY_AWAIT=1
+ fi
+ if [[ "$1" =~ ss ]]; then
+ XP_OPT_SPEEDY_AWAIT=0
+ fi
+ return 1
+}
+
+xpns_load_arg_options() {
+ # Extract flag options only.
+ local _pattern=
+ xpns_load_flag_options "$(xpns_extract_matched "$1" "^-${XP_FLAG_OPTIONS}+")" > /dev/null
+ if [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*I ]]; then
+ # Behavior like this.
+ # -IAAA -- XP_REPSTR="AAA"
+ # -I AAA BBB -- XP_REPSTR="AAA", XP_ARGS=("BBB")
+ # -I"AAA BBB" -- XP_REPSTR="AAA BBB"
+ # -IAAA BBB -- XP_REPSTR="AAA", XP_ARGS=("BBB")
+ # -I -d ... -- XP_REPSTR=""
+ _pattern="^-${XP_FLAG_OPTIONS}*I(.+)"
+ if [[ "$1" =~ $_pattern ]]; then
+ XP_REPSTR="${BASH_REMATCH[1]}"
+ return 0
+ elif ! [[ "$2" =~ ^-.* ]]; then
+ XP_REPSTR="$2"
+ return 0
+ else
+ xpns_msg_error "invalid argument '$2' for -I option"
+ exit ${XP_EINVAL}
+ fi
+ elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*l ]]; then
+ _pattern="^-${XP_FLAG_OPTIONS}*l(.+)"
+ if [[ "$1" =~ $_pattern ]]; then
+ XP_OPT_USE_PRESET_LAYOUT=1
+ XP_LAYOUT="$(cat <<<"${BASH_REMATCH[1]}" | xpns_layout_short2long)"
+ xpns_is_valid_layout "${XP_LAYOUT}"
+ return 0
+ elif ! [[ "$2" =~ ^-.* ]]; then
+ XP_OPT_USE_PRESET_LAYOUT=1
+ XP_LAYOUT="$(cat <<<"$2" | xpns_layout_short2long )"
+ xpns_is_valid_layout "${XP_LAYOUT}"
+ return 0
+ else
+ xpns_msg_error "invalid argument '$2' for -l option"
+ exit ${XP_EINVAL}
+ fi
+ elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*c ]]; then
+ _pattern="^-${XP_FLAG_OPTIONS}*c(.+)"
+ if [[ "$1" =~ $_pattern ]]; then
+ XP_CMD_UTILITY="${BASH_REMATCH[1]}"
+ XP_OPT_CMD_UTILITY=1
+ return 0
+ elif ! [[ "$2" =~ ^-.* ]]; then
+ XP_CMD_UTILITY="$2"
+ XP_OPT_CMD_UTILITY=1
+ return 0
+ else
+ xpns_msg_error "invalid argument '$2' for -c option"
+ exit ${XP_EINVAL}
+ fi
+ elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*n ]]; then
+ _pattern="^-${XP_FLAG_OPTIONS}*n([0-9]+)"
+ if [[ "$1" =~ $_pattern ]]; then
+ XP_MAX_PANE_ARGS="${BASH_REMATCH[1]}"
+ return 0
+ elif [[ "$2" =~ ^[0-9]+$ ]]; then
+ XP_MAX_PANE_ARGS="$2"
+ return 0
+ else
+ xpns_msg_error "invalid argument '$2' for -n option"
+ exit ${XP_EINVAL}
+ fi
+ elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*S ]]; then
+ _pattern="^-${XP_FLAG_OPTIONS}*S(.+)"
+ if [[ "$1" =~ $_pattern ]]; then
+ XP_SOCKET_PATH="${BASH_REMATCH[1]}"
+ return 0
+ elif ! [[ "$2" =~ ^-.* ]]; then
+ XP_SOCKET_PATH="$2"
+ return 0
+ else
+ xpns_msg_error "invalid argument '$2' for -S option"
+ exit ${XP_EINVAL}
+ fi
+ elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*C ]]; then
+ _pattern="^-${XP_FLAG_OPTIONS}*C([0-9]+)"
+ if [[ "$1" =~ $_pattern ]]; then
+ XP_OPT_CUSTOM_SIZE_COLS="${BASH_REMATCH[1]}"
+ return 0
+ elif [[ "$2" =~ ^[0-9]+$ ]];then
+ XP_OPT_CUSTOM_SIZE_COLS="$2"
+ return 0
+ else
+ xpns_msg_error "invalid argument '$2' for -C option"
+ exit ${XP_EINVAL}
+ fi
+ elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*R ]]; then
+ _pattern="^-${XP_FLAG_OPTIONS}*R([0-9]+)"
+ if [[ "$1" =~ $_pattern ]]; then
+ XP_OPT_CUSTOM_SIZE_ROWS="${BASH_REMATCH[1]}"
+ return 0
+ elif [[ "$2" =~ ^[0-9]+$ ]];then
+ XP_OPT_CUSTOM_SIZE_ROWS="$2"
+ return 0
+ else
+ xpns_msg_error "invalid argument '$2' for -R option"
+ exit ${XP_EINVAL}
+ fi
+ elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*B ]]; then
+ _pattern="^-${XP_FLAG_OPTIONS}*B(.+)"
+ if [[ "$1" =~ $_pattern ]]; then
+ XP_BEGIN_ARGS+=("${BASH_REMATCH[1]}")
+ return 0
+ else
+ XP_BEGIN_ARGS+=("$2")
+ return 0
+ fi
+ fi
+ return 0
+}
+
+xpns_load_long_options() {
+ if [[ "$1" =~ ^--help$ ]]; then
+ xpns_usage
+ exit 0
+ elif [[ "$1" =~ ^--version$ ]]; then
+ xpns_version
+ exit 0
+ elif [[ "$1" =~ ^--desync$ ]]; then
+ XP_OPT_IS_SYNC=0
+ return 1
+ elif [[ "$1" =~ ^--log-format=.*$ ]]; then
+ XP_OPT_LOG_STORE=1
+ TMUX_XPANES_LOG_FORMAT="${1#--log-format=}"
+ return 1
+ elif [[ "$1" =~ ^--log ]]; then
+ XP_OPT_LOG_STORE=1
+ if [[ "$1" =~ ^--log=.*$ ]]; then
+ TMUX_XPANES_LOG_DIRECTORY="${1#--log=}"
+ fi
+ return 1
+ elif [[ "$1" =~ ^--ssh$ ]]; then
+ XP_CMD_UTILITY="${XP_SSH_CMD_UTILITY}"
+ # Enable -t option as well
+ XP_OPT_SET_TITLE=1
+ XP_OPT_CHANGE_BORDER=1
+ # Enable -s option
+ XP_OPT_SPEEDY=1
+ XP_OPT_SPEEDY_AWAIT=1
+ return 1
+ elif [[ "$1" =~ ^--stay$ ]]; then
+ XP_OPT_ATTACH=0
+ return 1
+ elif [[ "$1" =~ ^--cols=[0-9]+$ ]]; then
+ XP_OPT_CUSTOM_SIZE_COLS="${1#--cols=}"
+ return 1
+ elif [[ "$1" =~ ^--rows=[0-9]+$ ]]; then
+ XP_OPT_CUSTOM_SIZE_ROWS="${1#--rows=}"
+ return 1
+ elif [[ "$1" =~ ^--bulk-cols=[0-9,]*[0-9]+$ ]]; then
+ XP_OPT_BULK_COLS="${1#--bulk-cols=}"
+ return 1
+ elif [[ "$1" =~ ^--debug$ ]]; then
+ XP_OPT_DEBUG=1
+ return 1
+ elif [[ "$1" =~ ^--dry-run$ ]]; then # For unit testing
+ XP_OPT_DRY_RUN=1
+ return 1
+ elif [[ "$1" =~ ^--ignore-size-limit$ ]]; then
+ XP_OPT_IGNORE_SIZE_LIMIT=1
+ return 1
+
+ ## ----------------
+ # Other options
+ ## ----------------
+ else
+ xpns_msg_error "invalid option -- '${1#--}'"
+ xpns_usage_warn
+ exit ${XP_EINVAL}
+ fi
+}
+
+xpns_parse_options() {
+ while (( $# > 0 )); do
+ case "$1" in
+ --)
+ if [[ ${XP_NO_OPT} -eq 1 ]]; then
+ XP_ARGS+=("$1")
+ shift
+ else
+ # Disable any more options
+ XP_NO_OPT=1
+ shift
+ fi
+ ;;
+ ## ----------------
+ # Long options
+ ## ----------------
+ --*)
+ if [[ ${XP_NO_OPT} -eq 1 ]]; then
+ XP_ARGS+=("$1")
+ shift
+ else
+ local _shift_count="0"
+ xpns_load_long_options "$@"
+ _shift_count="$?"
+ [[ "${_shift_count}" = "1" ]] && XP_OPTIONS+=("$1") && shift
+ fi
+ ;;
+ ## ----------------
+ # Short options
+ ## ----------------
+ -*)
+ if [[ ${XP_NO_OPT} -eq 1 ]]; then
+ XP_ARGS+=("$1")
+ shift
+ else
+ local _shift_count="0"
+ if [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*${XP_ARG_OPTIONS}. ]];then
+ xpns_load_arg_options "$@"
+ XP_OPTIONS+=("$1") && shift
+ elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*${XP_ARG_OPTIONS}$ ]] && [[ -n "${2-}" ]];then
+ xpns_load_arg_options "$@"
+ _shift_count="$?"
+ XP_OPTIONS+=("$1" "$2") && shift && shift
+ elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}+$ ]];then
+ xpns_load_flag_options "$1"
+ XP_OPTIONS+=("$1") && shift
+ ## ----------------
+ # Other options
+ ## ----------------
+ else
+ xpns_msg_error "Invalid option -- '${1#-}'"
+ xpns_usage_warn
+ exit ${XP_EINVAL}
+ fi
+ fi
+ ;;
+ ## ----------------
+ # Other arguments
+ ## ----------------
+ *)
+ XP_ARGS+=("$1")
+ XP_NO_OPT=1
+ shift
+ ;;
+ esac
+ done
+
+ # If there is any standard input from pipe,
+ # 1 line handled as 1 argument.
+ if [[ ! -t 0 ]]; then
+ XP_IS_PIPE_MODE=1
+ xpns_switch_pipe_mode
+ fi
+
+ # When no argument arr given, exit.
+ if [[ -z "${XP_ARGS[*]-}" ]]; then
+ xpns_msg_error "No arguments."
+ xpns_usage_warn
+ exit ${XP_EINVAL}
+ fi
+
+ if [[ -n "${XP_OPT_CUSTOM_SIZE_COLS-}" ]] || [[ -n "${XP_OPT_CUSTOM_SIZE_ROWS-}" ]]; then
+ if [[ "$XP_OPT_EXTRA" -eq 1 ]]; then
+ xpns_msg_warning "The columns/rows options (-C, --cols, -R, --rows) cannot be used with -x option. Ignored."
+ elif [[ "$XP_OPT_EXTRA" -eq 0 ]] && [[ "${XP_OPT_USE_PRESET_LAYOUT}" -eq 1 ]]; then
+ # This part is required to keep backward compatibility.
+ ## Users can simulate xpanes v3.x to set : alias xpanes="xpanes -lt"
+ xpns_msg_info "Columns/rows option (-C, --cols, -R, --rows) and -l option are provided. Disable -l. "
+ XP_OPT_USE_PRESET_LAYOUT=0
+ fi
+ fi
+
+ # Set default value in case of empty.
+ XP_CMD_UTILITY="${XP_CMD_UTILITY:-${XP_DEFAULT_CMD_UTILITY}}"
+ XP_REPSTR="${XP_REPSTR:-${XP_DEFAULT_REPSTR}}"
+
+ # To set command on pre_execution, set -c option manually.
+ if [[ ${XP_OPT_CMD_UTILITY} -eq 0 ]];then
+ XP_OPTIONS+=("-c" "${XP_CMD_UTILITY}")
+ fi
+
+}
+
+## --------------------------------
+# Main function
+## --------------------------------
+xpns_main() {
+ xpns_parse_options ${1+"$@"}
+ xpns_check_env "${XP_DEPENDENCIES}"
+ ## --------------------------------
+ # Parameter validation
+ ## --------------------------------
+ # When do dry-run flag is enabled, skip running (this is used to execute unit test of itself).
+ if [[ ${XP_OPT_DRY_RUN} -eq 1 ]]; then
+ return 0
+ fi
+ # Validate log directory.
+ if [[ ${XP_OPT_LOG_STORE} -eq 1 ]]; then
+ TMUX_XPANES_LOG_DIRECTORY=$(xpns_normalize_directory "${TMUX_XPANES_LOG_DIRECTORY}")
+ xpns_is_valid_directory "${TMUX_XPANES_LOG_DIRECTORY}" && \
+ TMUX_XPANES_LOG_DIRECTORY=$(cd "${TMUX_XPANES_LOG_DIRECTORY}" && pwd)
+ fi
+ ## --------------------------------
+ # If current shell is outside of tmux session.
+ ## --------------------------------
+ if [[ -z "${TMUX-}" ]]; then
+ xpns_pre_execution
+ ## --------------------------------
+ # If current shell is already inside of tmux session.
+ ## --------------------------------
+ else
+ xpns_execution
+ fi
+ exit 0
+}
+
+## --------------------------------
+# Entry Point
+## --------------------------------
+xpns_main ${1+"$@"}