Created
November 2, 2016 20:05
-
-
Save ontucker/9f51d990631ac19f46d92beea0c1e3e6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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