Skip to content

Instantly share code, notes, and snippets.

@ProLoser
Last active August 29, 2015 14:17
Show Gist options
  • Save ProLoser/a55406df872c83dd71ea to your computer and use it in GitHub Desktop.
Save ProLoser/a55406df872c83dd71ea to your computer and use it in GitHub Desktop.
Slate Config
$ = (function() {
$ = function() {
var args = _.toArray(arguments);
var sequence = false;
var func;
if (args.length == 1 && _.isArray(args[0])) {
sequence = true;
} else {
func = args.shift();
}
function call(win, func, args) {
try {
ret = win[func].apply(win, args);
} finally {
if (win.isOffscreen())
win.rescue();
}
}
function dispatch(win) {
if (sequence) {
_.each(args, function(sub) {
var func = sub.shift();
call(win, func, sub);
});
} else {
call(win, func, args);
}
}
return dispatch;
};
function Proxy(parent, keys, base) {
var proxy = {};
var _super = {};
_.each(keys, function(key) {
var value = parent[key];
if (_.isFunction(value)) {
_super[key] = proxy[key] = (function() {
var func = value;
return function() {
return func.apply(parent, arguments);
};
})();
} else {
_super[key] = proxy[key] = parent[key];
}
});
$.addNames(base, function(member, key) {
var parent = base.name;
if (proxy.title) {
var title = '"' + proxy.title() + '"';
if (proxy.app) {
var name = proxy.app().name();
title = '[' + name + ']: ' + title;
}
parent += '(' + title + ')';
}
return parent + '.' + key;
});
_.extend(proxy, base);
proxy._super = _super;
if (proxy.update)
proxy.update();
return proxy;
}
function ProxyMember() {
function wrapper() {
return wrapper._init.apply(wrapper, arguments);
}
return wrapper;
}
_.extend($, {
debug: false,
log: function() {
if (this.debug) {
S.log.apply(S, arguments);
}
},
addNames: function(obj, extra) {
_.each(obj, function(member, key) {
if (_.isFunction(member)) {
if (_.isFunction(extra)) {
member._name = extra(member, key);
} else {
member._name = key + '()';
}
}
});
},
backtrace: function(error) {
if (error) {
if (error instanceof Error) {
$.log('Error (line ' + error.line + ') : ' + error);
} else {
$.log('Error: ' + error);
}
}
var stack = [];
var current = arguments.callee.caller;
var iterations = 0;
while (current) {
iterations++;
if (iterations > 10) {
break; // nope nope nope
}
var fn = current;
var name = fn._name;
// determine the function name
if (! name)
name = (fn.name || fn);
var args = JSON.stringify(_.toArray(current.arguments));
args = args.replace(/^\[/, '(').replace(/]$/, ')');
var pretty = name + args;
stack.push(pretty);
// add a hotkey line to the stacktrace when we get to that point
if (fn._hotkey) {
var key = fn._hotkey;
if (_.contains(key, ':')) {
var parts = key.split(':');
key = parts[0];
mods = parts[1].replace(/;/g, '+');
if (mods) {
key = mods + '-' + key;
}
}
key = key.replace(/\b\S/g, function(a) { return a.toUpperCase(); });
stack.push('Hotkey Pressed: "' + key + '"');
}
current = current.caller;
}
$.log('Backtrace:');
for (var i = 0; i < stack.length; i++) {
$.log(' - ' + i + ': ' + stack[i]);
}
},
focus: function(name) {
name = name.replace('"', '\"');
return 'focus "' + name + '"';
},
chain: function() {
var ops = [];
_.each(arguments, function(op) {
if (_.isString(op)) {
op = slate.operationFromString(op);
}
if (_.isFunction(op)) {
var call = op;
op = function(win) {
return call($.window(win));
};
}
ops.push(op);
});
return slate.operation('chain', {operations: ops});
},
rect: ProxyMember(),
window: ProxyMember(),
screen: ProxyMember()
});
$.addNames($);
_.extend($.rect, {
_keys: ['x', 'y', 'width', 'height'],
_init: function(rect) {
if (rect === undefined) {
$.log('slate.js - warning: offscreen window?');
rect = {x: 0, y: 0, width: 1, height: 1};
}
return Proxy(rect, this._keys, $.rect);
}
});
_.extend($.screen, {
_keys: ['id', 'rect', 'visibleRect', 'isMain'],
_init: function(screen) {
if (screen == undefined) {
screen = S.screen();
}
return Proxy(screen, this._keys, $.screen);
},
rect: function() {
return $.rect(this._super.visibleRect());
},
update: function() {
var _super = this._super;
_.extend(this, this.rect());
this._super = _super;
}
});
_.extend($.window, {
_keys: [
'title', 'topLeft', 'size', 'rect', 'pid',
'focus', 'isMinimizedOrHidden', 'isMain',
'move', 'isMovable', 'resize', 'isResizable',
'doOperation', 'screen', 'app'
],
_init: function(win) {
if (win == undefined) {
win = S.window();
}
var keys =
win = Proxy(win, this._keys, $.window);
if (win.isOffscreen())
win.rescue();
return win;
},
// internals
rect: function() {
return $.rect(this._super.rect());
},
screen: function() {
return $.screen(this._super.screen());
},
update: function() {
// make sure to update if you use any internal move/resize ops
var _super = this._super;
_.extend(this, this.rect());
this._super = _super;
},
isOffscreen: function() {
return this.screen().visibleRect() === undefined;
},
rescue: function() {
this.op('throw 0 resize');
this.op('throw 0 resize');
},
op: function(cmd, args) {
var op;
if (args === undefined) {
op = slate.operationFromString(cmd);
} else {
op = slate.operation(cmd, args);
}
return this.doOperation(op);
}
});
return $;
})();
/* Slate monkeypatching
*
* Here, we replace slate commands and add some new ones.
*
* slate.alias(alias, modifier):
* Binds 'alias' to 'modifier'.
*
* Example:
* slate.alias('hyper', 'cmd;alt;shift');
* slate.bind('u:hyper', 'throw 0');
*
* slate.removeAlias(alias):
* Removes a previously created alias.
*
* slate.bind(key, operation):
* Bind a key to operation(s).
*
* Enhanced as follows:
*
* * Enables 'chain' functionality via array:
* slate.bind('a': ['throw 0', 'throw 1']);
*
* * Allows usage of string ops without calling operationFromString()
* * Overloads window objects passed to function callbacks.
* * Adds backtrace metadata.
*
* slate.bindAll(object):
* Bind key(s) to operation(s).
*
* Enhanced as follows:
* * Allows deeply nested bindings with inherited modifiers:
* slate.bind({
* hyper: {
* u: 'throw 0', // hyper+u
* shift: { // hyper+shift
* u: 'throw 1' // hyper+shift+u
* }
* }
* });
*
*/
(function() {
var windows = {};
var aliases = {};
var preHooks = [];
var postHooks = [];
function appendModifier(key, modifier) {
modifier = modifier.replace(':', ';');
if (key.indexOf(':') !== -1) {
return key + ';' + modifier;
} else {
return key + ':' + modifier;
}
}
function makeCallback(key, op) {
var func;
if (_.isArray(op)) {
var ops = [];
_.each(op, function(o) {
o = makeCallback(key, o);
ops.push(o);
});
return $.chain.apply(null, ops);
} else if (_.isString(op)) {
var strOp = op;
func = function(win) {
win.op(strOp);
};
} else if (_.isFunction(op)) {
func = op;
} else {
S.log('unknown op:', op, typeof op);
return null;
}
return function(win) {
try {
_.each(preHooks, function(callback) {
callback(key, win);
});
} catch (e) {
$.backtrace(e);
S.log('supressing exception in pre-hook.');
}
try {
return func(win);
} catch (e) {
$.backtrace(e);
throw e;
} finally {
try {
_.each(postHooks, function(callback) {
callback(key, win);
});
} catch (e) {
throw e;
}
}
};
}
var _bind = slate.bind;
_.extend(slate, {
alias: function(alias, mod) {
aliases[alias] = mod;
},
removeAlias: function(alias) {
delete aliases[alias];
},
pre: function(func) {
preHooks.push(func);
},
post: function(func) {
postHooks.push(func);
},
bind: function(key, op) {
_.each(aliases, function(mod, alias) {
key = key.replace(alias, mod);
});
if (_.isObject(op) && !(_.isArray(op) || _.isFunction(op))) {
// nested modifiers
slate.bindAll(op, key);
return;
}
op = makeCallback(key, op);
if (_.isFunction(op)) {
function callback(win) {
if (win === undefined) {
// this won't go well
return false;
}
// reuse proxy objects in callbacks
// this means you can store persistent data on a $.window()
var pid = win.pid();
if (_.has(windows, pid)) {
win = _.extend(windows[pid], $.window(win));
} else {
win = windows[pid] = $.window(win);
}
return op(win);
}
callback._hotkey = key;
_bind(key, callback);
} else if (op) {
_bind(key, op);
}
},
bindAll: function(obj, modifier) {
_.each(obj, function(op, key) {
if (modifier !== undefined) {
key = appendModifier(key, modifier);
}
slate.bind(key, op);
});
}
});
})();
// prototype+extensions
/* Rect methods.
* Keep in mind 'Window objects' and 'Screen objects'
* inherit all properties from their rect.
*
* rect.bordered(borderSize):
* Shrink the rect by a border and return it.
*
* rect.left():
* rect.right():
* rect.top():
* rect.bottom():
* Read the code below. It's simple wrapping/math.
*
* rect.midY():
* Returns the rect's vertical center.
*
* rect.midX():
* Returns the rect's horizontal center.
*
*/
_.extend($.rect, {
bordered: function(border) {
this.x += border / 2;
this.y += border / 2;
this.width -= border;
this.height -= border;
return this;
},
left: function() {
return this.x;
},
right: function() {
return this.x + this.width;
},
top: function() {
return this.y;
},
bottom: function() {
return this.y + this.height;
},
midY: function() {
return this.y + (this.height / 2);
},
midX: function() {
return this.x + (this.width / 2);
}
});
/* Window methods
*
* A few standard methods are replaced, and some operations
* are completely replaced with very similar object methods.
*
* window.barResize(dir, div, pos):
* Turns a window into a full-height or width bar across the screen,
* and pushes it to an edge.
*
* @dir = (left, right, top, bottom)
* @div (default=2) resizes the perpindicular side,
* with size based on the corresponding screen dimension
* @pos (default=0) can be used to place the resulting bar
* somewhere in the middle of the screen.
*
* window.center(dir, horiz, vert):
* Centers a window in direction, optionally changing the dimensions
* by a screen division horizontally/vertically.
*
* @dir: Pick one of the following:
* 1. any combination of (top, left, right, bottom).
* 2. one of "horiz", "vert", 'center"
* Valid choices include (but aren't limited to):
* "left", "top-left", "right", "bottom-right", "center", "horiz"
*
* @horiz, @vert (optional): see window.divResize() for details
* (You must provide both @horiz and @vert for the resize to take place).
*
* window.corner(dir, horiz, vert):
* Places a window in a corner, resizing by horiz and vert.
*
* @corner: Pick from:
* "top-left", "top-right", "bottom-left", "bottom-right"
*
* @horiz, @vert (optional): see window.divResize() for details
*
* window.divResize(horiz, vert):
* Resizes a window by dividing the current screen.
*
* @horiz (default=1): width = screen.width / @horiz
* @vert (default=1): height = screen.height / @vert
*
* window.move(args):
* Moves a window to args.x, args.y.
*
* Better than default .move() in the following ways:
* * Logs calls.
* * Refuses bogus coordinates, which can:
* * Crash Slate.
* * Lose your window into the ether.
* * Updates rect properties on the window (x, y, width, height).
*
* window.push(dir):
* Alias for window.snap(dir).
*
* window.resize(args):
* Resizes a window to args.width, args.height.
*
* Better than the default .resize() in the following ways:
* * Logs calls.
* * Refuses bogus width/height.
* * Guaranteed to end up <= your specified size, even in Terminal.app.
* * Won't get confused if the Window is in a weird position.
* * Updates rect properties on the window.
*
* window.snap(dir):
* Snaps a window to a direction, with slight bias
* to the top left of the screen for consistency.
*
* @dir: any combination of "left", "top", "right", "bottom"
* Examples: "left", "top-left", "left-top", "bottom", "bottom-right"
*
* window.throw(screen, resize):
* Aliased to 'toss'. Throw is a reserved keyword in JS.
*
* window.toss(screen, resize):
* Throws a window to a screen, resizing if desired.
*
* Advantages over default 'throw':
* * Resize consistently works the first time.
* * Better resize heuristics.
*/
_.extend($.window, {
barResize: function(dir, div, pos) {
div = (div || 2);
var s = this.screen();
if (dir == 'left' || dir == 'right') {
this.divResize(div, 1, pos, 0);
} else if (dir == 'top' || dir == 'bottom') {
this.divResize(1, div, 0, pos);
}
},
center: function(dir, horiz, vert) {
$.log('center', dir, horiz, vert);
if (horiz && vert)
this.divResize(horiz, vert);
var s = this.screen();
var width, height;
var x = this.x;
var y = this.y;
if (dir == 'left' || dir == 'right') {
this.center('vert');
this.snap(dir);
} else if (dir == 'top' || dir == 'bottom') {
this.center('horiz');
this.snap(dir);
} else if (dir == 'center') {
this.center('horiz');
this.center('vert');
} else if (dir == 'horiz') {
width = this.width;
x = s.midX() - (width / 2);
this.move({x: x, y: y});
} else if (dir == 'vert') {
height = this.height;
y = s.midY() - (height / 2);
this.move({x: x, y: y});
} else {
return;
}
},
corner: function(corner, horiz, vert) {
$.log('corner', JSON.stringify([corner, horiz, vert]));
this.divResize(horiz || 2, vert || 2);
this.snap(corner);
},
divResize: function(horiz, vert, horizPos, vertPos) {
var s = this.screen();
var width = s.width / (horiz || 1);
var height = s.height / (vert || 1);
this.resize({width: width, height: height});
if ( horizPos && ( ( horiz > 1 && horiz < 2) || horizPos == horiz - 1 ) )
width = s.width - width;
else
width = width * (horizPos || 0);
if ( vertPos && ( (vert > 1 && vert < 2) || vertPos && vertPos == vert - 1 ) )
height = s.height - height;
else
height = height * (vertPos || 0);
this.move({x: width, y: height});
},
move: function(args) {
// TODO: also allow calling with (x, y)
$.log('move', JSON.stringify(args));
var x = args.x;
var y = args.y;
if ((x !== 0 && !x) || (y !== 0 && !y)) {
$.backtrace('Invalid move destination.');
return;
}
this._super.move(args);
this.update();
},
push: function(dir) {
this.snap(dir);
},
resize: function(args) {
// TODO: allow calling with (width, height)
$.log('resize', JSON.stringify(args));
var width = args.width;
var height = args.height;
if ((width !== 0 && !width || width < 0) ||
(height !== 0 && !height || height < 0)) {
$.backtrace('Invalid resize.');
return;
}
var s = this.screen();
// give ourselves enough room to resize
var x = Math.min(this.x, s.right() - width);
var y = Math.min(this.y, s.bottom() - height);
this.move({x: x, y: y});
this._super.resize(args);
while (this.rect().height > height + 1) {
args.height -= 1;
if (args.height < 0)
break;
this._super.resize(args);
}
while (this.rect().width > width + 1) {
args.width -= 1;
if (args.width < 0)
break;
this._super.resize(args);
}
this.update();
},
snap: function(dir) {
// like slate's push, but rounds left/top to screen border/center
$.log('snap', dir);
var s = this.screen();
var x = this.x;
var y = this.y;
// corners
var match = dir.match(/^([^-]+)-([^-]+)$/);
if (match) {
this.snap(match[1]);
this.snap(match[2]);
return;
}
// cardinal directions
var mid;
if (dir == 'left') {
x = s.left();
} else if (dir == 'top') {
y = s.top();
} else if (dir == 'right') {
x = s.right() - this.width;
mid = Math.abs(x - s.midX());
if (this.width / mid < 0.05) {
x = s.midX();
}
} else if (dir == 'bottom') {
y = s.bottom() - this.height;
mid = Math.abs(y - s.midY());
if (this.height / mid < 0.05) {
x = s.midY();
}
}
this.move({x: x, y: y});
},
'throw': function() {
return this.toss.apply(this, arguments);
},
toss: function(num, resize) {
var count, current;
if (num === '+' || num === '-') {
count = slate.screenCount();
$.log('count', count);
current = this.screen().id();
$.log('current', current);
num = (num === '+') ? ++current : --current;
count--;
if (num > count) num = 0;
if (num < 0) num = count;
$.log('selected', num);
}
var screen = slate.screenForRef(num.toString());
var s = $.screen(screen);
if (resize)
this.resize(s.rect());
this.move(s.rect());
if (resize)
this.resize(s.rect());
}
});
/* File: slate.example.js
* Author: lunixbochs (Ryan Hileman) and ProLoser (Dean Sofer)
* Project: http://github.com/lunixbochs/reslate https://github.com/ProLoser/reslate
*/
// enable to see debug messages in Console.app
// $.debug = true;
slate.alias('hyper', 'ctrl;alt;cmd');
// begin config
slate.configAll({
defaultToCurrentScreen: true,
nudgePercentOf: 'screenSize',
resizePercentOf: 'screenSize',
undoOps: [
'active-snapshot',
'chain',
'grid',
'layout',
'move',
'resize',
'sequence',
'shell',
'push'
]
});
function position(win) {
$.log('position', 'cols:', win.splitx, 'rows:', win.splity, 'x:', win.offx, 'y:', win.offy);
var snapRight = (win.offx > 1 && win.offx < 2) && win.offx > 0,
snapBottom = (win.offy > 1 && win.offy < 2) && win.offy > 0;
win.divResize(win.splitx, win.splity, win.offx, win.offy, snapRight, snapBottom);
}
function reset(win, hard) {
if (!win.splitx || hard) {
win.splitx = 1;
win.splity = 1;
win.offx = 0;
win.offy = 0;
}
}
var screenCount = slate.screenCount();
steps = [1.5, 2, 3];
var lastStep = steps[steps.length-1];
function nextStep(step) {
var index = steps.indexOf(step);
return (index >= steps.length-1) ? steps[0] : steps[++index];
}
// bindings
slate.bindAll({
hyper: {
shift: {
left: function(win) {
reset(win);
if (win.offx === 0) {
if (screenCount > 1)
win.toss('-');
win.offx = Math.ceil(win.splitx) - 1;
} else {
win.offx--;
}
position(win);
},
right: function(win) {
reset(win);
win.offx++;
if (win.offx >= Math.ceil(win.splitx)) {
if (screenCount > 1)
win.toss('+');
win.offx = 0;
}
position(win);
},
up: function(win) {
reset(win);
if (win.offy === 0) {
win.offy = Math.ceil(win.splity) - 1;
} else {
win.offy--;
}
position(win);
},
down: function(win) {
reset(win);
win.offy++;
if (win.offy >= Math.ceil(win.splity))
win.offy = 0;
position(win);
}
},
left: function(win) {
reset(win);
if (win.offx === 0) {
if (screenCount > 1 && win.splitx === lastStep) {
reset(win, true);
win.toss('-', 'resize');
} else {
win.splitx = nextStep(win.splitx);
position(win);
}
} else {
win.offx--;
position(win);
}
},
j: function(win){this.left(win);},
right: function(win) {
reset(win);
if (win.offx === win.splitx - 1 || win.offx && win.splitx > 1 && win.splitx < 2) {
if (screenCount > 1 && win.splitx === lastStep) {
reset(win, true);
win.toss('+', 'resize');
} else {
var nextSplit = nextStep(win.splitx);
if (nextSplit > 2 || !win.offx)
win.offx++;
if (nextSplit < win.splitx)
win.offx = Math.ceil(nextSplit) - 1;
win.splitx = nextSplit;
position(win);
}
} else {
win.offx++;
position(win);
}
},
l: function(win){this.right(win);},
up: function(win) {
reset(win);
if (win.offy === 0) {
win.splity = nextStep(win.splity);
} else {
win.offy--;
}
position(win);
},
i: function(win){this.up(win);},
down: function(win) {
reset(win);
if (win.offy === win.splity - 1 || win.offy && win.splity > 1 && win.splity < 2) {
var nextSplit = nextStep(win.splity);
if (nextSplit > 2 || !win.offy)
win.offy++;
if (nextSplit < win.splity)
win.offy = Math.ceil(nextSplit) - 1;
win.splity = nextSplit;
} else {
win.offy++;
}
position(win);
},
k: function(win){this.down(win);},
space: function(win) {
reset(win, true);
position(win);
},
'return': [
$('barResize', 'left', 1),
$('center', 'center', 1.5, 1.25)
],
// throw to monitor
'`': ['throw 0 resize',
'throw 1 resize'],
// direct focus
// utility functions
f1: 'relaunch',
z: 'undo'
}
});
// Auto Snapshotting
// Returns name generated from screen resolutions
function getName() {
var name = [];
slate.eachScreen(function(screen){
var rect = screen.rect();
name.push(rect.width+'x'+rect.height);
});
return name;
}
// Save snapshot
function save(event, win){
$.log('saved', getName());
slate.operation("snapshot", {
name: getName().join('y'),
save: true
}).run();
// slate.default(getName(), getName().join('y'));
}
// Load snapshot
function load(event, win){
$.log('loaded', getName());
slate.operation("activate-snapshot", {
name: getName().join('y')
}).run();
}
// slate.on('windowMoved', save);
// slate.on('windowResized', save);
// slate.on('appOpened', load);
// TODO: Requires 'screenConfigurationChanged' for some reason
slate.on('screenConfigurationChanged', function(){
screenCount = slate.screenCount();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment