]> 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:

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