SCRIPT: Get List of All Window Names of "Windowed" Apps using JXA

scripting
jxa
windows

#1

UPDATED: 2018-11-16 16:15 GMT-6

Use Case

  • Get a List of All Window Names in All "Windowed" Running Apps
    (those apps that are running in the GUI front end of the macOS, and show in the Dock when running)
    • Does NOT include windows for background only apps (like those in the Apple menu)
  • Show How to use JXA
  • Demo using a couple of interesting, powerful JavaScript functions
    • reduce() -- to flatten an array
    • filter() -- to remove unwanted elements of an array
var ptyScriptName   = "Get List of All Window Names of Windowed Apps"
var ptyScriptVer     = "1.2"  //  Revise title
var ptyScriptDate   = "2018-11-16"
var ptyScriptAuthor = "JMichaelTX"


'use strict';
(function myMain() {      // this will auto-run when script is executed

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
RETURNS:  CSV List of Window Names

REQUIRED:
  1.  macOS 10.11.6+

TAGS:  @Lang.JXA @CAT.Windows @CAT.UI @type.Example @Auth.JMichaelTX

REF:  The following were used in some way in the writing of this script.

  1.  2018-11-03, JMichaelTX, Stack Overflow
      osascript is very slower than Script Editor
      https://stackoverflow.com/a/53136902/915019

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

var winList = Application("System Events").processes.whose(
    {backgroundOnly: {'=': false} }).windows.name();

// REF: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce

//--- Flatten 2D Array into 1D ---
var winList2 = winList.reduce(
        function(accumulator, currentValue) {
          return accumulator.concat(currentValue);
        },
        []
      );

//--- Remove Windows with No Title ---
winList2 = winList2.filter(e => (e !== ""));

//--- Create CSV List of Window Names ---
//    (use .join('\n') if you'd prefer each name on separate line)
var scriptResults = winList2.join(',')

return scriptResults;

}  // END of function myMain()
)();  // autorun

Questions?


#2

concatMap is another useful and fundamental abstraction, which (like both map and filter) can be defined in terms of reduce:

(() => {
    'use strict';

    const main = () =>
        concatMap(filter(s => 0 < s.length))(
            Application('System Events').processes.whose({
                backgroundOnly: false
            }).windows.name()
        ).join('\n')


    // GENERIC --------------------------------------------
    // https://github.com/RobTrew/prelude-jxa

    // ( 'Curried' versions here, all in terms of reduce )

    // concatMap :: (a -> [b]) -> [a] -> [b]
    const concatMap = f => xs =>
        xs.reduce((a, x) => a.concat(f(x)), []);


    // filter :: (a -> Bool) -> [a] -> [a]
    const filter = p => xs =>
        xs.reduce((a, x) => p(x) ? a.concat(x) : a, []);

    // For reference, not used here:

    // map :: (a -> b) -> [a] -> [b]
    const map = f => xs =>
        xs.reduceRight((a, x) => [f(x)].concat(a), []);

    return main();
})();


#3

Incidentally, we could simplify exclusion of empty strings (nameless windows) by writing:

const main = () =>
    concatMap(filter(Boolean))(
        Application('System Events').processes.where({
            backgroundOnly: false
        }).windows.name()
    ).join('\n')

#4

@JMichaelTX, I might have my brain switched off today, but what purpose does filtering by backgroundOnly: false serve that its omission wouldn't ?


#5

This whose clause eliminates all processes that don't have a UI, and thus would not have any windows, which makes the script run faster.


#6

To clarify, this script:

Application("System Events").processes
                            .whose({
                                 backgroundOnly: false
                            }).windows.name();

is demonstrably and noticeably quicker to execute than this script ?:

Application("System Events").processes.windows.name();

I'm unable to convince myself this is the case on my system, but perhaps I need to open many, many more windows.

One reason I bring this up is that backgroundOnly, whilst perhaps an efficient filter, is not consistently the most reliable for targeting windowed processes, having a tendency to report false negatives for some applications that live in the menu bar. Examples of this include SnippetsLab, Flume, Typinator, and others, which don't cease to be reported as being backgroundOnly even when they are brought into the foreground for interaction.

Omitting the filter does manage to catch these programs' windows when they are visible, although I noticed one false positive report for SnippetsLab whose window had been closed, but remained in the list for a period of time.


#7

Here, FWIW, it does seem to be worthwhile – seems to eliminate about 80% of the run-time needed on this system. Perhaps I have a lot of background processes ...

(based on a few runs with an unsophisticated form of testing):

carbon-2

and using this definition of main(), alternately with and without the where clause:

carbon-3


#8

Remember the objective here was to get the name of all open windows.
I believe for that purpose it does a good job. If you believe the results are more accurate not using it, and you don't mind (or don't see) the extra execution time, then don't use it.


#9

Which I believe it doesn’t achieve in instances of menu bar apps that have open windows, yet are registered as background only nonetheless, e.g. Typinator, SnippetsLab, ...


#10

So if you know you might have those type of windows, then don't use the "background only" clause.


#11

I was providing feedback, not stuck in an "if...then...else" dilemma wondering how to get out.