Created
December 10, 2021 18:24
-
-
Save rcarmo/5132874cdaf2755e42907508802e864f to your computer and use it in GitHub Desktop.
Phoenix configuration
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
// ******************************************************************************** | |
// Setup and TODO | |
// ******************************************************************************** | |
Phoenix.set({ | |
daemon: false, | |
openAtLogin: true | |
}); | |
/* | |
* API docs: https://github.com/kasper/phoenix/blob/master/docs/API.md | |
* | |
* TODO: | |
* - [ ] Drag and Drop Snap (can't be implemented in curren Phoenix, apparently) | |
* - [ ] New XMonad with precomputed layouts | |
* - [ ] Nearest Rectangle Match | |
* - [ ] Window overlay test | |
* - [ ] Cleanup positionInGrid | |
* - [x] More logical handling of sixths | |
* - [x] Hint Manager class | |
* - [x] Cleanup Constants | |
* - [x] Basic Space handling | |
* - [x] Move mouse pointer on rotation focus changes | |
* - [x] Centered popup | |
* - [x] Frame abstraction | |
*/ | |
// ******************************************************************************** | |
// Constants | |
// ******************************************************************************** | |
const INCREMENT = 50; | |
const PADDING = 8; | |
// DIRECTIONS | |
const NONE = "none", | |
COLS = "Cols", | |
F = FULL = "Full", | |
N = NORTH = "North", | |
S = SOUTH = "South", | |
E = EAST = "East", | |
W = WEST = "West", | |
NW = "North-West", | |
NE = "North-East", | |
SW = "South-West", | |
SE = "South-East"; | |
const TILING_MODES = [NONE, EAST, WEST, COLS]; | |
var MOD = ["shift", "command"]; | |
var ALTMOD = ["shift", "option"]; | |
var HINT_APPEARANCE = "dark"; | |
var HINT_BUTTON = "space"; | |
var HINT_CANCEL = "escape"; | |
var HINT_CHARS = "FJDKSLAGHRUEIWOVNCM"; | |
var LAST_POSITION = { | |
window: null, | |
grid: "", | |
positions: [] | |
} | |
var LAST_POSITION_INDEX = -1; | |
var ICON_CACHE = {} | |
// ******************************************************************************** | |
// Keyboard Bindings | |
// ******************************************************************************** | |
// Moom-like bindings | |
Key.on("return", ["control", "option"], () => {Window.focused().positionInGrid(4,0,3).centerMouse()}); | |
Key.on("left", ["control", "option"], () => steppedSizing(Window.focused(), [[4,0,2], [6,0,3], [16,0,9]])); | |
Key.on("right", ["control", "option"], () => steppedSizing(Window.focused(), [[4,1,3], [6,2,5], [16,6,15]])); | |
Key.on("up", ["control", "option"], () => {Window.focused().positionInGrid(6,0,5).centerMouse()}); | |
Key.on("down", ["control", "option"], () => steppedSizing(Window.focused(), [[8,2,13], [6,1,4]])); | |
Key.on("left", ["shift", "control", "option"], () => {Window.focused().positionInGrid(6,0,3).centerMouse()}); | |
Key.on("right", ["shift", "control", "option"], () => {Window.focused().positionInGrid(6,2,5).centerMouse()}); | |
Key.on("up", ["shift", "control", "option"], () => {Window.focused().reposition(NORTH).centerMouse()}); | |
Key.on("down", ["shift", "control", "option"], () => {Window.focused().reposition(SOUTH).centerMouse()}); | |
// Sixths | |
Key.on(",", ["shift", "control", "option"], () => {Window.focused().positionInGrid(6,0,0).centerMouse()}); | |
Key.on(".", ["shift", "control", "option"], () => {Window.focused().positionInGrid(6,1,1).centerMouse()}); | |
Key.on("-", ["shift", "control", "option"], () => {Window.focused().positionInGrid(6,2,2).centerMouse()}); | |
Key.on(",", ["control", "option"], () => {Window.focused().positionInGrid(6,3,3).centerMouse()}); | |
Key.on(".", ["control", "option"], () => {Window.focused().positionInGrid(6,4,4).centerMouse()}); | |
Key.on("-", ["control", "option"], () => {Window.focused().positionInGrid(6,5,5).centerMouse()}); | |
// Move horizontally between screens | |
Key.on("right", ["control", "option", "command"], () => Window.focused().toScreen(EAST).centerMouse()); | |
Key.on("left", ["control", "option", "command"], () => Window.focused().toScreen(WEST).centerMouse()); | |
// Move horizontally between spaces | |
Key.on("right", ["shift", "control", "option", "command"], () => Window.focused().toSpace(EAST)); | |
Key.on("left", ["shift", "control", "option", "command"], () => Window.focused().toSpace(WEST)); | |
// ******************************************************************************** | |
// Size steps | |
// ******************************************************************************** | |
function steppedSizing(win, gridPositions) { | |
if(!win.isEqual(LAST_POSITION.window)) { | |
LAST_POSITION_INDEX = 0; | |
LAST_POSITION.window = win; | |
} else if(JSON.stringify(LAST_POSITION.positions)!=JSON.stringify(gridPositions)) { | |
LAST_POSITION_INDEX = 0; | |
} | |
res = win.positionInGrid.apply(win, gridPositions[LAST_POSITION_INDEX]).centerMouse(); | |
LAST_POSITION.grid = gridPositions[LAST_POSITION_INDEX].join(","); | |
LAST_POSITION_INDEX = (LAST_POSITION_INDEX + 1) % gridPositions.length; | |
LAST_POSITION.positions = gridPositions; | |
return res | |
} | |
// ******************************************************************************** | |
// Hints | |
// ******************************************************************************** | |
class Hints { | |
constructor() { | |
this.active = false; | |
this.keys = []; | |
this.hints = {}; | |
this.escbind = null; | |
this.bsbind = null; | |
this.id = Math.random(); | |
} // constructor | |
cancel() { | |
for (var activator in this.hints) { | |
if(this.hints[activator]) | |
this.hints[activator].modal.close(); | |
}; | |
// remove all key bindings | |
Key.off(this.escbind); | |
Key.off(this.bsbind); | |
this.keys.map(Key.off); | |
// clear hints | |
this.hints = {}; | |
this.keys = []; | |
this.active = false; | |
return this; | |
} // cancel | |
show(windows, prefix) { | |
self = this; | |
prefix = prefix || ""; | |
// check if there are too many windows and recurse | |
if (windows.length > HINT_CHARS.length) { | |
var partitionSize = Math.floor(windows.length / HINT_CHARS.length); | |
var lists = _.toArray(_.groupBy(windows, function (win, k) { | |
return k % HINT_CHARS.length; | |
})); | |
for (var j = 0; j < HINT_CHARS.length; j++) { | |
return this.show(lists[j], prefix + HINT_CHARS[j]); | |
} | |
return this; | |
} | |
var self = this; | |
windows.forEach(function (win, i) { | |
var label = "", | |
hash = win.hash(); | |
if(!ICON_CACHE[hash]) { | |
ICON_CACHE[hash] = win.app().icon() | |
} | |
if (win.app().windows().length > 1) { | |
label += " | " + win.title().substr(0, 15) + (win.title().length > 15 ? "…" : ""); | |
} | |
var hint = Modal.build({ | |
text: prefix + HINT_CHARS[i] + label, | |
appearance: HINT_APPEARANCE, | |
icon: ICON_CACHE[hash], | |
weight: 16, | |
duration: 0, | |
}).attach(win); | |
var activators = Object.keys(self.hints); | |
// Check for overlaps - TODO: use Frame | |
for (var l = 0; l < activators.length; l++) { | |
var hint2 = self.hints[activators[l]].modal; | |
if ( | |
hint.origin.x < hint2.origin.x + hint2.frame().width + PADDING | |
&& hint.origin.x + hint.frame().width > hint2.origin.x - PADDING | |
&& hint.origin.y < hint2.origin.y + hint2.frame().height + PADDING | |
&& hint.origin.y + hint.frame().width > hint2.origin.y - PADDING | |
) { | |
hint.origin = { | |
x: hint.origin.x, | |
y: hint2.origin.y + hint2.frame().height + PADDING | |
}; | |
l = -1; | |
} | |
} | |
self.hints[prefix + HINT_CHARS[i]] = { | |
win: win, | |
modal: hint, | |
position: 0, | |
active: true | |
}; | |
}); | |
self.escbind = Key.on(HINT_CANCEL, [], function() { | |
self.cancel(); | |
}) | |
this.active = true; | |
return this; | |
} // show | |
activate() { | |
var self = this; | |
if(this.active) { | |
self.cancel(); | |
} else { | |
Event.once("mouseDidLeftClick", function() { | |
self.cancel(); | |
}); | |
this.show(Window.all({visible: true})); | |
var sequence = ""; | |
self.keys = []; | |
HINT_CHARS.split("").forEach(function (hintchar) { | |
// set up each individual hint handler | |
self.keys.push(Key.on(hintchar, [], function () { | |
sequence += hintchar; | |
for (var activator in self.hints) { | |
var hint = self.hints[activator]; | |
if (!hint.active) continue; | |
if (activator[hint.position] === hintchar) { | |
hint.position++; | |
if (hint.position === activator.length) { | |
hint.win.focus(); | |
Mouse.move({ | |
x: hint.modal.origin.x + hint.modal.frame().width / 2, | |
y: Screen.all()[0].frame().height - hint.modal.origin.y - hint.modal.frame().height / 2 | |
}); | |
return self.cancel(); | |
} | |
hint.modal.text = hint.modal.text.substr(1); | |
} else { | |
hint.modal.close(); | |
hint.active = false; | |
} | |
} | |
})); | |
}); | |
self.bsbind = Key.on("delete", [], function () { | |
if (!sequence.length) | |
self.cancel(); | |
var letter = sequence[sequence.length - 1]; | |
sequence = sequence.substr(0, sequence.length - 1); | |
for (var activator in self.hints) { | |
var hint = self.hints[activator]; | |
if (hint.active) { | |
hint.position--; | |
hint.modal.text = letter + hint.modal.text; | |
} else if (activator.substr(0, sequence.length) === sequence) { | |
hint.modal.show(); | |
hint.active = true; | |
} | |
} | |
}); | |
} | |
} // activate | |
} | |
// ******************************************************************************** | |
// Window Manager Abstraction | |
// ******************************************************************************** | |
class WindowManager { | |
constructor() { | |
this.tiling_modes = Array(Screen.all().length) | |
this.tiling_modes.fill(NONE) | |
this.timers = Array(Screen.all().length) | |
this.layouts = Array(Screen.all().length) | |
} | |
change_tiling_mode() { | |
const window = Window.focused(), | |
screen = window.screen(), | |
index = Screen.all().indexOf(screen), | |
visible = screen.windows({visible:true}) | |
// rotate tiling mode | |
this.tiling_modes[index] = TILING_MODES[(TILING_MODES.indexOf(this.tiling_modes[index])+1) % TILING_MODES.length] | |
Phoenix.log(index + " " + this.tiling_modes[index]) | |
this.layouts[index] = new Layout(screen, this.tiling_modes[index]) | |
var modal = Modal.build({ | |
text: "Tiling mode: " + this.tiling_modes[index], | |
appearance: HINT_APPEARANCE, | |
weight: 24, | |
icon: App.get('Phoenix').icon(), | |
duration: 0.5, | |
}).flash(screen) | |
Phoenix.log(this.layouts) | |
this.layouts[index].windows = visible | |
this.layouts[index].apply() | |
Phoenix.log(this.layouts) | |
return this | |
} | |
rotate(dir, focus_only) { | |
const window = Window.focused(), | |
screen = window.screen(), | |
index = Screen.all().indexOf(screen) | |
Phoenix.log(index + " " + dir + " " + this.layouts[index].windows.map(x=>x.hash())) | |
this.layouts[index].rotate(dir).apply(screen) | |
return this | |
} | |
} | |
const wm = new WindowManager() | |
Key.on("z", ["shift", "control"], () => wm.change_tiling_mode()) | |
Key.on("n", ["shift", "control"], () => wm.rotate(-1, false)); | |
Key.on("n", ["shift", "control", "option"], () => wm.rotate(-1, true)); | |
Key.on("m", ["shift", "control"], () => wm.rotate(1, false)); | |
Key.on("m", ["shift", "control", "option"], () => wm.rotate(1, true)); | |
// ******************************************************************************** | |
// Layout Abstraction | |
// ******************************************************************************** | |
class Layout { | |
constructor(screen, mode) { | |
this.frames = [] | |
this.windows = [] | |
switch(mode) { | |
case EAST: | |
this.east(screen) | |
break; | |
case WEST: | |
this.west(screen) | |
break; | |
case COLS: | |
this.cols(screen) | |
break; | |
} | |
} | |
east(screen) { | |
const f = screen.flippedVisibleFrame(), | |
v = screen.windows({visible:true}), | |
c = v.length-1, | |
w = f.width/2, | |
h = ~~(f.height/c), | |
self = this | |
this.screen = screen | |
if(v.length === 1) { | |
this.frames.push(new Frame(f.x, f.y, f.width, f.height).pad()) | |
} else { | |
// main window | |
self.frames.push(new Frame(f.x, f.y, w, f.height) | |
.displace(f.width/2, 0).pad()) | |
// secondary windows | |
for(var i=0; i<c; i++) { | |
self.frames.push(new Frame(f.x, f.y, w, h) | |
.displace(0, h*i).pad()) | |
} | |
} | |
return this | |
} | |
west(screen) { | |
const f = screen.flippedVisibleFrame(), | |
v = screen.windows({visible:true}), | |
c = v.length-1, | |
w = f.width/2, | |
h = ~~(f.height/c), | |
self = this | |
this.screen = screen | |
if(v.length === 1) { | |
self.frames.push(new Frame(f.x, f.y, f.width, f.height).pad()) | |
} else { | |
// main window | |
self.frames.push(new Frame(f.x, f.y, w, f.height) | |
.pad()) | |
// secondary windows | |
for(var i=0; i<c; i++) { | |
self.frames.push(new Frame(f.x, f.y, w, h) | |
.displace(w, h*i).pad()) | |
} | |
} | |
return this | |
} | |
cols(screen) { | |
const f = screen.flippedVisibleFrame(), | |
v = screen.windows({visible:true}), | |
c = v.length, | |
w = ~~(f.width/c), | |
h = f.height, | |
self = this | |
this.screen = screen | |
// all windows | |
for(var i=0; i<c; i++) { | |
self.frames.push(new Frame(f.x, f.y, w, h) | |
.displace(w*i, 0).pad()) | |
} | |
return this | |
} | |
none(screen) { | |
this.frames = [] | |
return this | |
} | |
rotate(dir) { | |
this.windows.rotate(dir) | |
return this | |
} | |
apply() { | |
const self = this | |
if(this.frames.length) | |
for(var i=0;i<self.windows.length;i++) { | |
self.windows[i].setFrame(self.frames[i]) | |
} | |
return this | |
} | |
} | |
// ******************************************************************************** | |
// Frame Abstraction | |
// ******************************************************************************** | |
class Frame { | |
constructor(x, y, width, height) { | |
this.x = x; | |
this.y = y; | |
this.width = width; | |
this.height = height; | |
} | |
pad() { | |
return new Frame( | |
this.x + PADDING/2, | |
this.y + PADDING/2, | |
this.width - PADDING, | |
this.height - PADDING | |
) | |
} | |
snap(screen, dir) { | |
var s = screen.flippedVisibleFrame(), | |
f = new Frame(this.x, this.y, this.width, this.height); | |
if ([E, NE, SE].indexOf(dir) > -1) f.x += s.width - f.width; | |
if ([SE, SW].indexOf(dir) > -1) f.y += s.height - f.height; | |
if (dir === F) f.width = s.width; | |
if ([F, E, W].indexOf(dir) > -1) f.height = s.height; | |
return f; | |
} | |
displace(x, y) { | |
return new Frame( | |
this.x + x, | |
this.y + y, | |
this.width, | |
this.height | |
) | |
} | |
log() { | |
Phoenix.log(this.x + ", " + this.y + ", " + this.width + ", " + this.height); | |
return this; | |
} | |
rect() { | |
return { | |
x: this.x, | |
y: this.y, | |
width: this.width, | |
height: this.height | |
} | |
} | |
ratio(a, b) { | |
var wr = b.width / a.width, | |
hr = b.height / a.height; | |
return ({x, y, width, height}) => { | |
x = Math.round(b.x + (x - a.x) * wr); | |
y = Math.round(b.y + (y - a.y) * hr); | |
width = Math.round(width * wr); | |
height = Math.round(height * hr); | |
return {x, y, width, height} | |
} | |
} | |
} | |
// ******************************************************************************** | |
// Window Extensions | |
// ******************************************************************************** | |
// Snap a window in a given direction | |
Window.prototype.to = function(direction) { | |
var s = this.screen(), | |
f = s.flippedVisibleFrame(), | |
frame = new Frame( | |
f.x, | |
f.y, | |
f.width / 2, | |
f.height / 2, | |
).snap(s, direction).pad(); | |
this.setFrame(frame); | |
}; | |
// Move a window to a given screen | |
Window.prototype.toScreen = function(dir) { | |
var screen = null; | |
if(dir===EAST) { | |
screen = Screen.all().filter((a) => {return a.origin().x > this.screen().origin().x}).sort((a,b) => {return a.origin().x > b.origin().x})[0] | |
} | |
else if(dir===WEST) { | |
screen = Screen.all().filter((a) => {return a.origin().x < this.screen().origin().x}).sort((a,b) => {return a.origin().x < b.origin().x})[0]; | |
} | |
if(screen) { | |
ratio = Frame.prototype.ratio(this.screen().flippedVisibleFrame(), screen.flippedVisibleFrame()); | |
this.setFrame(ratio(this.frame())); | |
} | |
return this; | |
}; | |
Window.prototype.reposition = function (dir) { | |
var l = LAST_POSITION.window, | |
g = LAST_POSITION.grid; | |
if(this.isEqual(l)) { | |
if(dir === NORTH) { | |
switch(g) { | |
case "4,0,2": this.positionInGrid(4,0,0); break; | |
case "4,2,2": this.positionInGrid(4,0,0); break; | |
case "4,1,3": this.positionInGrid(4,1,1); break; | |
case "4,3,3": this.positionInGrid(4,1,1); break; | |
case "6,0,3": this.positionInGrid(6,0,0); break; | |
case "6,3,3": this.positionInGrid(6,0,0); break; | |
case "6,2,5": this.positionInGrid(6,2,2); break; | |
case "6,5,5": this.positionInGrid(6,2,2); break; | |
case "6,1,4": this.positionInGrid(6,1,1); break; | |
case "6,4,4": this.positionInGrid(6,1,1); break; | |
} | |
} else if (dir === SOUTH) { | |
switch(g) { | |
case "4,0,2": this.positionInGrid(4,2,2); break; | |
case "4,0,0": this.positionInGrid(4,2,2); break; | |
case "4,1,3": this.positionInGrid(4,3,3); break; | |
case "4,1,1": this.positionInGrid(4,3,3); break; | |
case "6,0,3": this.positionInGrid(6,3,3); break; | |
case "6,0,0": this.positionInGrid(6,3,3); break; | |
case "6,2,5": this.positionInGrid(6,5,5); break; | |
case "6,2,2": this.positionInGrid(6,5,5); break; | |
case "6,1,4": this.positionInGrid(6,4,4); break; | |
case "6,1,1": this.positionInGrid(6,4,4); break; | |
} | |
} | |
} | |
return this | |
} | |
Window.prototype.positionInGrid = function (cells, start, end) { | |
LAST_POSITION = { | |
window: this, | |
grid: cells + "," + start + "," + end | |
} | |
var cols = ~~(cells / 2); | |
var screen = this.screen(); | |
var cellwidth = (screen.width() - ((cols - 1) * PADDING)) / cols; | |
var cellheight = (screen.height() - PADDING) / 2; | |
var startc = start % cols, | |
startw = ~~(start / cols), | |
startl = screen.origin().x + (cellwidth + PADDING) * startc, | |
startt = screen.origin().y + (cellheight + PADDING) * startw, | |
startr = startl + cellwidth, | |
startb = startt + cellheight; | |
var endc = end % cols, | |
endw = ~~(end / cols), | |
endl = screen.origin().x + (cellwidth + PADDING) * endc, | |
endt = screen.origin().y + (cellheight + PADDING) * endw, | |
endr = endl + cellwidth, | |
endb = endt + cellheight; | |
var frame = this.frame(); | |
frame.x = Math.min(startl, endl); | |
frame.y = Math.min(startt, endt); | |
frame.width = Math.max(startr, endr) - frame.x; | |
frame.height = Math.max(startb, endb) - frame.y; | |
this.setFrame(frame); | |
return this; | |
} | |
// Resize a window by coeff units in the given direction | |
// coeff: -n shrinks by pixels units, +n grows by n pixels | |
Window.prototype.resize = function (dir, coeff) { | |
var frame = this.frame() | |
if (dir === W) frame.x += coeff * -1 | |
if (dir === N) frame.y += coeff * -1 | |
if ([E, W].indexOf(dir) > -1) frame.width += coeff | |
if ([N, S].indexOf(dir) > -1) frame.height += coeff | |
this.setFrame(frame) | |
return this | |
} | |
Window.prototype.toSpace = function (dir) { | |
var curSpace = this.spaces()[0], | |
newSpace = curSpace.next() | |
if (dir === WEST) | |
newSpace = curSpace.previous() | |
curSpace.removeWindows([this]) | |
newSpace.addWindows([this]) | |
this.focus() | |
return this | |
} | |
Window.prototype.centerMouse = function() { | |
Mouse.move({ | |
x: this.frame().x + this.frame().width / 2, | |
y: this.frame().y + this.frame().height / 2 | |
}) | |
return this | |
} | |
Window.prototype.log = function() { | |
Phoenix.log(this.frame().x + "," + this.frame().y + "," + this.frame().width + "," + this.frame().height) | |
return this | |
} | |
// ******************************************************************************** | |
// Modal Extensions | |
// ******************************************************************************** | |
// Flash a modal in the center of a given screen | |
Modal.prototype.flash = function(screen) { | |
var tf = this.frame(), | |
sf = screen.frame(); | |
this.origin = { | |
x: sf.x + sf.width/2 - tf.width/2, | |
y: sf.y + sf.height/2 - tf.height/2 | |
} | |
this.show(); | |
return this; | |
} | |
Modal.prototype.attach = function(window) { | |
var tf = this.frame(), | |
wf = window.frame(), | |
sf = window.screen().frame(); | |
this.origin = { | |
x: Math.min( | |
Math.max(wf.x + wf.width/2 - tf.width/2, sf.x), | |
sf.x + sf.width - tf.width | |
), | |
y: Math.min( | |
Math.max(Screen.all()[0].frame().height - (wf.y + wf.height/2 + tf.height / 2), sf.y), | |
sf.y + sf.height - tf.height | |
) | |
}; | |
this.show(); | |
return this; | |
} | |
// ******************************************************************************** | |
// Screen Extensions | |
// ******************************************************************************** | |
Screen.prototype.width = function () { // DEPRECATED | |
return this.flippedVisibleFrame().width - PADDING * 2 | |
} | |
Screen.prototype.height = function () { // DEPRECATED | |
return this.flippedVisibleFrame().height - PADDING * 2 | |
} | |
Screen.prototype.origin = function () { // DEPRECATED | |
return { | |
x: this.flippedVisibleFrame().x + PADDING, | |
y: this.flippedVisibleFrame().y + PADDING | |
} | |
} | |
// ******************************************************************************** | |
// Utilities | |
// ******************************************************************************** | |
Array.prototype.rotate = (function() { | |
const unshift = Array.prototype.unshift, | |
splice = Array.prototype.splice | |
return function(count) { | |
var len = this.length >>> 0, | |
count = count >> 0; | |
unshift.apply(this, splice.call(this, count % len, len)); | |
return this; | |
} | |
})() | |
function opposite(dir) { | |
switch (dir) { | |
case N: return S; | |
case S: return N; | |
case E: return W; | |
case W: return E; | |
case NW: return SE; | |
case NE: return SW; | |
case SW: return NE; | |
case SE: return NW; | |
} | |
} | |
// ******************************************************************************** | |
// Startup | |
// ******************************************************************************** | |
const HintManager = new Hints() | |
Key.on("space", ["shift", "command"], () => HintManager.activate()); | |
Modal.build({ | |
text: "Ready", | |
appearance: HINT_APPEARANCE, | |
weight: 24, | |
icon: App.get('Phoenix').icon(), | |
duration: 0.5, | |
}).flash(Screen.all()[0]); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment