0

I'm wondering if there is a tool which can query which monitor is currently focused.

I'd like to have a script which changes the wallpaper on the monitor I am currently on (using xwallpaper). From what I understand, both the monitor number or the monitor name (i.e. HDMI1, eDP1) could be useful to achieve my goal.

Ultimately, the chosen wallpaper for the current monitor would be copied using a name which has this structure: wall-HDMI1.png or wall-eDP1.png

There is a partial solution where the accepted answer suggests guessing the current monitor using the mouse's position using xdotool. This seems finicky at best. Especially on a laptop that could be connected to multiple monitors using multiple different layouts.

1
  • 1
    what, exactly, do you mean by "focused"? are you trying to find which monitor is the primary monitor?
    – cas
    Commented Nov 17, 2021 at 3:33

1 Answer 1

4

If you want to know what the current primary monitor is, that's easy:

$ xrandr | grep primary
DP-2 connected primary 2560x1440+0+0 (normal left inverted right x axis y axis) 597mm x 336mm

or

$ xrandr | awk '/ primary / {print $1}'
DP-2

If you want to know which monitor the mouse pointer is currently on, that's a little more difficult. First you have to find out where the mouse-pointer is (e.g. with xdotool) and then you have to compare the mouse's X,Y co-ordinates to the starting and ending X & Y co-ordinates of each screen (e.g. with xrandr).

The following perl script uses xrandr and xdotool to do exactly that.

#!/usr/bin/perl

use strict;

my %M;    # hash to hold keys from 'xdotool getmouselocation --shell'
my $pipe; # file handle for the pipes we're going to open

# get the current cursor location into $M{X} and $M{Y}
open($pipe, "-|", qw(xdotool getmouselocation --shell)) ||
    die "couldn't open pipe from xdotool: $!\n";

while(<$pipe>) {
  chomp;
  my($key, $val) = split /=/;
  $M{$key} = $val;
};
close($pipe);

# compare mouse location to monitor co-ordinates
open($pipe, "-|", "xrandr") ||
    die "couldn't open pipe from xrandr: $!\n";

while(<$pipe>) {
  my ($display, $width, $height, $x_offset, $y_offset);

  next unless m/ connected /;
  my @F = split;
  $display = $F[0];

  # co-ordinates are on fourth field (F[3]) on the primary display
  # or on third field (F[2]) on non-primary displays. Perl arrays
  # start from zero, not one (same as in bash). 
  if ($F[2] eq "primary") {
    ($width, $height, $x_offset, $y_offset) = split /[x+]/, $F[3];
  } else {
    ($width, $height, $x_offset, $y_offset) = split /[x+]/, $F[2];
  };

  if ($M{X} >= $x_offset && $M{X} <= $width + $x_offset && 
      $M{Y} >= $y_offset && $M{Y} <= $height + $y_offset) {
    print "$display\n";
    last;
  };
};
close($pipe);

Save this as, e.g., ./get-focused-monitor.pl, make it executable with chmod +x get-focused-monitor.pl, and run it. It will tell you which monitor the mouse pointer is currently hovering over.

$ ./get-focused-monitor.pl 
DP-2

Run it as watch -n 0.1 ./get-focused-monitor.pl and thrill to the excitement of seeing the display name changing as you move the mouse from one display to another. I did that for tens of seconds and the excitement never varied. It was almost too much.


BTW, I currently have two 1440p monitors connected. A 27" monitor in landscape mode ("DP-2", the primary), and a 24" monitor pivoted to portrait mode("DP-0"). The xrandr output that this script parses on my system looks like this:

$ xrandr | grep \\bconnected
DP-0 connected 1440x2560+2560+0 left (normal left inverted right x axis y axis) 527mm x 296mm
DP-2 connected primary 2560x1440+0+0 (normal left inverted right x axis y axis) 597mm x 336mm

split /[x+]/ in the script splits the third or fourth field into an array of four integers using x or + as the delimiter: width, height, x offset, and y offset. See perldoc -f split for info on how perl's split() function works.


PS: Why perl? Because perl's easier - parsing text in while-read loops in shell sucks, and doing calculations in shell sucks at least as much, I don't want to deal with the usual shell white-space and quoting and word-splitting issues, or the extra-ordinarily ugly shell syntax for arrays. And shell isn't the only language that can pipe data to or from external programs. Most languages commonly used on unix & linux can do that.

Doing it in shell is not particularly difficult, just clumsy and awkward. Feel free to re-implement the algorithm in bash if you want, it's the method that's important, not the language.

BTW, the easiest way to do it in shell would be to run something like (xdotool getmouselocation --shell; xrandr) | awk '...' and have awk parse the different kinds of input from the two programs. or use perl instead of awk. Also worth noting is that like shell and perl, awk can also run external programs and process their output.

There's a little more setup work in perl (explicitly opening pipes), but that's boilerplate stuff. The rest of the code is much easier than in shell.

I could have used back-ticks or qx() (see perldoc -f qx) to do command substitution like in shell instead of using open() (see perldoc -f open). For example:

#!/usr/bin/perl

foreach (qx(xdotool getmouselocation --shell)) {
  m/^([^=]+)=(.*)$/;
  $M{$1} = $2;
};

foreach (qx(xrandr)) {
  next unless m/connected/;
  ($d, $w, $h, $xo, $yo) = m/^([^\s]+) .* (\d+)x(\d+)\+(\d+)\+(\d+)/;

  if ($M{X} >= $xo && $M{X} <= $w + $xo &&
      $M{Y} >= $yo && $M{Y} <= $h + $yo) {
    print "$d\n";
    last;
  }
}

This produces exactly the same output as the first script, but uses perl-style command substitution instead of explicitly open()-ing pipes. It also uses regular expression capture groups to extract the data instead of split().

BTW, perl has an extremely useful function called map. See perfoc -f map for details (and see also perldoc -f grep for a similarly useful built-in). It's an amazingly useful and versatile tool - one of the simplest things you can use it for is to do things like replace the first foreach loop with:

foreach (qx(xdotool getmouselocation --shell)) {
  map { $M{$1} = $2 } m/^([^=]+)=(.*)$/;
};

or even:

map { $M{$1} = $2 } m/^([^=]+)=(.*)$/ foreach (qx(xdotool getmouselocation --shell));

Both versions are basically the same: map { CODE BLOCK } ARRAY - and the array can be a variable that already exists, or an anonymous array, generated on-the-fly in whatever manner suits you at them time.

Regexes return an array when called in list context (the match or matches). In the examples above, the regex match operator (m/PATTERN/) is being applied to each element of a foreach loop which is, in turn, iterating through elements of an array (the array in this case being the output of xdotool getmouselocation --shell, one line per array element).

There are other ways that map can be used. Depending on how you call it, it can return a scalar value, an array, or a hash which can be assigned to variables or consumed by other functions.

Also BTW, foreach and for are synonyms in perl. Anywhere I've written foreach, you can replace it with for. for/foreach can be used to iterate over an array (list) as I've used it above, and it can also be used like a C-style for ($i = 1; $i < 10; $i++) loop.

There are probably perl modules for querying X directly to get the information needed about the displays and the mouse pointer, but it would have taken me longer to find them and read their docs than to just use the existing xdotool and xrandr tools.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .