6

Problem/Goal description

Ideally, I would like a good way of detecting from a shell script whether or not the window has focus. By a "good" way, I mean some way which requires minimal steps and preferably does not require sifting through each open window blindly to find mine based on title.

The purpose is for controlling notifications in many different scripts -- so I'm just looking for a general solution that can apply to any and all of them.

What I've come up with so far is roundabout and hacky -- it is as follows:

  1. Set my title to something unique or mechanically relevant (in my model, it is my PTS path or, more robustly, a UUID). Hope desperately that this title is not overridden by something.

  2. Get a list of all open windows, by title.

  3. Iterate through list to identify my window by matching it to the title element. (Note the possibility of errors here if another window happens to have that same title element.)

  4. Detect whether said window has focus or not.

It should be noted that I do not want to implement this, and will only do it as a last resort. So what I'm asking for here is something that is not this.

Compromises

This solution is obviously terrible, so I'd like to know if there is anything remotely better, in any way. I would prefer something portable, elegant, and perfect, but I recognize the potential need to compromise. By better I mean any of the following:

  1. A solution that only works with a specific terminal emulator, e.g. by having the terminal emulator itself set an environment variable allowing the script to detect which window it is in.

  2. A solution that does not require setting the title, and instead uses some other invisible marker in window state that is accessible and detectable from a shell script attached to said window.

  3. Recusing up the parent process ladder to find the parent terminal emulator PID, and working from there (Note that a solution that works by recusing up the the process tree to detect the parent process that started the script will only work if the script is running locally, so this solution is incomplete but still good!)

Conditions

I was getting questions about exactly what conditions my preferred solution is supposed to function under, and the answer is as many as possible. But at minimum, I would like something that works:

  1. In a single-tab terminal session running natively (default scenario).

  2. In terminal multiplexers like tmux. (Portability between different terminal multiplexers is preferred but really not required.)

Extras that I'd really appreciate (in order of importance), include:

  1. Ability to function on remote connections over telnet and SSH.

  2. Ability to distinguish which tab is open in a multi-tab terminal session.


Summary

I want a good way of finding what terminal emulator window my shell script is attached to, so that I can detect whether it has focus or not.

Note that I'm already aware of the mechanics of how to to iterate through open windows, and how to detect whether they have focus or not and what titles they have. I am aware of the existance of xdotool and xprop and this question is not about the basic mechanics of those tools (unless there is some hidden black magic feature I don't know about that side-steps the intrinsic hackiness of my current solution.)

The reason I don't want to that is because it's terrible. Any other solution that accomplishes the same thing?

Rui F Ribeiro
  • 55,929
  • 26
  • 146
  • 227
Alexandria P.
  • 149
  • 2
  • 10
  • 1
    Use [xdotool](https://github.com/jordansissel/xdotool/) – Ipor Sircer Nov 06 '18 at 07:45
  • @IporSircer Can you be more specific? – Alexandria P. Nov 06 '18 at 07:52
  • Read the manual: https://manpages.debian.org/stretch/xdotool/xdotool.1.en.html – Ipor Sircer Nov 06 '18 at 07:55
  • @IporSircer Uh... no offense, but I'm not sure that you understood my question. I know what `xdotool` does. I've used it before. I don't know what you intend me to do with it to solve the stated problem. – Alexandria P. Nov 06 '18 at 07:56
  • @IporSircer Perhaps I should clarify. I'm aware that you can search for windows with `xdotool`. I'm not asking for help implementing my hacky solution -- I'm asking for ideas about alternative solutions. – Alexandria P. Nov 06 '18 at 07:59
  • Your compromise #3 seems like a complete solution fully described to me. Is it just those listed caveats that are a problem for it? I think there's missing information about requirements here: for example, do you have only one terminal session and window per terminal emulator process? Are remote processes part of the use case, as is sort of implied in #3? If so, what is available or under your control about how those processes are launched? Do they have X forwarding? What about the terminal emulator, and how it's launched? What about the *script* itself? Is it always started with focus? Etc. – Michael Homer Nov 06 '18 at 08:49
  • 1
    @MichaelHomer Unfortunately, my use case is very broad and I want it to work in as many places as possible -- so I guess some amount of open-endedness is intrinsic to this question. That said, I've edited my explanation of what I'm looking for, to make it more clear. Thanks for your input! – Alexandria P. Nov 07 '18 at 03:57

3 Answers3

14

There's a FocusIn/FocusOut mode. To enable:

echo -ne '\e[?1004h'

To disable:

echo -ne '\e[?1004l'

On each focus event, you receive either \e[I (in) or \e[O (out) from the input stream.

GNOME Terminal (and other VTE based terminals) also report the current state when you enable this mode. That is, you can enable and then immediately disable it for querying the value once.

You can combine read with a timeout, or specifying to read 3 characters to get the response. Note however that it's subject to race condition, e.g. in case you have typed ahead certain characters.

egmont
  • 5,555
  • 1
  • 21
  • 28
  • Oh, that is _awesome_! It looks like exactly what I was looking for -- satisfying pretty much every use case. I believe since this is sent as character codes, I could get this to work even for remote connections over SSH... I'm really not sure how to do it with `read` though -- personally, my first thought upon reading your first paragraph was to pipe straight from the terminal device and watch for characters matching these. I'm testing that out now with `cat -v`. If there's any traps I haven't thought about with that approach, let me know! – Alexandria P. Nov 07 '18 at 04:03
  • Yeah, so it looks like read requests on terminal devices themselves interfere with terminal operation? Once you request a character it moves to the next character and other things that are reading it don't get to see it. I wonder if there's some way to do it without that. `cat` definitely won't work. – Alexandria P. Nov 07 '18 at 04:08
  • It's looking like, to get a normal terminal working with this sort of detection in the background, I'd have to have some process in between the each terminal emulator and the shell, as the filling in a pipe sandwich, to intercept those requests and act on them. Correct me if I'm wrong? – Alexandria P. Nov 07 '18 at 04:15
  • I meant the `read` shell command, see `help read`, such as `echo -ne '\e[?1004h\e[?1004l'; read -N 3 -t 0.5; echo $REPLY`. An intermediate layer stripping off the response can provide partial help in case of troubles (e.g. the response arriving later than expected, it at least won't screw up the rest of your script; plus can fix the typeahead case). It's pretty cumbersome, though. Asynchronous escape sequences are unfortunately a pain in the ... to handle robustly, due to their asynchronous nature, but the simple way of handling works _most_ of the time. – egmont Nov 07 '18 at 08:55
  • @AlexandriaP. fwiw, neither this will work inside tmux/screen; they do their own terminal emulation and block the escapes they don't know about. –  Nov 07 '18 at 11:30
  • That's terrible! I should harass the tmux devs and ask them to add support for codes like this. – Alexandria P. Nov 08 '18 at 05:45
  • Well, thanks for your input. I'll definitely have a use for this knowledge someday. ^.^ – Alexandria P. Nov 08 '18 at 05:46
  • Better yet, tmux should add a way to create exceptions for passing through arbitrary escape codes, so the change covers all possible use cases. – Alexandria P. Nov 08 '18 at 05:48
  • I think tmux does support passing through arbitrary codes. Not sure about the input (response) side of the story, though. – egmont Nov 08 '18 at 09:13
  • "I should harass the tmux devs" – you meant to _ask_ them, right? – egmont Nov 08 '18 at 09:13
  • @egmont I'm being tongue-in-cheek because I post so many Github issues that I often feel like a nuisance. :P – Alexandria P. Nov 09 '18 at 03:50
  • for the case at hand, it seems that tmux has a way to pass through the focus in/focus out escapes (/focus-events in man tmux(1)). no such luck with screen(1), though. –  Nov 10 '18 at 02:39
2
if [ "$(xdotool getwindowfocus)" -eq "$WINDOWID" ]; then
   echo I have the focus
fi

This will not work inside screen/tmux if they were started from somewhere else and just attached in the current window.

  • Cool! I had no idea about `$WINDOWID`and that's definitely looking like a useful thing... although you're right that process env var inheritance is definitely a problem... I think I could hack something together to get around that. – Alexandria P. Nov 07 '18 at 04:13
  • Okay, so, as far as I know, there's no non-hacky way for `tmux` to change environment variables in a shell it connects to -- but I can imagine a way of doing it, which would require hooks into the shell itself that are reading tmux's instructions somehow... Does that already exist? I want to try not to reinvent the wheel, here. – Alexandria P. Nov 07 '18 at 04:18
  • I guess you'll have to live with that limitation; even if tmux had some attach/detach hooks (I vaguely remember that it does), they will have to be installed for each user, which will turn everything into a mess. FWIW, passing of the `$WINDOWID` envvar is no better or worse than the passing of `$DISPLAY` or `$LANG`, and all terminal emulators are setting it. –  Nov 07 '18 at 11:35
  • 1
    Why is installing them for each user and machine a problem? That's how I handle all my dotfiles. – Alexandria P. Nov 08 '18 at 02:32
  • I'm getting "Error: can't open display (null) Failed creating new xdo instance." – Joshua Snider Aug 13 '21 at 22:55
  • 1
    @Joshua You don't have the `DISPLAY` environment variable set. As said, this doesn't work from within screen or tmux. –  Aug 14 '21 at 18:23
  • To get it working in tmux you need to add `WINDOWID` to tmux's `update-environment` option, then it will automatically update the variable correctly for every session. – Jan Larres Oct 17 '21 at 21:58
0

I coded up a solution using iTerm's Python API for macOS. Here is the Python daemon that stores data from iTerm in redis (Needs Brish:pip install -U brish):

#!/usr/bin/env python3

import AppKit
bundle = "com.googlecode.iterm2"
if not AppKit.NSRunningApplication.runningApplicationsWithBundleIdentifier_(bundle):
    AppKit.NSWorkspace.sharedWorkspace().launchApplication_("iTerm")

import os
from brish import z, zp
os.environ["ITERM2_COOKIE"] = z("""osascript -e 'tell application "iTerm2" to request cookie' """).outrs

import asyncio
import iterm2

async def main(connection):
    app = await iterm2.async_get_app(connection)
    async with iterm2.FocusMonitor(connection) as monitor:
        while True:
            update = await monitor.async_get_next_update()
            window = app.current_terminal_window
            if (update.active_session_changed or update.selected_tab_changed or update.window_changed) and window.current_tab:
                if update.window_changed:
                    zp('redis-cli set iterm_focus {update.window_changed.event.name} 2>&1')
                zp('redis-cli set iterm_active_session {window.current_tab.active_session_id} 2>&1')


iterm2.run_forever(main)

And here is the shell wrappers:

function iterm-session-active() {
    redis-cli --raw get iterm_active_session
}

function iterm-session-my() {
    if [[ "$ITERM_SESSION_ID" =~ '[^:]*:(.*)' ]] ; then
        ec "$match[1]"
    else
        return 1
    fi
}

function iterm-session-is-active() {
    [[ "$(iterm-session-active)" == "$(iterm-session-my)" ]]
}

function iterm-focus-get() {
    redis-cli --raw get iterm_focus
}

function iterm-focus-is() {
    [[ "$(iterm-focus-get)" == TERMINAL_WINDOW_BECAME_KEY ]]
}

Click on the links to see the latest versions of the code from my git repo. You'll need to clean up some unnecessary stuff from the code there though.

HappyFace
  • 1,493
  • 9
  • 21