Search Titles of Safari or Chrome Tabs in Multiple Windows and Bring Match to Front

I asked @viticci about this on Twitter and he suggested that Keyboard Maestro had this capability.

I often have many Safari windows open, and each of those windows have many tabs.

I'm looking for something that would allow me to type a hotkey and then some text, and bring the matching Safari tab to the front. Ideally it would show the titles of the matching tabs as I typed underneath the query (ala Alfred) and allow me to "cursor" to the one I want to bring to front, but just being able to search for a "keyword" and bring the matching window/tab to the front would still be great timesaver.

Any suggestions?

The macro linked below does most of what you’d like. It’s from 2012. though I’m currently running 10.10.11 and the macro still work fine.

MacDrifter: Find Safari Tabs by Substring

Another variant ( uses JavaScript for Applications ):

Find and activate matching Safari tab.kmmacros (20.3 KB)

// Rob Trew MIT license 2015
(function () {
  'use strict';

  // Monadic bind (chain) for lists
  function mb(xs, f) {
    return [].concat.apply([], xs.map(f));
  }

  // s -> [window, tab]
  function tabsMatching(s) {
    return mb(
      appSafari.windows.whose({
        _not: [{
          'document': null
    }]
      })(),
      function (w) { // each window
        return mb(w.tabs(),
          function (t) { // each tab
            return (
              t.name().toLowerCase().indexOf(s) !== -1 ||
              t.url().toLowerCase().indexOf(s) !== -1
            ) ? [[w, t]] : [];
          }
        )
      }
    );
  }

  // DIALOG INVITING SEARCH STRING (OR CLICK ON OK TO LIST ALL TABS)
  var a = Application.currentApplication(),
    sa = (a.includeStandardAdditions = true, a);

  sa.activate();
  
  var s = sa.displayDialog("Search:", {
    withTitle: "Find Safari tab",
    defaultAnswer: '',
    defaultButton: "OK"
  }).textReturned.toLowerCase();


  var appSafari = Application("Safari"),
    lstFound = tabsMatching(s),
    lstChosen = [],
    lngFound = lstFound.length;


  // MULTIPLE MATCHES ? OFFER CHOICE
  if (lngFound > 1) {
    var lstNames = lstFound.map(function (x) {
      return x[1].name();
    }).sort();

    sa.activate();

    var varChosen = sa.chooseFromList(lstNames, {
      withTitle: "Find Safari tab",
      withPrompt: "Choose tab:",
      defaultItems: [lstNames[0]],
      multipleSelectionsAllowed: false
    });
    if (varChosen) {
      lstChosen = tabsMatching(varChosen[0].toLowerCase());
    };
  } else lstChosen = lstFound;
  
  
  // ACTIVATE WINDOW AND TAB, RETURNING NAME & URL
  return lstChosen.map(function (oTab) {
    appSafari.activate();

    var w = oTab[0],
      t = oTab[1];

    w.visible = false;
    w.currentTab = t;
    w.visible = true;

    return '[' + t.name() + '](' + t.url() + ')';
  }).join('\n');

})();

Or a version which works with Safari and or Chrome:

Find and activate specific tab (Chrome and:or Safari).kmmacros (22.4 KB)

// Rob Trew MIT license 2015
// Ver 0.2: Works with Safari and or Google 
//      (searches all tabs of both, if both open)
(function (blnTypeString) {
  'use strict';

  // s -> [window, tab]
  function tabsMatching(s, lstBrowsers) {
    return mb(lstBrowsers,                  function (b) { // Each browser,
      var blnChrome = b.name()[0] === "G";

    return mb(browserWindows(b, blnChrome), function (w) { // each window,
    return mb(w.tabs(),                     function (t) { // each tab.

      return ([t.name(), t.url()].reduce(function (acc, x) {
        return acc ? acc : (x.toLowerCase().indexOf(s) !== -1);
      }, false)) ? [[b, w, t]] : [];
      
    })})});
  }

  // () -> [app]
  function browserNames() {
    return Application("System Events").applicationProcesses.whose({
      _or: [{
        name: "Safari"
      }, {
        name: "Google Chrome"
      }]
    })().map(function (a) {
      return a.name();
    });
  }

  // app -> Bool -> [window]
  function browserWindows(a, blnChrome) {
    return blnChrome ? a.windows() : a.windows.whose({
      _not: [{
        'document': null
      }]
    })();
  }

  // Monadic bind (chain) for lists
  // [a] -> (a -> [b]) -> [b]
  function mb(xs, f) {
    return [].concat.apply([], xs.map(f));
  }

  // Int -> Int -> String
  function padNum(n, lngDigits) {
    return Array(
      lngDigits - n.toString().length + 1
    ).join('0') + n;
  }

  // CHROME AND/OR SAFARI ?
  var a = Application.currentApplication(),
    sa = (a.includeStandardAdditions = true, a),
    lstBrowserNames = browserNames(),
    lstBrowsers = lstBrowserNames.map(function (x) {
      return Application(x);
    }),
    strBrowsers = lstBrowserNames.join(' or '),
    strTitle = "Find and activate " + strBrowsers + " tab";
    
  sa.activate();

  //  DIALOG INVITING SEARCH STRING (CLICK ON OK TO LIST ALL TABS)
  //  OR STRAIGHT TO LIST OF ALL TABS ?
  var s = blnTypeString ? sa.displayDialog("Search:", {
    withTitle: strTitle,
    defaultAnswer: '',
    defaultButton: "OK"
  }).textReturned.toLowerCase() : '';

  var lstFound = tabsMatching(s, lstBrowsers),
    lstChosen = [],
    lngFound = lstFound.length;

  // MULTIPLE MATCHES ? OFFER CHOICE
  if (lngFound > 1) {
    var lngDigits = lngFound.toString().length,
      lstNames = lstFound.map(function (x, i) {
        var strName = x[2].name();
        return padNum(i, lngDigits) + '\t' + (strName || x[2].url());
      }).concat('').concat(lstFound.map(function (x, i) {
        return padNum(i, lngDigits) + '\t' + x[2].url();
      }));

    sa.activate();

    var varChosen = sa.chooseFromList(lstNames, {
      withTitle: strTitle,
      withPrompt: "Choose a " + strBrowsers + " tab:",
      defaultItems: [''],
      multipleSelectionsAllowed: false
    });
    if (varChosen && varChosen[0].length) {
      lstChosen = [lstFound[parseInt(varChosen[0].split('\t')[0], 10)]];
    };
  } else lstChosen = lstFound;

  // ACTIVATE WINDOW AND TAB, RETURNING NAME & URL
  return lstChosen.map(function (oTab) {
    var b = (oTab[0]),
      w = (oTab[1]),
      t = (oTab[2]);

    b.activate();
    w.visible = false;

    if (b.name()[0] === "G") {
      var idTab = t.id();
      
      w.activeTabIndex = w.tabs().reduce(function (acc, x, i) {
        return idTab === x.id() ? i : acc;
      }, 0) + 1;
    } else w.currentTab = t;
    
    w.visible = true;

    return '[' + t.name() + '](' + t.url() + ')';
  }).join('\n');

})(false);
4 Likes

Hey Eddie,

I've been using variations on this script/macro for well over a decade.

-Chris

Rob, very impressive JavaScript.
Works wonders, but is very difficult to follow.

I'm getting this warning msg in the final notify. Everything seems to work OK, just wondering what it means.

2015-12-05 15:39:29.996 osascript[4636:350967] warning: failed to get scripting definition from /usr/bin/osascript; it may not be scriptable.
jxa - Google Search

That’s a standard warning which osascript seems to put out when it encounters JS rather than AS. The trick is to uncheck ‘Include Errors’ in the action settings.

Chris and Rob:

Thank you very much!

Rob, I’ve started out using your Safari-only macro for now (I’m not currently using Chrome). One thing I noticed: after typing text to match in the dialog, when it brings the window/tab to the front, a Safari window on the other display also come to the front. Is that just a side-effect of activating Safari via AppleScript, or is there some way to prevent that?

Thanks again! These macros are amazingly helpful.

I think that’s a consequence of using Application.activate() - the whole browser application comes to the front.

I’ll look to see if there’s a way of doing it for one Safari window only - I saw a note somewhere about a technique for going though the shell to activate a Chrome window, rather than the whole application, but I think it may create an additional window, rather than reusing the existing tab.

I missed this discussion when I wrote this post: Go to the Keystroke Maestro forum. The macro is much simpler, but may be a better fit for some purposes.

Discourse question: what mechanism makes a link to a macro appear to the right like the two above?

Just post a link to another topic, and the forum software will automatically show it on the right column.

Quite impressive. Suggestion: Escape should close the dialog, like Cancel. When you have a long list of tabs it is tedious to drag the mouse all the way to the bottom in order to cancel.

The gray bar in the middle of the list should have a title inside it. I assume it separates Chrome tabs from Safari tabs*, but I don’t know which is on top and which is on bottom. This would become more important if…

can get a version of this that also works for Firefox? I realize their browser implementation is unusual, it might require an entirely different approach (in particular programming inside Firefox with their functions and web page structural i haven’t looked at your code closely enough to guess what kind of effort would be required to implement it in Firefox.

I have found a bizarre case, but I can’t give you much information about it. The same problem occurs with this script:


tell application "Safari"
    activate
    repeat with theWindow in (every window where its document is not missing value)
        log " " & the name of theWindow as item
        repeat with theTab in every tab of theWindow
            log "      " & the name of theTab
        end repeat
    end repeat
end tell

What happens is that there are two tabs —the same two — that get included for every window that are not in those windows. I do recognize them, and I am pretty sure they are in some window, but according to the output of my script and to Safari’s Window menu, they are not the front tab of any window.

Your script finds many copies of these tabs too. If I select one of the occurrences of these tabs I get to a window that (a) does not contain that tab and (b) even though it has 4 tabs, none of them are active — the window is empty.

I have quit and restarted Safari but the same problem was there. I don’t doubt that if I close all the windows, quit Safari, and reopen it the problem will have been solved, and I have never noticed this problem before, so I guess I can just let it pass, but might you have any idea what’s going on.

I don’t want to lose my windows and tabs – there are about 250 open. Now, I know this ridiculous, and in fact the script I showed above is the outline of one I am working on that saves all the window names and tab links to an HTML file. I am working on another script that reopens the windows and tabs. (As far as I know there is no session saver add-on for Safari, and I am not aware of any scripts that do this.) These scripts should not be difficult, but I have encountered a variety of interesting problems along the way, some of which I have posted to the applescript-users mailing list.

I'm curious. What is the use case that requires 250 windows/tabs open at once?

Hey Mitchell,

Sure there is

Session Restore on Apple's Safari Extension's site.

My preference by far: Sessions

Try out my script (in the post below) to visualize your windows and tabs:

I've written several scripts over the years to save and restore Safari sessions, but these days I just use the Sessions extension.

I was playing with one not too long ago to write them to an HTML file, but I can't find it right now.

-Chris

Hey Mitchell,

While I like brevity as much as the next guy, I generally find this type of construct to be counterproductive – because you can't look a the every-window list to visualize or debug.

This will get you everything you need:

tell application "Safari"
   set {tabNameList, tabURL} to {name, URL} of tabs of (windows where its document is not missing value)
end tell

Iterating through the text lists it creates is much faster than other methods (I've spent hours proving this).

Each list item represents the tabs of a window. so you can segregate by window if desired.

tabNameList

{
  {
    "Apple Mac OS X Software & Apps - Discover & Download : MacUpdate", 
    "MacScripter / Odd FileMaker result", 
    "MacScripter / Convert content of txt file back to a list of lists", 
    "MacScripter / copy source files to multiple new filenames in new folders from csv"
  }, 
  {
    "Search titles of Safari or Chrome tabs in multiple windows and bring match to front - general - Keyboard Maestro Discourse"
  }, 
  {
    "Forms and Form Fields :: Eloquent JavaScript"
  }, 
}, 

tabURL

{
  {
    "http://www.macupdate.com/", 
    "http://macscripter.net/viewtopic.php?id=45101", 
    "http://macscripter.net/viewtopic.php?id=45099", 
    "http://macscripter.net/viewtopic.php?id=45095"
  }, 
  {
    "https://forum.keyboardmaestro.com/t/search-titles-of-safari-or-chrome-tabs-in-multiple-windows-and-bring-match-to-front/2516/15"
  }, 
  {
    "http://eloquentjavascript.net/18_forms.html"
  }, 
}

-Chris

Thanks. I never even knew there were macros past the first page. In the past there were so few macros, and search never seemed to find anything, that I had given up on there being anyway to find extensions other than scrolling, and I didn't see any way to get past the first page (is there one?). But it was dumb of me to not to Google for it.

I like that formulation. The code I showed is not my real code, it was a minimization that revealed the problem. What yours doesn't do that led me into some contortions was that I thought I'd be clever and not only restore the tabs of each window but bring to the front the tab that had been at the front when I took the snapshot.

Looks good. Too bad it will be frozen at the end of this year.