From: martin f. krafft Date: Fri, 1 May 2020 00:11:53 +0000 (+1200) Subject: add xpanes X-Git-Url: https://git.madduck.net/etc/tmux.git/commitdiff_plain/fca5938e92781c271ae6a863b60c4f904ae7e697 add 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