#!/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] [ ...] _EOS_ } xpns_usage() { cat < ...] OPTIONS: -h,--help Display this help and exit. -V,--version Output version information and exit. -B Run before processing in each pane. Multiple options are allowed. -c Set 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 Replacing one or more occurrences of 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 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 Set the maximum number of 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 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[=] Enable logging and store log files to ~/.cache/xpanes/logs or . --log-format= Make name of log files follow . 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 # 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 ... | xpns_log_filenames # 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 # Return: # Normalized ## -------------------------------- 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 # Return: # Absolute path of the ## -------------------------------- 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 # 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 # 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 # 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 , 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 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 , # Append the space and at the end of the # 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+"$@"}