Skip to content

Instantly share code, notes, and snippets.

@ontucker
Created November 2, 2016 20:05
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ontucker/9f51d990631ac19f46d92beea0c1e3e6 to your computer and use it in GitHub Desktop.
Save ontucker/9f51d990631ac19f46d92beea0c1e3e6 to your computer and use it in GitHub Desktop.
// FindMode - phoenix binding for an interactive window-finding mode
// * bind to a key by calling the instance's 'bind' method, which has the
// same first two args as phoenix's Phoenix.bind() call. The third argument
// is an array of other key bindings which should be temporarily disabled
// while in the FindMode mode.
// * while in Find mode:
// - hit escape to exit the mode (todo: and restore the window stack)
// - type a regex to interactively auto-raise the first window that matches
// - backspace to remove chars from the regex
// - hit tab to cycle to the next matching window for the current regex
// - hit enter to exit the mode and leave the currently-selected window active
// - hit cmd-enter to exit the mode and raise all matching windows
// - hit shift-enter to exit the mode and maximize the selected window
//
// Example usage:
//
// // Unrelated binding which should be disabled while in Find Mode:
// var otherBinding = Phoenix.bind('enter', ['cmd'], function() { ... });
//
// // The Find Mode object and binding it. Note that 'otherBinding' is
// // passed in the final argument to findMode.bind and it will automatically
// // be disabled and re-enabled
// var findMode = new FindMode();
// var findModeBinding = findMode.bind('f', hyper, [otherBinding]);
// disabling finding hidden windows for now because the El Capitan upgrade
// caused the allWindows() API call to be extremely slow.
var findHiddenWindows = false;
var FindMode = function() {
function FindMode() {
this.unbindables = [];
this.selectIndex = 0;
this.foundWindows = [];
this.input = "";
var self = this;
var selfbinders = ["enterMode", "exitMode", "exitAbort", "exitSelect", "exitSelectMaximized", "exitSelectAll", "nextFound"];
_(selfbinders).each(function(name) {
self[name] = FindMode.prototype[name].bind(self);
});
}
FindMode.prototype.bind = function(key, modifiers, unbindables) {
if(this.unbindables) {
this.unbindables = this.unbindables.concat(unbindables);
}
var binding = Phoenix.bind(key, modifiers, this.enterMode.bind(this));
this.unbindables.push(binding);
return binding;
}
FindMode.prototype.enterMode = function() {
Phoenix.log("entering FindMode");
//api.alert("entering mode");
_(this.unbindables).each(function(b) {
b.disable();
});
Phoenix.log("setting up bindings");
this.setupBindings();
this.input = "";
Phoenix.log("setting status");
status("find: ");
}
FindMode.prototype.exitMode = function() {
Phoenix.log("exiting FindMode");
_(this.bindings).each(function(b) {
b.disable();
});
_(this.unbindables).each(function(b) {
b.enable();
});
status();
}
FindMode.prototype.setupBindings = function() {
if(!this.bindings) {
this.bindings = [];
Phoenix.log("binding special keys");
this.bindings.push(Phoenix.bind('escape', [], this.exitAbort));
this.bindings.push(Phoenix.bind('return', [], this.exitSelect));
this.bindings.push(Phoenix.bind('return', ['shift'], this.exitSelectMaximized));
this.bindings.push(Phoenix.bind('return', ['cmd'], this.exitSelectAll));
this.bindings.push(Phoenix.bind('tab', [], this.nextFound));
Phoenix.log("binding input keys");
var self = this;
// pattern input keys:
// create shifted and unshifted bindings for all keys in 'keys', plus backspace
for(var i=0; i<keys.length; i++) {
var key = keys.substr(i,1);
var shifted = shiftKeys.substr(i,1);
var keyHandler = this.handleInput.bind(this, key);
var shiftedKeyHandler = this.handleInput.bind(this, shifted);
this.bindings.push(Phoenix.bind(key, [], keyHandler));
this.bindings.push(Phoenix.bind(key, ['shift'], shiftedKeyHandler));
}
Phoenix.log("binding delete key");
this.bindings.push(Phoenix.bind('delete', [], this.handleInput.bind(this, 'delete')));
Phoenix.log("finished with bindings");
} else {
Phoenix.log("re-enabling FindMode bindings");
_(this.bindings).each(function(b) {
b.enable();
});
}
}
function clearStatus() { status(); }
function status(msg) {
if(this.modal && !msg) {
this.modal.close();
this.modal = null;
}
if(msg) {
if(!this.modal) {
this.modal = new Modal();
}
this.modal.message = msg;
this.modal.show();
}
}
function allWindowSet() {
Phoenix.log("getting window set");
var visWindows = Window.windows();
return visWindows;
// the following is very, very slow on El Capitan for some reason. However,
// Phoenix v2 returns all windows in Window.windows() so I don't think we
// need this.
//var allWindows = Window.otherWindowsOnAllScreens();
if(!findHiddenWindows) return visWindows;
var minWindows = _(allWindows).filter(function(w) { return w.isMinimized(); });
Phoenix.log("filtered minWindows down from " + allWindows.length + " to " + minWindows.length);
return visWindows.concat(minWindows);
}
FindMode.prototype.findAndRaiseWindow = function(pat) {
var pat = new RegExp(pat, "i");
var currentWindow = Window.focusedWindow();
var windows = allWindowSet();
var found = _(windows).filter(function(w) {
var windowString = appTitleFilter(w.app().name()) + " " + w.title();
var result = windowString.match(pat);
return result;
});
if(found.length > 0) {
var target = found[0];
Phoenix.log("found a window, target = " + target.title() + ", currentWindow = " + currentWindow.title())
if(currentWindow.hash() == target.hash()) {
target.minimize();
} else {
raise(target);
}
}
};
FindMode.prototype.findWindows = function findWindows() {
var pat = new RegExp(this.input, "i");
Phoenix.log("getting windows");
var windows = allWindowSet();
Phoenix.log("got " + windows.length + " windows");
Phoenix.log("using pat: " + this.input);
this.foundWindows = _(windows).filter(function(w) {
var windowString = appTitleFilter(w.app().name()) + " " + w.title();
var result = windowString.match(pat);
Phoenix.log("testing " + windowString + " against " + pat + ": " + (result?"match":"no match"));
return result;
});
var count = this.foundWindows.length;
status("find: " + this.input + "\n" + count + ((count == 1) ? " result" : " results"));
this.selectIndex = 0;
}
FindMode.prototype.handleInput = function(inp) {
if(inp == "delete") {
this.input = this.input.substr(0, this.input.length - 1);
} else {
this.input += inp;
}
this.findWindows();
this.raiseFound();
}
FindMode.prototype.raiseFound = function(maximize) {
if(this.foundWindows.length == 0) return;
var w = this.foundWindows[this.selectIndex % this.foundWindows.length];
//api.alert("raising " + w.title() + " for input " + this.input);
raise(w, maximize);
}
FindMode.prototype.raiseAll = function() {
for(var i=this.foundWindows.length - 1; i>=0; i--) {
raise(this.foundWindows[i]);
}
}
FindMode.prototype.nextFound = function() {
this.selectIndex++;
this.raiseFound();
}
FindMode.prototype.exitAbort = function() {
//fixme: would be nice to restore window stack here
this.exitMode();
//api.alert("exitAbort");
}
FindMode.prototype.exitSelect = function() {
this.raiseFound();
this.exitMode();
//api.alert("exitSelect");
}
FindMode.prototype.exitSelectMaximized = function() {
this.raiseFound(true);
this.exitMode();
//api.alert("exitSelectMaximized");
}
FindMode.prototype.exitSelectAll = function() {
this.raiseAll();
this.exitMode();
//api.alert("exitSelectAll");
}
//todo: support more keys, add null handlers so unsupported keys get ignored
var keys = 'abcdefghijklmnopqrstuvwxyz1234567890.';
var shiftKeys = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()>';
// note: shiftKeys must have same offsets in string as corresponding 'keys'
// I'm never going to be interested in typing the "google" part of "Google Chrome",
// so let's do some custom transformations on app titles to make it easier to find
// other windows that might have "google" in their titles. More of these will come.
function appTitleFilter(t) {
var subs = [
{ pat: /^Google Chrome/, sub: "Chrome" }
];
_(subs).each(function(sub) {
t = t.replace(sub.pat, sub.sub);
});
return t;
}
function raise(w, andMaximize) {
if(w) {
if(w.isMinimized()) {
w.unminimize();
}
// api.log("raising " + w.title() + " for input " + this.input);
w.focus();
if(andMaximize) {
w.setFrame(w.screen().visibleFrameInRectangle());
}
return w;
}
}
return FindMode;
}();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment