From fca5938e92781c271ae6a863b60c4f904ae7e697 Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Fri, 1 May 2020 12:11:53 +1200 Subject: [PATCH 1/5] add xpanes --- .bin/xpanes | 2057 +++++++++++++++++++++++++++++++++++++++++++++ .gitignore.d/tmux | 1 + 2 files changed, 2058 insertions(+) create mode 100755 .bin/xpanes diff --git a/.bin/xpanes b/.bin/xpanes new file mode 100755 index 0000000..5b3d1f9 --- /dev/null +++ b/.bin/xpanes @@ -0,0 +1,2057 @@ +#!/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+"$@"} diff --git a/.gitignore.d/tmux b/.gitignore.d/tmux index c398bca..d34ad59 100644 --- a/.gitignore.d/tmux +++ b/.gitignore.d/tmux @@ -1,5 +1,6 @@ * !/.bin/mtmux !/.bin/tmux +!/.bin/xpanes !/.gitignore.d/tmux !/.tmux.conf -- 2.39.2 From 9031b81462561d356b270e24fb49c6405a7a29e7 Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Thu, 22 Dec 2022 09:29:24 +0100 Subject: [PATCH 2/5] disable mouse by default --- .tmux.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tmux.conf b/.tmux.conf index a6a0419..73a51e9 100644 --- a/.tmux.conf +++ b/.tmux.conf @@ -9,7 +9,7 @@ bind-key 0 select-window -t :=10 set-option -g status-bg black set-option -g status-fg white -set-option -g mouse on +# set-option -g mouse on set-option -g aggressive-resize on -- 2.39.2 From daee4cd318dd9149ec2aa85bf6d5139fd3f699de Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Thu, 22 Dec 2022 09:38:53 +0100 Subject: [PATCH 3/5] linger after a client exits --- .tmux.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tmux.conf b/.tmux.conf index 73a51e9..a2b6a6c 100644 --- a/.tmux.conf +++ b/.tmux.conf @@ -14,7 +14,7 @@ set-option -g status-fg white set-option -g aggressive-resize on # if a session is killed, kill the client -set-option -g detach-on-destroy on +# set-option -g detach-on-destroy on set-option -g set-titles on set-option -g set-titles-string "#{?#{==:#{pane_title},#{host_short}},#h:#W,#T}" -- 2.39.2 From 93b15f29293e906ec9799542b6c5070550d94120 Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Thu, 22 Dec 2022 09:39:15 +0100 Subject: [PATCH 4/5] kill client on respawn --- .tmux.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tmux.conf b/.tmux.conf index a2b6a6c..4ac2f70 100644 --- a/.tmux.conf +++ b/.tmux.conf @@ -30,6 +30,6 @@ bind-key -r -T root C-Down select-pane -D bind-key -r -T root C-Left select-pane -L bind-key -r -T root C-Right select-pane -R -bind-key -T prefix '{' respawn-pane +bind-key -T prefix '{' respawn-pane -k bind-key -T prefix '}' kill-pane set-option -g remain-on-exit on -- 2.39.2 From 87174fb5e41daba4e9e39ab6ffc1d3d2cbfe301a Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Thu, 22 Dec 2022 09:40:16 +0100 Subject: [PATCH 5/5] update xpanes to 4.1.3 --- .bin/xpanes | 1170 ++++++++++++++++++++++++++++----------------------- 1 file changed, 635 insertions(+), 535 deletions(-) diff --git a/.bin/xpanes b/.bin/xpanes index 5b3d1f9..27c139f 100755 --- a/.bin/xpanes +++ b/.bin/xpanes @@ -5,17 +5,14 @@ readonly XP_SHELL="/usr/bin/env bash" # @Filename xpanes set -u -readonly XP_VERSION="4.1.1" +readonly XP_VERSION="4.1.3" ## trap might be updated in 'xpns_pre_execution' function -trap 'rm -f "${XP_CACHE_HOME}"/__xpns_*$$; rm -f "${XP_DEFAULT_SOCKET_PATH}"' EXIT +trap 'rm -f "${XP_CACHE_HOME}"/__xpns_*$$; xpns_clean_session' EXIT ## -------------------------------- # Error constants ## -------------------------------- -# Undefined or General errors -readonly XP_EUNDEF=1 - # Invalid option/argument readonly XP_EINVAL=4 @@ -49,7 +46,7 @@ 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_THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && 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. @@ -90,13 +87,14 @@ TMUX_XPANES_LOG_DIRECTORY="${TMUX_XPANES_LOG_DIRECTORY:-${XP_DEFAULT_TMUX_XPANES # Initialize Options ## -------------------------------- # options which work individually. -readonly XP_FLAG_OPTIONS="[hVdetxs]" +# 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 {} " +readonly XP_OFS="${XP_OFS:- }" XP_OPTIONS=() XP_ARGS=() XP_STDIN=() @@ -158,7 +156,7 @@ xpns_msg_warning() { } xpns_msg_debug() { - if [[ $XP_OPT_DEBUG -eq 1 ]];then + if [[ $XP_OPT_DEBUG -eq 1 ]]; then xpns_msg "Debug" "$(date "+[%F_%T]"):${FUNCNAME[1]}:$1" fi } @@ -180,7 +178,7 @@ _EOS_ } xpns_usage() { - cat < /dev/null' # In tmux 1.6, 'tmux set-window-option' might be stopped in case of redirection. -xpns_restore_allow_rename () { +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 + 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 @@ -325,33 +332,35 @@ xpns_restore_allow_rename () { # func "11" "2" # => 6 # 11 / 2 = 5.5 => ceiling => 6 -xpns_ceiling () { - local _divide="$1";shift +xpns_ceiling() { + local _divide="$1" + shift local _by="$1" - printf "%s\\n" $(( ( _divide + _by - 1 ) / _by )) + 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 +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 )) + _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}" + ((_lower_count > 0)) && eval "printf '${_lower} %.0s' {1..$_lower_count}" } # echo 3 3 3 3 | func # => 3 6 9 12 -xpns_nums_accumulate_sum () { +xpns_nums_accumulate_sum() { local s=0 while read -r n; do - ((s = s + n )) + ((s = s + n)) printf "%s " "$s" done < <( cat | tr ' ' '\n') } @@ -371,12 +380,12 @@ xpns_nums_accumulate_sum () { # 1 [] [] [] [] => 4 rows # 2 [] [] [] [] => 4 rows # 3 [] => 1 rows -xpns_nums_transpose () { +xpns_nums_transpose() { local _colnum="$1" local _spaces= local _result= xpns_msg_debug "column num = $_colnum, input = $*" - _spaces="$(for i in "$@";do + _spaces="$(for i in "$@"; do printf "%${i}s\\n" done)" @@ -386,7 +395,7 @@ xpns_nums_transpose () { ## $ 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)" + done < <(xpns_seq 1 "${_colnum}") | xpns_newline2space)" xpns_msg_debug "result = $_result" printf "%s\\n" "$_result" } @@ -399,33 +408,36 @@ xpns_nums_transpose () { # 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 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 + ((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 + 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 + # If both values 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 )) + 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}" @@ -441,7 +453,7 @@ xpns_adjust_col_row() { # ccc-3 # # Eval is used because associative array is not supported before bash 4.2 -xpns_unique_line () { +xpns_unique_line() { local _val_name while read -r line; do _val_name="__xpns_hash_$(printf "%s" "${line}" | xpns_value2key)" @@ -468,19 +480,17 @@ xpns_unique_line () { # ccc-2_1234_20160101.log # ccc-3_1234_20160101.log # -xpns_log_filenames () { +xpns_log_filenames() { local _arg_fmt="$1" local _full_fmt= _full_fmt="$(date "+${_arg_fmt}")" - cat \ - | \ + 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" + xpns_unique_line | + while read -r _arg; do + cat <<< "${_full_fmt}" | + sed "s/\\[:ARG:\\]/${_arg}/g" | + sed "s/\\[:PID:\\]/$$/g" done } @@ -547,18 +557,15 @@ xpns_arr2args() { if [[ $# -lt 1 ]]; then return 0 fi - for i in "$@" ;do + for i in "$@"; do _arg="${i}" # Use 'cat <<<"input"' command instead of 'echo', # because such the command recognizes option like '-e'. - cat <<<"${_arg}" \ - | \ + cat <<< "${_arg}" | # Escaping single quotations. - sed "s/'/'\"'\"'/g" \ - | \ + sed "s/'/'\"'\"'/g" | # Surround argument with single quotations. - sed "s/^/'/;s/$/' /" \ - | \ + sed "s/^/'/;s/$/' /" | # Remove new lines tr -d '\n' done @@ -568,14 +575,16 @@ xpns_arr2args() { # ex, $2 = 'aaa bbb ccc' # return = aaa-12345(PID) xpns_generate_window_name() { - local _unprintable_str="${1-}"; shift + 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}-$$" ) + 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. +# Convert any string (including multi-byte chars) 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' } @@ -590,7 +599,10 @@ xpns_key2value() { # Remove empty lines # This function behaves like `awk NF` xpns_rm_empty_line() { - { cat; printf "\\n";} | while IFS= read -r line;do + { + cat + printf "\\n" + } | while IFS= read -r line; do # shellcheck disable=SC2086 set -- ${line-} if [[ $# != 0 ]]; then @@ -599,47 +611,41 @@ xpns_rm_empty_line() { 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 _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 + 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 ))" \ + pipe-pane -t "${_window_name}.$((_idx + _index_offset))" \ "cat >> '${_log_dir}/${_logfile}'" # Tilde expansion does not work here. - _idx=$(( _idx + 1 )) + _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}" + 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 _wait_id="$1" + shift local _fifo= _fifo="${XP_CACHE_HOME}/__xpns_${_wait_id}" xpns_msg_debug "Notify to $_fifo" @@ -647,7 +653,8 @@ xpns_notify() { } xpns_notify_logging() { - local _window_name="$1" ; shift + local _window_name="$1" + shift local _args_num=$(($# - 1)) for i in $(xpns_seq 0 "${_args_num}"); do xpns_notify "log_${_window_name}-${i}-$$" @@ -655,7 +662,8 @@ xpns_notify_logging() { } xpns_notify_sync() { - local _window_name="$1" ; shift + local _window_name="$1" + shift local _args_num=$(($# - 1)) for i in $(xpns_seq 0 "${_args_num}"); do xpns_notify "sync_${_window_name}-${i}-$$" & @@ -663,12 +671,15 @@ xpns_notify_sync() { } 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 + 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 + 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-}" @@ -677,24 +688,28 @@ xpns_is_window_alive() { } xpns_inject_title() { - local _target_pane="$1" ;shift - local _message="$1" ;shift + 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}" )" + _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 _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 + 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 @@ -706,39 +721,44 @@ xpns_is_pane_title_required() { # Set pane titles for each pane for -t option xpns_set_titles() { - local _window_name="$1" ; shift - local _index_offset="$1" ; shift + 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 )) + for arg in "$@"; do + _pane_index=$((_index + _index_offset)) xpns_inject_title "${_window_name}.${_pane_index}" "${arg}" - _index=$(( _index + 1 )) + _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 _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 + for arg in "$@"; do _exec_cmd="${_cmd//${_repstr}/${arg}}" - _pane_index=$(( _index + _index_offset )) + _pane_index=$((_index + _index_offset)) ${TMUX_XPANES_EXEC} send-keys -t "${_window_name}.${_pane_index}" "${_exec_cmd}" C-m - _index=$(( _index + 1 )) + _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 _window_name="$1" + shift local _args_num="$1" ## ---------------- # Default behavior @@ -759,14 +779,14 @@ xpns_organize_panes() { # # Generate sequential number descending order. # seq is not used because old version of -# seq does not generate descending oorder. +# seq does not generate descending order. # $ xpns_seq 3 0 # 3 # 2 # 1 # 0 # -xpns_seq () { +xpns_seq() { local _num1="$1" local _num2="$2" eval "printf \"%d\\n\" {$_num1..$_num2}" @@ -787,21 +807,28 @@ xpns_wait_func() { # 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 _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 )) + _last_idx=$((${#args[@]} - 1)) - for i in $(xpns_seq $_last_idx 0) - do + 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]}}" @@ -825,17 +852,23 @@ xpns_split_window() { } # -# Create new panes to the existing window. +# Create new panes on 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 + 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. @@ -852,7 +885,7 @@ xpns_prepare_extra_panes() { "$@" } -xpns_get_joined_begin_commands () { +xpns_get_joined_begin_commands() { local _commands="$1" if [[ "${#XP_BEGIN_ARGS[*]}" -lt 1 ]]; then printf "%s" "${_commands}" @@ -861,13 +894,19 @@ xpns_get_joined_begin_commands () { 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 +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 @@ -891,11 +930,15 @@ xpns_inject_wait_command () { 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 +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. @@ -903,7 +946,7 @@ xpns_new_window () { 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 ) + _window_id=$(${TMUX_XPANES_EXEC} new-window -n "${_window_name}" -F '#{window_id}' -P) fi else # Keep background @@ -916,11 +959,15 @@ xpns_new_window () { 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 +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}")" @@ -930,11 +977,15 @@ xpns_new_pane_vertical () { 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 +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 @@ -942,15 +993,23 @@ xpns_split_pane_horizontal () { 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 +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" @@ -972,7 +1031,7 @@ xpns_prepare_window () { local _rest_row= local _offset= - _cell_height=$(( ( _window_height - _row + 1 ) / _row )) + _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}")" @@ -980,14 +1039,14 @@ xpns_prepare_window () { _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 + for ((i = _row - 1; i > 0; i--)); do _col="${_cols[i]}" - _cell_width=$(( ( _window_width - _col + 1 ) / _col )) + _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 )) + ((_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]}}" @@ -995,34 +1054,34 @@ xpns_prepare_window () { _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 )) + 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 )) + _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 )) + _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 )) + _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 )) + _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 )) + _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}")" @@ -1036,21 +1095,21 @@ xpns_is_session_running() { } # Remove unnecessary session files as much as possible -# to let xpanes avoids to load old .tmux.conf. +# to make xpanes avoid loading 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 + 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 + for _socket in "${XP_CACHE_HOME}"/socket.*; do xpns_msg_debug "file = ${_socket}" - if ! xpns_is_session_running "${_socket}" ;then + if ! xpns_is_session_running "${_socket}"; then xpns_msg_debug "socket(${_socket}) is not running. Remove it" rm -f "${_socket}" else @@ -1060,18 +1119,25 @@ xpns_clean_session() { } # -# Split a new window which was created by tmux into multiple panes. +# Split a new window 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 + 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}" @@ -1107,7 +1173,7 @@ xpns_prepare_preset_layout_window() { # Check whether given command is in the PATH or not. xpns_check_env() { local _cmds="$1" - while read -r cmd ; do + 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." @@ -1119,7 +1185,7 @@ xpns_check_env() { fi done < <(echo "${_cmds}" | tr ' ' '\n') - if ! mkdir -p "${XP_CACHE_HOME}";then + if ! mkdir -p "${XP_CACHE_HOME}"; then xpns_msg_warning "failed to create cache directory '${XP_CACHE_HOME}'." fi @@ -1127,11 +1193,11 @@ xpns_check_env() { 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 + "${TMUX_XPANES_TMUX_VERSION}" ); then : "Supported tmux version" else xpns_msg_warning \ -"'${XP_THIS_FILE_NAME}' may not work correctly! Please check followings. + "'${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." @@ -1149,13 +1215,48 @@ xpns_pipe_filter() { fi } -xpns_set_args_per_pane() { - local _pane_num="$1"; shift +# Merge array's element by combining with space. +# i.e +# array=(1 2 3 4 5) +# func 3 "array" +# => array is going to be ("1 2 3" "4 5") +xpns_merge_array_elements() { + local _line="" + local _pane_num="$1" + shift + local _arr_name="$1" + shift local _filtered_args=() - while read -r _line; do + eval 'set -- "${'"$_arr_name"'[@]}"' + local _num="$#" + for ((i = 1; i <= _num; i++)); do + if [[ -z "$_line" ]]; then + _line="$1" + else + _line="${_line}${XP_OFS}$1" + fi + shift + if ((i % _pane_num == 0)); then + _filtered_args+=("${_line}") + _line="" + fi + done + if [[ -n "$_line" ]]; then _filtered_args+=("${_line}") - done < <(xargs -n "${_pane_num}" <<<"$(xpns_arr2args "${XP_ARGS[@]}")") - XP_ARGS=("${_filtered_args[@]}") + fi + eval "$_arr_name"'=("${_filtered_args[@]}")' +} + +xpns_newline2space() { + local _result="" + while read -r _line; do + if [[ -z "$_result" ]]; then + _result="$_line" + else + _result="${_result}${XP_OFS}${_line}" + fi + done + printf "%s\\n" "${_result}" } xpns_get_window_height_width() { @@ -1177,7 +1278,7 @@ xpns_get_window_height_width() { fi fi if [[ $XP_IS_PIPE_MODE -eq 0 ]]; then - if _result=$(stty size 2> /dev/null) && [[ "$_result" =~ $_pattern ]];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" @@ -1185,13 +1286,16 @@ xpns_get_window_height_width() { return 0 fi else - if ! type ps > /dev/null 2>&1 ;then + 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 $$) + { + read -r # Remove first line + read -r _dev + } < <(ps -o tty -p $$ 2> /dev/null) ## If it's Linux, -F option is used - if _result=$(stty -F "/dev/${_dev}" size 2> /dev/null) && [[ "$_result" =~ $_pattern ]];then + 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" @@ -1199,7 +1303,7 @@ xpns_get_window_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 + 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" @@ -1212,11 +1316,16 @@ xpns_get_window_height_width() { } 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 _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 @@ -1225,16 +1334,16 @@ xpns_check_cell_size_bulk() { 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 )) + ((i >= _cols)) && ((_cols = i)) + ((_sum_cell = _sum_cell + i)) done - if (( _sum_cell != _cell_num )) ;then + 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 )) + 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 }" @@ -1245,7 +1354,7 @@ xpns_check_cell_size_bulk() { xpns_msg_debug " Col[]| ${_all_cols[*]}" xpns_msg_debug " -----+------------------------..." - if [[ "$_ignore_flag" -ne 1 ]] && ( (( cell_height < 2 )) || (( cell_width < 2 )) ); then + 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 @@ -1253,26 +1362,32 @@ xpns_check_cell_size_bulk() { } 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 _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 + 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 + 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 + 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}")" @@ -1282,8 +1397,8 @@ xpns_check_cell_size() { _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 )) + 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 }" @@ -1294,7 +1409,7 @@ xpns_check_cell_size() { xpns_msg_debug " Col[]| ${_all_cols_num}" xpns_msg_debug " -----+------------------------..." - if [[ "$_ignore_flag" -ne 1 ]] && ( (( cell_height < 2 )) || (( cell_width < 2 )) ); then + 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 @@ -1306,7 +1421,7 @@ xpns_pre_execution() { local _opts4args="" local _args4args="" - if [[ ${XP_OPT_EXTRA} -eq 1 ]];then + if [[ ${XP_OPT_EXTRA} -eq 1 ]]; then xpns_msg_error "'-x' must be used within the running tmux session." exit ${XP_EINVAL} fi @@ -1316,8 +1431,8 @@ xpns_pre_execution() { 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 )) + 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 @@ -1372,13 +1487,13 @@ xpns_pre_execution() { # 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 + 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. + "Execute below command line to re-attach the new session. ${TMUX_XPANES_EXEC} -S ${XP_SOCKET_PATH} attach-session -t ${XP_SESSION_NAME} @@ -1396,8 +1511,8 @@ xpns_execution() { 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}" + if [[ ${XP_IS_PIPE_MODE} -eq 0 ]] && [[ -n "${XP_MAX_PANE_ARGS-}" ]]; then + xpns_merge_array_elements "${XP_MAX_PANE_ARGS}" 'XP_ARGS' fi ## Fix window size and define pane size @@ -1439,25 +1554,26 @@ xpns_execution() { 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 + 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 )) + _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) + "${XP_ARGS[0]}" | + xpns_value2key + ) fi ## -------------------- # Prepare window and panes ## -------------------- - if [[ ${XP_OPT_EXTRA} -eq 1 ]];then + if [[ ${XP_OPT_EXTRA} -eq 1 ]]; then xpns_prepare_extra_panes \ "${_window_name}" \ "${_pane_base_index}" \ @@ -1468,7 +1584,7 @@ xpns_execution() { "${XP_REPSTR}" \ "${XP_CMD_UTILITY}" \ "${XP_ARGS[@]}" - elif [[ ${XP_OPT_USE_PRESET_LAYOUT} -eq 1 ]];then + elif [[ ${XP_OPT_USE_PRESET_LAYOUT} -eq 1 ]]; then xpns_prepare_preset_layout_window \ "${_window_name}" \ "${_pane_base_index}" \ @@ -1480,7 +1596,7 @@ xpns_execution() { "${XP_REPSTR}" \ "${XP_CMD_UTILITY}" \ "${XP_ARGS[@]}" - elif [[ ${XP_OPT_USE_PRESET_LAYOUT} -eq 0 ]];then + elif [[ ${XP_OPT_USE_PRESET_LAYOUT} -eq 0 ]]; then xpns_prepare_window \ "${_window_name}" \ "${XP_OPT_LOG_STORE}" \ @@ -1494,11 +1610,11 @@ xpns_execution() { 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. + ## Check status of the window. If no window exists, there is nothing to do any more and just 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 + 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. @@ -1528,14 +1644,14 @@ xpns_execution() { 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 + 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 + if [[ $XP_OPT_SPEEDY -eq 1 ]]; then xpns_notify_sync \ "${_window_name}" \ "${XP_ARGS[@]}" @@ -1586,7 +1702,7 @@ xpns_execution() { fi # In case of -x, this statement is skipped to keep the original window name - if [[ ${XP_OPT_EXTRA} -eq 0 ]];then + if [[ ${XP_OPT_EXTRA} -eq 0 ]]; then # Restore original window name. ${TMUX_XPANES_EXEC} \ rename-window -t "${_window_name}" \ @@ -1632,13 +1748,11 @@ xpns_switch_pipe_mode() { fi fi - while read -r line; - do + while read -r line; do XP_STDIN+=("${line}") - done < <(cat | xpns_rm_empty_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. @@ -1678,7 +1792,7 @@ xpns_layout_short2long() { xpns_is_valid_layout() { local _layout="${1-}" local _pat='^(tiled|even-horizontal|even-vertical|main-horizontal|main-vertical)$' - if ! [[ $_layout =~ $_pat ]] ; then + if ! [[ $_layout =~ $_pat ]]; then xpns_msg_error "Invalid layout '${_layout}'." exit ${XP_ELAYOUT} fi @@ -1688,7 +1802,7 @@ xpns_warning_before_extra() { local _ans= local _synchronized= _synchronized="$(xpns_get_local_tmux_conf "synchronize-panes")" - if [[ "on" == "${_synchronized}" ]];then + 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]: " @@ -1699,289 +1813,276 @@ xpns_warning_before_extra() { 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 +xpns_opt_check_num() { + local _option="$1" + shift + local _arg="$1" + if [[ -n "$_arg" ]] && [[ -z "${_arg//[0-9]/}" ]]; then + return 0 fi - return 1 + xpns_msg_error "Invalid argument '$_arg' for $_option option" + exit ${XP_EINVAL} } -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 +xpns_opt_check_str() { + local _option="$1" + shift + local _arg="$1" + if [[ -n "$_arg" ]]; then + return 0 fi - return 0 + xpns_msg_error "Invalid argument '$_arg' for $_option option" + exit ${XP_EINVAL} } -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} +xpns_opt_check_num_csv() { + local _option="$1" + shift + local _arg="$1" + if [[ "$1" =~ ^([0-9][0-9]*,?)+$ ]]; then + return 0 fi + xpns_msg_error "Invalid argument '$_arg' for $_option option" + exit ${XP_EINVAL} } xpns_parse_options() { - while (( $# > 0 )); do - case "$1" in + while (($# > 0)); do + opt="$1" + shift + if [[ ${XP_NO_OPT} -eq 1 ]]; then + XP_ARGS+=("$opt") + continue + fi + + ## Skip regularization if the arg is empty or --log= option + if [[ -n "$opt" ]] && [[ -n "${opt##--log=*}" ]]; then + ## -ovalue → -o value + if [[ -z "${opt##-${XP_ARG_OPTIONS}?*}" ]]; then + set -- "${opt#??}" ${1+"$@"} + opt="${opt%$1}" + ## -abc → -a -bc + elif [[ -z "${opt##-[!-]?*}" ]]; then + set -- "-${opt#??}" ${1+"$@"} + opt="${opt%${1#-}}" + ## --option=value → --option value + elif [[ -z "${opt##--*=*}" ]]; then + set -- "${opt#--*=}" ${1+"$@"} + opt="${opt%%=*}" + fi + fi + + case "$opt" 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") + --help) + xpns_usage + exit 0 + ;; + --version) + xpns_version + exit 0 + ;; + --desync) + XP_OPTIONS+=("$opt") + XP_OPT_IS_SYNC=0 + ;; + --log-format) + xpns_opt_check_str "$opt" "$1" + XP_OPTIONS+=("$opt") + XP_OPT_LOG_STORE=1 + TMUX_XPANES_LOG_FORMAT="$1" + XP_OPTIONS+=("$1") shift - else - local _shift_count="0" - xpns_load_long_options "$@" - _shift_count="$?" - [[ "${_shift_count}" = "1" ]] && XP_OPTIONS+=("$1") && shift - fi - ;; + ;; + --log=*) + XP_OPT_LOG_STORE=1 + XP_OPTIONS+=("$opt") + TMUX_XPANES_LOG_DIRECTORY="${opt#--log=}" + xpns_opt_check_str "${opt%=}" "$TMUX_XPANES_LOG_DIRECTORY" + ;; + --log) + XP_OPT_LOG_STORE=1 + XP_OPTIONS+=("$opt") + ;; + --ssh) + XP_OPTIONS+=("$opt") + XP_CMD_UTILITY="${XP_SSH_CMD_UTILITY}" + # Enable -t option + XP_OPT_SET_TITLE=1 + XP_OPT_CHANGE_BORDER=1 + # Enable -s option + XP_OPT_SPEEDY=1 + XP_OPT_SPEEDY_AWAIT=1 + ;; + --stay) + XP_OPTIONS+=("$opt") + XP_OPT_ATTACH=0 + ;; + --cols) + xpns_opt_check_num "$opt" "$1" + XP_OPTIONS+=("$opt") + XP_OPT_CUSTOM_SIZE_COLS="$1" + XP_OPTIONS+=("$1") + shift + ;; + --rows) + xpns_opt_check_num "$opt" "$1" + XP_OPTIONS+=("$opt") + XP_OPT_CUSTOM_SIZE_ROWS="$1" + XP_OPTIONS+=("$1") + shift + ;; + --bulk-cols) + xpns_opt_check_num_csv "$opt" "$1" + XP_OPTIONS+=("$opt") + XP_OPT_BULK_COLS="$1" + XP_OPTIONS+=("$1") + shift + ;; + --debug) + XP_OPTIONS+=("$opt") + XP_OPT_DEBUG=1 + ;; + --dry-run) + XP_OPTIONS+=("$opt") + XP_OPT_DRY_RUN=1 + ;; + --ignore-size-limit) + XP_OPTIONS+=("$opt") + XP_OPT_IGNORE_SIZE_LIMIT=1 + ;; ## ---------------- - # Short options + # Short options without argument ## ---------------- - -*) - 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 - ## ---------------- + -h) + xpns_usage + exit 0 + ;; + -V) + xpns_version + exit 0 + ;; + -x) + XP_OPTIONS+=("$opt") + 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 + ;; + -d) + XP_OPTIONS+=("$opt") + XP_OPT_IS_SYNC=0 + ;; + -e) + XP_OPTIONS+=("$opt") + XP_REPSTR="{}" + XP_CMD_UTILITY="{}" + ;; + -t) + XP_OPTIONS+=("$opt") + if ( xpns_tmux_is_greater_equals 2.3 ); then + XP_OPT_SET_TITLE=1 + XP_OPT_CHANGE_BORDER=1 else - xpns_msg_error "Invalid option -- '${1#-}'" - xpns_usage_warn - exit ${XP_EINVAL} + xpns_msg_warning "-t option cannot be used by tmux version less than 2.3. Disabled." + sleep 1 fi - fi - ;; + ;; + -s) + XP_OPTIONS+=("$opt") + XP_OPT_SPEEDY=1 + XP_OPT_SPEEDY_AWAIT=1 + if [[ -z "${1#-s}" ]]; then + XP_OPT_SPEEDY_AWAIT=0 + XP_OPTIONS+=("$1") + shift + fi + ;; + ## ---------------- + # Short options with argument + ## ---------------- + -I) + xpns_opt_check_str "$opt" "$1" + XP_OPTIONS+=("$opt") + XP_REPSTR="$1" + XP_OPTIONS+=("$1") + shift + ;; + -l) + xpns_opt_check_str "$opt" "$1" + XP_OPTIONS+=("$opt") + XP_OPT_USE_PRESET_LAYOUT=1 + XP_LAYOUT="$(printf '%s\n' "$1" | xpns_layout_short2long)" + xpns_is_valid_layout "${XP_LAYOUT}" + XP_OPTIONS+=("$1") + shift + ;; + -c) + # -c allows empty + # xpns_opt_check_str "$opt" "$1" + XP_OPTIONS+=("$opt") + XP_CMD_UTILITY="$1" + XP_OPT_CMD_UTILITY=1 + XP_OPTIONS+=("$1") + shift + ;; + -n) + xpns_opt_check_num "$opt" "$1" + XP_OPTIONS+=("$opt") + XP_MAX_PANE_ARGS="$1" + XP_OPTIONS+=("$1") + shift + ;; + -S) + xpns_opt_check_str "$opt" "$1" + XP_OPTIONS+=("$opt") + XP_SOCKET_PATH="$1" + XP_OPTIONS+=("$1") + shift + ;; + -C) + xpns_opt_check_num "$opt" "$1" + XP_OPTIONS+=("$opt") + XP_OPT_CUSTOM_SIZE_COLS="$1" + XP_OPTIONS+=("$1") + shift + ;; + -R) + xpns_opt_check_num "$opt" "$1" + XP_OPTIONS+=("$opt") + XP_OPT_CUSTOM_SIZE_ROWS="$1" + XP_OPTIONS+=("$1") + shift + ;; + -B) + # -B allows empty + # xpns_opt_check_str "$opt" "$1" + XP_OPTIONS+=("$opt") + XP_BEGIN_ARGS+=("$1") + XP_OPTIONS+=("$1") + shift + ;; + -*) + xpns_msg_error "Invalid option -- '${opt}'" + xpns_usage_warn + exit ${XP_EINVAL} + ;; ## ---------------- # Other arguments ## ---------------- *) - XP_ARGS+=("$1") - XP_NO_OPT=1 - shift - ;; + XP_ARGS+=("$opt") + XP_NO_OPT=1 + ;; esac done - # If there is any standard input from pipe, # 1 line handled as 1 argument. if [[ ! -t 0 ]]; then @@ -1989,7 +2090,7 @@ xpns_parse_options() { xpns_switch_pipe_mode fi - # When no argument arr given, exit. + # When no argument is given, exit. if [[ -z "${XP_ARGS[*]-}" ]]; then xpns_msg_error "No arguments." xpns_usage_warn @@ -2012,10 +2113,9 @@ xpns_parse_options() { 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 + if [[ ${XP_OPT_CMD_UTILITY} -eq 0 ]]; then XP_OPTIONS+=("-c" "${XP_CMD_UTILITY}") fi - } ## -------------------------------- @@ -2034,8 +2134,8 @@ xpns_main() { # 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) + 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. -- 2.39.2