]> git.madduck.net Git - etc/tmux.git/blob - .bin/xpanes

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

5b3d1f963a990a5a0be885f41d2c62322c3909ad
[etc/tmux.git] / .bin / xpanes
1 #!/usr/bin/env bash
2 readonly XP_SHELL="/usr/bin/env bash"
3
4 # @Author Yamada, Yasuhiro
5 # @Filename xpanes
6
7 set -u
8 readonly XP_VERSION="4.1.1"
9
10 ## trap might be updated in 'xpns_pre_execution' function
11 trap 'rm -f "${XP_CACHE_HOME}"/__xpns_*$$; rm -f "${XP_DEFAULT_SOCKET_PATH}"' EXIT
12
13 ## --------------------------------
14 # Error constants
15 ## --------------------------------
16 # Undefined or General errors
17 readonly XP_EUNDEF=1
18
19 # Invalid option/argument
20 readonly XP_EINVAL=4
21
22 # Could not open tty.
23 readonly XP_ETTY=5
24
25 # Invalid layout.
26 readonly XP_ELAYOUT=6
27
28 # Impossible layout: Small pane
29 readonly XP_ESMLPANE=7
30
31 # Log related exit status is 2x.
32 ## Could not create a directory.
33 readonly XP_ELOGDIR=20
34
35 ## Could not directory to store logs is not writable.
36 readonly XP_ELOGWRITE=21
37
38 # User's intentional exit is 3x
39 ## User exit the process intentionally by following warning message.
40 readonly XP_EINTENT=30
41
42 ## All the panes are closed before processing due to user's options/command.
43 readonly XP_ENOPANE=31
44
45 # Necessary commands are not found
46 readonly XP_ENOCMD=127
47
48 # ===============
49
50 # XP_THIS_FILE_NAME is supposed to be "xpanes".
51 readonly XP_THIS_FILE_NAME="${0##*/}"
52 readonly XP_THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-${(%):-%N}}")" && pwd)"
53 readonly XP_ABS_THIS_FILE_NAME="${XP_THIS_DIR}/${XP_THIS_FILE_NAME}"
54
55 # Prevent cache directory being created under root / directory in any case.
56 # This is quite rare case (but it can be happened).
57 readonly XP_USER_HOME="${HOME:-/tmp}"
58
59 # Basically xpanes follows XDG Base Direcotry Specification.
60 # https://specifications.freedesktop.org/basedir-spec/basedir-spec-0.6.html
61 XDG_CACHE_HOME="${XDG_CACHE_HOME:-${XP_USER_HOME}/.cache}"
62 readonly XP_CACHE_HOME="${XDG_CACHE_HOME}/xpanes"
63
64 # This is supposed to be xpanes-12345(PID)
65 readonly XP_SESSION_NAME="${XP_THIS_FILE_NAME}-$$"
66 # Temporary window name is tmp-12345(PID)
67 readonly XP_TMP_WIN_NAME="tmp-$$"
68 readonly XP_EMPTY_STR="EMPTY"
69
70 readonly XP_SUPPORT_TMUX_VERSION_LOWER="1.8"
71
72 # Check dependencies just in case.
73 # Even POSIX compliant commands are only used in this program.
74 # `xargs`, `sleep`, `mkfifo` are omitted because minimum functions can work without them.
75 readonly XP_DEPENDENCIES="${XP_DEPENDENCIES:-tmux grep sed tr od echo touch printf cat sort pwd cd mkfifo}"
76
77 ## --------------------------------
78 # User customizable shell variables
79 ## --------------------------------
80 TMUX_XPANES_EXEC=${TMUX_XPANES_EXEC:-tmux}
81 TMUX_XPANES_PANE_BORDER_FORMAT="${TMUX_XPANES_PANE_BORDER_FORMAT:-#[bg=green,fg=black] #T #[default]}"
82 TMUX_XPANES_PANE_BORDER_STATUS="${TMUX_XPANES_PANE_BORDER_STATUS:-bottom}"
83 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'}
84 XP_DEFAULT_TMUX_XPANES_LOG_FORMAT="[:ARG:].log.%Y-%m-%d_%H-%M-%S"
85 TMUX_XPANES_LOG_FORMAT="${TMUX_XPANES_LOG_FORMAT:-${XP_DEFAULT_TMUX_XPANES_LOG_FORMAT}}"
86 XP_DEFAULT_TMUX_XPANES_LOG_DIRECTORY="${XP_CACHE_HOME}/logs"
87 TMUX_XPANES_LOG_DIRECTORY="${TMUX_XPANES_LOG_DIRECTORY:-${XP_DEFAULT_TMUX_XPANES_LOG_DIRECTORY}}"
88
89 ## --------------------------------
90 # Initialize Options
91 ## --------------------------------
92 # options which work individually.
93 readonly XP_FLAG_OPTIONS="[hVdetxs]"
94 # options which need arguments.
95 readonly XP_ARG_OPTIONS="[ISclnCRB]"
96 readonly XP_DEFAULT_LAYOUT="tiled"
97 readonly XP_DEFAULT_REPSTR="{}"
98 readonly XP_DEFAULT_CMD_UTILITY="echo {} "
99 readonly XP_SSH_CMD_UTILITY="ssh -o StrictHostKeyChecking=no {} "
100 XP_OPTIONS=()
101 XP_ARGS=()
102 XP_STDIN=()
103 XP_BEGIN_ARGS=()
104 XP_IS_PIPE_MODE=0
105 XP_OPT_IS_SYNC=1
106 XP_OPT_DRY_RUN=0
107 XP_OPT_ATTACH=1
108 XP_OPT_LOG_STORE=0
109 XP_REPSTR=""
110 XP_DEFAULT_SOCKET_PATH_BASE="${XP_CACHE_HOME}/socket"
111 XP_DEFAULT_SOCKET_PATH="${XP_DEFAULT_SOCKET_PATH_BASE}.$$"
112 XP_SOCKET_PATH="${XP_SOCKET_PATH:-${XP_DEFAULT_SOCKET_PATH}}"
113 XP_NO_OPT=0
114 XP_OPT_CMD_UTILITY=0
115 XP_CMD_UTILITY=""
116 XP_LAYOUT="${XP_DEFAULT_LAYOUT}"
117 XP_MAX_PANE_ARGS=""
118 XP_OPT_SET_TITLE=0
119 XP_OPT_CHANGE_BORDER=0
120 XP_OPT_EXTRA=0
121 XP_OPT_SPEEDY=0
122 XP_OPT_SPEEDY_AWAIT=0
123 XP_OPT_USE_PRESET_LAYOUT=0
124 XP_OPT_CUSTOM_SIZE_COLS=
125 XP_OPT_CUSTOM_SIZE_ROWS=
126 XP_OPT_BULK_COLS=
127 XP_WINDOW_WIDTH=
128 XP_WINDOW_HEIGHT=
129 XP_COLS=
130 XP_COLS_OFFSETS=
131 XP_OPT_DEBUG=0
132 XP_OPT_IGNORE_SIZE_LIMIT=0
133
134 ## --------------------------------
135 # Logger
136 #   $1 -- Log level (i.e Warning, Error)
137 #   $2 -- Message
138 #   i.e
139 #      xpanes:Error: invalid option.
140 #
141 # This log format is created with reference to openssl's one.
142 #   $ echo | openssl -a
143 #   openssl:Error: '-a' is an invalid command.
144 ## --------------------------------
145 xpns_msg() {
146   local _loglevel="$1"
147   local _msgbody="$2"
148   local _msg="${XP_THIS_FILE_NAME}:${_loglevel}: ${_msgbody}"
149   printf "%s\\n" "${_msg}" >&2
150 }
151
152 xpns_msg_info() {
153   xpns_msg "Info" "$1"
154 }
155
156 xpns_msg_warning() {
157   xpns_msg "Warning" "$1"
158 }
159
160 xpns_msg_debug() {
161   if [[ $XP_OPT_DEBUG -eq 1 ]];then
162     xpns_msg "Debug" "$(date "+[%F_%T]"):${FUNCNAME[1]}:$1"
163   fi
164 }
165
166 xpns_msg_error() {
167   xpns_msg "Error" "$1"
168 }
169
170 xpns_usage_warn() {
171   xpns_usage_short >&2
172   echo "Try '${XP_THIS_FILE_NAME} --help' for more information." >&2
173 }
174
175 xpns_usage_short() {
176   cat << _EOS_
177 Usage: ${XP_THIS_FILE_NAME} [OPTIONS] [argument ...]
178 Usage(Pipe mode): command ... | ${XP_THIS_FILE_NAME} [OPTIONS] [<command> ...]
179 _EOS_
180 }
181
182 xpns_usage() {
183   cat <<USAGE
184 Usage:
185   ${XP_THIS_FILE_NAME} [OPTIONS] [argument ...]
186
187 Usage(Pipe mode):
188   command ... | ${XP_THIS_FILE_NAME} [OPTIONS] [<command> ...]
189
190 OPTIONS:
191   -h,--help                    Display this help and exit.
192   -V,--version                 Output version information and exit.
193   -B <begin-command>           Run <begin-command> before processing <command> in each pane. Multiple options are allowed.
194   -c <command>                 Set <command> to be executed in each pane. Default is \`echo {}\`.
195   -d,--desync                  Make synchronize-panes option off in new window.
196   -e                           Execute given arguments as is. Same as \`-c '{}'\`
197   -I <repstr>                  Replacing one or more occurrences of <repstr> in command provided by -c or -B. Default is \`{}\`.
198   -C NUM,--cols=NUM            Number of columns of window layout.
199   -R NUM,--rows=NUM            Number of rows of window layout.
200   -l <layout>                  Set the preset of window layout. Recognized layout arguments are:
201                                t    tiled
202                                eh   even-horizontal
203                                ev   even-vertical
204                                mh   main-horizontal
205                                mv   main-vertical
206   -n <number>                  Set the maximum number of <argument> taken for each pane.
207   -s                           Speedy mode: Run command without opening an interactive shell.
208   -ss                          Speedy mode AND close a pane automatically at the same time as process exiting.
209   -S <socket-path>             Set a full alternative path to the server socket.
210   -t                           Display each argument on the each pane's border as their title.
211   -x                           Create extra panes in the current active window.
212   --log[=<directory>]          Enable logging and store log files to ~/.cache/xpanes/logs or <directory>.
213   --log-format=<FORMAT>        Make name of log files follow <FORMAT>. Default is \`${XP_DEFAULT_TMUX_XPANES_LOG_FORMAT}\`.
214   --ssh                        Same as \`-t -s -c 'ssh -o StrictHostKeyChecking=no {}'\`.
215   --stay                       Do not switch to new window.
216   --bulk-cols=NUM1[,NUM2 ...]  Set number of columns on multiple rows (i.e, "2,2,2" represents 2 cols x 3 rows).
217   --debug                      Print debug message.
218
219 Copyright (c) 2019 Yamada, Yasuhiro
220 Released under the MIT License.
221 https://github.com/greymd/tmux-xpanes
222 USAGE
223 }
224
225 # Show version number
226 xpns_version() {
227   echo "${XP_THIS_FILE_NAME} ${XP_VERSION}"
228 }
229
230 # Get version number for tmux
231 xpns_get_tmux_version() {
232   local _tmux_version=""
233   if ! ${TMUX_XPANES_EXEC} -V &> /dev/null; then
234     # From tmux 0.9 to 1.3, there is no -V option.
235     _tmux_version="tmux 0.9-1.3"
236   else
237     _tmux_version="$( ${TMUX_XPANES_EXEC} -V )"
238   fi
239   ( read -r _ _ver; echo "${_ver}" ) <<<"${_tmux_version}"
240 }
241
242 # Check whether the prefered tmux version is greater than host's tmux version.
243 # $1 ... Prefered version.
244 # $2 ... Host tmux version(optional).
245 # In case of tmux version is 1.7, the result will be like this.
246 # 0 is true, 1 is false.
247 ##  arg  -> result
248 #   func 1.5  1.7 -> 0
249 #   func 1.6  1.7 -> 0
250 #   func 1.7  1.7 -> 0
251 #   func 1.8  1.7 -> 1
252 #   func 1.9  1.7 -> 1
253 #   func 1.9a 1.7 -> 1
254 #   func 2.0  1.7 -> 1
255 xpns_tmux_is_greater_equals() {
256   local _check_version="$1"
257   local _tmux_version="${2:-$(xpns_get_tmux_version)}"
258   # Simple numerical comparison does not work because there is the version like "1.9a".
259   if [[ "$( (echo "${_tmux_version}"; echo "${_check_version}") | sort -n | head -n 1)" != "${_check_version}" ]];then
260     return 1
261   else
262     return 0
263   fi
264 }
265
266 xpns_get_local_tmux_conf() {
267   local _conf_name="$1"
268   local _session="${2-}"
269   {
270     if [[ -z "${_session-}" ]];then
271       ${TMUX_XPANES_EXEC} show-window-options
272     else
273       ${TMUX_XPANES_EXEC} -S "${_session}" show-window-options
274     fi
275   } | grep "^${_conf_name}" \
276     | ( read -r _ _v; printf "%s\\n" "${_v}" )
277 }
278
279 xpns_get_global_tmux_conf() {
280   local _conf_name="$1"
281   local _session="${2-}"
282   {
283     if [[ -z "${_session-}" ]];then
284       ${TMUX_XPANES_EXEC} show-window-options -g
285     else
286       ${TMUX_XPANES_EXEC} -S "${_session}" show-window-options -g
287     fi
288   } | grep "^${_conf_name}" \
289     | ( read -r _ _v; printf "%s\\n" "${_v}" )
290 }
291
292 # Disable allow-rename because
293 # window separation does not work correctly
294 # if "allow-rename" option is on
295 xpns_suppress_allow_rename () {
296   local _default_allow_rename="$1"
297   local _session="${2-}"
298   if [[ "${_default_allow_rename-}" == "on"  ]]; then
299     ## Temporary, disable "allow-rename"
300     xpns_msg_debug "'allow-rename' option is 'off' temporarily."
301     if [[ -z "${_session-}" ]];then
302       ${TMUX_XPANES_EXEC} set-window-option -g allow-rename off
303     else
304       ${TMUX_XPANES_EXEC} -S "${_session}" set-window-option -g allow-rename off
305     fi
306   fi
307 }
308
309 # Restore default "allow-rename"
310 # Do not write like 'xpns_restore_allow_rename "some value" "some value" > /dev/null'
311 # In tmux 1.6, 'tmux set-window-option' might be stopped in case of redirection.
312 xpns_restore_allow_rename () {
313   local _default_allow_rename="$1"
314   local _session="${2-}"
315   if [[ "${_default_allow_rename-}" == "on"  ]]; then
316     xpns_msg_debug "Restore original value of 'allow-rename' option."
317     if [[ -z "${_session-}" ]];then
318       ${TMUX_XPANES_EXEC} set-window-option -g allow-rename on
319     else
320       ${TMUX_XPANES_EXEC} -S "${_session}" set-window-option -g allow-rename on
321     fi
322   fi
323 }
324
325 # func "11" "2"
326 #  => 6
327 # 11 / 2 = 5.5 => ceiling => 6
328 xpns_ceiling () {
329   local _divide="$1";shift
330   local _by="$1"
331   printf "%s\\n" $(( ( _divide + _by - 1 ) / _by ))
332 }
333
334 # func "10" "3"
335 #  => 4 3 3
336 # Divide 10 into 3 parts as equally as possible.
337 xpns_divide_equally () {
338   local _number="$1";shift
339   local _count="$1"
340   local _upper _lower _upper_count _lower_count
341   _upper="$(xpns_ceiling "$_number" "$_count")"
342   _lower=$(( _upper - 1 ))
343   _lower_count=$(( _upper * _count - _number ))
344   _upper_count=$(( _count - _lower_count ))
345   eval "printf '${_upper} %.0s' {1..$_upper_count}"
346   (( _lower_count > 0 )) && eval "printf '${_lower} %.0s' {1..$_lower_count}"
347 }
348
349 # echo 3 3 3 3 | func
350 # => 3 6 9 12
351 xpns_nums_accumulate_sum () {
352   local s=0
353   while read -r n; do
354     ((s = s + n ))
355     printf "%s " "$s"
356   done < <( cat | tr ' ' '\n')
357 }
358
359 # func 3 2 2 2
360 # => 4 4 1
361 #
362 # For example, "3 2 2 2" represents following cell positions
363 #   1  2  3
364 # 1 [] [] [] => 3 rows
365 # 2 [] []    => 2 rows
366 # 3 [] []    => 2 rows
367 # 4 [] []    => 2 rows
368 #
369 # After the transposition, it must be "4 4 1" which represents below
370 #   1  2  3  4
371 # 1 [] [] [] [] => 4 rows
372 # 2 [] [] [] [] => 4 rows
373 # 3 []          => 1 rows
374 xpns_nums_transpose () {
375   local _colnum="$1"
376   local _spaces=
377   local _result=
378   xpns_msg_debug "column num = $_colnum, input = $*"
379   _spaces="$(for i in "$@";do
380     printf "%${i}s\\n"
381   done)"
382
383   # 'for' statement does not work somehow
384   _result="$(while read -r i; do
385     ## This part is depending on the following 'cut' behavior
386     ## $ echo 1234 | cut -c 5
387     ## => result is supposed to be empty
388     printf "%s\\n" "$_spaces" | cut -c "$i" | grep -c ' '
389   done < <(xpns_seq 1 "${_colnum}") | xargs)"
390   xpns_msg_debug "result = $_result"
391   printf "%s\\n" "$_result"
392 }
393
394 # Adjust size of columns and rows in accordance with given N
395 # func <col> <row> <N>
396 # i.e:
397 #     func "" "" 20
398 #       => returns 4 5
399 #     func "6" 0 20
400 #       => returns 6 4
401 xpns_adjust_col_row() {
402   local col="${1:-0}" ;shift
403   local row="${1:-0}" ;shift
404   local N="$1"   ;shift
405   local fix_col_flg
406   local fix_row_flg
407   (( col != 0 )) && fix_col_flg=1 || fix_col_flg=0
408   (( row != 0 )) && fix_row_flg=1 || fix_row_flg=0
409
410   # This is just a author (@greymd)'s preference.
411   if (( fix_col_flg == 0 )) && (( fix_row_flg == 0 )) && (( N == 2)) ;then
412     col=2
413     row=1
414     printf "%d %d\\n" "${col}" "${row}"
415     return
416   fi
417
418   # If both valures are provided, col is used.
419   if (( fix_col_flg == 1 )) && (( fix_row_flg == 1 ));then
420     row=0
421     fix_row_flg=0
422   fi
423   # This algorhythm is almost same as tmux default
424   #   https://github.com/tmux/tmux/blob/2.8/layout-set.c#L436
425   while (( col * row < N )) ;do
426     (( fix_row_flg != 1 )) && (( row = row + 1 ))
427     if (( col * row < N ));then
428       (( fix_col_flg != 1 )) &&  (( col = col + 1 ))
429     fi
430   done
431   printf "%d %d\\n" "${col}" "${row}"
432 }
433
434 # Make each line unique by adding index number
435 # echo aaa bbb ccc aaa ccc ccc | xargs -n 1 | xpns_unique_line
436 #  aaa-1
437 #  bbb-1
438 #  ccc-1
439 #  aaa-2
440 #  ccc-2
441 #  ccc-3
442 #
443 # Eval is used because associative array is not supported before bash 4.2
444 xpns_unique_line () {
445   local _val_name
446   while read -r line; do
447     _val_name="__xpns_hash_$(printf "%s" "${line}" | xpns_value2key)"
448     # initialize variable
449     eval "${_val_name}=\${${_val_name}:-0}"
450     # increment variable
451     eval "${_val_name}=\$(( ++${_val_name} ))"
452     printf "%s\\n" "${line}-$(eval printf "%s" "\$${_val_name}")"
453   done
454 }
455
456 #
457 # Generate log file names from given arguments.
458 # Usage:
459 #        echo <arg1> <arg2> ... | xpns_log_filenames <FORMAT>
460 # Return:
461 #        File names.
462 # Example:
463 #        $ echo aaa bbb ccc aaa ccc ccc | xargs -n 1 | xpns_log_filenames '[:ARG:]_[:PID:]_%Y%m%d.log'
464 #        aaa-1_1234_20160101.log
465 #        bbb-1_1234_20160101.log
466 #        ccc-1_1234_20160101.log
467 #        aaa-2_1234_20160101.log
468 #        ccc-2_1234_20160101.log
469 #        ccc-3_1234_20160101.log
470 #
471 xpns_log_filenames () {
472   local _arg_fmt="$1"
473   local _full_fmt=
474   _full_fmt="$(date "+${_arg_fmt}")"
475   cat \
476     | \
477     # 1st argument + '-' + unique number (avoid same argument has same name)
478     xpns_unique_line \
479     | while read -r _arg
480     do
481       cat <<<"${_full_fmt}" \
482         | sed "s/\\[:ARG:\\]/${_arg}/g" \
483         | sed "s/\\[:PID:\\]/$$/g"
484     done
485 }
486
487 ## --------------------------------
488 # Normalize directory by making following conversion.
489 #  * Tilde expansion.
490 #  * Remove the slash '/' at the end of the dirname.
491 # Usage:
492 #        xpns_normalize_directory <direname>
493 # Return:
494 #        Normalized <dirname>
495 ## --------------------------------
496 xpns_normalize_directory() {
497   local _dir="$1"
498   # Remove end of slash '/'
499   _dir="${_dir%/}"
500   # tilde expansion
501   _dir="${_dir/#~/${HOME}}"
502   printf "%s\\n" "${_dir}"
503 }
504
505 ## --------------------------------
506 # Ensure existence of given directory
507 # Usage:
508 #        xpns_is_valid_directory <direname>
509 # Return:
510 #        Absolute path of the <dirname>
511 ## --------------------------------
512 xpns_is_valid_directory() {
513   local _dir="$1"
514   local _checkfile="${XP_THIS_FILE_NAME}.$$"
515   # Check directory.
516   if [[ ! -d "${_dir}" ]]; then
517     # Create directory
518     if mkdir "${_dir}"; then
519       xpns_msg_info "${_dir} is created."
520     else
521       xpns_msg_error "Failed to create ${_dir}"
522       exit ${XP_ELOGDIR}
523     fi
524   fi
525   # Try to create file.
526   #   Not only checking directory permission,
527   #   but also i-node and other misc situations.
528   if ! touch "${_dir}/${_checkfile}"; then
529     xpns_msg_error "${_dir} is not writable."
530     rm -f "${_dir}/${_checkfile}"
531     exit ${XP_ELOGWRITE}
532   fi
533   rm -f "${_dir}/${_checkfile}"
534 }
535
536 # Convert array to string which is can be used as command line argument.
537 # Usage:
538 #       xpns_arr2args <array object>
539 # Example:
540 #       array=(aaa bbb "ccc ddd" eee "f'f")
541 #       xpns_arr2args "${array[@]}"
542 #       @returns "'aaa' 'bbb' 'ccc ddd' 'eee' 'f\'f'"
543 # Result:
544 xpns_arr2args() {
545   local _arg=""
546   # If there is no argument, usage will be shown.
547   if [[ $# -lt 1 ]]; then
548     return 0
549   fi
550   for i in "$@" ;do
551     _arg="${i}"
552     # Use 'cat <<<"input"' command instead of 'echo',
553     # because such the command recognizes option like '-e'.
554     cat <<<"${_arg}" \
555       | \
556       # Escaping single quotations.
557       sed "s/'/'\"'\"'/g" \
558       | \
559       # Surround argument with single quotations.
560       sed "s/^/'/;s/$/' /" \
561       | \
562       # Remove new lines
563       tr -d '\n'
564   done
565 }
566
567 # Extract first field to generate window name.
568 # ex, $2     =  'aaa bbb ccc'
569 #   return   =  aaa-12345(PID)
570 xpns_generate_window_name() {
571   local _unprintable_str="${1-}"; shift
572   # Leave first 200 characters to prevent
573   # the name exceed the maximum length of tmux window name (2000 byte).
574   printf "%s\\n" "${1:-${_unprintable_str}}" \
575     | ( read -r _name _ && printf "%s\\n" "${_name:0:200}-$$" )
576 }
577
578 # Convert string to another string which can be handled as tmux window name.
579 xpns_value2key() {
580   od -v -tx1 -An  | tr -dc 'a-zA-Z0-9' | tr -d '\n'
581 }
582
583 # Restore string encoded by xpns_value2key function.
584 xpns_key2value() {
585   read -r _key
586   # shellcheck disable=SC2059
587   printf "$(printf "%s" "$_key" | sed 's/../\\x&/g')"
588 }
589
590 # Remove empty lines
591 # This function behaves like `awk NF`
592 xpns_rm_empty_line() {
593   { cat; printf "\\n";} | while IFS= read -r line;do
594     # shellcheck disable=SC2086
595     set -- ${line-}
596     if [[ $# != 0 ]]; then
597       printf "%s\\n" "${line}"
598     fi
599   done
600 }
601
602 # Extract matched patterns from string
603 # $ xpns_extract_matched "aaa123bbb" "[0-9]{3}"
604 # => "123"
605 xpns_extract_matched() {
606   local _args="$1" ;shift
607   local _regex="($1)"
608   if [[ $_args =~ $_regex ]];then
609     printf "%s" "${BASH_REMATCH[0]}"
610   fi
611 }
612
613 # Enable logging feature to the all the panes in the target window.
614 xpns_enable_logging() {
615   local _window_name="$1"     ; shift
616   local _index_offset="$1"    ; shift
617   local _log_dir="$1"         ; shift
618   local _log_format="$1"      ; shift
619   local _unprintable_str="$1" ; shift
620   local _args=("$@")
621   local _args_num=$(($# - 1))
622   # Generate log files from arguments.
623   local _idx=0
624   while read -r _logfile ; do
625     # Start logging
626     xpns_msg_debug "Start logging pipe-pane(cat >> '${_log_dir}/${_logfile}')"
627     ${TMUX_XPANES_EXEC} \
628       pipe-pane -t "${_window_name}.$(( _idx + _index_offset ))" \
629       "cat >> '${_log_dir}/${_logfile}'" # Tilde expansion does not work here.
630     _idx=$(( _idx + 1 ))
631   done < <(
632   for i in $(xpns_seq 0 "${_args_num}")
633   do
634     # Replace empty string.
635     printf "%s\\n" "${_args[i]:-${_unprintable_str}}"
636   done | xpns_log_filenames "${_log_format}"
637   )
638 }
639
640 ## Print "1" on the particular named pipe
641 xpns_notify() {
642   local _wait_id="$1" ; shift
643   local _fifo=
644   _fifo="${XP_CACHE_HOME}/__xpns_${_wait_id}"
645   xpns_msg_debug "Notify to $_fifo"
646   printf "%s\\n" 1 > "$_fifo" &
647 }
648
649 xpns_notify_logging() {
650   local _window_name="$1" ; shift
651   local _args_num=$(($# - 1))
652   for i in $(xpns_seq 0 "${_args_num}"); do
653     xpns_notify "log_${_window_name}-${i}-$$"
654   done
655 }
656
657 xpns_notify_sync() {
658   local _window_name="$1" ; shift
659   local _args_num=$(($# - 1))
660   for i in $(xpns_seq 0 "${_args_num}"); do
661     xpns_notify "sync_${_window_name}-${i}-$$" &
662   done
663 }
664
665 xpns_is_window_alive() {
666   local _window_name="$1" ;shift
667   local _speedy_await_flag="$1" ;shift
668   local _def_allow_rename="$1" ;shift
669   if ! ${TMUX_XPANES_EXEC} display-message -t "$_window_name" -p > /dev/null 2>&1 ;then
670     xpns_msg_info "All the panes are closed before displaying the result."
671     if [[ "${_speedy_await_flag}" -eq 0 ]] ;then
672       xpns_msg_info "Use '-s' option instead of '-ss' option to avoid this behavior."
673     fi
674     xpns_restore_allow_rename "${_def_allow_rename-}"
675     exit ${XP_ENOPANE}
676   fi
677 }
678
679 xpns_inject_title() {
680   local _target_pane="$1" ;shift
681   local _message="$1"     ;shift
682   local _pane_tty=
683   _pane_tty="$( ${TMUX_XPANES_EXEC} display-message -t "${_target_pane}" -p "#{pane_tty}" )"
684   printf "\\033]2;%s\\033\\\\" "${_message}" > "${_pane_tty}"
685   xpns_msg_debug "target_pane=${_target_pane} pane_title=${_message} pane_tty=${_pane_tty}"
686 }
687
688 xpns_is_pane_title_required() {
689   local _title_flag="$1"   ; shift
690   local _extra_flag="$1"   ; shift
691   local _pane_border_status=
692   _pane_border_status=$(xpns_get_local_tmux_conf "pane-border-status")
693   if [[ $_title_flag -eq 1 ]]; then
694     return 0
695   elif [[ ${_extra_flag} -eq 1 ]] && \
696        [[ "${_pane_border_status}" != "off" ]] && \
697        [[ -n "${_pane_border_status}" ]] ;then
698     ## For -x option
699     # Even the -t option is not specified, it is required to inject pane title here.
700     # Because user expects the title is displayed on the pane if the original window is
701     # generated from tmux-xpanes with -t option.
702     return 0
703   fi
704   return 1
705 }
706
707 # Set pane titles for each pane for -t option
708 xpns_set_titles() {
709   local _window_name="$1"  ; shift
710   local _index_offset="$1" ; shift
711   local _index=0
712   local _pane_index=
713   for arg in "$@"
714   do
715     _pane_index=$(( _index + _index_offset ))
716     xpns_inject_title "${_window_name}.${_pane_index}" "${arg}"
717     _index=$(( _index + 1 ))
718   done
719 }
720
721 # Send command to the all the panes in the target window.
722 xpns_send_commands() {
723   local _window_name="$1"  ; shift
724   local _index_offset="$1" ; shift
725   local _repstr="$1"       ; shift
726   local _cmd="$1"          ; shift
727   local _index=0
728   local _pane_index=
729   local _exec_cmd=
730   for arg in "$@"
731   do
732     _exec_cmd="${_cmd//${_repstr}/${arg}}"
733     _pane_index=$(( _index + _index_offset ))
734     ${TMUX_XPANES_EXEC} send-keys -t "${_window_name}.${_pane_index}" "${_exec_cmd}" C-m
735     _index=$(( _index + 1 ))
736   done
737 }
738
739 # Separate window vertically, when the number of panes is 1 or 2.
740 xpns_organize_panes() {
741   local _window_name="$1" ; shift
742   local _args_num="$1"
743   ## ----------------
744   # Default behavior
745   ## ----------------
746   if [[ "${_args_num}" -eq 1 ]]; then
747     ${TMUX_XPANES_EXEC} select-layout -t "${_window_name}" even-horizontal
748   elif [[ "${_args_num}" -gt 1 ]]; then
749     ${TMUX_XPANES_EXEC} select-layout -t "${_window_name}" tiled
750   fi
751   ## ----------------
752   # Update layout
753   ## ----------------
754   if [[ "${XP_LAYOUT}" != "${XP_DEFAULT_LAYOUT}" ]]; then
755     ${TMUX_XPANES_EXEC} select-layout -t "${_window_name}" "${XP_LAYOUT}"
756   fi
757 }
758
759 #
760 # Generate sequential number descending order.
761 # seq is not used because old version of
762 # seq does not generate descending oorder.
763 # $ xpns_seq 3 0
764 # 3
765 # 2
766 # 1
767 # 0
768 #
769 xpns_seq () {
770   local _num1="$1"
771   local _num2="$2"
772   eval "printf \"%d\\n\" {$_num1..$_num2}"
773 }
774
775 xpns_wait_func() {
776   local _wait_id="$1"
777   local _fifo="${XP_CACHE_HOME}/__xpns_${_wait_id}"
778   local _arr=("$_fifo")
779   local _fifo_arg=
780   _fifo_arg=$(xpns_arr2args "${_arr[@]}")
781   xpns_msg_debug "mkfifo $_fifo"
782   mkfifo "${_fifo}"
783   xpns_msg_debug "grep -q 1 ${_fifo_arg}"
784   printf "%s\\n" "grep -q 1 ${_fifo_arg}"
785 }
786
787 # Split a new window into multiple panes.
788 #
789 xpns_split_window() {
790   local _window_name="$1"     ; shift
791   local _log_flag="$1"        ; shift
792   local _title_flag="$1"      ; shift
793   local _speedy_flag="$1"     ; shift
794   local _await_flag="$1"      ; shift
795   local _pane_base_index="$1" ; shift
796   local _repstr="$1"          ; shift
797   local _cmd_template="$1"    ; shift
798   local _exec_cmd=
799   local _sep_count=0
800   local args=("$@")
801   _last_idx=$(( ${#args[@]} - 1 ))
802
803   for i in $(xpns_seq $_last_idx 0)
804   do
805     xpns_msg_debug "Index:${i} Argument:${args[i]}"
806     _sep_count=$((_sep_count + 1))
807     _exec_cmd="${_cmd_template//${_repstr}/${args[i]}}"
808
809     ## Speedy mode
810     if [[ $_speedy_flag -eq 1 ]]; then
811
812       _exec_cmd=$(xpns_inject_wait_command "${_log_flag}" "${_title_flag}" "${_speedy_flag}" "${_await_flag}" "$i" "${_exec_cmd}")
813       # Execute command as a child process of default-shell.
814       ${TMUX_XPANES_EXEC} split-window -t "${_window_name}" -h -d "${_exec_cmd}"
815     else
816       # Open login shell and execute command on the interactive screen.
817       ${TMUX_XPANES_EXEC} split-window -t "${_window_name}" -h -d
818     fi
819     # Restraining that size of pane's width becomes
820     # less than the minimum size which is defined by tmux.
821     if [[ ${_sep_count} -gt 2 ]]; then
822       ${TMUX_XPANES_EXEC} select-layout -t "${_window_name}" tiled
823     fi
824   done
825 }
826
827 #
828 # Create new panes to the  existing window.
829 # Usage:
830 #    func <window name> <offset of index> <number of pane>
831 #
832 xpns_prepare_extra_panes() {
833   local _window_name="$1"     ; shift
834   local _pane_base_index="$1" ; shift
835   local _log_flag="$1"        ; shift
836   local _title_flag="$1"      ; shift
837   local _speedy_flg="$1"      ; shift
838   local _await_flg="$1"       ; shift
839   # specify a pane which has the biggest index number.
840   #   Because pane_id may not be immutable.
841   #   If the small number of index is specified here, correspondance between pane_title and command can be slip off.
842   ${TMUX_XPANES_EXEC} select-pane -t "${_window_name}.${_pane_base_index}"
843
844   # split window into multiple panes
845   xpns_split_window \
846     "${_window_name}" \
847     "${_log_flag}" \
848     "${_title_flag}" \
849     "${_speedy_flg}" \
850     "${_await_flg}" \
851     "${_pane_base_index}" \
852     "$@"
853 }
854
855 xpns_get_joined_begin_commands () {
856   local _commands="$1"
857   if [[ "${#XP_BEGIN_ARGS[*]}" -lt 1 ]]; then
858     printf "%s" "${_commands}"
859     return
860   fi
861   printf "%s\\n" "${XP_BEGIN_ARGS[@]}" "${_commands}"
862 }
863
864 xpns_inject_wait_command () {
865   local _log_flag="$1"    ; shift
866   local _title_flag="$1"  ; shift
867   local _speedy_flg="$1"  ; shift
868   local _await_flg="$1"   ; shift
869   local _idx="$1"         ; shift
870   local _exec_cmd="$1"    ; shift
871
872   ## Speedy mode + logging
873   if [[ "${_log_flag}" -eq 1 ]] && [[ "${_speedy_flg}" -eq 1 ]]; then
874     # Wait for start of logging
875     # Without this part, logging thread may start after new process is finished.
876     # Execute function to wait for logging start.
877     _exec_cmd="$(xpns_wait_func "log_${_window_name}-${_idx}-$$")"$'\n'"${_exec_cmd}"
878   fi
879
880   ## Speedy mode (Do not allow to close panes before the separation is finished).
881   if [[ "${_speedy_flg}" -eq 1 ]]; then
882     _exec_cmd="$(xpns_wait_func "sync_${_window_name}-${_idx}-$$")"$'\n'${_exec_cmd}
883   fi
884
885   ## -s: Speedy mode (Not -ss: Speedy mode + nowait)
886   if [[ "${_await_flg}" -eq 1 ]]; then
887     local _msg
888     _msg="$(xpns_arr2args "${TMUX_XPANES_PANE_DEAD_MESSAGE}" | sed 's/"/\\"/g')"
889     _exec_cmd="${_exec_cmd}"$'\n'"${XP_SHELL} -c \"printf -- ${_msg} >&2 && read\""
890   fi
891   printf "%s" "${_exec_cmd}"
892 }
893
894 xpns_new_window () {
895   local _window_name="$1" ; shift
896   local _attach_flg="$1"  ; shift
897   local _speedy_flg="$1"  ; shift
898   local _exec_cmd="$1"    ; shift
899   local _window_id=
900
901   # Create new window.
902   if [[ "${_attach_flg}" -eq 1 ]]; then
903     if [[ "${_speedy_flg}" -eq 1 ]]; then
904       _window_id=$(${TMUX_XPANES_EXEC} new-window -n "${_window_name}" -F '#{window_id}' -P "${_exec_cmd}")
905     else
906       _window_id=$(${TMUX_XPANES_EXEC} new-window -n "${_window_name}" -F '#{window_id}' -P )
907     fi
908   else
909     # Keep background
910     if [[ "${_speedy_flg}" -eq 1 ]]; then
911       _window_id=$(${TMUX_XPANES_EXEC} new-window -n "${_window_name}" -F '#{window_id}' -P -d "${_exec_cmd}")
912     else
913       _window_id=$(${TMUX_XPANES_EXEC} new-window -n "${_window_name}" -F '#{window_id}' -P -d)
914     fi
915   fi
916   printf "%s" "${_window_id}"
917 }
918
919 xpns_new_pane_vertical () {
920   local _window_id="$1"   ; shift
921   local _cell_height="$1" ; shift
922   local _speedy_flg="$1"  ; shift
923   local _exec_cmd="$1"    ; shift
924   local _pane_id=
925   if [[ "${_speedy_flg}" -eq 1 ]]; then
926     _pane_id="$(${TMUX_XPANES_EXEC} split-window -t "$_window_id" -v -d -l "${_cell_height}" -F '#{pane_id}' -P "${_exec_cmd}")"
927   else
928     _pane_id="$(${TMUX_XPANES_EXEC} split-window -t "$_window_id" -v -d -l "${_cell_height}" -F '#{pane_id}' -P)"
929   fi
930   printf "%s\\n" "${_pane_id}"
931 }
932
933 xpns_split_pane_horizontal () {
934   local _target_pane_id="$1" ; shift
935   local _cell_width="$1"     ; shift
936   local _speedy_flg="$1"     ; shift
937   local _exec_cmd="$1"       ; shift
938   if [[ "${_speedy_flg}" -eq 1 ]]; then
939     ${TMUX_XPANES_EXEC} split-window -t "$_target_pane_id" -h -d -l "$_cell_width" "${_exec_cmd}"
940   else
941     ${TMUX_XPANES_EXEC} split-window -t "$_target_pane_id" -h -d -l "$_cell_width"
942   fi
943 }
944
945 xpns_prepare_window () {
946   local _window_name="$1"     ; shift
947   local _log_flag="$1"        ; shift
948   local _title_flag="$1"      ; shift
949   local _attach_flg="$1"      ; shift
950   local _speedy_flg="$1"      ; shift
951   local _await_flg="$1"       ; shift
952   local _repstr="$1"          ; shift
953   local _cmd_template="$1"    ; shift
954   local _args=("$@")
955   local _window_height="$XP_WINDOW_HEIGHT"
956   local _window_width="$XP_WINDOW_WIDTH"
957   local _col="$XP_OPT_CUSTOM_SIZE_COLS"
958   local _row="$XP_OPT_CUSTOM_SIZE_ROWS"
959   local _cols=("${XP_COLS[@]}")
960   local _cols_offset=("${XP_COLS_OFFSETS[@]}")
961   local _exec_cmd=
962   local _pane_id=
963   local _first_pane_id=
964   local _window_id=
965   local _cell_height=
966   local _cell_width=
967   local _top_pane_height=
968   local _current_pane_width=
969   local i=
970   local j=
971   local _rest_col=
972   local _rest_row=
973   local _offset=
974
975   _cell_height=$(( ( _window_height - _row + 1 ) / _row ))
976   ## Insert first element
977   _exec_cmd="${_cmd_template//${_repstr}/${_args[0]}}"
978   _exec_cmd="$(xpns_inject_wait_command "${_log_flag}" "${_title_flag}" "${_speedy_flg}" "${_await_flg}" 0 "${_exec_cmd}")"
979   _window_id=$(xpns_new_window "${_window_name}" "${_attach_flg}" "${_speedy_flg}" "${_exec_cmd}")
980   _first_pane_id=$(${TMUX_XPANES_EXEC} display-message -t "$_window_id" -p -F '#{pane_id}' | head -n 1)
981
982   ## Start from last row
983   for (( i = _row - 1 ; i > 0 ; i-- ));do
984     _col="${_cols[i]}"
985     _cell_width=$(( ( _window_width - _col + 1 ) / _col ))
986     xpns_msg_debug "_col=$_col"
987     (( _offset = _cols_offset[i] ))
988     for (( j = 0 ; j < _col ; j++ ));do
989       if (( j == 0 )) ;then
990         (( idx = _offset - _col ))
991         # Create new row
992         # Insert first element of the row first
993         _exec_cmd="${_cmd_template//${_repstr}/${_args[idx]}}"
994         _exec_cmd="$(xpns_inject_wait_command "${_log_flag}" "${_title_flag}" "${_speedy_flg}" "${_await_flg}" "${idx}" "${_exec_cmd}")"
995         _pane_id=$(xpns_new_pane_vertical "${_window_name}" "${_cell_height}" "${_speedy_flg}" "${_exec_cmd}")
996       fi
997       # Separate row into columns
998       if (( j != 0 )) ;then
999         (( idx = _offset - j ))
1000         _exec_cmd="${_cmd_template//${_repstr}/${_args[idx]}}"
1001         _exec_cmd="$(xpns_inject_wait_command "${_log_flag}" "${_title_flag}" "${_speedy_flg}" "${_await_flg}" "${idx}" "${_exec_cmd}")"
1002         ## Separate row into columns
1003         _current_pane_width=$(${TMUX_XPANES_EXEC} display-message -t "$_pane_id" -p '#{pane_width}' | head -n 1)
1004         _rest_col=$(( _col - j + 1 ))
1005         _cell_width=$(( ( _current_pane_width - _rest_col + 1 ) / _rest_col ))
1006         xpns_split_pane_horizontal "$_pane_id" "$_cell_width" "${_speedy_flg}" "${_exec_cmd}"
1007       fi
1008     done
1009
1010     # Adjust height
1011     _top_pane_height=$(${TMUX_XPANES_EXEC} display-message -t "$_window_id" -p '#{pane_height}' | head -n 1)
1012     _rest_row=$(( i ))
1013     xpns_msg_debug "_top_pane_height=$_top_pane_height _rest_row=$_rest_row"
1014     _cell_height=$(( ( _top_pane_height - _rest_row + 1 ) / _rest_row ))
1015   done
1016
1017   # Split first row into columns
1018   _col="${_cols[0]}"
1019   _cell_width=$(( ( _window_width - _col + 1 ) / _col ))
1020   for (( j = 1 ; j < _col ; j++ ));do
1021     idx=$(( _cols_offset[0] - j ))
1022     # Adjust width
1023     _current_pane_width=$(${TMUX_XPANES_EXEC} display-message -t "$_first_pane_id" -p '#{pane_width}' | head -n 1)
1024     _rest_col=$(( _col - j + 1 ))
1025     _cell_width=$(( ( _current_pane_width - _rest_col + 1 ) / _rest_col ))
1026     ## Split top row into columns
1027     _exec_cmd="${_cmd_template//${_repstr}/${_args[idx]}}"
1028     _exec_cmd="$(xpns_inject_wait_command "${_log_flag}" "${_title_flag}" "${_speedy_flg}" "${_await_flg}" "${idx}" "${_exec_cmd}")"
1029     xpns_split_pane_horizontal "${_first_pane_id}" "${_cell_width}" "${_speedy_flg}" "${_exec_cmd}"
1030   done
1031 }
1032
1033 xpns_is_session_running() {
1034   local _socket="$1"
1035   ${TMUX_XPANES_EXEC} -S "${_socket}" list-session > /dev/null 2>&1
1036 }
1037
1038 # Remove unnecessary session files as much as possible
1039 # to let xpanes avoids to load old .tmux.conf.
1040 xpns_clean_session() {
1041   if [[ "${XP_SOCKET_PATH}" != "${XP_DEFAULT_SOCKET_PATH}" ]]; then
1042     return
1043   fi
1044   # Delete old socket file (xpanes v3.1.0 or before).
1045   if [[ -e "${XP_DEFAULT_SOCKET_PATH_BASE}" ]]; then
1046     if ! xpns_is_session_running "${XP_DEFAULT_SOCKET_PATH_BASE}" ;then
1047       xpns_msg_debug "socket(${XP_DEFAULT_SOCKET_PATH_BASE}) is not running. Remove it"
1048       rm -f "${XP_DEFAULT_SOCKET_PATH_BASE}"
1049     fi
1050   fi
1051   for _socket in "${XP_CACHE_HOME}"/socket.* ;do
1052     xpns_msg_debug "file = ${_socket}"
1053     if ! xpns_is_session_running "${_socket}" ;then
1054       xpns_msg_debug "socket(${_socket}) is not running. Remove it"
1055       rm -f "${_socket}"
1056     else
1057       xpns_msg_debug "socket(${_socket}) is running. Keep ${_socket}"
1058     fi
1059   done
1060 }
1061
1062 #
1063 # Split a new window which was created by tmux into multiple panes.
1064 # Usage:
1065 #    xpns_prepare_preset_layout_window <window name> <offset of index> <number of pane> <attach or not>
1066 #
1067 xpns_prepare_preset_layout_window() {
1068   local _window_name="$1"     ; shift
1069   local _pane_base_index="$1" ; shift
1070   local _log_flag="$1"        ; shift
1071   local _title_flag="$1"      ; shift
1072   local _attach_flg="$1"      ; shift
1073   local _speedy_flg="$1"      ; shift
1074   local _await_flg="$1"       ; shift
1075   # Create new window.
1076   if [[ "${_attach_flg}" -eq 1 ]]; then
1077     ${TMUX_XPANES_EXEC} new-window -n "${_window_name}"
1078   else
1079     # Keep background
1080     ${TMUX_XPANES_EXEC} new-window -n "${_window_name}" -d
1081   fi
1082
1083   # specify a pane which has the youngest number of index.
1084   ${TMUX_XPANES_EXEC} select-pane -t "${_window_name}.${_pane_base_index}"
1085
1086   # split window into multiple panes
1087   xpns_split_window \
1088     "${_window_name}" \
1089     "${_log_flag}" \
1090     "${_title_flag}" \
1091     "${_speedy_flg}" \
1092     "${_await_flg}" \
1093     "${_pane_base_index}" \
1094     "$@"
1095
1096   ### If the first pane is still remaining,
1097   ### panes cannot be organized well.
1098   # Delete the first pane
1099   ${TMUX_XPANES_EXEC} kill-pane -t "${_window_name}.${_pane_base_index}"
1100
1101   # Select second pane here.
1102   #   If the command gets error, it would most likely be caused by user (XP_ENOPANE).
1103   #   Suppress error message here and announce it in xpns_execution.
1104   ${TMUX_XPANES_EXEC} select-pane -t "${_window_name}.${_pane_base_index}" > /dev/null 2>&1
1105 }
1106
1107 # Check whether given command is in the PATH or not.
1108 xpns_check_env() {
1109   local _cmds="$1"
1110   while read -r cmd ; do
1111     if ! type "${cmd}" > /dev/null 2>&1; then
1112       if [[ "${cmd}" == "tmux" ]] && [[ "${TMUX_XPANES_EXEC}" == "tmux" ]]; then
1113         xpns_msg_error "${cmd} is required. Install ${cmd} or set TMUX_XPANES_EXEC variable."
1114         exit ${XP_ENOCMD}
1115       elif [[ "${cmd}" != "tmux" ]]; then
1116         xpns_msg_error "${cmd} is required."
1117         exit ${XP_ENOCMD}
1118       fi
1119     fi
1120   done < <(echo "${_cmds}" | tr ' ' '\n')
1121
1122   if ! mkdir -p "${XP_CACHE_HOME}";then
1123     xpns_msg_warning "failed to create cache directory '${XP_CACHE_HOME}'."
1124   fi
1125
1126   # Do not omit this part, this is used by testing.
1127   TMUX_XPANES_TMUX_VERSION="${TMUX_XPANES_TMUX_VERSION:-$(xpns_get_tmux_version)}"
1128   if ( xpns_tmux_is_greater_equals \
1129     "${XP_SUPPORT_TMUX_VERSION_LOWER}" \
1130     "${TMUX_XPANES_TMUX_VERSION}" ) ;then
1131     : "Supported tmux version"
1132   else
1133     xpns_msg_warning \
1134 "'${XP_THIS_FILE_NAME}' may not work correctly! Please check followings.
1135 * tmux is installed correctly.
1136 * Supported tmux version is installed.
1137   Version ${XP_SUPPORT_TMUX_VERSION_LOWER} and over is officially supported."
1138   fi
1139
1140   return 0
1141 }
1142
1143 xpns_pipe_filter() {
1144   local _number="${1-}"
1145   if [[ -z "${_number-}" ]]; then
1146     cat
1147   else
1148     xargs -n "${_number}"
1149   fi
1150 }
1151
1152 xpns_set_args_per_pane() {
1153   local _pane_num="$1"; shift
1154   local _filtered_args=()
1155   while read -r _line; do
1156     _filtered_args+=("${_line}")
1157   done < <(xargs -n "${_pane_num}" <<<"$(xpns_arr2args "${XP_ARGS[@]}")")
1158   XP_ARGS=("${_filtered_args[@]}")
1159 }
1160
1161 xpns_get_window_height_width() {
1162   local _height=
1163   local _width=
1164   local _result=
1165   local _dev=
1166   local _pattern='^([0-9]+)[ \t]+([0-9]+)$'
1167
1168   if ! type stty > /dev/null 2>&1; then
1169     xpns_msg_debug "'stty' does not exist: Failed to get window height and size. Skip checking"
1170     return 1
1171   fi
1172
1173   ## This condition is used for unit testing
1174   if [[ -z "${XP_IS_PIPE_MODE-}" ]]; then
1175     if [[ ! -t 0 ]]; then
1176       XP_IS_PIPE_MODE=1
1177     fi
1178   fi
1179   if [[ $XP_IS_PIPE_MODE -eq 0 ]]; then
1180     if _result=$(stty size 2> /dev/null) && [[ "$_result" =~ $_pattern ]];then
1181       _height="${BASH_REMATCH[1]}"
1182       _width="${BASH_REMATCH[2]}"
1183       xpns_msg_debug "window height: $_height, width: $_width"
1184       printf "%s\\n" "$_height $_width"
1185       return 0
1186     fi
1187   else
1188     if ! type ps > /dev/null 2>&1 ;then
1189       xpns_msg_debug "'ps' does not exist: Failed to get window height and size. Skip checking"
1190       return 1
1191     fi
1192     { read -r; read -r _dev; } < <(ps -o tty -p $$)
1193     ## If it's Linux, -F option is used
1194     if _result=$(stty -F "/dev/${_dev}" size 2> /dev/null) && [[ "$_result" =~ $_pattern ]];then
1195       _height="${BASH_REMATCH[1]}"
1196       _width="${BASH_REMATCH[2]}"
1197       xpns_msg_debug "window height: $_height, width: $_width"
1198       printf "%s\\n" "$_height $_width"
1199       return 0
1200     fi
1201     ## If it's BSD, macOS, -F option is used
1202     if _result=$(stty -f "/dev/${_dev}" size 2> /dev/null) && [[ "$_result" =~ $_pattern ]];then
1203       _height="${BASH_REMATCH[1]}"
1204       _width="${BASH_REMATCH[2]}"
1205       xpns_msg_debug "window height: $_height, width: $_width"
1206       printf "%s\\n" "$_height $_width"
1207       return 0
1208     fi
1209     return 1
1210   fi
1211   return 1
1212 }
1213
1214 xpns_check_cell_size_bulk() {
1215   local _cell_num="$1"    ; shift
1216   local _bulk_cols="$1"   ; shift
1217   local _win_height="$1"  ; shift
1218   local _win_width="$1"   ; shift
1219   local _ignore_flag="$1" ; shift
1220   local _all_cols=()
1221   # shellcheck disable=SC2178
1222   local _cols=0
1223   local _rows=0
1224   local _sum_cell=0
1225   IFS="," read -r -a _all_cols <<< "${_bulk_cols}"
1226   _rows="${#_all_cols[@]}"
1227   for i in "${_all_cols[@]}"; do
1228     (( i >= _cols )) && (( _cols = i ))
1229     (( _sum_cell = _sum_cell + i ))
1230   done
1231   if (( _sum_cell != _cell_num )) ;then
1232     xpns_msg_error "Number of cols does not equals to the number of arguments."
1233     xpns_msg_error "Expected (# of args) : $_cell_num, Actual (--bulk-cols) : $_sum_cell)."
1234     return ${XP_ELAYOUT:-6}
1235   fi
1236   local cell_height=$(( ( _win_height - _rows + 1 ) / _rows ))
1237   local cell_width=$(( ( _win_width - _cols + 1 ) / _cols ))
1238
1239   ## Display basic information
1240   xpns_msg_debug "Window: { Height: $_win_height, Width: $_win_width }"
1241   xpns_msg_debug "Cell: { Height: $cell_height, Width: $cell_width }"
1242   xpns_msg_debug "# Of Panes: ${_cell_num}"
1243   xpns_msg_debug "         | Row[0] --...--> Row[MAX]"
1244   xpns_msg_debug "    -----+------------------------..."
1245   xpns_msg_debug "    Col[]| ${_all_cols[*]}"
1246   xpns_msg_debug "    -----+------------------------..."
1247
1248   if [[ "$_ignore_flag" -ne 1 ]] && ( (( cell_height < 2 )) || (( cell_width < 2 )) ); then
1249     xpns_msg_error "Expected pane size is too small (height: $cell_height lines, width: $cell_width chars)"
1250     return ${XP_ESMLPANE:-7}
1251   fi
1252   printf "%s\\n" "${_cols} ${_rows} ${_all_cols[*]}"
1253 }
1254
1255 xpns_check_cell_size() {
1256   local _cell_num="$1"    ; shift
1257   local _cols="$1"        ; shift
1258   local _rows="$1"        ; shift
1259   local _win_height="$1"  ; shift
1260   local _win_width="$1"   ; shift
1261   local _ignore_flag="$1" ; shift
1262   local _all_cols_num=
1263   local _all_rows=()
1264
1265   if [[ -n "${_cols-}" ]] && [[ -n "${_rows-}" ]];then
1266     xpns_msg_warning "Both col size and row size are provided. Col size is preferentially going to be applied."
1267   fi
1268   ## if col is only defined
1269   if [[ -n "${_cols-}" ]] ;then
1270     read -r _cols _rows < <(xpns_adjust_col_row "${_cols-}" 0 "${_cell_num}")
1271     IFS=" " read -r -a _all_rows <<< "$(xpns_divide_equally "${_cell_num}" "${_cols}")"
1272     _all_cols_num="$(xpns_nums_transpose "${_all_rows[@]}")"
1273
1274   ## if row is only defined
1275   elif [[ -n "${_rows-}" ]] ;then
1276     read -r _cols _rows < <(xpns_adjust_col_row 0 "${_rows-}" "${_cell_num}")
1277     _all_cols_num="$(xpns_divide_equally "${_cell_num}" "${_rows}")"
1278
1279   ## if both are undefined
1280   else
1281     read -r _cols _rows < <(xpns_adjust_col_row 0 0 "${_cell_num}")
1282     _all_cols_num="$(xpns_divide_equally "${_cell_num}" "${_rows}")"
1283   fi
1284
1285   local cell_height=$(( ( _win_height - _rows + 1 ) / _rows ))
1286   local cell_width=$(( ( _win_width - _cols + 1 ) / _cols ))
1287
1288   ## Display basic information
1289   xpns_msg_debug "Window: { Height: $_win_height, Width: $_win_width }"
1290   xpns_msg_debug "Cell: { Height: $cell_height, Width: $cell_width }"
1291   xpns_msg_debug "# Of Panes: ${_cell_num}"
1292   xpns_msg_debug "         | Row[0] --...--> Row[MAX]"
1293   xpns_msg_debug "    -----+------------------------..."
1294   xpns_msg_debug "    Col[]| ${_all_cols_num}"
1295   xpns_msg_debug "    -----+------------------------..."
1296
1297   if [[ "$_ignore_flag" -ne 1 ]] && ( (( cell_height < 2 )) || (( cell_width < 2 )) ); then
1298     xpns_msg_error "Expected pane size is too small (height: $cell_height lines, width: $cell_width chars)"
1299     return "${XP_ESMLPANE:-7}"
1300   fi
1301   printf "%s\\n" "${_cols} ${_rows} ${_all_cols_num}"
1302 }
1303
1304 # Execute from Normal mode1
1305 xpns_pre_execution() {
1306   local _opts4args=""
1307   local _args4args=""
1308
1309   if [[ ${XP_OPT_EXTRA} -eq 1 ]];then
1310     xpns_msg_error "'-x' must be used within the running tmux session."
1311     exit ${XP_EINVAL}
1312   fi
1313
1314   # Run as best effort.
1315   # Because after the tmux session is created, cols and rows would be provided by tmux.
1316   IFS=" " read -r XP_WINDOW_HEIGHT XP_WINDOW_WIDTH < <(xpns_get_window_height_width) && {
1317     local _arg_num="${#XP_ARGS[@]}"
1318     local _cell_num _tmp_col_row_cols _tmp_cols
1319     if [[ -n "$XP_MAX_PANE_ARGS" ]] && (( XP_MAX_PANE_ARGS > 1 ));then
1320       _cell_num=$(( _arg_num / XP_MAX_PANE_ARGS ))
1321     else
1322       _cell_num="${_arg_num}"
1323     fi
1324     if [[ -n "${XP_OPT_BULK_COLS}" ]]; then
1325       _tmp_col_row_cols="$(xpns_check_cell_size_bulk \
1326         "${_cell_num}" \
1327         "${XP_OPT_BULK_COLS}" \
1328         "${XP_WINDOW_HEIGHT}" \
1329         "${XP_WINDOW_WIDTH}" \
1330         "${XP_OPT_IGNORE_SIZE_LIMIT:-0}")"
1331       local _exit_status="$?"
1332       [[ $_exit_status -eq ${XP_ELAYOUT} ]] && exit ${XP_ELAYOUT}
1333       [[ $_exit_status -eq ${XP_ESMLPANE} ]] && exit ${XP_ESMLPANE}
1334     else
1335       _tmp_col_row_cols="$(xpns_check_cell_size \
1336         "${_cell_num}" \
1337         "${XP_OPT_CUSTOM_SIZE_COLS-}" \
1338         "${XP_OPT_CUSTOM_SIZE_ROWS-}" \
1339         "${XP_WINDOW_HEIGHT}" \
1340         "${XP_WINDOW_WIDTH}" \
1341         "${XP_OPT_IGNORE_SIZE_LIMIT:-0}")"
1342       [[ $? -eq ${XP_ESMLPANE} ]] && exit ${XP_ESMLPANE}
1343     fi
1344
1345     IFS=" " read -r XP_OPT_CUSTOM_SIZE_COLS XP_OPT_CUSTOM_SIZE_ROWS _tmp_cols <<< "$_tmp_col_row_cols"
1346     IFS=" " read -r -a XP_COLS <<< "${_tmp_cols}"
1347     IFS=" " read -r -a XP_COLS_OFFSETS <<< "$(printf "%s\\n" "${XP_COLS[*]}" | xpns_nums_accumulate_sum)"
1348     xpns_msg_debug "Options: $(xpns_arr2args "${XP_OPTIONS[@]}")"
1349     xpns_msg_debug "Arguments: $(xpns_arr2args "${XP_ARGS[@]}")"
1350   }
1351
1352   # Append -- flag.
1353   # Because any arguments may have `-`
1354   if [[ ${XP_NO_OPT} -eq 1 ]]; then
1355     XP_ARGS=("--" "${XP_ARGS[@]}")
1356   fi
1357
1358   # If there is any options, escape them.
1359   if [[ -n "${XP_OPTIONS[*]-}" ]]; then
1360     _opts4args=$(xpns_arr2args "${XP_OPTIONS[@]}")
1361   fi
1362   _args4args=$(xpns_arr2args "${XP_ARGS[@]}")
1363
1364   # Run as best effort
1365   xpns_clean_session || true
1366
1367   # Create new session.
1368   ${TMUX_XPANES_EXEC} -S "${XP_SOCKET_PATH}" new-session \
1369     -s "${XP_SESSION_NAME}" \
1370     -n "${XP_TMP_WIN_NAME}" \
1371     -d "${XP_ABS_THIS_FILE_NAME} ${_opts4args} ${_args4args}"
1372
1373   # Avoid attaching (for unit testing).
1374   if [[ ${XP_OPT_ATTACH} -eq 1 ]]; then
1375     if ! ${TMUX_XPANES_EXEC} -S "${XP_SOCKET_PATH}" attach-session -t "${XP_SESSION_NAME}" \
1376       && [[ ${XP_IS_PIPE_MODE} -eq 1 ]]; then
1377       ## In recovery case, overwrite trap to keep socket file
1378       trap 'rm -f "${XP_CACHE_HOME}"/__xpns_*$$;' EXIT
1379
1380       xpns_msg "Recovery" \
1381 "Execute below command line to re-attach the new session.
1382
1383 ${TMUX_XPANES_EXEC} -S ${XP_SOCKET_PATH} attach-session -t ${XP_SESSION_NAME}
1384
1385 "
1386       exit ${XP_ETTY}
1387     fi
1388   fi
1389 }
1390
1391 # Execute from inside of tmux session
1392 xpns_execution() {
1393   local _pane_base_index=
1394   local _window_name=
1395   local _last_args_idx=
1396   local _def_allow_rename=
1397   local _pane_count=0
1398
1399   if [[ ${XP_IS_PIPE_MODE} -eq 0 ]] && [[ -n "${XP_MAX_PANE_ARGS-}" ]];then
1400     xpns_set_args_per_pane "${XP_MAX_PANE_ARGS}"
1401   fi
1402
1403   ## Fix window size and define pane size
1404   {
1405     local  _tmp_col_row_cols _tmp_cols
1406     IFS=" " read -r XP_WINDOW_HEIGHT XP_WINDOW_WIDTH < \
1407       <(${TMUX_XPANES_EXEC} display-message -p '#{window_height} #{window_width}')
1408     if [[ -n "${XP_OPT_BULK_COLS}" ]]; then
1409       _tmp_col_row_cols="$(xpns_check_cell_size_bulk \
1410         "${#XP_ARGS[@]}" \
1411         "${XP_OPT_BULK_COLS}" \
1412         "${XP_WINDOW_HEIGHT}" \
1413         "${XP_WINDOW_WIDTH}" \
1414         "${XP_OPT_IGNORE_SIZE_LIMIT:-0}")"
1415       local _exit_status="$?"
1416       [[ $_exit_status -eq ${XP_ELAYOUT} ]] && exit ${XP_ELAYOUT}
1417       [[ $_exit_status -eq ${XP_ESMLPANE} ]] && exit ${XP_ESMLPANE}
1418     else
1419       _tmp_col_row_cols="$(xpns_check_cell_size \
1420         "${#XP_ARGS[@]}" \
1421         "${XP_OPT_CUSTOM_SIZE_COLS-}" \
1422         "${XP_OPT_CUSTOM_SIZE_ROWS-}" \
1423         "${XP_WINDOW_HEIGHT}" \
1424         "${XP_WINDOW_WIDTH}" \
1425         "${XP_OPT_IGNORE_SIZE_LIMIT:-0}")"
1426       [[ $? -eq ${XP_ESMLPANE} ]] && exit ${XP_ESMLPANE}
1427     fi
1428     IFS=" " read -r XP_OPT_CUSTOM_SIZE_COLS XP_OPT_CUSTOM_SIZE_ROWS _tmp_cols <<< "$_tmp_col_row_cols"
1429     IFS=" " read -r -a XP_COLS <<< "${_tmp_cols}"
1430     IFS=" " read -r -a XP_COLS_OFFSETS <<< "$(printf "%s\\n" "${XP_COLS[*]}" | xpns_nums_accumulate_sum)"
1431     xpns_msg_debug "Options: $(xpns_arr2args "${XP_OPTIONS[@]}")"
1432     xpns_msg_debug "Arguments: $(xpns_arr2args "${XP_ARGS[@]}")"
1433   }
1434
1435   _pane_base_index=$(xpns_get_global_tmux_conf 'pane-base-index')
1436   _last_args_idx=$((${#XP_ARGS[@]} - 1))
1437   _def_allow_rename="$(xpns_get_global_tmux_conf 'allow-rename')"
1438
1439   xpns_suppress_allow_rename "${_def_allow_rename-}"
1440   XP_CMD_UTILITY="$(xpns_get_joined_begin_commands "${XP_CMD_UTILITY}")"
1441
1442   if [[ ${XP_OPT_EXTRA} -eq 1 ]];then
1443     # Reuse existing window name
1444     # tmux 1.6 does not support -F option
1445     _window_name="$( ${TMUX_XPANES_EXEC} display -p -F "#{window_id}" )"
1446     _pane_count="$( ${TMUX_XPANES_EXEC} list-panes | grep -c . )"
1447     _pane_base_index=$(( _pane_base_index + _pane_count - 1 ))
1448     _pane_active_pane_id=$(${TMUX_XPANES_EXEC} display -p -F "#{pane_id}")
1449   else
1450     _window_name=$(
1451       xpns_generate_window_name \
1452         "${XP_EMPTY_STR}" \
1453         "${XP_ARGS[0]}" \
1454         | xpns_value2key)
1455   fi
1456
1457   ## --------------------
1458   # Prepare window and panes
1459   ## --------------------
1460   if [[ ${XP_OPT_EXTRA} -eq 1 ]];then
1461     xpns_prepare_extra_panes \
1462       "${_window_name}" \
1463       "${_pane_base_index}" \
1464       "${XP_OPT_LOG_STORE}" \
1465       "${XP_OPT_SET_TITLE}" \
1466       "${XP_OPT_SPEEDY}" \
1467       "${XP_OPT_SPEEDY_AWAIT}" \
1468       "${XP_REPSTR}" \
1469       "${XP_CMD_UTILITY}" \
1470       "${XP_ARGS[@]}"
1471   elif [[ ${XP_OPT_USE_PRESET_LAYOUT} -eq 1 ]];then
1472     xpns_prepare_preset_layout_window \
1473       "${_window_name}" \
1474       "${_pane_base_index}" \
1475       "${XP_OPT_LOG_STORE}" \
1476       "${XP_OPT_SET_TITLE}" \
1477       "${XP_OPT_ATTACH}" \
1478       "${XP_OPT_SPEEDY}" \
1479       "${XP_OPT_SPEEDY_AWAIT}" \
1480       "${XP_REPSTR}" \
1481       "${XP_CMD_UTILITY}" \
1482       "${XP_ARGS[@]}"
1483   elif [[ ${XP_OPT_USE_PRESET_LAYOUT} -eq 0 ]];then
1484     xpns_prepare_window \
1485       "${_window_name}" \
1486       "${XP_OPT_LOG_STORE}" \
1487       "${XP_OPT_SET_TITLE}" \
1488       "${XP_OPT_ATTACH}" \
1489       "${XP_OPT_SPEEDY}" \
1490       "${XP_OPT_SPEEDY_AWAIT}" \
1491       "${XP_REPSTR}" \
1492       "${XP_CMD_UTILITY}" \
1493       "${XP_ARGS[@]}"
1494   fi
1495
1496   ## With -ss option, it is possible to close all the panes as of here.
1497   ## Check status of the window. If no window exists, there is nothing to do execpt to exit.
1498   xpns_msg_debug "xpns_is_window_alive:1: After window separation"
1499   xpns_is_window_alive "${_window_name}" "${XP_OPT_SPEEDY_AWAIT}" "${_def_allow_rename-}"
1500
1501   if [[ ${XP_OPT_EXTRA} -eq 1 ]];then
1502     # Set offset to avoid sending command to the original pane.
1503     _pane_base_index=$((_pane_base_index + 1))
1504     # Avoid to make layout even-horizontal even if there are many panes.
1505     # in xpns_organize_panes
1506     _last_args_idx=$((_last_args_idx + _pane_count))
1507     # Re-select the windown that was active before.
1508     ${TMUX_XPANES_EXEC} select-pane -t "${_window_name}.${_pane_active_pane_id}"
1509   fi
1510
1511   if [[ ${XP_OPT_LOG_STORE} -eq 1 ]]; then
1512     xpns_enable_logging \
1513       "${_window_name}" \
1514       "${_pane_base_index}" \
1515       "${TMUX_XPANES_LOG_DIRECTORY}" \
1516       "${TMUX_XPANES_LOG_FORMAT}" \
1517       "${XP_EMPTY_STR}" \
1518       "${XP_ARGS[@]}"
1519
1520     if [[ $XP_OPT_SPEEDY -eq 1 ]]; then
1521       xpns_notify_logging \
1522         "${_window_name}" \
1523         "${XP_ARGS[@]}"
1524     fi
1525   fi
1526
1527   xpns_msg_debug "xpns_is_window_alive:2: After logging"
1528   xpns_is_window_alive "${_window_name}" "${XP_OPT_SPEEDY_AWAIT}" "${_def_allow_rename-}"
1529
1530   # Set pane titles for each pane.
1531   if xpns_is_pane_title_required "${XP_OPT_SET_TITLE}" "${XP_OPT_EXTRA}" ;then
1532     xpns_set_titles \
1533       "${_window_name}" \
1534       "${_pane_base_index}" \
1535       "${XP_ARGS[@]}"
1536   fi
1537
1538   if [[ $XP_OPT_SPEEDY -eq 1 ]];then
1539     xpns_notify_sync \
1540       "${_window_name}" \
1541       "${XP_ARGS[@]}"
1542   fi
1543
1544   xpns_msg_debug "xpns_is_window_alive:3: After setting title"
1545   xpns_is_window_alive "${_window_name}" "${XP_OPT_SPEEDY_AWAIT}" "${_def_allow_rename-}"
1546
1547   # Sending operations for each pane.
1548   # With -s option, command is already sent.
1549   if [[ $XP_OPT_SPEEDY -eq 0 ]]; then
1550     xpns_send_commands \
1551       "${_window_name}" \
1552       "${_pane_base_index}" \
1553       "${XP_REPSTR}" \
1554       "${XP_CMD_UTILITY}" \
1555       "${XP_ARGS[@]}"
1556   fi
1557
1558   xpns_msg_debug "xpns_is_window_alive:4: After sending commands"
1559   xpns_is_window_alive "${_window_name}" "${XP_OPT_SPEEDY_AWAIT}" "${_def_allow_rename-}"
1560
1561   ## With -l <layout>, panes are organized.
1562   ## As well as -x, they are re-organized.
1563   if [[ $XP_OPT_USE_PRESET_LAYOUT -eq 1 ]] || [[ ${XP_OPT_EXTRA} -eq 1 ]]; then
1564     xpns_organize_panes \
1565       "${_window_name}" \
1566       "${_last_args_idx}"
1567   fi
1568
1569   # Enable broadcasting
1570   if [[ ${XP_OPT_IS_SYNC} -eq 1 ]] && [[ ${XP_OPT_EXTRA} -eq 0 ]]; then
1571     ${TMUX_XPANES_EXEC} \
1572       set-window-option -t "${_window_name}" \
1573       synchronize-panes on
1574   fi
1575
1576   ## In case of -t option
1577   if [[ ${XP_OPT_SET_TITLE} -eq 1 ]] && [[ ${XP_OPT_CHANGE_BORDER} -eq 1 ]]; then
1578     # Set border format
1579     ${TMUX_XPANES_EXEC} \
1580       set-window-option -t "${_window_name}" \
1581       pane-border-format "${TMUX_XPANES_PANE_BORDER_FORMAT}"
1582     # Show border status
1583     ${TMUX_XPANES_EXEC} \
1584       set-window-option -t "${_window_name}" \
1585       pane-border-status "${TMUX_XPANES_PANE_BORDER_STATUS}"
1586   fi
1587
1588   # In case of -x, this statement is skipped to keep the original window name
1589   if [[ ${XP_OPT_EXTRA} -eq 0 ]];then
1590     # Restore original window name.
1591     ${TMUX_XPANES_EXEC} \
1592       rename-window -t "${_window_name}" \
1593       -- "$(printf "%s\\n" "${_window_name}" | xpns_key2value)"
1594   fi
1595
1596   xpns_restore_allow_rename "${_def_allow_rename-}"
1597 }
1598
1599 ## ----------------
1600 # Arrange options for pipe mode
1601 #  * argument -> command
1602 #  * stdin -> argument
1603 ## ----------------
1604 xpns_switch_pipe_mode() {
1605   local _pane_num4new_term=""
1606   if [[ -n "${XP_ARGS[*]-}" ]] && [[ -n "${XP_CMD_UTILITY-}" ]]; then
1607     xpns_msg_error "Both arguments and other options (like '-c', '-e') which updates <command> are given."
1608     exit ${XP_EINVAL}
1609   fi
1610
1611   if [[ -z "${TMUX-}" ]]; then
1612     xpns_msg_warning "Attached session is required for 'Pipe mode'."
1613     # This condition is used when the following situations.
1614     #   * Enter from outside of tmux session(Normal mode1)
1615     #   * Pipe mode.
1616     #   * -n option.
1617     #
1618     # For example:
1619     #     (Normal mode1)$ echo {a..g} | ./xpanes -n 2
1620     # => This will once create the new window like this.
1621     #     (inside of tmux session)$ ./xpanes '-n' '2' 'a' 'b' 'c' 'd' 'e' 'f' 'g'
1622     #     => After the window is closed, following panes would be left.
1623     #     (pane 1)$ echo a b
1624     #     (pane 2)$ echo c d
1625     #     (pane 3)$ echo e f
1626     #     (pane 4)$ echo g
1627     # In order to create such the query,
1628     # separate all the argument into minimum tokens
1629     # with xargs -n 1
1630     if [[ -n "${XP_MAX_PANE_ARGS-}" ]]; then
1631       _pane_num4new_term=1
1632     fi
1633   fi
1634
1635   while read -r line;
1636   do
1637     XP_STDIN+=("${line}")
1638   done < <(cat | xpns_rm_empty_line | \
1639     xpns_pipe_filter "${_pane_num4new_term:-${XP_MAX_PANE_ARGS}}")
1640
1641
1642   # Merge them into command.
1643   if [[ -n "${XP_ARGS[*]-}" ]]; then
1644     # Attention: It might be wrong result if IFS is changed.
1645     XP_CMD_UTILITY="${XP_ARGS[*]}"
1646   fi
1647
1648   # If there is empty -I option or user does not assign the <repstr>,
1649   # Append the space and <repstr> at the end of the <command>
1650   # This is same as the xargs command of GNU.
1651   # i.e,
1652   #   $ echo 10 | xargs seq
1653   #     => seq 10
1654   # Whith is same as
1655   #   $ echo 10 | xargs -I@ seq @
1656   #     => seq 10
1657   if [[ -z "${XP_REPSTR}" ]]; then
1658     XP_REPSTR="${XP_DEFAULT_REPSTR}"
1659     if [[ -n "${XP_ARGS[*]-}" ]]; then
1660       XP_CMD_UTILITY="${XP_ARGS[*]-} ${XP_REPSTR}"
1661     fi
1662   fi
1663
1664   # Deal with stdin as arguments.
1665   XP_ARGS=("${XP_STDIN[@]-}")
1666 }
1667
1668 xpns_layout_short2long() {
1669   sed \
1670     -e 's/^t$/tiled/' \
1671     -e 's/^eh$/even-horizontal/' \
1672     -e 's/^ev$/even-vertical/' \
1673     -e 's/^mh$/main-horizontal/' \
1674     -e 's/^mv$/main-vertical/' \
1675     -e ';'
1676 }
1677
1678 xpns_is_valid_layout() {
1679   local _layout="${1-}"
1680   local _pat='^(tiled|even-horizontal|even-vertical|main-horizontal|main-vertical)$'
1681   if ! [[ $_layout =~ $_pat ]]  ; then
1682     xpns_msg_error "Invalid layout '${_layout}'."
1683     exit ${XP_ELAYOUT}
1684   fi
1685 }
1686
1687 xpns_warning_before_extra() {
1688   local _ans=
1689   local _synchronized=
1690   _synchronized="$(xpns_get_local_tmux_conf "synchronize-panes")"
1691   if [[ "on" == "${_synchronized}" ]];then
1692     xpns_msg_warning "Panes are now synchronized.
1693 '-x' option may cause unexpected behavior on the synchronized panes."
1694     printf "Are you really sure? [y/n]: "
1695     read -r _ans
1696     if ! [[ "${_ans-}" =~ ^[yY]  ]]; then
1697       return 1
1698     fi
1699   fi
1700 }
1701
1702 xpns_load_flag_options() {
1703   if [[ "$1" =~ h ]]; then
1704     xpns_usage
1705     exit 0
1706   fi
1707   if [[ "$1" =~ V ]]; then
1708     xpns_version
1709     exit 0
1710   fi
1711   if [[ "$1" =~ x ]]; then
1712     XP_OPT_EXTRA=1
1713     XP_OPT_USE_PRESET_LAYOUT=1 ## Layout presets must be used with -x
1714     if ! xpns_warning_before_extra; then
1715       exit ${XP_EINTENT}
1716     fi
1717   fi
1718   if [[ "$1" =~ d ]]; then
1719     XP_OPT_IS_SYNC=0
1720   fi
1721   if [[ "$1" =~ e ]]; then
1722     XP_REPSTR="{}"
1723     XP_CMD_UTILITY="{}"
1724   fi
1725   if [[ "$1" =~ t ]]; then
1726     if ( xpns_tmux_is_greater_equals 2.3 ) ; then
1727       XP_OPT_SET_TITLE=1
1728       XP_OPT_CHANGE_BORDER=1
1729     else
1730       xpns_msg_warning "-t option cannot be used by tmux version less than 2.3. Disabled."
1731       sleep 1
1732     fi
1733   fi
1734   if [[ "$1" =~ s ]]; then
1735     XP_OPT_SPEEDY=1
1736     XP_OPT_SPEEDY_AWAIT=1
1737   fi
1738   if [[ "$1" =~ ss ]]; then
1739     XP_OPT_SPEEDY_AWAIT=0
1740   fi
1741   return 1
1742 }
1743
1744 xpns_load_arg_options() {
1745   # Extract flag options only.
1746   local _pattern=
1747   xpns_load_flag_options "$(xpns_extract_matched "$1" "^-${XP_FLAG_OPTIONS}+")" > /dev/null
1748   if [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*I ]]; then
1749     # Behavior like this.
1750     # -IAAA       -- XP_REPSTR="AAA"
1751     # -I AAA BBB  -- XP_REPSTR="AAA", XP_ARGS=("BBB")
1752     # -I"AAA BBB" -- XP_REPSTR="AAA BBB"
1753     # -IAAA BBB   -- XP_REPSTR="AAA", XP_ARGS=("BBB")
1754     # -I -d ...   -- XP_REPSTR=""
1755     _pattern="^-${XP_FLAG_OPTIONS}*I(.+)"
1756     if [[ "$1" =~ $_pattern ]]; then
1757       XP_REPSTR="${BASH_REMATCH[1]}"
1758       return 0
1759     elif ! [[ "$2" =~ ^-.* ]]; then
1760       XP_REPSTR="$2"
1761       return 0
1762     else
1763       xpns_msg_error "invalid argument '$2' for -I option"
1764       exit ${XP_EINVAL}
1765     fi
1766   elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*l ]]; then
1767     _pattern="^-${XP_FLAG_OPTIONS}*l(.+)"
1768     if [[ "$1" =~ $_pattern ]]; then
1769       XP_OPT_USE_PRESET_LAYOUT=1
1770       XP_LAYOUT="$(cat <<<"${BASH_REMATCH[1]}" | xpns_layout_short2long)"
1771       xpns_is_valid_layout "${XP_LAYOUT}"
1772       return 0
1773     elif ! [[ "$2" =~ ^-.* ]]; then
1774       XP_OPT_USE_PRESET_LAYOUT=1
1775       XP_LAYOUT="$(cat <<<"$2" |  xpns_layout_short2long )"
1776       xpns_is_valid_layout "${XP_LAYOUT}"
1777       return 0
1778     else
1779       xpns_msg_error "invalid argument '$2' for -l option"
1780       exit ${XP_EINVAL}
1781     fi
1782   elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*c ]]; then
1783     _pattern="^-${XP_FLAG_OPTIONS}*c(.+)"
1784     if [[ "$1" =~ $_pattern ]]; then
1785       XP_CMD_UTILITY="${BASH_REMATCH[1]}"
1786       XP_OPT_CMD_UTILITY=1
1787       return 0
1788     elif ! [[ "$2" =~ ^-.* ]]; then
1789       XP_CMD_UTILITY="$2"
1790       XP_OPT_CMD_UTILITY=1
1791       return 0
1792     else
1793       xpns_msg_error "invalid argument '$2' for -c option"
1794       exit ${XP_EINVAL}
1795     fi
1796   elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*n ]]; then
1797     _pattern="^-${XP_FLAG_OPTIONS}*n([0-9]+)"
1798     if [[ "$1" =~ $_pattern ]]; then
1799       XP_MAX_PANE_ARGS="${BASH_REMATCH[1]}"
1800       return 0
1801     elif [[ "$2" =~ ^[0-9]+$ ]]; then
1802       XP_MAX_PANE_ARGS="$2"
1803       return 0
1804     else
1805       xpns_msg_error "invalid argument '$2' for -n option"
1806       exit ${XP_EINVAL}
1807     fi
1808   elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*S ]]; then
1809     _pattern="^-${XP_FLAG_OPTIONS}*S(.+)"
1810     if [[ "$1" =~ $_pattern ]]; then
1811       XP_SOCKET_PATH="${BASH_REMATCH[1]}"
1812       return 0
1813     elif ! [[ "$2" =~ ^-.* ]]; then
1814       XP_SOCKET_PATH="$2"
1815       return 0
1816     else
1817       xpns_msg_error "invalid argument '$2' for -S option"
1818       exit ${XP_EINVAL}
1819     fi
1820   elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*C ]]; then
1821     _pattern="^-${XP_FLAG_OPTIONS}*C([0-9]+)"
1822     if [[ "$1" =~ $_pattern ]]; then
1823       XP_OPT_CUSTOM_SIZE_COLS="${BASH_REMATCH[1]}"
1824       return 0
1825     elif [[ "$2" =~ ^[0-9]+$ ]];then
1826       XP_OPT_CUSTOM_SIZE_COLS="$2"
1827       return 0
1828     else
1829       xpns_msg_error "invalid argument '$2' for -C option"
1830       exit ${XP_EINVAL}
1831     fi
1832   elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*R ]]; then
1833     _pattern="^-${XP_FLAG_OPTIONS}*R([0-9]+)"
1834     if [[ "$1" =~ $_pattern ]]; then
1835       XP_OPT_CUSTOM_SIZE_ROWS="${BASH_REMATCH[1]}"
1836       return 0
1837     elif [[ "$2" =~ ^[0-9]+$ ]];then
1838       XP_OPT_CUSTOM_SIZE_ROWS="$2"
1839       return 0
1840     else
1841       xpns_msg_error "invalid argument '$2' for -R option"
1842       exit ${XP_EINVAL}
1843     fi
1844   elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*B ]]; then
1845     _pattern="^-${XP_FLAG_OPTIONS}*B(.+)"
1846     if [[ "$1" =~ $_pattern ]]; then
1847       XP_BEGIN_ARGS+=("${BASH_REMATCH[1]}")
1848       return 0
1849     else
1850       XP_BEGIN_ARGS+=("$2")
1851       return 0
1852     fi
1853   fi
1854   return 0
1855 }
1856
1857 xpns_load_long_options() {
1858   if [[ "$1" =~ ^--help$ ]]; then
1859     xpns_usage
1860     exit 0
1861   elif [[ "$1" =~ ^--version$ ]]; then
1862     xpns_version
1863     exit 0
1864   elif [[ "$1" =~ ^--desync$ ]]; then
1865     XP_OPT_IS_SYNC=0
1866     return 1
1867   elif [[ "$1" =~ ^--log-format=.*$ ]]; then
1868     XP_OPT_LOG_STORE=1
1869     TMUX_XPANES_LOG_FORMAT="${1#--log-format=}"
1870     return 1
1871   elif [[ "$1" =~ ^--log ]]; then
1872     XP_OPT_LOG_STORE=1
1873     if [[ "$1" =~ ^--log=.*$  ]]; then
1874       TMUX_XPANES_LOG_DIRECTORY="${1#--log=}"
1875     fi
1876     return 1
1877   elif [[ "$1" =~ ^--ssh$ ]]; then
1878     XP_CMD_UTILITY="${XP_SSH_CMD_UTILITY}"
1879     # Enable -t option as well
1880     XP_OPT_SET_TITLE=1
1881     XP_OPT_CHANGE_BORDER=1
1882     # Enable -s option
1883     XP_OPT_SPEEDY=1
1884     XP_OPT_SPEEDY_AWAIT=1
1885     return 1
1886   elif [[ "$1" =~ ^--stay$ ]]; then
1887     XP_OPT_ATTACH=0
1888     return 1
1889   elif [[ "$1" =~ ^--cols=[0-9]+$ ]]; then
1890     XP_OPT_CUSTOM_SIZE_COLS="${1#--cols=}"
1891     return 1
1892   elif [[ "$1" =~ ^--rows=[0-9]+$ ]]; then
1893     XP_OPT_CUSTOM_SIZE_ROWS="${1#--rows=}"
1894     return 1
1895   elif [[ "$1" =~ ^--bulk-cols=[0-9,]*[0-9]+$ ]]; then
1896     XP_OPT_BULK_COLS="${1#--bulk-cols=}"
1897     return 1
1898   elif [[ "$1" =~ ^--debug$ ]]; then
1899     XP_OPT_DEBUG=1
1900     return 1
1901   elif [[ "$1" =~ ^--dry-run$ ]]; then # For unit testing
1902     XP_OPT_DRY_RUN=1
1903     return 1
1904   elif [[ "$1" =~ ^--ignore-size-limit$ ]]; then
1905     XP_OPT_IGNORE_SIZE_LIMIT=1
1906     return 1
1907
1908   ## ----------------
1909   # Other options
1910   ## ----------------
1911   else
1912     xpns_msg_error "invalid option -- '${1#--}'"
1913     xpns_usage_warn
1914     exit ${XP_EINVAL}
1915   fi
1916 }
1917
1918 xpns_parse_options() {
1919   while (( $# > 0 )); do
1920     case "$1" in
1921       --)
1922       if [[ ${XP_NO_OPT} -eq 1 ]]; then
1923         XP_ARGS+=("$1")
1924         shift
1925       else
1926         # Disable any more options
1927         XP_NO_OPT=1
1928         shift
1929       fi
1930       ;;
1931       ## ----------------
1932       # Long options
1933       ## ----------------
1934       --*)
1935       if [[ ${XP_NO_OPT} -eq 1 ]]; then
1936         XP_ARGS+=("$1")
1937         shift
1938       else
1939         local _shift_count="0"
1940         xpns_load_long_options "$@"
1941         _shift_count="$?"
1942         [[ "${_shift_count}" = "1" ]] && XP_OPTIONS+=("$1") && shift
1943       fi
1944       ;;
1945       ## ----------------
1946       # Short options
1947       ## ----------------
1948       -*)
1949       if [[ ${XP_NO_OPT} -eq 1 ]]; then
1950         XP_ARGS+=("$1")
1951         shift
1952       else
1953         local _shift_count="0"
1954         if [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*${XP_ARG_OPTIONS}. ]];then
1955           xpns_load_arg_options "$@"
1956           XP_OPTIONS+=("$1") && shift
1957         elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}*${XP_ARG_OPTIONS}$ ]] && [[ -n "${2-}" ]];then
1958           xpns_load_arg_options "$@"
1959           _shift_count="$?"
1960           XP_OPTIONS+=("$1" "$2") && shift && shift
1961         elif [[ "$1" =~ ^-${XP_FLAG_OPTIONS}+$  ]];then
1962           xpns_load_flag_options "$1"
1963           XP_OPTIONS+=("$1") && shift
1964         ## ----------------
1965         # Other options
1966         ## ----------------
1967         else
1968           xpns_msg_error "Invalid option -- '${1#-}'"
1969           xpns_usage_warn
1970           exit ${XP_EINVAL}
1971         fi
1972       fi
1973       ;;
1974       ## ----------------
1975       # Other arguments
1976       ## ----------------
1977       *)
1978       XP_ARGS+=("$1")
1979       XP_NO_OPT=1
1980       shift
1981       ;;
1982     esac
1983   done
1984
1985   # If there is any standard input from pipe,
1986   # 1 line handled as 1 argument.
1987   if [[ ! -t 0 ]]; then
1988     XP_IS_PIPE_MODE=1
1989     xpns_switch_pipe_mode
1990   fi
1991
1992   # When no argument arr given, exit.
1993   if [[ -z "${XP_ARGS[*]-}" ]]; then
1994     xpns_msg_error "No arguments."
1995     xpns_usage_warn
1996     exit ${XP_EINVAL}
1997   fi
1998
1999   if [[ -n "${XP_OPT_CUSTOM_SIZE_COLS-}" ]] || [[ -n "${XP_OPT_CUSTOM_SIZE_ROWS-}" ]]; then
2000     if [[ "$XP_OPT_EXTRA" -eq 1 ]]; then
2001       xpns_msg_warning "The columns/rows options (-C, --cols, -R, --rows) cannot be used with -x option. Ignored."
2002     elif [[ "$XP_OPT_EXTRA" -eq 0 ]] && [[ "${XP_OPT_USE_PRESET_LAYOUT}" -eq 1 ]]; then
2003       # This part is required to keep backward compatibility.
2004       ## Users can simulate xpanes v3.x to set : alias xpanes="xpanes -lt"
2005       xpns_msg_info "Columns/rows option (-C, --cols, -R, --rows) and -l option are provided. Disable -l. "
2006       XP_OPT_USE_PRESET_LAYOUT=0
2007     fi
2008   fi
2009
2010   # Set default value in case of empty.
2011   XP_CMD_UTILITY="${XP_CMD_UTILITY:-${XP_DEFAULT_CMD_UTILITY}}"
2012   XP_REPSTR="${XP_REPSTR:-${XP_DEFAULT_REPSTR}}"
2013
2014   # To set command on pre_execution, set -c option manually.
2015   if [[ ${XP_OPT_CMD_UTILITY} -eq 0 ]];then
2016     XP_OPTIONS+=("-c" "${XP_CMD_UTILITY}")
2017   fi
2018
2019 }
2020
2021 ## --------------------------------
2022 # Main function
2023 ## --------------------------------
2024 xpns_main() {
2025   xpns_parse_options ${1+"$@"}
2026   xpns_check_env "${XP_DEPENDENCIES}"
2027   ## --------------------------------
2028   # Parameter validation
2029   ## --------------------------------
2030   # When do dry-run flag is enabled, skip running (this is used to execute unit test of itself).
2031   if [[ ${XP_OPT_DRY_RUN} -eq 1 ]]; then
2032     return 0
2033   fi
2034   # Validate log directory.
2035   if [[ ${XP_OPT_LOG_STORE} -eq 1 ]]; then
2036     TMUX_XPANES_LOG_DIRECTORY=$(xpns_normalize_directory "${TMUX_XPANES_LOG_DIRECTORY}")
2037     xpns_is_valid_directory "${TMUX_XPANES_LOG_DIRECTORY}" && \
2038     TMUX_XPANES_LOG_DIRECTORY=$(cd "${TMUX_XPANES_LOG_DIRECTORY}" && pwd)
2039   fi
2040   ## --------------------------------
2041   # If current shell is outside of tmux session.
2042   ## --------------------------------
2043   if [[ -z "${TMUX-}" ]]; then
2044     xpns_pre_execution
2045   ## --------------------------------
2046   # If current shell is already inside of tmux session.
2047   ## --------------------------------
2048   else
2049     xpns_execution
2050   fi
2051   exit 0
2052 }
2053
2054 ## --------------------------------
2055 # Entry Point
2056 ## --------------------------------
2057 xpns_main ${1+"$@"}