I bought 2 potable monitors and I switched to yabai and a mechanical keyboard. I'm working on a CLI for yabai.
You have many windows in your space but only some of them are relevant to your work. Using cmd+back_tick
and cmd-tab
makes you lose focus and time.
Use bookmarks to quickly switch between the relevant windows. In my case I use F1-5
to navigate to the relevant windows and S-F1-5
to add the relevant windows. You can repeat S-F1-5
to cycle through all visible windows of a space. Every space has its own bookmars. You can use multiple spaces for multiple activities.
- Use https://github.com/keycastr/keycastr to debug the configuration
- Install Karabiner to disable
cmd+back_tick
andcmd-tab
. This will allow to unlearn the anti-pattern. - Get used to
mash
(cmd+opt+ctrl
) andsmash
(mash
+shift
). - Map
f1-5
tomash 1-5
with Karabiner. - Map
shift-f1-5
tosmash 1-5
with Karabiner. - Map
mash 1-5
tonav_window
(navigate to a window with a bookmark) with Phoenix. - Map
smash 1-5
toadd_window
(add bookmark to a window) with Phoenix. - Read https://github.com/jasonm23/Phoenix-config to understand how to convert this
README.md
in a Phoneix config file.
Buy 3 monitors and use a tiling window manager like https://github.com/koekeishiya/yabai. This was not an option for me.
Harpoon Window Manager is built on top of amazing other projects like
- https://github.com/ThePrimeagen/harpoon (just the main idea)
- https://github.com/kasper/phoenix
- https://github.com/jasonm23/Phoenix-config
- https://karabiner-elements.pqrs.org/
Constants
MARGIN_X = 0
MARGIN_Y = 0
Phoenix.notify("Phoenix config loading")
Phoenix.set({
daemon: false,
openAtLogin: true
})
Logging
let log = function (o, label = "XXX") {
Phoenix.log(`${label} ${JSON.stringify(o)}`)
}
Shortcuts for focused
focused = () => Window.focused()
Window.prototype.screenFrame = function(screen) {
return (screen != null ? screen.flippedVisibleFrame() : void 0) || this.screen().flippedVisibleFrame()
}
Window.prototype.fullGridFrame = function() {
return this.calculateGrid({y: 0, x: 0, width: 1, height: 1})
}
Calculate the grid based on the parameters, x
, y
, width
, height
, (returning an object rectangle
)
Window.prototype.calculateGrid = function({x, y, width, height}) {
return {
y: Math.round(y * this.screenFrame().height) + MARGIN_Y + this.screenFrame().y,
x: Math.round(x * this.screenFrame().width) + MARGIN_X + this.screenFrame().x,
width: Math.round(width * this.screenFrame().width) - 2.0 * MARGIN_X,
height: Math.round(height * this.screenFrame().height) - 2.0 * MARGIN_Y
}
}
Temporary storage for frames
lastFrames = {}
Window to grid
Window.prototype.toGrid = function({x, y, width, height}) {
let rect = this.calculateGrid({x, y, width, height})
return this.setFrame(rect)
}
Toggle a window to full screen or revert to it's former frame size.
Window.prototype.toFullScreen = function(toggle = true) {
if (!_.isEqual(this.frame(), this.fullGridFrame())) {
this.rememberFrame()
return this.toGrid({y: 0, x: 0, width: 1, height: 1})
} else if (toggle && lastFrames[this.uid()]) {
this.setFrame(lastFrames[this.uid()])
return this.forgetFrame()
}
}
Remember and forget frames
Window.prototype.uid = function() {
return `${this.app().name()}::${this.title()}`
}
Window.prototype.rememberFrame = function() {
return lastFrames[this.uid()] = this.frame()
}
Window.prototype.forgetFrame = function() {
return delete lastFrames[this.uid()]
}
This is the logic for the Harpoon Window Manager
harpoon = Storage.get('harpoon')
modal = {n: null, ts: 0}
timeout = 2000
const showPopup = str => {
let frame = Screen.main().frame()
let modal = Modal.build({
duration: 1.0,
text: str
})
modal.origin = {
x: (frame.width / 2) - modal.frame().width / 2,
y: frame.height - 100
}
modal.show()
}
const init_harpoon = (space_id, n) => {
if (!_.isObject(harpoon)) { harpoon = {} }
if (!_.isObject(harpoon[space_id])) { harpoon[space_id] = {} }
if (!_.isObject(harpoon[space_id][n])) {
harpoon[space_id][n] = {index: -1, win_id: null}
}
}
const add_window = n => {
if (modal.n != n || Date.now() - modal.ts > timeout) {
showPopup(n)
}
const space_id = Space.active().hash()
init_harpoon(space_id, n)
const wins = Space.active().windows({visible: true})
const focused_id = focused().hash()
if (wins.length == 0) { return }
if (modal.n != n || Date.now() - modal.ts > timeout) {
// assign to the focused window
let index
_.each(wins, (win, key) => {
if (focused_id == win.hash()) {
index = key
}
})
harpoon[space_id][n].index = index
harpoon[space_id][n].win_id = focused_id
} else {
// cycle and then assign
index = harpoon[space_id][n].index + 1
index = index % wins.length
if (focused_id == wins[index].hash()) {
index += 1
index = index % wins.length
}
wins[index].focus()
harpoon[space_id][n].index = index
harpoon[space_id][n].win_id = focused().hash()
}
Storage.set('harpoon', harpoon)
modal.n = n
modal.ts = Date.now()
}
const nav_window = n => {
const space_id = Space.active().hash()
init_harpoon(space_id, n)
let win_id = harpoon[space_id][n].win_id
if (_.isNull(win_id)) {
showPopup("404")
return
}
let found = false;
_.each(Space.active().windows({visible: true}), win => {
if (win.hash() == win_id) {
win.focus()
found = true;
}
})
if (!found) { showPopup("404") }
}
let showAppName = () => {
let name = focused().app().name()
let frame = focused().screenFrame()
let modal = Modal.build({
duration: 2,
text: `App: ${name}`
})
modal.origin = {
x: (frame.width / 2) - modal.frame().width / 2,
y: frame.height - 100
}
modal.show()
}
(It's pretty cool, but it's clearly a bezel ;)
Alias Phoenix.bind
as bind_key
, to make the binding table extra
readable.
keys = []
The bind_key
method includes the unused description
parameter,
This is to allow future functionality i.e. help mechanisms, describe bindings etc.
const bind_key = (key, description, modifier, fn) => keys.push(Key.on(key, modifier, fn))
Mash is Cmd + Alt/Opt + Ctrl pressed together.
const mash = 'cmd-alt-ctrl'.split('-')
Smash is Mash + shift
const smash = 'cmd-alt-ctrl-shift'.split('-')
Toggle maximize for the current window
bind_key('M', 'Maximize Window', mash, () => focused().toFullScreen())
Harpoon shortcuts
bind_key('1', 'Nav F1', mash, () => nav_window("F1"))
bind_key('1', 'Add F1', smash, () => add_window("F1"))
bind_key('2', 'Nav F2', mash, () => nav_window("F2"))
bind_key('2', 'Add F2', smash, () => add_window("F2"))
bind_key('3', 'Nav F3', mash, () => nav_window("F3"))
bind_key('3', 'Add F3', smash, () => add_window("F3"))
bind_key('4', 'Nav F4', mash, () => nav_window("F4"))
bind_key('4', 'Add F4', smash, () => add_window("F4"))
bind_key('5', 'Nav F5', mash, () => nav_window("F5"))
bind_key('5', 'Add F5', smash, () => add_window("F5"))
All done...
Phoenix.notify("All ok.")