commit 538c6520f4719531899c0726ae91e61a96a89f6a
Author: Luxferre <lux@ferre>
Date: Wed, 29 Mar 2023 17:35:47 +0300
Initial upload
Diffstat:
A | COPYING | | | 26 | ++++++++++++++++++++++++++ |
A | README.md | | | 83 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | bopher-ng.sh | | | 332 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
3 files changed, 441 insertions(+), 0 deletions(-)
diff --git a/COPYING b/COPYING
@@ -0,0 +1,26 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <https://unlicense.org>
+
+
diff --git a/README.md b/README.md
@@ -0,0 +1,83 @@
+# Bopher-NG: A better Gopher browser in pure Bash
+
+> “Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.”
+> _― Antoine de Saint-Exupéry, Airman's Odyssey_
+
+## What is it?
+
+Bopher-NG is an ambitious attempt to write a full-featured Gopher client/browser in under 300 SLOC of pure Bash code. It started off as a really crude and unoptimized prototype developed right in [this blog post](https://chronovir.us/2023/03/28/I-wrote-a-browser/) for educational purposes.
+
+Improvements over the original Bopher from that post:
+
+- mouse support (where possible)
+- smoother rendering and scrolling
+- better edge-case stability
+- multi-level navigation history (although you can only go back)
+- status bar with currently opened resource name
+- ability to accept `gopher://` URLs from the command line
+- ability to save the URL to the currently viewed resource into the stash text file
+- ability to force-download pages instead of viewing regardless of their type
+
+"Pure Bash" means any external commands must only be used if absolutely required. For now, Bopher-NG only depends on three external commands (all POSIX-compliant):
+
+- `cat` (to handle possibly binary data read from the socket)
+- `stty` (to fetch current terminal size in rows and columns)
+- `date` (to shape entry timestamps when stashing links)
+
+## Which Bash versions are supported?
+
+Bopher-NG was only tested on Bash 5.1, but should support any version from 4.2 and up.
+
+## How to start Bopher-NG?
+
+Just run it specifying all the necessary components of the Gopher resource:
+
+```
+bopher-ng.sh host[ port][ selector][ type]
+```
+
+Or, you can just specify a valid `gopher://` URL:
+
+```
+bopher-ng.sh gopher://host[:port]/[type][selector]
+```
+
+E.g. running `bopher-ng.sh gopher://texto-plano.xyz:70/0comandos.txt` is the same as `bopher-ng.sh texto-plano.xyz 70 comandos.txt 0` and will open the same document.
+
+In both cases, only the host is required. If everything else is omitted, the port will default to 70, the selector will default to `/` and the resource type will default to 1.
+
+## How to control Bopher-NG?
+
+- Scrolling: Up/Down arrow keys, k/j keys or mouse scroll wheel (if supported)
+- Page scrolling: PgUp/PgDn keys or h/l keys
+- Link navigation (keyboard): s - focus on the next link, w - focus on the previous link, Enter/Space - visit the focused link (or download if it points to a binary file), d - force-download the contents of the link
+- Link navigation (mouse, if supported): left click on the link - focus and visit (or download if it points to a binary file), middle click - focus and force-download the contents
+- Go back: b or right mouse click anywhere (if supported)
+- Stash the link to the currently open resource: S (shift+s)
+- Quit the browser: q
+
+## What is the link stash?
+
+Link stash is a viable and interoperable alternative to both bookmarks and clipboard that can be implemented with pure Bash. Essentially, it's an append-only text file at a fixed location (`~/.bopher-links.txt` by default) where the user can instruct Bopher-NG to save the link to the currently viewed resource. For your convenience, links are stashed with a UTC-based timestamp and in the `gopher://` format, so they can be copied from the file later and used in other browsers.
+
+You can override the `BOPHER_LINKSTASH` environment variable to change the location and name of this file if you need to.
+
+## Where do the downloads go?
+
+By default, they go to your current working directory, i.e. the directory you are running this script from. To override this with a fixed path, set the `BOPHER_DLDIR` environment variable.
+
+## Which platforms is the mouse supported on?
+
+Bopher-NG's mouse input is supported on any terminals that support the 1000 **and** 1006 mouse reporting modes. This includes but is not limited to:
+
+- Linux GUI terminal emulators (xterm, Konsole, urxvt, any VTE-based terminal emulator);
+- Linux bare terminals via GPM + [LCXterm](https://gitlab.com/AutumnMeowMeow/lcxterm) (sorry, I don't know why no one implemented it in GPM itself in 23 years);
+- macOS Terminal;
+- OpenGL-based xterm-compatible terminal emulators like WezTerm, Kitty, Alacritty etc.
+
+Note that this support is required at the client side only. For instance, you can safely run `bopher-ng.sh` on any compatible Bash version in an SSH session on your terminal and still be able to use mouse there.
+
+
+## What is the license on this?
+
+Fully public domain
diff --git a/bopher-ng.sh b/bopher-ng.sh
@@ -0,0 +1,332 @@
+#!/bin/bash
+# BopherNG: a bit more advanced Gopher client in pure Bash
+# with a few external command dependencies (cat, stty, date)
+# Created by Luxferre in 2023, released into public domain
+#
+# Improvements over the original Bopher:
+# - mouse support
+# - smoother rendering and scrolling
+# - better edge-case stability
+# - multi-level navigation history (although you can only go back)
+# - status bar with currently opened resource name
+# - ability to accept gopher:// URLs from the command line
+# - ability to save the URL to the currently viewed resource into the stash text file
+# - ability to force-download pages instead of viewing regardless of their type
+
+# save the command line parameters
+
+START_HOST="$1"
+START_PORT="$2"
+START_SEL="$3"
+START_TYPE="$4"
+
+if [[ "$START_HOST" =~ ^gopher:// ]]; then # parse the URL
+ preurl="${START_HOST#gopher://}" # remove the scheme to ease parsing
+ hostport="${preurl%%/*}" # extract the host+:port part (where :port is also optional)
+ selpath="${preurl##$hostport}" # extract the selector+path part
+ START_HOST="${hostport%%:*}" # extract the hostname
+ START_PORT="${hostport:(( 1 + ${#START_HOST} ))}" # extract the port
+ START_TYPE="${selpath:1:2}" # extract the type character
+ START_SEL="${selpath:2}" # extract the selector
+fi
+[[ -z "$START_HOST" ]] && echo "Usage: bopher-ng.sh host[ port][ selector][ type] OR bopher-ng.sh gopher://host[:port]/[type][selector]" && exit 1
+[[ -z "$START_PORT" ]] && START_PORT=70
+[[ -z "$START_SEL" ]] && START_SEL="/"
+[[ -z "$START_TYPE" ]] && START_TYPE=1 # default request type is a Gophermap
+
+# define external paths
+[[ -z "$BOPHER_LINKSTASH" ]] && BOPHER_LINKSTASH="$HOME/.bopher-links.txt" # file to stash links to
+[[ -z "$BOPHER_DLDIR" ]] && BOPHER_DLDIR="$PWD" # downloads directory
+
+# styling used in the TUI
+ESC="\x1b"
+ERESET="$ESC[0m" # reset the styling
+ERRCOLOR="$ESC[31;1m" # red bold
+LINKCOLOR="$ESC[36;1m" # cyan bold
+FOCUSATTR="$ESC[7;2m" # swap foreground and background and brightness level
+CLS="$ESC[2J" # clear the entire screen
+CURRESET="$ESC[0;0H" # reset the visual cursor
+LINECLR="$ESC[2K" # clear current line
+BELOWCLR="$ESC[0J" # clear everything below
+ALTBUFON="$ESC[?47h" # use the alternative screen buffer
+ALTBUFOFF="$ESC[?47l" # return to the default screen buffer
+
+# mouse event support switches
+MOUSEON="$ESC[?1000;1006h" # term command to start sending mouse input events as control sequences (semi-long format, no drag)
+MOUSEOFF="$ESC[?1000;1006l" # term command to stop sending mouse input events as control sequences (semi-long format, no drag)
+# in the semi-long format without drag, every mouse control sequence starts with "$ESC[<" and then has three fields:
+# button_attr;xcoord;ycoord[Mm]
+
+clear_term() { # clear the terminal on Ctrl+C interrupt
+ printf '%b' "$MOUSEOFF$ERESET$ALTBUFOFF"
+ exit 0
+}
+trap clear_term INT
+
+# fetch any Gopher resource
+gophetch() { # args: host, port, selector[, input]
+ exec 3<>/dev/tcp/$1/$2 # bind the descriptor 3 to a /dev/tcp pseudo-device
+ if [[ -z "$4" ]]; then
+ printf '%s\r\n' "$3" >&3 # send the selector string (printf is more reliable)
+ else
+ printf '%s\t%s\r\n' "$3" "$4" >&3 # send the selector + tab + input string
+ fi
+ cat <&3 # fetch and output the result (we have to use cat as the result may be binary)
+ exec 3<&- # close the descriptor to make it reusable
+}
+
+# parse a single Gophermap line according to the current host and port
+# Return format: ACTION[tab]DESCRIPTION[tab]HOST[tab]PORT[tab]SELECTOR[tab]TYPE
+# where ACTION can be:
+# - E (echo the description - this line is non-navigable)
+# - P (print the file)
+# - D (download the file)
+# - M (render a Gophermap)
+# - I (ask for the user input and render a Gophermap)
+gmparse() { # args: line, curhost, curport
+ local line="$1"
+ [[ ! "$line" == *[$'\t']* ]] && line="i$line" # treat non-standard plain tabless lines as information lines
+ readarray -d $'\t' -t fields < <(printf '%s' "${line%%$'\r'}") # also remove a trailing CR in case the response is CRLF
+ local rtype="${fields[0]:0:1}" # resource type
+ local desc="${fields[0]:1}" # resource description
+ local sel="${fields[1]}" # resource selector
+ local rhost="${fields[2]}" # resource hostname
+ local rport="${fields[3]}" # resource port
+ if [[ -z "$sel$rhost$rport" ]]; then # if all fields except description are empty
+ sel="$desc" # then assume our selector is in the description field
+ fi
+ [[ -z "$rhost" ]] && rhost="$2" # fill in the missing hostname
+ [[ -z "$rport" ]] && rport="$3" # fill in the missing port number
+ local action='D' # all unknown types are set to download
+ if [[ 'i' == "$rtype" || '3' == "$rtype" || 'h' == "$rtype" ]]; then # handle information lines
+ action='E'
+ [[ '3' == "$rtype" ]] && desc="$ERRCOLOR$desc$ERESET" # wrap error messages in the coloration terminal commands
+ # handle external URLs as information lines of a special kind
+ [[ 'h' == "$rtype" ]] && desc="$desc: ${sel#URL:}"
+ fi
+ [[ '0' == "$rtype" ]] && action='P' # plain text is plain text
+ [[ '1' == "$rtype" ]] && action='M' # it's a Gophermap
+ [[ '7' == "$rtype" ]] && action='I' # it's a Gophermap with user input (search or whatever)
+ printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$action" "$desc" "$rhost" "$rport" "$sel" "$rtype" # output the final formatted line
+}
+
+# convert AM line back to gopher:// URL
+amtogopher() { # args: AM line
+ readarray -d $'\t' -t fields < <(printf '%s' "$1")
+ printf 'gopher://%s:%s/%s%s' "${fields[2]}" "${fields[3]}" "${fields[5]}" "${fields[4]}"
+}
+
+# AM entry clicker function
+amclick() { # args: AM line, output: AM line(s)
+ readarray -d $'\t' -t fields < <(printf '%s' "$1")
+ local action="${fields[0]}"
+ local desc="${fields[1]}"
+ local rhost="${fields[2]}"
+ local rport="${fields[3]}"
+ local sel="${fields[4]}"
+ if [[ 'D' == "$action" ]]; then # download
+ local fname="${sel##*/}" # pure-bash version of basename
+ [[ -z "$fname" ]] && fname='dl.dat' # just make sure that it's not empty
+ gophetch "$rhost" "$rport" "$sel" > "$BOPHER_DLDIR/$fname"
+ printf 'E\tDownloaded %s to %s\n' "$(amtogopher "$1")" "$BOPHER_DLDIR/$fname"
+ elif [[ 'I' == "$action" || 'M' == "$action" || 'P' == "$action" ]]; then # text content
+ local input=''
+ if [[ 'I' == "$action" ]]; then
+ printf '%b' "$LINECLR" # clear the current line on the terminal
+ read -p "Enter input for $rhost: " input
+ fi
+ readarray -t lines < <(gophetch "$rhost" "$rport" "$sel" "$input")
+ for line in "${lines[@]}"; do # iterate over every fetched line
+ if [[ 'P' == "$action" ]]; then # generate a plaintext AM entry
+ printf 'E\t%s\n' "${line%%$'\r'}" # remove a trailing CR if it's there
+ else # generate a proper Gophermap AM entry
+ gmparse "$line" "$rhost" "$rport"
+ fi
+ done
+ fi
+}
+
+declare -a SCREENBUF # screen buffer array placeholder
+declare -a NAVIGABLES # navigable indexes array placeholder
+declare -a AMHISTORY # history array containing the AM lines
+CURRENTAM='' # the AM line corresponding to the currently loaded resource
+CURLINKINDEX=0 # this is an index in the NAVIGABLES array, not SCREENBUF
+FOCUSLINEINDEX=-1 # this is an index in the SCREENBUF array
+CURSCROLLPOS=0 # current scrolling position
+read -r TERMROWS TERMCOLS < <(stty size) # get current terminal size info
+
+# main content rendering method (offscreen rendering)
+amrender() {
+ read -r TERMROWS TERMCOLS < <(stty size) # update current terminal size info
+ local upperbound=$(( $CURSCROLLPOS + $TERMROWS - 1 )) # leaving out the last line for status
+ local buflen=${#SCREENBUF[@]} # total length of the screen buffer
+ (( $upperbound > $buflen )) && upperbound=$buflen
+ local output="$CLS$CURRESET" # start output by clearing the screen and resetting the cursor
+ for ((idx=CURSCROLLPOS;idx<upperbound;idx++)); do # only iterate over eligible buffer lines by index
+ local amline="${SCREENBUF[$idx]}" # current AM line
+ readarray -d $'\t' -t fields < <(printf '%s' "$amline")
+ local action="${fields[0]}" # to display an AM entry, we only need two of its fields
+ local desc="${fields[1]}"
+ if [[ 'E' == "$action" ]]; then # plain text
+ output="${output}$desc\n"
+ else # not plain text
+ if [[ "$idx" == "$FOCUSLINEINDEX" ]]; then # if the current link is focused, then...
+ output="${output}$FOCUSATTR" # print the focus attribute (the link contains ERESET anyway)
+ fi
+ output="${output}$LINKCOLOR=> ${desc}$ERESET\n"
+ fi
+ done
+ # final preparations:
+ # - clear everything that remains and reset the cursor to the last line
+ # - output current resource to the status line
+ # - reset the cursor once again
+ output="${output}$BELOWCLR$ESC[$TERMROWS;0H$FOCUSATTR$(amtogopher "$CURRENTAM")$ERESET$ESC[$TERMROWS;0H"
+ printf '%b' "$output" # finally, print the entire output with a single call
+}
+trap 'amrender' WINCH # rerender on terminal size change
+
+scroll() { # args: delta (usually 1 or -1)
+ local buflen=${#SCREENBUF[@]} # total length of the screen buffer
+ local newpos=$(( $CURSCROLLPOS + $1 )) # add the delta
+ local maxscroll=$(( $buflen - $TERMROWS + 1 ))
+ (( $newpos >= $maxscroll )) && newpos=$maxscroll
+ (( $newpos < 0 )) && newpos=0
+ if (( $newpos != $CURSCROLLPOS )); then
+ CURSCROLLPOS=$newpos
+ (( $CURSCROLLPOS < 0 )) && CURSCROLLPOS=0
+ amrender # rerender the contents only if the position really changed
+ fi
+}
+
+jumplink() { # args: delta (usually 1 or -1)
+ local navlen=${#NAVIGABLES[@]} # total length of the nav array
+ if (( $navlen > 1 )); then # do something only if there is at least two navigables
+ local newindex=$(( $CURLINKINDEX + $1 )) # add the delta
+ (( $newindex < 0 )) && newindex=$(( $navlen - 1 )) # wrap around at the beginning
+ (( $newindex >= $navlen )) && newindex=0 # wrap around at the end
+ CURLINKINDEX=$newindex # update the global navigable index
+ FOCUSLINEINDEX=${NAVIGABLES[$CURLINKINDEX]}
+ CURSCROLLPOS=$(( $FOCUSLINEINDEX - $(( $TERMROWS / 2 )) )) # attempt to center scrolling position at the currently focused link
+ (( $CURSCROLLPOS < 0 )) && CURSCROLLPOS=0
+ scroll 0 # auto-adjust scrolling if the link is at the end
+ amrender # trigger the rendering on update
+ fi
+}
+
+amload() { # args: AM line[, forcedl]
+ CURRENTAM="$1" # update current AM value
+ printf '%b' "${LINECLR}Loading..."
+ [[ ! -z "$2" ]] && CURRENTAM="D${CURRENTAM:1}" # modify CURRENTAM first char to the download type
+ readarray -t SCREENBUF < <(amclick "$CURRENTAM") # populate screen buffer by loading the resource from the AM entry
+ NAVIGABLES=() # reset the navigable indexes
+ CURLINKINDEX=0 # reset the link number
+ CURSCROLLPOS=0 # reset the scrolling position
+ for idx in "${!SCREENBUF[@]}"; do # iterate over every fetched line by index
+ local amline="${SCREENBUF[$idx]}" # current AM line
+ local action="${amline:0:1}" # action character
+ if [[ ! 'E' == "$action" ]]; then # for all link-related entries...
+ NAVIGABLES+=("$idx") # append this index as navigable
+ fi
+ done
+ FOCUSLINEINDEX=${NAVIGABLES[0]} # set the focused line index to the first available one
+ amrender # render the contents
+ jumplink 0 # focus on the current link
+}
+
+clicklink() { # click the currently focused link
+ if (( $FOCUSLINEINDEX > -1 )); then
+ AMHISTORY+=("$CURRENTAM") # update the history chain
+ amload "${SCREENBUF[$FOCUSLINEINDEX]}" "$1"
+ fi
+}
+
+goback() { # remove the last AM line from history and go there
+ local histlen=${#AMHISTORY[@]}
+ if (( $histlen > 1 )); then # at least one entry must be present
+ local lastidx=$(( $histlen - 1 ))
+ local histline="${AMHISTORY[$lastidx]}"
+ AMHISTORY=("${AMHISTORY[@]:0:$lastidx}")
+ amload "$histline"
+ fi
+}
+
+stashlink() { # stash the link to the currently viewed resource
+ local gopherlink="$(amtogopher "$CURRENTAM")"
+ printf '[%s] %s\n' "$(date -u '+%F %T')" "$gopherlink" >> $BOPHER_LINKSTASH
+ printf 'Stashed %s%b' "$gopherlink" "$ERESET$ESC[$TERMROWS;0H" # just reset the cursor to the last line
+}
+
+# Focus and click a link on a Gophermap
+mouseclick() { # args: x, y[, forcedl] (start with index 1)
+ local curcol=$1
+ local currow=$2
+ local lineindex="$(( $currow + $CURSCROLLPOS - 1 ))" # determine the AM line index we're pointing to in the screen buffer
+ local targetline="${SCREENBUF[$lineindex]}" # load the line we're testing
+ local action="${targetline:0:1}" # get its action
+ if [[ ! 'E' == "$action" ]]; then # only proceed if it's a link-related AM line
+ readarray -d $'\t' -t fields < <(printf '%s' "$targetline")
+ local desc="${fields[1]}"
+ local dlen=$(( ${#desc} + 3 )) # consider the => link prefix
+ if (( $curcol <= $dlen )); then # only load if the click was within the line
+ FOCUSLINEINDEX=$lineindex
+ amrender
+ AMHISTORY+=("$CURRENTAM") # update the history chain
+ amload "$targetline" "$3"
+ fi
+ fi
+}
+
+readmouseinput() { # correctly read the remaining mouse input in semi-long mode
+ read -r -s -d ';' battr # the button attribute is before the first semicolon
+ read -r -s -d ';' mx # the X coordinate is between the semicolons
+ local my='' # allocate a buffer to fetch Y coordinate
+ local c='' # cache character
+ while [[ "$c" != 'm' && "$c" != 'M' ]]; do
+ my="$my$c"
+ read -s -n 1 c
+ done # now, my contains our Y coordinate and c contains the press/release specifier
+ printf '%d %d %d %c\n' "$battr" "$mx" "$my" "$c"
+}
+
+
+# entry point code starts here
+
+printf '%b' "$ALTBUFON$CLS" # enter ALTBUF mode and clear the screen
+
+# now, load the resource provided from the command line and start the main loop
+
+START_GMLINE="$(printf '%s\t%s\t%s\t%s' "$START_TYPE" "$START_SEL" "$START_HOST" "$START_PORT")"
+START_AM="$(gmparse "$START_GMLINE")"
+AMHISTORY+=("$START_AM") # start the history chain
+amload "$START_AM"
+
+cmdbuf='' # allocate key command buffer and mouse coordinate variables
+mouseX=''
+mouseY=''
+mouseStatus='M'
+while true; do
+ mousecmd='' # mouse command buffer
+ printf '%b' "$MOUSEON" # enable mouse events before reading
+ read -n 1 -s cmdbuf # read the character, tildas will be ignored
+ if [[ "$cmdbuf" == $'\x1b' ]]; then # here's the control character
+ read -n 2 -s cmdbuf # read 2 additional characters
+ if [[ "$cmdbuf" == "[<" ]]; then # we have a mouse event, read the button attribute and the coordinates
+ read -r -s mousecmd mouseX mouseY mouseStatus < <(readmouseinput)
+ fi
+ fi
+ printf '%b' "$MOUSEOFF" # don't generate mouse events outside the input sequence
+ [[ "$mousecmd" == '2' && "$mouseStatus" == 'M' ]] && cmdbuf='b' # right click is automatically assigned to back command
+ [[ "$cmdbuf" == '[A' || "$cmdbuf" == 'k' || "$mousecmd" == '64' ]] && scroll -1
+ [[ "$cmdbuf" == '[B' || "$cmdbuf" == 'j' || "$mousecmd" == '65' ]] && scroll 1
+ [[ "$cmdbuf" == '[5' || "$cmdbuf" == 'h' ]] && scroll $(( 1 - $TERMROWS )) # PgUp
+ [[ "$cmdbuf" == '[6' || "$cmdbuf" == 'l' ]] && scroll $(( $TERMROWS - 1 )) # PgDn
+ [[ "$cmdbuf" == 's' ]] && jumplink 1
+ [[ "$cmdbuf" == 'w' ]] && jumplink -1
+ [[ "$cmdbuf" == 'q' ]] && printf '%b' "$ERESET$ALTBUFOFF" && exit 0
+ [[ "$cmdbuf" == $'\0a' ]] && clicklink
+ [[ "$cmdbuf" == 'd' ]] && clicklink 1 # same as click but with force-download flag
+ [[ "$cmdbuf" == 'b' ]] && goback
+ [[ "$cmdbuf" == 'S' ]] && stashlink
+ [[ "$mousecmd" == '0' && "$mouseStatus" == 'M' ]] && mouseclick $mouseX $mouseY # we only process left click with coordinates here
+ [[ "$mousecmd" == '1' && "$mouseStatus" == 'M' ]] && mouseclick $mouseX $mouseY 1 # we only process middle click with coordinates here
+done