From 04c67168f72971430ab3e7b9a1f3feb0bb1f73c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gr=C3=B6ber?= Date: Sat, 10 Sep 2011 21:14:03 +0200 Subject: [PATCH] Add mouse tracking and X11 clipboard sync plugin. --- plugins/mouse/mouse.plugin.zsh | 645 +++++++++++++++++++++++++++++++++ 1 file changed, 645 insertions(+) create mode 100644 plugins/mouse/mouse.plugin.zsh diff --git a/plugins/mouse/mouse.plugin.zsh b/plugins/mouse/mouse.plugin.zsh new file mode 100644 index 000000000..68a5402a1 --- /dev/null +++ b/plugins/mouse/mouse.plugin.zsh @@ -0,0 +1,645 @@ +# zsh mouse (and X clipboard) support v1.5 +# +# QUICKSTART: jump to "how to use" below. +# +# currently supported: +# - VT200 mouse tracking (at least xterm, gnome-terminal, rxvt) +# - GPM on Linux little-endian systems such as i386 (at least) +# - X clipboard handling if xsel(1) or xclip(1) is available (see +# note below). +# +# addionnaly, if you are using xterm and don't want to use the mouse +# tracking system, you can map some button click events so that they +# send \E[M^X[ where is the character 0x20 + (0, 1, 2) +# , are the coordinate of the mouse pointer. This is usually done +# by adding those lines to your resource file for XTerm (~/.Xdefaults +# for example): +# +# XTerm.VT100.translations: #override\ +# Mod4 : ignore()\n\ +# Mod4 : ignore()\n\ +# Mod4 : ignore()\n\ +# Mod4 : string(0x1b) string("[M ") dired-button()\n\ +# Mod4 : string(0x1b) string("[M!") dired-button()\n\ +# Mod4 : string(0x1b) string("[M") string(0x22) dired-button()\n\ +# Mod4 ,: string(0x10)\n\ +# Mod4 ,: string(0xe) +# +# That maps the button click events with the modifier 4 (when you hold +# the Key [possibly Windows keys] under recent versions of +# XFree86). The last two lines are for an easy support of the mouse +# wheel (map the mouse wheel events to ^N and ^P) +# +# Remember that even if you use the mouse tracking, you can still have +# access to the normal xterm selection mechanism by holding the +# key. +# +# Note about X selection. +# By default, xterm uses the PRIMARY selection instead of CLIPBOARD +# for copy-paste. You may prefer changing that if you want +# to insert the CLIPBOARD and a better communication +# between xterm and clipboard based applications like mozilla. +# A way to do that is to add those resources: +# XTerm.VT100.translations: #override\ +# Shift ~Ctrl Insert:insert-selection(\ +# CLIPBOARD, CUT_BUFFER0, PRIMARY) \n\ +# Shift Ctrl Insert:insert-selection(\ +# PRIMARY, CUT_BUFFER0, CLIPBOARD) \n\ +# ~Ctrl ~Meta: select-end(PRIMARY,CUT_BUFFER0,CLIPBOARD) +# +# and to run a clipboard manager application such as xclipboard +# (whose invocation you may want to put in your X session startup +# file). ( inserts the PRIMARY selection as does +# the middle mouse button). (without xclipboard, the clipboard +# content is lost whenever the text is no more selected). +# +# How to use: +# +# add to your ~/.zshrc: +# . /path/to/this-file +# zle-toggle-mouse +# +# and if you want to be able to toggle on/off the mouse support: +# bindkey -M emacs '\em' zle-toggle-mouse +# # m to toggle the mouse in emacs mode +# bindkey -M vicmd M zle-toggle-mouse +# # M for vi (cmd) mode +# +# clicking on the button 1: +# moves the cursor to the pointed location +# clicking on the button 2: +# inserts zsh cutbuffer at pointed location. If $DISPLAY is set and +# either the xsel(1) or xclip(1) command is available, then it's the +# content of the X clipboard instead that is pasted (and stored into +# zsh cutbuffer). +# clicking on the button 3: +# stores the text between the cursor and the pointed localion +# into zsh cutbuffer. Additionaly, if $DISPLAY is set and either the +# xclip(1) or xsel(1) command is available, that text is put on the +# clipboard. +# +# If xsel or xlip is available, and $DISPLAY is set (and you're in a +# xterm-like terminal (even though that feature is terminal +# independant)), all the keys (actually widgets) that deal with zsh +# cut buffer have been modified so that the X CLIPBOARD selection is +# used. So , ... will put the killed region on the X +# clipboard. vi mode "p" or emacs "" will paste the X CLIPBOARD +# selection. Only the keys that delete one character are not affected +# (, , ). Additionnaly, the primary selection (what +# is mouse highlighted and that you paste with the middle button) is put +# on the clipboard (and so made available to zsh) when you press +# or or X (emacs mode) or X (vicmd +# mode). (note that your terminal may already do that by default, also +# note that your terminal may paste the primary selection and not the +# clipboard on , you may change that if you find it +# confusing (see above)) +# +# for GPM, you may change the list of modifiers (Shift, Alt...) that +# need to be on for the event to be accepted (see below). +# +# kterm: same as for xterm, but replace XTerm with KTerm in the resource +# customization +# hanterm: same as for xterm, but replace XTerm with Hanterm in the +# resource customization. +# Eterm: the paste(clipboard) actions don't seem to work, future +# versions of mouse.zsh may include support for X cutbuffers or revert +# back to PRIMARY selection to provide a better support for Eterm. +# gnome-terminal: you may want to bind some keys to Edit->{copy,paste} +# multi-gnome-terminal: selection looks mostly bogus to me +# rxvt,aterm,[ckgt]aterm,mlterm,pterm: no support for clipboard. +# GNUstep terminal: no mouse support but support for clipboard via menu +# KDE x-terminal-emulator: works OK except mouse button3 that is mapped +# to the context menu. Use Ctrl-Insert to put the selection on the +# clipboard. +# dtterm: no mouse support but the selection works OK. +# +# bugs: +# - the GPM support was not much tested (was tested with gpm 1.19.6 on +# a linux 2.6.9, AMD Athlon) +# - mouse positionning doesn't work properly in "vared" if a prompt +# was provided (vared -p ) +# +# Todo: +# - write proper documentation +# - customization through zstyles. +# +# Author: +# Stephane Chazelas +# +# Changes: +# v1.5 2005-03-12: bug fixes (GPM now works again), xclip prefered over +# xsel as xsel is bogus. +# v1.4 2005-03-01: puts both words on the cut buffer +# support for CUT_BUFFER0 via xprop. +# v1.3 2005-02-28: support for more X terminals, tidy-up, separate +# mouse support from clipboard support +# v1.2 2005-02-24: support for vi-mode. X clipboard mirroring zsh cut buffer +# when possible. Bug fixes. +# v1.1 2005-02-20: support for X selection through xsel or xclip +# v1.0 2004-11-18: initial release + +# UTILITY FUNCTIONS + +zle-error() { + local IFS=" " + if [[ -n $WIDGET ]]; then + # error message if zle active + zle -M -- "$*" + else + # on stderr otherwise + print -ru2 -- "$*" + fi +} + +# SELECTION/CLIPBOARD FUNCTIONS + +set-x-clipboard() { return 0; } +get-x-clipboard() { return 1; } + +# find a command to read from/write to the X selections +if whence xclip > /dev/null 2>&1; then + x_selection_tool="xclip -sel p" + x_clipboard_tool="xclip -sel c" +elif whence xsel > /dev/null 2>&1; then + x_selection_tool="xsel -p" + x_clipboard_tool="xsel -b" +else + x_clipboard_tool= + x_selection_tool= +fi +if [[ -n $x_clipboard_tool ]]; then + eval ' + get-x-clipboard() { + (( $+DISPLAY )) || return 1 + local r + r=$('$x_clipboard_tool' -o < /dev/null 2> /dev/null && print .) + r=${r%.} + if [[ -n $r && $r != $CUTBUFFER ]]; then + killring=("$CUTBUFFER" "${(@)killring[1,-2]}") + CUTBUFFER=$r + fi + } + set-x-clipboard() { + (( ! $+DISPLAY )) || + print -rn -- "$1" | '$x_clipboard_tool' -i 2> /dev/null + } + push-x-cut_buffer0() { + # retrieve the CUT_BUFFER0 property via xprop and store it on the + # CLIPBOARD selection + (( $+DISPLAY )) || return 1 + local r + r=$(xprop -root -notype 8s \$0 CUT_BUFFER0 2> /dev/null) || return 1 + r=${r#CUT_BUFFER0\"} + r=${r%\"} + r=${r//\'\''/\\\'\''} + eval print -rn -- \$\'\''$r\'\'' | '$x_clipboard_tool' -i 2> /dev/null + } + push-x-selection() { + # puts the PRIMARY selection onto the CLIPBOARD + # failing that call push-x-cut_buffer0 + (( $+DISPLAY )) || return 1 + local r + if r=$('$x_selection_tool' -o < /dev/null 2> /dev/null && print .) && + r=${r%?} && + [[ -n $r ]]; then + print -rn -- $r | '$x_clipboard_tool' -i 2> /dev/null + else + push-x-cut_buffer0 + fi + } + ' + # redefine the copying widgets so that they update the clipboard. + for w in copy-region-as-kill vi-delete vi-yank vi-change vi-change-whole-line vi-change-eol; do + eval ' + '$w'() { + zle .'$w' + set-x-clipboard $CUTBUFFER + } + zle -N '$w + done + + # that's a bit more complicated for those ones as we have to + # re-implement the special behavior that does that if you call several + # of those widgets in sequence, the text on the clipboard is the + # whole text cut, not just the text cut by the latest widget. + for w in ${widgets[(I).*kill-*]}; do + if [[ $w = *backward* ]]; then + e='$CUTBUFFER$scb' + else + e='$scb$CUTBUFFER' + fi + eval ' + '${w#.}'() { + local scb=$CUTBUFFER + local slw=$LASTWIDGET + local sbl=${#BUFFER} + + zle '$w' + (( $sbl == $#BUFFER )) && return + if [[ $slw = (.|)(backward-|)kill-* ]]; then + killring=("${(@)killring[2,-1]}") + CUTBUFFER='$e' + fi + set-x-clipboard $CUTBUFFER + } + zle -N '${w#.} + done + + zle -N push-x-selection + zle -N push-x-cut_buffer0 + + # put the current selection on the clipboard upon + # X or X: + if (( $+terminfo[kSI] )); then + bindkey -M emacs "$terminfo[kSI]" push-x-selection + bindkey -M viins "$terminfo[kSI]" push-x-selection + bindkey -M vicmd "$terminfo[kSI]" push-x-selection + fi + if (( $+terminfo[kich1] )); then + # according to terminfo + bindkey -M emacs "\e$terminfo[kich1]" push-x-selection + bindkey -M viins "\e$terminfo[kich1]" push-x-selection + bindkey -M vicmd "\e$terminfo[kich1]" push-x-selection + fi + # hardcode ^[[2;3~ which is sent by on xterm + bindkey -M emacs '\e[2;3~' push-x-selection + bindkey -M viins '\e[2;3~' push-x-selection + bindkey -M vicmd '\e[2;3~' push-x-selection + # hardcode ^[^[[2;5~ which is sent by on some terminals + bindkey -M emacs '\e\e[2~' push-x-selection + bindkey -M viins '\e\e[2~' push-x-selection + bindkey -M vicmd '\e\e[2~' push-x-selection + + # hardcode ^[[2;5~ which is sent by on xterm + # some terminals have already such a feature builtin (gnome/KDE + # terminals), others have no distinguishable character sequence sent + # by + bindkey -M emacs '\e[2;5~' push-x-selection + bindkey -M viins '\e[2;5~' push-x-selection + bindkey -M vicmd '\e[2;5~' push-x-selection + + # for terminal without an insert key: + bindkey -M vicmd X push-x-selection + bindkey -M emacs '^XX' push-x-selection + + # the convoluted stuff below is to work around two problems: + # 1- we can't just redefine the widgets as then yank-pop would + # stop working + # 2- we can't just rebind the keys to as + # then we'll loose the numeric argument + propagate-numeric() { + # next key (\e[0-dum) is mapped to , plus the + # targeted widget with NUMERIC restored. + case $KEYMAP in + vicmd) + bindkey -M vicmd -s '\e[0-dum' $'\e[1-dum'$NUMERIC${KEYS/x/};; + *) + bindkey -M $KEYMAP -s '\e[0-dum' $'\e[1-dum'${NUMERIC//(#m)?/$'\e'$MATCH}${KEYS/x/};; + esac + } + zle -N get-x-clipboard + zle -N propagate-numeric + bindkey -M emacs '\e[1-dum' get-x-clipboard + bindkey -M vicmd '\e[1-dum' get-x-clipboard + bindkey -M emacs '\e[2-dum' yank + bindkey -M emacs '\e[2-xdum' propagate-numeric + bindkey -M emacs -s '^Y' $'\e[2-xdum\e[0-dum' + bindkey -M vicmd '\e[3-dum' vi-put-before + bindkey -M vicmd '\e[3-xdum' propagate-numeric + bindkey -M vicmd -s 'P' $'\e[3-xdum\e[0-dum' + bindkey -M vicmd '\e[4-dum' vi-put-after + bindkey -M vicmd '\e[4-xdum' propagate-numeric + bindkey -M vicmd -s 'p' $'\e[4-xdum\e[0-dum' +fi + + +# MOUSE FUNCTIONS + +zle-update-mouse-driver() { + # default is no mouse support + [[ -n $ZLE_USE_MOUSE ]] && zle-error 'Sorry: mouse not supported' + ZLE_USE_MOUSE= +} + + +if [[ $TERM = *[xeEk]term* || + $TERM = *mlterm* || + $TERM = *rxvt* || + $TERM = *screen* || + ($TERM = *linux* && -S /dev/gpmctl) + ]]; then + + set-status() { return $1; } + + handle-mouse-event() { + emulate -L zsh + local bt=$1 + + case $bt in + 3) + return 0;; # Process on press, discard release + # mlterm sends 3 on mouse-wheel-up but also on every button + # release, so it's unusable + 64) + # eterm, rxvt, gnome/KDE terminal mouse wheel + zle up-line-or-history + return;; + 4|65) + # mlterm or eterm, rxvt, gnome/KDE terminal mouse wheel + zle down-line-or-history + return;; + esac + local mx=$2 my=$3 last_status=$4 + local cx cy i + setopt extendedglob + + print -n '\e[6n' # query cursor position + + local match mbegin mend buf= + + while read -k i && buf+=$i && [[ $buf != *\[([0-9]##)\;[0-9]##R ]]; do :; done + # read response from terminal. + # note that we may also get a mouse tracking btn-release event, + # which would then be discarded. + + [[ $buf = (#b)*\[([0-9]##)\;[0-9]##R ]] || return + cy=$match[1] # we don't need cx + + local cur_prompt + + # trying to guess the current prompt + case $CONTEXT in + (vared) + if [[ $0 = zcalc ]]; then + cur_prompt=${ZCALCPROMPT-'%1v> '} + setopt nopromptsubst nopromptbang promptpercent + # (ZCALCPROMPT is expanded with (%)) + fi;; + # if vared is passed a prompt, we're lost + (select) + cur_prompt=$PS3;; + (cont) + cur_prompt=$PS2;; + (start) + cur_prompt=$PS1;; + esac + + # if promptsubst, then we need first to do the expansions (to + # be able to remove the visual effects) and disable further + # expansions + [[ -o promptsubst ]] && cur_prompt=${${(e)cur_prompt}//(#b)([\\\$\`])/\\$match} + + # restore the exit status in case $PS relies on it + set-status $last_status + + # remove the visual effects and do the prompt expansion + cur_prompt=${(S%%)cur_prompt//(#b)(%([BSUbsu]|{*%})|(%[^BSUbsu{}]))/$match[3]} + + # we're now looping over the whole editing buffer (plus the last + # line of the prompt) to compute the (x,y) position of each char. We + # store the characters i for which x(i) <= mx < x(i+1) for every + # value of y in the pos array. We also get the Y(CURSOR), so that at + # the end, we're able to say which pos element is the right one + + local -a pos # array holding the possible positions of + # the mouse pointer + local -i n x=0 y=1 cursor=$((${#cur_prompt}+$CURSOR+1)) + local Y + + buf=$cur_prompt$BUFFER + for ((i=1; i<=$#buf; i++)); do + (( i == cursor )) && Y=$y + n=0 + case $buf[i] in + ($'\n') # newline + : ${pos[y]=$i} + (( y++, x=0 ));; + ($'\t') # tab advance til next tab stop + (( x = x/8*8+8 ));; + ([$'\0'-$'\037'$'\200'-$'\237']) + # characters like ^M + n=2;; + (*) + n=1;; + esac + while + (( x >= mx )) && : ${pos[y]=$i} + (( x >= COLUMNS )) && (( x=0, y++ )) + (( n > 0 )) + do + (( x++, n-- )) + done + done + : ${pos[y]=$i} ${Y:=$y} + + local mouse_CURSOR + if ((my + Y - cy > y)); then + mouse_CURSOR=$#BUFFER + elif ((my + Y - cy < 1)); then + mouse_CURSOR=0 + else + mouse_CURSOR=$(($pos[my + Y - cy] - ${#cur_prompt} - 1)) + fi + + case $bt in + (0) + # Button 1. Move cursor. + CURSOR=$mouse_CURSOR + ;; + + (1) + # Button 2. Insert selection at mouse cursor postion. + get-x-clipboard + BUFFER=$BUFFER[1,mouse_CURSOR]$CUTBUFFER$BUFFER[mouse_CURSOR+1,-1] + (( CURSOR = $mouse_CURSOR + $#CUTBUFFER )) + ;; + + (2) + # Button 3. Copy from cursor to mouse to cutbuffer. + killring=("$CUTBUFFER" "${(@)killring[1,-2]}") + if (( mouse_CURSOR < CURSOR )); then + CUTBUFFER=$BUFFER[mouse_CURSOR+1,CURSOR+1] + else + CUTBUFFER=$BUFFER[CURSOR+1,mouse_CURSOR+1] + fi + set-x-clipboard $CUTBUFFER + ;; + esac + } + + zle -N handle-mouse-event + + handle-xterm-mouse-event() { + local last_status=$? + emulate -L zsh + local bt mx my + + # either xterm mouse tracking or binded xterm event + # read the event from the terminal + read -k bt # mouse button, x, y reported after \e[M + bt=$((#bt & 0x47)) + read -k mx + read -k my + if [[ $mx = $'\030' ]]; then + # assume event is \E[Mdired-button()(^X\EG) + read -k mx + read -k mx + read -k my + (( my = #my - 31 )) + (( mx = #mx - 31 )) + else + # that's a VT200 mouse tracking event + (( my = #my - 32 )) + (( mx = #mx - 32 )) + fi + handle-mouse-event $bt $mx $my $last_status + } + + zle -N handle-xterm-mouse-event + + if [[ $TERM = *linux* && -S /dev/gpmctl ]]; then + # GPM mouse support + if zmodload -i zsh/net/socket; then + + zle-update-mouse-driver() { + if [[ -n $ZLE_USE_MOUSE ]]; then + if (( ! $+ZSH_GPM_FD )); then + if zsocket -d 9 /dev/gpmctl; then + ZSH_GPM_FD=$REPLY + # gpm initialisation: + # request single click events with given modifiers + local -A modifiers + modifiers=( + none 0 + shift 1 + altgr 2 + ctrl 4 + alt 8 + left-shift 16 + right-shift 32 + left-ctrl 64 + right-ctrl 128 + caps-shift 256 + ) + local min max + # modifiers that need to be on + min=$((modifiers[none])) + # modifiers that may be on + max=$min + + # send 16 bytes: + # 1-2: LE short: requested events (btn down = 0x0004) + # 3-4: LE short: event passed through (~GPM_HARD=0xFEFF) + # 5-6: LE short: min modifiers + # 7-8: LE short: max modifiers + # 9-12: LE int: pid + # 13-16: LE int: virtual console number + + print -u$ZSH_GPM_FD -n "\4\0\377\376\\$(([##8]min&255 + ))\\$(([##8]min>>8))\\$(([##8]max&255))\\$(([##8]max>>8 + ))\\$(([##8]$$&255))\\$(([##8]$$>>8&255))\\$(( + [##8]$$>>16&255))\\$(( [##8]$$>>24))\\$(( + [##8]${TTY#/dev/tty}))\0\0\0" + zle -F $ZSH_GPM_FD handle-gpm-mouse-event + else + zle-error 'Error: unable to connect to GPM' + ZLE_USE_MOUSE= + fi + fi + else + # ZLE_USE_MOUSE disabled, close GPM connection + if (( $+ZSH_GPM_FD )); then + eval "exec $ZSH_GPM_FD>&-" + # what if $ZSH_GPM_FD > 9 ? + zle -F $ZSH_GPM_FD # remove the handler + unset ZSH_GPM_FD + fi + fi + } + + handle-gpm-mouse-event() { + local last_status=$? + local event i + if read -u$1 -k28 event; then + local buttons x y + (( buttons = ##$event[1] )) + (( x = ##$event[9] + ##$event[10] << 8 )) + (( y = ##$event[11] + ##$event[12] << 8 )) + zle handle-mouse-event $(( (5 - (buttons & -buttons)) / 2 )) $x $y $last_status + zle -R # redraw buffer + else + zle -M 'Error: connection to GPM lost' + ZLE_USE_MOUSE= + zle-update-mouse-driver + fi + } + fi + else + # xterm-like mouse support + zmodload -i zsh/parameter # needed for $functions + + zle-update-mouse-driver() { + if [[ -n $WIDGET ]]; then + if [[ -n $ZLE_USE_MOUSE ]]; then + print -n '\e[?1000h' + else + print -n '\e[?1000l' + fi + fi + } + + if [[ $functions[precmd] != *ZLE_USE_MOUSE* ]]; then + functions[precmd]+=' + [[ -n $ZLE_USE_MOUSE ]] && print -n '\''\e[?1000h'\' + fi + if [[ $functions[preexec] != *ZLE_USE_MOUSE* ]]; then + functions[preexec]+=' + [[ -n $ZLE_USE_MOUSE ]] && print -n '\''\e[?1000l'\' + fi + + bindkey -M emacs '\e[M' handle-xterm-mouse-event + bindkey -M viins '\e[M' handle-xterm-mouse-event + bindkey -M vicmd '\e[M' handle-xterm-mouse-event + + if [[ $TERM = *Eterm* ]]; then + # Eterm sends \e[5Mxxxxx on drag events, be want to discard them + discard-mouse-drag() { + local junk + read -k5 junk + } + zle -N discard-mouse-drag + bindkey -M emacs '\e[5M' discard-mouse-drag + bindkey -M viins '\e[5M' discard-mouse-drag + bindkey -M vicmd '\e[5M' discard-mouse-drag + fi + fi + +fi + +zle-toggle-mouse() { + # If no prefix, toggle state. + # If positive prefix, turn on. + # If zero or negative prefix, turn off. + + # Allow this to be used as a normal function, too. + if [[ -n $1 ]]; then + local PREFIX=$1 + fi + if (( $+PREFIX )); then + if (( PREFIX > 0 )); then + ZLE_USE_MOUSE=1 + else + ZLE_USE_MOUSE= + fi + else + if [[ -n $ZLE_USE_MOUSE ]]; then + ZLE_USE_MOUSE= + else + ZLE_USE_MOUSE=1 + fi + fi + zle-update-mouse-driver +} +zle -N zle-toggle-mouse + +zle-toggle-mouse \ No newline at end of file