Cycle through windows of the same application using wmcrtl - linux

I am configuring xbindkeys to change window focus using shortcuts.
For example, I managed to create a shortcut to focus on a an application window, let's say a terminator window:
wmctrl -xa terminator
Unfortunately it focuses always at the same terminator window, preventing me to cycle through the terminator windows.
Could you suggest me a command to focus on a terminator window and, if pressed again, will cycle through all the terminator windows, please?
UPDATE 30 Mar 2013
I modified this script
http://lars.st0ne.at/blog/switch%20between%20windows%20within%20the%20same%20application
to make a script such that
script.sh NAME
focus on application NAME or cycle through all the windows of NAME if a window of it is already focused, but it doesn't work properly.
Here is the script
win_class=$1 # 'terminator' # $1
# get list of all windows matching with the class above
win_list=$(wmctrl -x -l | grep -i $win_class | awk '{print $1}' )
# get id of the focused window
active_win_id=$(xprop -root | grep '^_NET_ACTIVE_W' | awk -F'# 0x' '{print $2}')
# get next window to focus on, removing id active
switch_to=$(echo $win_list | sed s/.*$active_win_id// | awk '{print $1}')
# if the current window is the last in the list ... take the first one
if [ "$switch_to" == '' ];then
switch_to=$(echo $win_list | awk '{print $1}')
fi
# switch to window
wmctrl -i -a $switch_to
The script does focus on a windows of the application, and cycle through them until it reach a window, I guess the last created. At that point, cycling doesn't work anymore.

I've found a problem in the script, if no window has focus.
May you try the following modified script:
#!/bin/bash
win_class=$1 # 'terminator' # $1
# get list of all windows matching with the class above
win_list=$(wmctrl -x -l | grep -i $win_class | awk '{print $1}' )
# get id of the focused window
active_win_id=$(xprop -root | grep '^_NET_ACTIVE_W' | awk -F'# 0x' '{print $2}')
if [ "$active_win_id" == "0" ]; then
active_win_id=""
fi
# get next window to focus on, removing id active
switch_to=$(echo $win_list | sed s/.*$active_win_id// | awk '{print $1}')
# if the current window is the last in the list ... take the first one
if [ "$switch_to" == '' ];then
switch_to=$(echo $win_list | awk '{print $1}')
fi
# switch to window
wmctrl -i -a $switch_to

The script works for me.
Anyway, it seems that the script does not find the active window in you case. Therefore it manages to switch to your application but fails to cycle through. It switches to the fist window in $win_list because, the sed command fails to remove the active window ( and all list entries before ) from $win_list.
Try the the following command:
xprop -root _NET_ACTIVE_WINDOW
The output should be something like this:
_NET_ACTIVE_WINDOW(WINDOW): window id # 0x2400005
The property "_NET_ACTIVE_WINDOW" is part of the EWMH standard. see: http://standards.freedesktop.org/wm-spec/wm-spec-1.3.html
Maybe you are using a non EWMH( Extended Window Manager Hint ) compliant window manager!
Which WM are you using?
... some window manager allow to enable EWMH compatibility via configuration or plugin.

After adapting the script by st0ne, I have a version that works generically (don't need to specify the app_name). Hope that is useful to somebody. :)
#!/bin/bash
active_win_id=`xprop -root | grep '^_NET_ACTIVE_W' | awk -F'# 0x' '{print $2}' | awk -F', ' '{print $1}'`
if [ "$active_win_id" == "0" ]; then
active_win_id=""
fi
app_name=`wmctrl -lx | grep $active_win_id | awk '{print $3}'`
workspace_number=`wmctrl -d | grep '\*' | cut -d' ' -f 1`
win_list=`wmctrl -lx | grep -ri $app_name | grep " $workspace_number " | awk '{print $1}'`
# get next window to focus on, removing id active
switch_to=`echo $win_list | sed s/.*$active_win_id// | awk '{print $1}'`
# if the current window is the last in the list ... take the first one
if [ "$switch_to" == "" ];then
switch_to=`echo $win_list | awk '{print $1}'`
fi
if [[ -n "${switch_to}" ]]
then
(wmctrl -ia "$switch_to") &
else
if [[ -n "$2" ]]
then
($2) &
fi
fi
exit 0

I encountered a small issue1 with tkt028's answer, but I liked what they were doing in terms of handling any generic application. But I also liked how st0ne's answer handles cycling through the windows of a specifically named application. So I combined the approaches.
My script takes an optional first argument to specify an application whose windows should be cycled. If no such windows are found, and if the optional second argument was provided, it falls back to launching the command specified by the second argument.
If no arguments are provided at all, then it just cycles through the windows of the currently active application.
#!/bin/bash
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
echo "Cycle through windows of the active, or specified, application."
echo ""
echo "Usage: $(basename $0) [window_class_name [application_launcher]]"
echo ""
echo " window_class_name: regex string specifying an application's window name,"
echo " as specified by the third column of"
echo " 'wmctrl -l -x'"
echo " application_launcher: application to optionally launch if no windows"
echo " matching window_class_name are found"
echo ""
echo "If no arguments are specified, cycles through the windows of the active application."
exit
fi
# get ID of active window
active_win_id=`xprop -root | grep '^_NET_ACTIVE_W' | awk -F'# 0x' '{print $2}' | awk -F', ' '{print $1}'`
if [ "$active_win_id" == "0" ]; then
active_win_id=""
fi
if [[ -n "$1" ]]; then
# get app name from input argument
app_name="$1"
else
# get corresponding app name
app_name="${app_name:-$(wmctrl -lx | grep $active_win_id | awk '{print $3}')}"
fi
# get active workspace number
workspace_number=`wmctrl -d | grep '\*' | cut -d' ' -f 1`
# get list of windows corresponding to the desired app
win_list=`wmctrl -lx | grep -i $app_name | grep " $workspace_number " | awk '{print $1}'`
# get next window of app to focus on
#
# (Parses $win_list as a single string, removing everything except the token
# after the active ID. If active ID is sole token or last token, string will be
# left unmodified, producing an array from which we'll extract the first element.)
# Note: If active window was not of class app_name, then this will end up
# selecting the first window of app_name, if running. Otherwise, we'll fall
# through to launching a new instance of the app in the else of the next block.
switch_to=($(echo $win_list | sed "s/.*\<\(0x0\+\)\?$active_win_id\>\s*\(\<0x[0-9a-f]\+\>\).*/\2/"))
# if we have a valid window to switch to, do so
if [[ -n "${switch_to}" ]]; then
wmctrl -ia "${switch_to[0]}"
exit $?
else
# if the user specified a fallback application to run if target window
# was not found, try to launch it
if [[ -n "$2" ]]; then
$2 &
# check whether process corresponding to PID of background
# process we just launched is still running
ps -p $! > /dev/null
exit $?
else
exit $?
fi
fi
1 The recursive grep on this line in tkt028's answer didn't work in my environment. Maybe it's dependent on your version of grep.
win_list=`wmctrl -lx | grep -ri $app_name | grep " $workspace_number " | awk '{print $1}'`
I simply removed the r argument from the grep, and then their script worked as advertised.
win_list=`wmctrl -lx | grep -i $app_name | grep " $workspace_number " | awk '{print $1}'`

Related

Is there a simpler way to find the name of the screen holding the mouse in bash

I have made a script to get the name of the screen holding the mouse using xdotool for mouse location and xrandr for the screen setup. Is there a simpler way of doing it, either by built-in function or by altering my script
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
## Get mouse positions and save directly to variables X/Y
eval $(xdotool getmouselocation --shell)
### get connected and active screen sizes/placements
### regex matches format e.g. 1920x1080+0+0
screens=$(xrandr | grep -oP '^\w+.* (\d+x\d+\+\d+\+\d+)')
## Loop through screens and see if mouse position is within the screen
for screen in $screens
do
name=$(echo "$screen" | grep -oP "^\w+")
scr_info=$(echo $screen | awk '{print $NF}')
# Extract screen width/height and offset
screen_w=$(echo $scr_info | awk -F '[x+]' '{print $1}')
screen_h=$(echo $scr_info | awk -F '[x+]' '{print $2}')
screen_x=$(echo $scr_info | awk -F '[x+]' '{print $3}')
screen_y=$(echo $scr_info | awk -F '[x+]' '{print $4}')
# Check if mouse coord is within screen
if [ "$X" -ge "$screen_x" ] && [ "$X" -lt "$(( screen_x + screen_w))" ] &&
[ "$Y" -ge "$screen_y" ] && [ "$Y" -lt "$(( screen_y + screen_h))" ]
then
echo "$name"
fi
done
By reducing the amount of times awk command is used, your script will definitely run faster.
Another way to improve performance is by using grep more often, since it will most likely be faster than awk in several scenarios.
On the excerpt below we will use grep to filter the resolution and position from the xrandr string, and a single awk command to make an array.
scr_info=($(echo $screen | grep -Po '\d{0,5}x\d{0,5}\+\d{0,5}\+\d{0,5}' | awk -F '[x+]' '{print $1"\n"$2"\n"$3"\n"$4}'))
screen_w="${scr_info[0]}"
screen_h="${scr_info[1]}"
screen_x="${scr_info[2]}"
screen_y="${scr_info[3]}"

Getting number of newlines and storing each in a variable

I am making a script that will let you choose between which interface you want to use.
I need a way to get the interfaces and store each of them in a variable.
Here is my code, but it only gets the interfaces:
Interfaces=$(ifconfig | awk '{print $1}' | grep ':' | tr -d ':')
You need to only check the lines that contain the interface name, not the lines with details. In ifconfig, detail lines start with a space; in ip, interface lines start with a number.
In bash, you can use select to create a simple menu:
#! /bin/bash
select interface in $(ip link show | grep '^[0-9]' | cut -f2 -d:) ; do
if [[ $interface ]] ; then
echo You selected $interface
break
fi
done
or
select interface in $(ifconfig -a | grep -v '^ ' | cut -f1 -d' ') ; do
if [[ $interface ]] ; then
echo You selected $interface
break
fi
done

Multiple variables into one variable with wildcard

I have this script:
#!/bin/bash
ping_1=$(ping -c 1 www.test.com | tail -1| awk '{print $4}' | cut -d '/' -f 2 | sed 's/\.[^.]*$//')
ping_2=$(ping -c 1 www.test1.com | tail -1| awk '{print $4}' | cut -d '/' -f 2 | sed 's/\.[^.]*$//')
ping_3=$(ping -c 1 www.test2.com | tail -1| awk '{print $4}' | cut -d '/' -f 2 | sed 's/\.[^.]*$//')
ping_4=$(ping -c 1 www.test3.com | tail -1| awk '{print $4}' | cut -d '/' -f 2 | sed 's/\.[^.]*$//' )
Then I would like to treat the outputs of ping_1-4 in one variable. Something like this:
#!/bin/bash
if [ "$ping_*" -gt 50 ]; then
echo "One ping is to high"
else
echo "The pings are fine"
fi
Is there a possibility in bash to read these variables with some sort of wildcard?
$ping_*
Did nothing for me.
The answer to your stated problem is that yes, you can do this with parameter expansion in bash (but not in sh):
#!/bin/bash
ping_1=foo
ping_2=bar
ping_etc=baz
for var in "${!ping_#}"
do
echo "$var is set to ${!var}"
done
will print
ping_1 is set to foo
ping_2 is set to bar
ping_etc is set to baz
Here's man bash:
${!prefix*}
${!prefix#}
Names matching prefix. Expands to the names of variables whose
names begin with prefix, separated by the first character of the
IFS special variable. When # is used and the expansion appears
within double quotes, each variable name expands to a separate
word.
The answer to your actual problem is to use arrays instead.
I don't think there's such wildcard.
But you could use a loop to iterate over values, for example:
exists_too_high() {
for value; do
if [ "$value" -gt 50 ]; then
return 0
fi
done
return 1
}
if exists_too_high "$ping_1" "$ping_2" "$ping_3" "$ping_4"; then
echo "One ping is to high"
else
echo "The pings are fine"
fi
You can use "and" (-a) param:
if [ $ping_1 -gt 50 -a \
$ping_2 -gt 50 -a \
$ping_3 -gt 50 -a ]; then
...
...
Or instead of defining a lot of variables, you can make an array and check with a loop:
pings+=($(ping -c 1 www.test.com | tail -1| awk '{print $4}' | cut -d '/' -f 2 | sed 's/\.[^.]*$//'))
pings+=($(ping -c 1 www.test1.com | tail -1| awk '{print $4}' | cut -d '/' -f 2 | sed 's/\.[^.]*$//'))
pings+=($(ping -c 1 www.test2.com | tail -1| awk '{print $4}' | cut -d '/' -f 2 | sed 's/\.[^.]*$//'))
pings+=($(ping -c 1 www.test3.com | tail -1| awk '{print $4}' | cut -d '/' -f 2 | sed 's/\.[^.]*$//' ))
too_high=0
for ping in ${pings[#]}; do
if [ $ping -gt 50 ]; then
too_high=1
break
fi
done
if [ $too_high -eq 1 ]; then
echo "One ping is to high"
else
echo "The pings are fine"
fi
To complement the existing, helpful answers with an array-based solution that demonstrates:
several advanced Bash techniques (robust array handling, compound conditionals, handling the case where pinging fails)
an optimized way to extract the average timing from ping's output by way of a single sed command (works with both GNU and BSD/macOS sed).
reporting the servers that either took too long or failed to respond by name.
#!/usr/bin/env bash
# Determine the servers to ping as an array.
servers=( 'www.test.com' 'www.test1.com' 'www.test2.com' 'www.test3.com' )
# Initialize the array in which timings will be stored, paralleling the
# "${servers[#]}" array.
avgPingTimes=()
# Initialize the array that stores the names of the servers that either took
# too long to respond (on average), or couldn't pe pinged at all.
failingServers=()
# Determine the threshold above which a timing is considered too high, in ms.
# Note that a shell variable should contain at least 1 lowercase character.
kMAX_TIME=50
# Determine how many pings to send per server to calculate the average timing
# from.
kPINGS_PER_SERVER=1
for server in "${servers[#]}"; do
# Ping the server at hand, extracting the integer portion of the average
# timing.
# Note that if pinging fails, $avgPingTime will be empty.
avgPingTime="$(ping -c "$kPINGS_PER_SERVER" "$server" |
sed -En 's|^.* = [^/]+/([^.]+).+$|\1|p')"
# Check if the most recent ping failed or took too long and add
# the server to the failure array, if so.
[[ -z $avgPingTime || $avgPingTime -gt $kMAX_TIME ]] && failingServers+=( "$server" )
# Add the timing to the output array.
avgPingTimes+=( "$avgPingTime" )
done
if [[ -n $failingServers ]]; then # pinging at least 1 server took too long or failed
echo "${#failingServers[#]} of the ${#servers[#]} servers took too long or couldn't be pinged:"
printf '%s\n' "${failingServers[#]}"
else
echo "All ${#servers[#]} servers responded to pings in a timely fashion."
fi
Yes bash can list variables that begin with $ping_, by using its internal compgen -v command, (see man bash under SHELL BUILTIN COMMANDS), i.e.:
for f in `compgen -v ping_` foo ; do
eval p=\$$f
if [ "$p" -gt 50 ]; then
echo "One ping is too high"
break 1
fi
[ $f=foo ] && echo "The pings are fine"
done
Note the added loop item foo -- if the loop gets through all the variables, then print "the pings are fine".

String comparison in a Bash script called from terminal or keyboard shortcut gives different output

I have two monitors and I am calling a bash script to move the current window to the monitor above. Below there is a sample of my code (I changed the variable names to be more descriptive).
The problem is in the last if-statement if [[ $currentWindowPosition == "above" ]]. When I call the script from terminal and my window in the the monitor above I get:
currentWindowPosition=above!
The window will stay on this monitor
but when I call it from a keyboard shortcut (and redirect the output to a file) I get:
currentWindowPosition=above!
The current monitor is on the bottom
How come? I also tried different versions of that test but without success. Thank you in advance.
yOfCurrentWindow=$(xwininfo -id $(xprop -root | awk '/_NET_ACTIVE_WINDOW\(WINDOW\)/{print $NF}') | grep "Absolute upper-left Y:" | cut -d: -f2)
yResolutionOfmonitor1=$(xrandr | awk '/ connected/ { print $3 }' | sed -n '1 p' | awk 'BEGIN{FS="x"} { print $2}' | awk 'BEGIN{FS="+"} {print $1}')
if [ $yOfCurrentWindow -lt $yResolutionOfmonitor1 ]
then currentWindowPosition="above"
else currentWindowPosition="below"
fi
echo "currentWindowPosition=$currentWindowPosition!"
if [[ $currentWindowPosition == "above" ]]
then echo "The window will stay on this monitor"
else echo "The current monitor is on the bottom"
fi
Jdamian's suggestion to use:
if [ "$currentWindowPosition" = below ]
works. Notice the single equal sign.

Optimizing Bash script, subshell removal

I have a bash script that lists the amount of ip addresses connected on a port. My issue is, is that with large amounts of connections it is slow as poo. I think it is because of the subshells in use, but I am having trouble removing them without borking the rest of the script. Here is the script in its entirety as it is fairly short:
#!/bin/bash
portnumber=80
reversedns_enabled=0
[ ! -z "${1}" ] && portnumber=${1}
[ ! -z "${2}" ] && reversedns_enabled=${2}
#this will hold all of our ip addresses extracted from netstat
ipaddresses=""
#get all of our connected ip addresses
while read line; do
ipaddress=$( echo ${line} | cut -d' ' -f5 | sed s/:[^:]*$// )
ipaddresses="${ipaddresses}${ipaddress}\n"
done < <( netstat -ano | grep -v unix | grep ESTABLISHED | grep \:${portnumber} )
#remove trailing newline
ipaddresses=${ipaddresses%%??}
#output of program
finaloutput=""
#get our ip addresses sorted, uniq counted, and reverse sorted based on amount of uniq
while read line; do
if [[ ${reversedns_enabled} -eq 1 ]]; then
reversednsname=""
#we use justipaddress to do our nslookup(remove the count of uniq)
justipaddress=$( echo ${line} | cut -d' ' -f2 )
reversednsstring=$( host ${justipaddress} )
if echo "${reversednsstring}" | grep -q "domain name pointer"; then
reversednsname=$( echo ${reversednsstring} | grep -o "pointer .*" | cut -d' ' -f2 )
else
reversednsname="reverse-dns-not-found"
fi
finaloutput="${finaloutput}${line} ${reversednsname}\n"
else
finaloutput="${finaloutput}${line}\n"
fi
done < <( echo -e ${ipaddresses} | uniq -c | sort -r )
#tabulate that sheet son
echo -e ${finaloutput} | column -t
The majority of the time spent is doing this operation: echo ${line} | cut -d' ' -f5 | sed s/:[^:]*$// what is the best way to inline this to produce a faster script. It takes well over a second with 1000 concurrent users (which is my base target, although should be able to process more without using up all of my cpu).
You could reduce that with cut -d' ' <<< "$line" | sed .... You could write a more complex sed script and avoid the use of cut.
But the real benefit would be in avoiding the loop so there's only one sed (or awk or perl or …) script involved. I'd probably look to reduce it to ipaddresses=$(netstat -ano | awk '...') so that instead of 3 grep processes, plus one cut and sed per line, there was just a single awk process.
ipaddresses=$(netstat -ano |
awk " /unix/ { next } # grep -v unix
!/ESTABLISHED/ { next } # grep ESTABLISHED
!/:${portnumber}/ { next } # grep :${portnum} "'
{ sub(/:[^:]*$/, "", $5); print $5; }'
)
That's probably rather clumsy, but it is a fairly direct transliteration of the existing code. Watch for the quotes to get ${portnumber} into the regex.
Since you feed the list of IP addresses into uniq -c and sort -r. You probably should use sort -rn, and you could use awk to do the uniq -c, too.
The only bit that you can't readily improve is host; that seems to only take one host or IP address argument at a time, so you have to run it for each name or address.
I'll take a stab at a couple of issues:
The following line from the script which performs incremental string concatenation will not be be efficient without the means to allocate a reasonable buffer:
ipaddresses="${ipaddresses}${ipaddress}\n"
For another, using a while loop with read line when a pipeline will do is significantly worse than the pipeline. Try something like this instead of the first loop:
netstat -ano |
grep -v 'unix' |
grep 'ESTABLISHED' |
grep "\:${portnumber}" |
cut -d' ' -f5 |
sed 's/:[^:]*$//' |
while read line; do ...
Also, try combining at least two of the three sequential grep commands into one invocation of grep.
If nothing else, this will mean you are no longer spawning a pipeline which creates new cut and sed processes for each line of input processed in the first loop.
Here is a whole script optimized & refactored:
#!/bin/bash
portnumber=80
reversedns_enabled=0
[[ $1 ]] && portnumber=$1
[[ $2 ]] && reversedns_enabled=$2
#this will hold all of our ip addresses extracted from netstat
ipaddresses=''
#get all of our connected ip addresses
while IFS=' :' read -r type _ _ _ _ ipaddress port state _; do
if [[ $type != 'unix' && $port == "$portnumber" && $state == 'ESTABLISHED' ]]; then
ipaddresses+="$ipaddress\n"
fi
done < <(netstat -ano)
#remove trailing newline
ipaddresses=${ipaddresses%%??}
#output of program
finalOutput=""
#get our ip addresses sorted, uniq counted, and reverse sorted based on amount of uniq
while read -r line; do
if (( reversedns_enabled == 1 )); then
reverseDnsName=""
#we use justipaddress to do our nslookup(remove the count of uniq)
read -r _ justipaddress _ <<< "$line"
reverseDnsString=$(host "$justipaddress")
if [[ $reverseDnsString == *'domain name pointer'* ]]; then
reverseDnsName=${reverseDnsName##*domain name pointer }
else
reverseDnsName="reverse-dns-not-found"
fi
finalOutput+="$line $reverseDnsName\n"
else
finalOutput+="$line\n"
fi
done < <(echo -e "$ipaddresses" | sort -ur)
#tabulate that sheet son
echo -e "$finalOutput" | column -t
As you can see, there are almost no external tools used (no sed, awk or grep). Awesome!

Resources