i3-cycle-focus
Version:
Simulates an alt-tab operation for the i3 window manager. Shifts the mouse cursor position to the center of focus. Cycles visible windows only.
282 lines (260 loc) • 7.5 kB
JavaScript
/**
* @author Justin Collier <jpcxme@gmail.com>
* @license MIT
* @see {@link http://github.com/jpcx/i3-cycle-focus|GitHub}
*/
const i3 = require('i3').createClient()
const dp = require('deep-props')
const { spawn } = require('child_process')
const mode = process.argv[2]
/**
* Throws an error to the window manager by using the i3-nagbar tool.
*
* @function throwToI3
* @param {Error} err - Error being thrown.
*/
const throwToI3 = err => {
spawn('i3-nagbar', [
'-t',
'error',
'-m',
`i3-cycle-focus has thrown an error! ${err.message || err}`
])
process.exit()
}
/**
* Array of workspace information.
*
* @typedef {Array} I3Data-workspaces
*/
/**
* Object containing i3 tree structure.
*
* @typedef {Object} I3Data-tree
*/
/**
* Contains information about the i3 state.
*
* @typedef {Object} I3Data
* @property {I3Data-workspaces} workspaces - Array of workspace information.
* @property {I3Data-tree} tree - Object containing i3 tree structure.
*/
/**
* Loads i3 workspaces and tree using the i3 module.
*
* @function loadI3Data
* @return {I3Data} I3 data.
*/
const loadI3Data = () =>
new Promise((resolve, reject) => {
i3.workspaces((err, workspaces) => {
if (err) reject(err)
i3.tree((err, tree) => {
if (err) reject(err)
resolve({ workspaces, tree })
})
})
})
/**
* Creates an array of locations and window IDs used for retrieving information about content windows within the {@link I3Data-tree}.
*
* @typedef {Object} WindowLocator
* @property {Array} path - Path to window ID
* @property {number} value - Window ID
*/
/**
* Gets an array of content window locators.
*
* @function getContentWindowLocators
* @param {I3Data-tree} tree - Object containing i3 tree structure.
* @return {WindowLocator[]} Array of window locators
*/
const getContentWindowLocators = tree =>
dp.extract(tree).reduce((a, v) => {
if (v.path.slice(-1)[0] === 'window' && v.value) {
a.push(v)
}
return a
}, [])
/**
* Gets an array of visible workspace names.
*
* @function getVisibleWorkspaces
* @param {I3Data-workspaces} workspaces - Array of workspace information.
* @return {Set} Set of names of visible workspaces.
*/
const getVisibleWorkspaces = workspaces => {
if (!Array.isArray(workspaces)) workspaces = [workspaces]
const names = new Set()
for (let ws of workspaces) {
if (ws.visible === true) {
names.add(ws.name)
}
}
return names
}
/**
* Checks if a window locator is within a visible workspace.
*
* @function isWithinVisibleWorkspace
* @param {I3Data-tree} tree - Object containing i3 tree structure.
* @param {Set} visibleWorkspaces - Set of names of visible workspaces.
* @param {WindowLocator} locator - Window locator.
* @return {boolean} True if within visible workspace.
*/
const isWithinVisibleWorkspace = (tree, visibleWorkspaces, locator) => {
for (let i = 1; i < locator.path.length; i++) {
const parent = dp.get(tree, locator.path.slice(0, -1 * i))
if (parent.type === 'workspace' && visibleWorkspaces.has(parent.name)) {
return true
}
}
return false
}
/**
* Gets a list of windows located within visible workspaces.
*
* @function getVisibleWindows
* @param {I3Data-tree} tree - Object containing i3 tree structure.
* @param {Set} visibleWorkspaces - Set of names of visible workspaces.
* @param {WindowLocator[]} locators - Array of window locators.
* @return {Array} IDs of visible windows.
*/
const getVisibleWindows = (tree, visibleWorkspaces, locators) => {
const visibleWindows = []
for (let locator of locators) {
if (isWithinVisibleWorkspace(tree, visibleWorkspaces, locator)) {
visibleWindows.push(locator.value)
}
}
return visibleWindows
}
/**
* Gets the window ID of the current focused window.
*
* @function getFocusedWindow
* @param {I3Data-tree} tree - Object containing i3 tree structure.
* @return {number} ID of focused window.
*/
const getFocusedWindow = tree =>
dp.extract(tree).reduce((a, v) => {
if (v.path.slice(-1)[0] === 'window' && v.value) {
if (dp.get(tree, v.path.slice(0, -1)).focused === true) {
return v.value
}
}
return a
})
/**
* Coordinates object.
*
* @typedef {Object} Coords
* @property {number} x - Integer value of x coordinate.
* @property {number} y - Integer value of y coordinate.
*/
/**
* Gets the coordinates of the center of a given window.
*
* @function getWindowCenterCoords
* @param {I3Data-tree} tree - Object containing i3 tree structure.
* @param {number} windowID - ID of focused window.
* @return {Coords} Coordinates of center.
*/
const getWindowCenterCoords = (tree, windowID) =>
dp.extract(tree).reduce((a, v) => {
if (v.path.slice(-1)[0] === 'window' && v.value === windowID) {
const rect = dp.get(tree, v.path.slice(0, -1)).rect
return {
x: rect.x + ~~(rect.width / 2),
y: rect.y + ~~(rect.height / 2)
}
}
return a
})
/**
* Gets the ID of the window that should be focused next.
*
* @function getNextWindowID
* @param {I3Data} i3Data - I3 Data.
* @return {number} ID of desired window.
*/
const getNextWindowID = i3Data => {
const locators = getContentWindowLocators(i3Data.tree)
const visibleWorkspaces = getVisibleWorkspaces(i3Data.workspaces)
const visibleWindows = getVisibleWindows(
i3Data.tree,
visibleWorkspaces,
locators
)
const focusedWindow = getFocusedWindow(i3Data.tree)
if (visibleWindows.includes(focusedWindow)) {
let index = visibleWindows.indexOf(focusedWindow)
if (mode !== 'reverse' && mode !== '--reverse') {
if (++index < visibleWindows.length) {
return visibleWindows[index]
} else {
return visibleWindows[0]
}
} else {
if (--index >= 0) {
return visibleWindows[index]
} else {
return visibleWindows.slice(-1)[0]
}
}
} else {
return visibleWindows[0]
}
}
/**
* Moves the mouse cursor to a desired coordinate set.
*
* @function moveMouse
* @param {Coords} - Coordinate object.
* @return {Promise} - Resolves once xdotool process has exited; rejects on child process errors.
*/
const moveMouse = coords =>
new Promise((resolve, reject) => {
try {
const xdotoolProcess = spawn('xdotool', ['mousemove', coords.x, coords.y])
xdotoolProcess.on('exit', code => {
resolve()
})
xdotoolProcess.on('error', err => {
reject(err)
})
} catch (err) {
reject(err)
}
})
/**
* Focuses the window with a given ID.
*
* @function focusWindow
* @param {number} windowID - ID of desired window.
*/
const focusWindow = windowID =>
new Promise((resolve, reject) => {
i3.command(`[id="${windowID}"] focus`, (err, data) => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
loadI3Data()
.then(i3Data => {
const windowID = getNextWindowID(i3Data)
const coords = getWindowCenterCoords(i3Data.tree, windowID)
focusWindow(windowID)
.then(() => {
moveMouse(coords)
.then(() => process.exit())
.catch(throwToI3)
})
.catch(throwToI3)
})
.catch(throwToI3)