├── test └── run_test.sh ├── .github └── ISSUE_TEMPLATE │ └── mousexterm-issue-template.md ├── README.md └── mouse.sh /test/run_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export gs_root_path="$(readlink -f "${BASH_SOURCE[0]}")" 4 | gs_root_path="$(dirname "$gs_root_path")" 5 | gs_root_path="$(dirname "$gs_root_path")" 6 | # shellcheck disable=SC1091 # Not following 7 | source "$gs_root_path/mouse.sh" 8 | 9 | mouse_track_start 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/mousexterm-issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: MouseXterm issue template 3 | about: In order to retrieve nice info from users 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | * [ ] I run MouseXterm with the command below 11 | * [ ] I get 12 | * [ ] I would expect 13 | 14 | ```bash 15 | eval "$(curl -X GET https://raw.githubusercontent.com/tinmarino/mouse_xterm/master/mouse.sh)" && mouse_track_start 16 | # Play with mouse, take screenshot 17 | mouse_track_report 18 | # Copy paste output here 19 | ``` 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mouse support on readline 2 | 3 | The following code enables clicks to move cursor in bash/readline on terminal emulators. 4 | 5 | 1. Enable xterm mouse tracking reporting 6 | 2. Set readline bindings to consume the escape sequence generated by clicks 7 | 8 | Tested on xterm and: alacritty, kitty, gnome-terminal 9 | 10 | ## Quickstart 11 | 12 | In a bash shell, source [mouse.sh](./mouse.sh). 13 | 14 | ```bash 15 | eval "$(curl -X GET https://raw.githubusercontent.com/tinmarino/mouse_xterm/master/mouse.sh)" && mousetrack_start 16 | ``` 17 | 18 | Or permanently 19 | 20 | ```bash 21 | git clone --depth=1 https://github.com/tinmarino/mouse_xterm Mouse && cd Mouse 22 | source mouse.sh && mousetrack_start # This can be in your bashrc 23 | ``` 24 | 25 | ## TODO 26 | 27 | * Avoid terminal blinking when trigger readline 28 | * Clearify arithmetic 29 | * Create a tmux bind-key to MouseDown1Pane (currently it is select-pane) 30 | * Take care of other buttons: 2 and 3 that are entering escpe sequeence in terminal 31 | * Pressing Escape and mouse is escaping the mouse and then do not get the readline binding 32 | 33 | ## Xterm 34 | 35 | Xterm have a mouse tracking feature 36 | 37 | ```bash 38 | printf '\e[?1000;1006;1015h' # Enable tracking 39 | printf '\e[?1000;1006;1015l' # Disable tracking 40 | read # Read and prrint stdin full escape sequences, escape look like ^[, click like ^[[<0;36;26M 41 | man console_codes # Some of them 42 | vim /usr/share/doc/xterm/ctlseqs.txt.gz # ctlseqs local documentation 43 | ``` 44 | 45 | * Mouse click looks like `\e[<0;3;21M` and a release `\e[<0;3;21`. Where `2` is x (from left) and `22` is y (from top) 46 | * Mouse whell up : `\e[<64;3;21M` 47 | * Mouse whell down : `\e[<65;3;21M` 48 | * Press `C-v` after enabling the mouse tracking to see that 49 | 50 | ## Bash, Readline 51 | 52 | Multiple lines: press `` for line continuation (or just ``, if `bind '"\n": self-insert'`) 53 | 54 | Readline can trigger a bash callback 55 | 56 | ```bash 57 | bind -x '"\e[<64;": mouse_void_cb' # Cannot be put in .inputrc 58 | bind '"\C-h" : "$(date) \e\C-e\ef\ef\ef\ef\ef"' #Can be put in .inputrc 59 | ``` 60 | 61 | Readline can call multiple functions 62 | 63 | ```bash 64 | # Mouse cursor to begining-of-line before calling click callback 65 | bind '"\C-98" : beginning-of-line' 66 | bind -x '"\C-99" : mouse_0_cb' 67 | bind '"\e[<0;": "\C-98\C-99"' 68 | ``` 69 | 70 | Readline callback can change cursor (point) position with `READLINE_POINT` environment variable 71 | 72 | ```bash 73 | bind -x '"\C-h" : xterm_test' 74 | function xterm_test { 75 | printf "%s" "line is $READLINE_LINE and point $READLINE_POINT and mark $READLINE_LINE" 76 | READLINE_POINT=24 # The cursor position (0 for begining of command) 77 | READLINE_LINE='coco' # The command line current content 78 | } 79 | ``` 80 | 81 | 82 | ## Perl (reply) 83 | 84 | TODO no comment yet, I could not invoke a readline command or I would have lost $term->{point} 85 | 86 | ## Python (ipython) 87 | 88 | Ipython supports mouse. See [Ipython/terminal/shortcuts](https://github.com/ipython/ipython/blob/master/IPython/terminal/shortcuts.py) -> [Prompt-toolkit/bingin.mouse](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/key_binding/bindings/mouse.py) 89 | 90 | ipython --TerminalInteractiveShell.mouse_support=True 91 | 92 | Or to enable at startup write in `.ipython/profile_default/ipython_config.py` 93 | 94 | c = get_config() 95 | c.TerminalInteractiveShell.mouse_support 96 | 97 | ## Limitations 98 | 99 | * OK : bash, ipython3, tmux 100 | * NO : python, reply 101 | * DISABLED : vim 102 | 103 | ## Changelog 104 | 105 | * Feature: If at after last character of a line, put cursor at lat char of this line <= and not the next line as calculated now 106 | * Get log with call depth 107 | * Fix: sleep at read cursor if keep cmouse click 108 | * Add date to log 109 | 110 | ## Links 111 | 112 | * [Xterm control sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) 113 | * [Ctrl keys as used in vim source](https://github.com/vim/vim/blob/master/src/libvterm/doc/seqs.txt) 114 | * [zsh script for mouse tracking](https://github.com/stephane-chazelas/misc-scripts/blob/master/mouse.zsh) : the same but in zsh (not bash) 115 | * [term-mouse](https://github.com/CoderPuppy/term-mouse): the same but in Js 116 | * [so: how to expand PS1](https://stackoverflow.com/questions/3451993/how-to-expand-ps1) 117 | * [so: how to get cursor position](https://unix.stackexchange.com/questions/88296/get-vertical-cursor-position) 118 | * [doc: dec_ansi_parser with drawing](https://vt100.net/emu/dec_ansi_parser) 119 | * [doc: A Prompt the Width of Your Term](https://tldp.org/HOWTO/Bash-Prompt-HOWTO/x869.html) 120 | * [doc: list of ANSI ctlseq](https://www.aivosto.com/articles/control-characters.html) 121 | -------------------------------------------------------------------------------- /mouse.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | : <<'END_DOC' 3 | Mouse click to move cursor 4 | 5 | DESCRIPTION: 6 | Your readline cursor should move on mouse click 7 | 8 | USAGE: 9 | source mouse.sh && mousetrack_start 10 | `ctrl+l` to renable track (automatically disable when you want to scrool) 11 | 12 | DEPENDS: 13 | xterm, readline 14 | 15 | CODE: 16 | 1. mousetrack_start binds the mouse strokes and enables xterm mouse reports 17 | - The g_mousetrack_d_binding dictionary declare the keystrokes and binding 18 | 2. mousetrack_cb_click is called at each mouse click 19 | 3. mousetrack_work_null is setting the READLINE_POINT apropriately 20 | - Its stdin is redirected to /dev/null to avoid late surprises 21 | 22 | BUTTON BIT FIELD 23 | 1 | 0 | MB1 pressed 24 | 1 | 1 | MB2 pressed 25 | 1 | 2 | MB3 pressed 26 | 1 | 3 | release 27 | 2 | 4 | Shift modifier 28 | 3 | 8 | Meta modifier 29 | 4 | 16 | Ctrl modifer 30 | 5 | 32 | Motion 31 | 6 | 64 | Wheel 32 | 7 | 128| 33 | 34 | 32 => On button-motion events, xterm adds 32 to the event code (the third character, Cb) 35 | 64 => Xterm Wheel: mice may return buttons 4 and 5. Those buttons are represented by the same event codes as buttons 1 and 2 respectively, except that 64 is added to the event code. 36 | From: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Normal-tracking-mode 37 | 38 | LICENSE: 39 | Copyright 2019-2023 Tinmarino 40 | This program is free software: you can redistribute it and/or modify 41 | it under the terms of the GNU General Public License as published by 42 | the Free Software Foundation, either version 3 of the License, or 43 | (at your option) any later version. 44 | END_DOC 45 | 46 | #set -u 47 | 48 | # Escape sequences 49 | declare -g g_mousetrack_echo_enable=$'\033[?1000;1002;1006;1015h' 50 | declare -g g_mousetrack_echo_disable=$'\033[?1000;1002;1006;1015l' 51 | declare -g g_mousetrack_echo_get_cursor_pos=$'\033[6n' 52 | 53 | # Binding : seq -> bash function 54 | # -- Prefixed by \033[ 55 | declare -gA g_mousetrack_d_binding=( 56 | # Click (0) Begining of line + X click cb 57 | # -- ^[[<0;29;18M 58 | [<0;]=mousetrack_cb_click 59 | [<1;]=mousetrack_cb_click2 60 | [<2;]=mousetrack_cb_click3 61 | [<3;]=mousetrack_cb_void # should be mouse release 62 | [<32;]=mousetrack_cb_drag1 63 | [<33;]=mousetrack_cb_void # middle 64 | [<34;]=mousetrack_cb_void # right 65 | [<64;]=mousetrack_cb_scroll_up 66 | [<65;]=mousetrack_cb_scroll_down 67 | # Xterm motion, actually normal click 68 | [32;]=mousetrack_cb_click # xterm mouse click 69 | [33;]=mousetrack_cb_click2 70 | [34;]=mousetrack_cb_click3 71 | [35;]=mousetrack_cb_void # xterm mouse release 72 | # Xterm whell 73 | [64;]=mousetrack_cb_drag1 # xterm mouse release 74 | [65;]=mousetrack_cb_drag1 # xterm mouse release 75 | # Xerm scroll empirical 76 | [96;]=mousetrack_cb_scroll_up 77 | [97;]=mousetrack_cb_scroll_down 78 | ) 79 | declare -g g_mousetrack_prompt_command='mousetrack_prompt_command_keep_me_at_end; ' 80 | mousetrack_cb_scroll_up(){ mousetrack_tmux_proxy 'tmux copy-mode -e \; send-keys -X -N 5 scroll-up'; } 81 | mousetrack_cb_scroll_down(){ mousetrack_tmux_proxy ''; } 82 | mousetrack_cb_click2(){ mousetrack_tmux_proxy 'tmux paste-buffer'; } 83 | mousetrack_cb_click3(){ mousetrack_tmux_proxy " 84 | tmux display-menu -T '#[align=centre]#{pane_index} (#{pane_id})' \ 85 | 'Horizontal Split' 'h' 'split-window -h' \ 86 | 'Vertical Split' 'v' 'split-window -v' \ 87 | 'Swap Up' 'u' 'swap-pane -U' \ 88 | 'Swap Down' 'd' 'swap-pane -D' \ 89 | '#{?pane_marked_set,,-}Swap Marked' 's' 'swap-pane' \ 90 | 'Kill' 'X' 'kill-pane' \ 91 | 'Respawn' 'R' 'respawn-pane -k' \ 92 | '#{?pane_marked,Unmark,Mark}' 'm' 'select-pane -m'\ 93 | '#{?window_zoomed_flag,Unzoom,Zoom}' 'z' 'resize-pane -Z' 94 | "; } 95 | mousetrack_cb_drag1(){ mousetrack_tmux_proxy 'tmux copy-mode -e \; send-keys -X begin-selection'; } 96 | mousetrack_cb_drag2(){ mousetrack_tmux_proxy 'tmux copy-mode -e \; send-keys -X begin-selection'; } 97 | mousetrack_cb_drag3(){ mousetrack_tmux_proxy 'tmux copy-mode -e \; send-keys -X begin-selection'; } 98 | 99 | 100 | # Cursor position : 50;1 (x;y) if click on line 1, column 50: starting at 1;1 101 | declare -gi g_mousetrack_i_cursor_x=0 102 | declare -gi g_mousetrack_i_cursor_y=0 103 | 104 | # Readline begining of line, used to resure the PS1 size (especially x) 105 | declare -gi g_mousetrack_i_bol_x=0 106 | declare -gi g_mousetrack_i_bol_y=0 107 | 108 | # Keystrokes : usually output by the terminal 109 | declare -g g_mousetrack_key='' 110 | declare -g g_mousetrack_ps='' 111 | 112 | # Mouse track status 113 | # -- 0 Not tracking 114 | # -- 1 Tracking 115 | declare -gi g_mousetrack_b_status=0 116 | 117 | # Tmux command to launch 118 | declare -g g_mousetrack_tmux_cmd='' 119 | 120 | declare -g g_mousetrack_logfile="${TMPDIR:-/tmp}/xterm_monitor.log" 121 | 122 | mousetrack_version(){ 123 | : 'Print current MouseTrack version 124 | Used to report issues 125 | ' 126 | printf '0.02' 127 | } 128 | 129 | 130 | mousetrack_verify_ps1(){ 131 | : 'Used to report issues 132 | Depends: xxd command 133 | See: https://stackoverflow.com/questions/3451993/how-to-expand-ps1 134 | ' 135 | >&2 echo -e "\nP0: Printinf Raw PS1" 136 | local ps=$PS1 137 | echo -n "$ps" | xxd >&2 138 | 139 | >&2 echo -e "\nP1: Expanding (require bash 4.4)" 140 | ps=${ps@P} 141 | echo -n "$ps" | xxd >&2 142 | 143 | >&2 echo -e "\nP2: Removing everything 01 and 02" 144 | shopt -s extglob 145 | ps=${ps//$'\x01'*([^$'\x02'])$'\x02'} 146 | echo -n "$ps" | xxd >&2 147 | 148 | >&2 echo -e "\nP3: Checking warnings" 149 | if [[ "$ps" =~ [\x07\x1b\x9c] ]]; then 150 | # Check if escape inside 151 | # 07 => BEL 152 | # 1b => ESC 153 | # 9C => ST 154 | >&2 echo 'Warning: There is an escape code in your PS1 which is not betwwen \[ \]' 155 | >&2 echo "Tip: put \[ \] around your escape codes (ctlseqs + associated parameters)" 156 | echo -n "$ps" | xxd >&2 157 | # Check printable characters <= 20 .. 7e, and newline 158 | # -- Remove the trailing 0x0a (BEL) 159 | elif [[ "$ps" =~ [^[:graph:][:space:]] ]]; then 160 | >&2 echo 'Warning: There is a non printable character in PS1 which is not between \[ \]' 161 | >&2 echo "Tip: put \[ \] around your escape codes (ctlseqs + associated parameters)" 162 | echo "$ps" 163 | echo -n "$ps" | xxd >&2 164 | fi 165 | 166 | # Echo result 167 | >&2 echo -e "\nP4: Printing PS1 display lenght" 168 | echo "${#ps}" 169 | } 170 | 171 | 172 | mousetrack_report(){ 173 | : 'Main function to report an issue' 174 | mousetrack_run(){ 175 | echo -e "\n\e[34m----------------------------------------------------------" 176 | echo -e "MouseTrack run: $*\e[0m" 177 | "$@" 178 | } 179 | mousetrack_run mousetrack_verify_ps1 180 | mousetrack_run pstree -sp $$ 181 | mousetrack_run uname -a 182 | mousetrack_run echo "$PROMPT_COMMAND" 183 | mousetrack_run tail -n 500 "$g_mousetrack_logfile" 184 | unset -f mousetrack_run 185 | } 186 | 187 | 188 | mousetrack_log(){ 189 | : 'Log for debug' 190 | local s_pad_template=-------------------------------------------- 191 | local pad="${s_pad_template:0:$(( (${#FUNCNAME[@]} - 1) * 2 ))}" 192 | { printf '%(%T)T: %s %b\n' -1 "$pad" "$*" &>> "$g_mousetrack_logfile"; } &> /dev/null 193 | } 194 | 195 | 196 | mousetrack_echo_enable(){ 197 | : 'Enable xterm mouse reporting (high)' 198 | mousetrack_log "Enabling mouse tracking" 199 | printf '%b' "$g_mousetrack_echo_enable" 200 | g_mousetrack_b_status=1 201 | } 202 | 203 | 204 | mousetrack_echo_disable(){ 205 | : 'Disable xterm mouse reporting (low)' 206 | mousetrack_log "Disabling mouse tracking" 207 | printf '%b' "$g_mousetrack_echo_disable" 208 | g_mousetrack_b_status=0 209 | } 210 | 211 | 212 | mousetrack_read_keys_remaining(){ 213 | : 'Read the keys left from stdin 214 | In: Stdin (until m) 215 | Out: g_mousetrack_key 216 | :arg1: timout in second 217 | ' 218 | local timeout=${1:-0.001} 219 | g_mousetrack_key='' 220 | while read -r -n 1 -t "$timeout" c; do 221 | g_mousetrack_key="$g_mousetrack_key$c" 222 | # M and m for click, R for get_cursor_pos 223 | [[ $c == M || $c == m || $c == R || $c == '' ]] && break 224 | done 225 | 226 | mousetrack_log "Read remaining: '$g_mousetrack_key'" 227 | } 228 | 229 | 230 | mousetrack_consume_keys(){ 231 | local s_consumed='' 232 | while read -r -n 1 -t 0 c; do 233 | s_consumed="$s_consumed$c" 234 | done 235 | 236 | mousetrack_log "Consumed: '$s_consumed'" 237 | } 238 | 239 | 240 | mousetrack_read_cursor_pos(){ 241 | : 'Read cursor_pos <- xterm <- readline 242 | Out: 243 | gi_cursor_{x,y} 50;1 (x;y) if click on line 1, column 50: starting at 1;1 244 | See: https://unix.stackexchange.com/questions/88296/get-vertical-cursor-position 245 | ' 246 | local -i i_row=0 i_col=0 # Cannot be declared as integer. read command would fail 247 | local row_read 248 | 249 | 250 | # Read it 251 | { 252 | # Clean stdin 253 | mousetrack_consume_keys 254 | 255 | # Set echo style (no echo) 256 | local oldstty; oldstty=$(stty -g) 257 | stty raw -echo min 0 258 | 259 | # Ask cursor pos 260 | IFS=';' read -r -dR -p "$g_mousetrack_echo_get_cursor_pos" row_read i_col 261 | 262 | # Reset echo style (display keystrokes) 263 | stty "$oldstty" 264 | } 0 )); then 271 | (( g_mousetrack_i_bol_x = i_col )) 272 | (( g_mousetrack_i_bol_y = i_row )) 273 | mousetrack_log "Bol: x=$g_mousetrack_i_bol_x, y=$g_mousetrack_i_bol_y, POINT=$READLINE_POINT" 274 | else 275 | (( g_mousetrack_i_cursor_x = i_col )) 276 | (( g_mousetrack_i_cursor_y = i_row )) 277 | mousetrack_log "Cursor: x=$g_mousetrack_i_cursor_x, y=$g_mousetrack_i_cursor_y" 278 | fi 279 | } 280 | 281 | 282 | mousetrack_read_bol(){ 283 | : 'Move the cursor to Begining of realine line' 284 | bind '"\za": beginning-of-line' # C-a 285 | bind '"\ea": end-of-line' # C-e 286 | bind '"\eb": set-mark' # C-space 287 | 288 | # Move cursor to BOL 289 | printf '%b' 'za' > /dev/tty 290 | echo -e "\e[12H" 291 | 292 | READLINE_POINT=100 293 | 294 | mousetrack_read_cursor_pos bol 295 | 296 | mousetrack_log "TEMP: $g_mousetrack_i_bol_x, $g_mousetrack_i_bol_y" 297 | } 298 | 299 | 300 | mousetrack_trap_debug(){ 301 | : 'Trap for stopping track at command spawn (like vim) 302 | Callback : traped to debug : Disable XTERM escape 303 | ' 304 | 305 | # Log 306 | mousetrack_log "In trap for: track_status=$g_mousetrack_b_status, command=$BASH_COMMAND" 307 | 308 | # Clause: mouse track disabled yet 309 | (( g_mousetrack_b_status == 0 )) \ 310 | && { mousetrack_log "Trap to disable mouse: disregarded <= already disabled"; return; } 311 | 312 | # Clause: bash is completing 313 | [[ -v COMP_LINE && -n "$COMP_LINE" ]] \ 314 | && { mousetrack_log "Trap to disable mouse: disregarded <= in bash completion routine"; return; } 315 | 316 | # Clause: don't cause a preexec for $PROMPT_COMMAND 317 | [[ "$BASH_COMMAND" == "$PROMPT_COMMAND" ]] \ 318 | && { mousetrack_log "Trap to disable mouse: disregarded <= in prompt command routine"; return; } 319 | 320 | # Clause: bound from myself for example at scroll 321 | [[ "$BASH_COMMAND" =~ ^mousetrack* ]] \ 322 | && { mousetrack_log "Trap to disable mouse: disregarded <= self call from mousetrack"; return; } 323 | 324 | # Log 325 | mousetrack_log "Trap to disable mouse passed all clauses => Stoping mouse tracking..." 326 | 327 | # Disable mouse as callback 328 | mousetrack_echo_disable 329 | } 330 | 331 | 332 | mousetrack_work_null(){ 333 | : 'Core arithmetic, redirecting stdin < null' 334 | local -i i_row_offset=0 i_readline_point=0 335 | 336 | mousetrack_log 337 | mousetrack_log 338 | mousetrack_log "---------------- Mouse click with $g_mousetrack_key" 339 | 340 | # Clause: g_mousetrack_key defined: I need coordinates 341 | [[ -z "$g_mousetrack_key" ]] && { 342 | mousetrack_log "WARNING: a click without coordinate associated" 343 | return 0 344 | } 345 | 346 | # Clause: Only work for M 347 | local mode=${g_mousetrack_key: -1} 348 | [[ "$mode" == m ]] && { 349 | mousetrack_log "Release ignored" 350 | return 0 351 | } 352 | 353 | # # Log readline pre value 354 | # mousetrack_log "Readline point, line, mark..." 355 | # mousetrack_log "$(echo "$READLINE_POINT, $READLINE_LINE, $READLINE_MARK" | xxd)" 356 | 357 | # Get click (x1, y1) 358 | local xy=${g_mousetrack_key:0:-1} 359 | local -i i_click_x=${xy%%;*} 360 | local -i i_click_y=${xy##*;} 361 | mousetrack_log "Click: x=$i_click_x, y=$i_click_y" 362 | 363 | # Get Cursor position (x0, y0) 364 | # -- This creates flinkering 365 | # mousetrack_read_cursor_pos 366 | 367 | # Calculate PS1 len 368 | local -i i_ps1; i_ps1=$(mousetrack_ps1_len) 369 | 370 | # Calculate line position 371 | (( i_row_offset = i_click_y - g_mousetrack_i_cursor_y )) 372 | (( i_row_offset < 0 )) && (( i_row_offset = 0 )) 373 | 374 | # Retrieve lines 375 | readarray -t a_line <<< "$READLINE_LINE" 376 | # Log line 377 | local s_line 378 | local -i i_line_log=1 379 | for s_line in "${a_line[@]}"; do 380 | mousetrack_log "Array line: $((i_line_log++)): $s_line" 381 | done 382 | 383 | # Add y <= Parse preceding rows 384 | local -i i_current_row=0 385 | local -i i_current_sub_row=0 # Wrap 386 | local -i i_visual_row=0 # sum 387 | while (( i_current_row + i_current_sub_row <= i_row_offset )); do 388 | # Search wrap 389 | local i_max_len=$COLUMNS 390 | (( i_current_row == 0 && i_current_sub_row == 0 )) && (( i_max_len -= i_ps1 )) 391 | local s_line=${a_line[$i_current_row]} 392 | local i_line=${#s_line} 393 | 394 | (( i_visual_row == 0 )) \ 395 | && (( i_current_sub_row > 0 )) \ 396 | && (( i_line -= i_ps1 )) 397 | 398 | # If in wrap second line: Remove the already parsed lines 399 | (( i_current_sub_row > 0 )) \ 400 | && (( i_line -= COLUMNS * i_current_sub_row )) 401 | 402 | mousetrack_log "Line: $i_current_row + $i_current_sub_row = $i_visual_row, point0=$i_readline_point, max: ${#READLINE_LINE}" 403 | 404 | # Clause: Do not append the last line len 405 | # -- So clicking below will position cursor on last line 406 | # Todo, this does no take long wrap into account, and break too fast 407 | if (( i_visual_row == i_row_offset )); then 408 | # && (( i_line < i_max_len )); then 409 | mousetrack_log "Arith41: break" 410 | break 411 | fi 412 | 413 | local -i i_to_add_line=0 414 | if (( i_line > i_max_len )); then 415 | mousetrack_log "Arith0: line:$i_line, max:$i_max_len, col=$COLUMNS, sub=$i_current_sub_row" 416 | # Wrap 417 | (( i_current_sub_row += 1 )) 418 | (( i_to_add_line = i_max_len )) 419 | else 420 | mousetrack_log "Arith1: line:$i_line, max:$i_max_len, col=$COLUMNS, sub=$i_current_sub_row" 421 | (( i_current_row += 1 )); (( i_current_sub_row = 0 )) 422 | (( i_to_add_line = i_line + 1 )) 423 | fi 424 | 425 | # Clause: Stop if last line 426 | if (( i_readline_point + i_to_add_line >= ${#READLINE_LINE} )); then 427 | mousetrack_log "Arith42: break" 428 | break 429 | fi 430 | 431 | (( i_readline_point += i_to_add_line )) 432 | (( i_visual_row +=1 )) 433 | done 434 | 435 | # Add x 436 | #if (( i_visual_row == i_row_offset )); then 437 | local -i i_last_add=$(( i_click_x - g_mousetrack_i_cursor_x )) 438 | mousetrack_log "R1: $i_readline_point, Line: $i_line, Add: $i_last_add" 439 | # -- Feature: if click after the line last character, stay on this line 440 | (( i_visual_row == 0 )) && (( i_last_add -= i_ps1 )) 441 | # ---- TODO: restore from above 442 | (( i_last_add > i_line )) && (( i_last_add = i_line )) 443 | (( i_readline_point += i_last_add - 1 )) 444 | mousetrack_log "R2: $i_readline_point" 445 | mousetrack_log "i_row_offset=$i_row_offset, i_visual_row=$i_visual_row, i_readline_point=$i_readline_point" 446 | #fi 447 | 448 | # Move cursor 449 | export READLINE_POINT=$i_readline_point 450 | 451 | # Log readline post value 452 | mousetrack_log "Readline post: $READLINE_POINT, $READLINE_LINE, $READLINE_MARK" 453 | } 454 | 455 | 456 | mousetrack_cb_click(){ 457 | : 'Callback for mouse button 0 click/release' 458 | # Hi 459 | mousetrack_log '' 460 | mousetrack_log "==> Click start at $(date +"%T.%6N")" 461 | 462 | # Disable mouse to avoid an other click during the call 463 | mousetrack_echo_disable 464 | trap mousetrack_echo_enable RETURN 465 | 466 | mousetrack_read_keys_remaining 0.001 467 | 468 | # Do not accept input while processing 469 | mousetrack_work_null < /dev/null 470 | 471 | # Redraw to avoid long blink (still have a short one) # redraw-current-line 472 | #printf '\e[0n' 473 | 474 | # Bye 475 | mousetrack_log "<== Click end at $(date +"%T.%6N")" 476 | mousetrack_log '' 477 | 478 | return 0 479 | } 480 | 481 | mousetrack_ps1_len(){ 482 | : 'Get display lenght of PS1 483 | Ref1: https://stackoverflow.com/questions/3451993/how-to-expand-ps1 484 | ' 485 | g_mousetrack_ps=$PS1 486 | local -i res=${#PS1} 487 | 488 | # Expand: Warning, need bash 4.4 489 | g_mousetrack_ps=${g_mousetrack_ps@P} 490 | 491 | # Just consider last line 492 | g_mousetrack_ps=${g_mousetrack_ps##*$'\n'} 493 | 494 | # Remove everything 01 and 02 495 | shopt -s extglob 496 | g_mousetrack_ps=${g_mousetrack_ps//$'\x01'*([^$'\x02'])$'\x02'} 497 | 498 | # Sanitize, in case 499 | g_mousetrack_ps=$(LC_ALL=C sed ' 500 | # Safety 501 | s/\x01\|\x02//g; 502 | 503 | # Safety Remove OSC https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands 504 | # 20 .. 7e => printable characters 505 | # 07 => BEL 506 | # 9C => ST 507 | # 1b 5C => ESC + BS 508 | s/\x1b\][0-9;]*[\x20-\x7e]*\([\x07\x9C]\|\x1b\\\)//g; 509 | 510 | # Safety: Remove all escape sequences https://superuser.com/questions/380772/removing-ansi-color-codes-from-text-stream 511 | s/\x1b\[[0-9;]*[a-zA-Z]//g; 512 | ' <<< "$g_mousetrack_ps") 513 | 514 | # Bye 515 | res=${#g_mousetrack_ps} 516 | mousetrack_log "PS1 (calculated): len=$res, content='$g_mousetrack_ps'" 517 | #mousetrack_log "$(echo -n "$g_mousetrack_ps" | xxd)" 518 | 519 | # Return 520 | echo "$res" 521 | } 522 | 523 | mousetrack_cb_void(){ 524 | : 'Callback : clean xterm and disable mouse escape' 525 | mousetrack_read_keys_remaining 0.001 526 | mousetrack_log "Cb: Void with: $g_mousetrack_key" 527 | } 528 | 529 | mousetrack_tmux_get_command(){ 530 | : 'Get the tmux command to rebind' 531 | g_mousetrack_tmux_cmd="$(tmux list-keys -T root "$1" | sed "s/^[^W]*$1 /tmux /")" 532 | #g_mousetrack_tmux_cmd="$(echo 'if-shell -F -t = "#{mouse_any_flag}" "send-keys -M" "if -Ft= \"#{pane_in_mode}\" \"send-keys -M\" \"copy-mode -et=\""')" 533 | } 534 | 535 | 536 | mousetrack_tmux_proxy(){ 537 | local s_tmux_cmd="$1" 538 | mousetrack_read_keys_remaining 0.001 539 | mousetrack_log "Cb: tmux proxy cmd: $s_tmux_cmd, keys remaining: $g_mousetrack_key" 540 | 541 | # Tmux case 542 | if command -v tmux &> /dev/null \ 543 | && [[ -n "$TMUX" ]] \ 544 | ; then 545 | # Launch mux scroll: 546 | # In job so that realine binding returns before => I can see the current line 547 | # In subshell to avoid job control stderr 548 | mousetrack_log "Cb: tmux proxy2 start job $s_tmux_cmd" 549 | ( { 550 | sleep 0.01 551 | #mousetrack_tmux_get_command "$s_tmux_cmd" 552 | # shellcheck disable=SC2046,SC2086 # Quote this to prevent wor 553 | eval "$s_tmux_cmd" 554 | #tmux if-shell -F -t = "#{||:#{mouse_any_flag},#{pane_in_mode}}" "select-pane -t=; send-keys -M" "display-menu -t= -xM -yM -T \"#[align=centre]#{pane_index} (#{pane_id})\" '#{?mouse_word,Search For #[underscore]#{=/9/...:mouse_word},}' 'C-r' {copy-mode -t=; send -Xt= search-backward \"#{q:mouse_word}\"} '#{?mouse_word,Type #[underscore]#{=/9/...:mouse_word},}' 'C-y' {send-keys -l -- \"#{q:mouse_word}\"} '#{?mouse_word,Copy #[underscore]#{=/9/...:mouse_word},}' 'c' {set-buffer -- \"#{q:mouse_word}\"} '#{?mouse_line,Copy Line,}' 'l' {set-buffer -- \"#{q:mouse_line}\"} '' 'Horizontal Split' 'h' {split-window -h} 'Vertical Split' 'v' {split-window -v} '' 'Swap Up' 'u' {swap-pane -U} 'Swap Down' 'd' {swap-pane -D} '#{?pane_marked_set,,-}Swap Marked' 's' {swap-pane} '' 'Kill' 'X' {kill-pane} 'Respawn' 'R' {respawn-pane -k} '#{?pane_marked,Unmark,Mark}' 'm' {select-pane -m} '#{?window_zoomed_flag,Unzoom,Zoom}' 'z' {resize-pane -Z}" 555 | mousetrack_log "Cb: Button 2, tmux async finish $s_tmux_cmd -> $g_mousetrack_tmux_cmd" 556 | mousetrack_log "tmux $g_mousetrack_tmux_cmd" 557 | } & ) 558 | return 0 559 | fi 560 | 561 | mousetrack_cb_void 562 | } 563 | 564 | 565 | mousetrack_cb_scroll_down(){ 566 | mousetrack_log 'Cb: Scroll Down' 567 | mousetrack_read_keys_remaining 0.001 568 | } 569 | 570 | 571 | mousetrack_set_bindings(){ 572 | : 'Set all global bindings (mouse event callbacks)' 573 | local s_keyseq='' 574 | mousetrack_log 'Set bindings' 575 | for s_keyseq in "${!g_mousetrack_d_binding[@]}"; do 576 | mousetrack_log "Bind: $s_keyseq => ${g_mousetrack_d_binding[$s_keyseq]}" 577 | local s_fct=${g_mousetrack_d_binding[$s_keyseq]} 578 | bind -x "\"\033[$s_keyseq\":$s_fct" 579 | done 580 | } 581 | 582 | 583 | mousetrack_unset_bindings(){ 584 | : 'Unset all global bindings (mouse event callbacks)' 585 | local s_keyseq='' 586 | mousetrack_log 'Unset bindings' 587 | for s_keyseq in "${!g_mousetrack_d_binding[@]}"; do 588 | mousetrack_log "Unbind: $s_keyseq => ${g_mousetrack_d_binding[$s_keyseq]}" 589 | local s_fct=${g_mousetrack_d_binding[$s_keyseq]} 590 | bind -r "\033[$s_keyseq" 591 | done 592 | } 593 | 594 | 595 | mousetrack_prompt_command_keep_me_at_end(){ 596 | : 'Disable mouse tracking' 597 | command -v mousetrack_echo_enable &> /dev/null \ 598 | && mousetrack_echo_enable 599 | } 600 | 601 | 602 | mousetrack_start(){ 603 | : 'Init : Enable mouse tracking' 604 | mousetrack_set_bindings 605 | 606 | # Disable mouse tracking before each command 607 | trap mousetrack_trap_debug DEBUG 608 | 609 | # Enable mouse tracking after command return 610 | if [[ ! "$PROMPT_COMMAND" == *"$g_mousetrack_prompt_command"* ]]; then 611 | PROMPT_COMMAND=${PROMPT_COMMAND//$g_mousetrack_prompt_command} 612 | fi 613 | 614 | # Append ";" in case PROMPT_COMMAND is already defined 615 | if [[ -v PROMPT_COMMAND ]] && [[ -n "$PROMPT_COMMAND" ]] && [[ ! "$PROMPT_COMMAND" =~ \;[[:space:]]*$ ]]; then 616 | PROMPT_COMMAND+="; " 617 | fi 618 | 619 | # Append my command 620 | PROMPT_COMMAND+=$g_mousetrack_prompt_command 621 | 622 | # Enable now anyway 623 | mousetrack_echo_enable 624 | } 625 | 626 | 627 | mousetrack_stop(){ 628 | : 'Finish : Disable mouse tracking' 629 | mousetrack_echo_disable 630 | 631 | # Remove echo_enable from PROMPT_COMMAND 632 | if [[ ! "$PROMPT_COMMAND" =~ $g_mousetrack_prompt_command ]]; then 633 | export PROMPT_COMMAND=${PROMPT_COMMAND//$g_mousetrack_prompt_command/} 634 | fi 635 | 636 | # Unset binding 637 | mousetrack_unset_bindings 638 | } 639 | 640 | 641 | # The fonction must be exported in case a bash subshell is entered 642 | export -f mousetrack_prompt_command_keep_me_at_end 643 | --------------------------------------------------------------------------------