9

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?

7
  • 1
    Use xdotool Commented Nov 6, 2018 at 7:45
  • @IporSircer Can you be more specific? Commented Nov 6, 2018 at 7:52
  • Read the manual: manpages.debian.org/stretch/xdotool/xdotool.1.en.html Commented Nov 6, 2018 at 7: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. Commented Nov 6, 2018 at 7:56
  • 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! Commented Nov 7, 2018 at 3:57

3 Answers 3

18

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.

12
  • 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! Commented Nov 7, 2018 at 4: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. Commented Nov 7, 2018 at 4: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? Commented Nov 7, 2018 at 4: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
    Commented Nov 7, 2018 at 8:55
  • 1
    "I should harass the tmux devs" – you meant to ask them, right?
    – egmont
    Commented Nov 8, 2018 at 9:13
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.

7
  • Cool! I had no idea about $WINDOWIDand 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. Commented Nov 7, 2018 at 4: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. Commented Nov 7, 2018 at 4: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.
    – user313992
    Commented Nov 7, 2018 at 11:35
  • 1
    Why is installing them for each user and machine a problem? That's how I handle all my dotfiles. Commented Nov 8, 2018 at 2:32
  • 1
    @Joshua You don't have the DISPLAY environment variable set. As said, this doesn't work from within screen or tmux.
    – user313992
    Commented Aug 14, 2021 at 18:23
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.

You must log in to answer this question.

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