i3-shade
Version:
Auto show/hide floating windows in i3
176 lines (159 loc) • 6.01 kB
JavaScript
const sprintf = require('sprintf-js').sprintf
const exec = require('child_process').exec
class Shade {
constructor(markPref, exemptMarkPref, socketPath, exemptComStr, fallbackCom, peekMargin, doUrgent) {
this.markPref = markPref
this.exemptMarkPref = exemptMarkPref
this.socketPath = socketPath
this.exemptComStr = exemptComStr
this.exemptMarkPat = exemptMarkPref + "%s"
this.peekMargin = peekMargin;
this.fcsdWsNum
this.fcsdWinId
this.fcsdWinMarks
this.i3 = null
this.handlersSet = false
this.fallbackCom = null
this.fallbackCom = fallbackCom
this.modeToggleTimeout
this.doUrgent = doUrgent
}
connect = function(callbacks) {
this.callbacks = callbacks ?? {}
this.i3 = require('i3').createClient({path: this.socketPath}, (stream) => {})
// Get the initial focused WS num
this.i3.workspaces((err, json) => {
this.fcsdWsNum = json.find(ws => ws.focused).num
this.callbacks.connect?.call()
})
this.i3.on('workspace', this.handleWorkspaceEvent.bind(this))
this.i3.on('binding', this.handleBindingEvent.bind(this))
this.i3.on('window', this.handleWindowEvent.bind(this))
}
handleWorkspaceEvent = function(event) {
if (event && event.change == "focus") {
this.fcsdWsNum = event.current.num
}
}
handleBindingEvent = function(event) {
// Toggle mark to exempt from shading
if (event.binding?.command == this.exemptComStr) {
let mark = sprintf(this.exemptMarkPat, this.fcsdWinId)
this.i3.command(
sprintf('[con_id=%s] mark --add --toggle %s', this.fcsdWinId, mark),
this.callbacks.markExempt?.bind(this)
)
}
if (event.binding.command == 'focus mode_toggle') {
if (
this.fallbackCom &&
(this.modeToggleTimeout == undefined || this.modeToggleTimeout?._destroyed)
) {
this.i3.command(
this.fallbackCom,
this.callbacks.fallback?.bind(this)
)
} else clearTimeout(this.modeToggleTimeout)
}
}
handleWindowEvent = async function(event) {
this.fcsdWinId = event.container.id
this.fcsdWinMarks = event.container.marks
if (event.change == 'mark') {
this.callbacks.unmarkShaded?.call(this, null, event)
}
if (event.change == 'focus') {
this.modeToggleTimeout = setTimeout(
() => {clearTimeout(this.modeToggleTimeout)},
200
)
let float_val = event.container.floating
// Tiled window focused
if (['user_off', 'auto_off'].includes(float_val) ) {
this.i3.tree((err, resp) => {
// Get the focused workspace node
let wsnode = resp.
nodes.find(_ => _.name != '__i3' && _.id == resp.focus[0]).
nodes.find(_ => _.type == 'con' && _.name == 'content').
nodes.find(_ => _.type == 'workspace' && _.num == this.fcsdWsNum)
// Get all the floating containers on the current output/workspace
let fnodes = wsnode.
floating_nodes.flatMap(_ => _.nodes);
const isNormalOrUnknown = function(node) {
return ["normal", "unknown"].includes(node.window_type)
}
// Test of window eligiblility (neither exempted nor already shaded)
const isUnmarked = function(node) {
return !node.marks.some( _ =>
_.startsWith(this.markPref) || _.startsWith(this.exemptMarkPref)
)
}.bind(this)
const isEligible = function(node) {
return isNormalOrUnknown(node) && isUnmarked(node)
}
// Filter for eligble windows and loop through them.
let elNodes = fnodes.filter(node => isEligible(node))
elNodes.forEach(node => {
// let winHeight = node.rect.height + node.deco_rect.height - this.peekMargin
let winHeight = node.rect.height - this.peekMargin
// Calculate the offset, in case the workspace's display's vertical rect doesn't start at 0
// let winOffset = winHeight - wsnode.rect.y
let winOffset = winHeight - Math.max(0, wsnode.rect.y)
let markCom = sprintf(
'[con_id=%s] mark --add %s%d_%d_%s, move position %d px -%d px',
node.id, this.markPref, node.rect.x, node.rect.y + node.deco_rect.height, node.id, node.rect.x, winOffset
)
this.i3.command(
markCom,
(err, resp) => {
if (err) {
console.error(err)
}
this.callbacks.markShaded?.call(this, err, resp)
}
)
})
if (this.doUrgent) {
elNodes.forEach(node => {
let winIdD = node.window
let winIdX = sprintf('0x%x', winIdD)
console.log(winIdD, winIdX)
let urgentCom = sprintf(
'wmctrl -i -r %s -v -b %s',
winIdX,
'add,demands_attention'
)
exec(urgentCom,
((error, stdout, stderr) => {
if (error) {
console.log(`${error.name}: ${error.message}`);
return;
}
console.log(`stdout: ${stdout}`)
console.log(`stderr: ${stderr}`)
})
)
})
}
})
}
// Floating window focused
else {
let foccon = event.container
let mark = foccon.marks.filter(mark => mark.startsWith(this.markPref) && !mark.startsWith(this.exemptMarkPref))[0]
if (mark) {
let toks = mark.split("_")
this.i3.command(
sprintf('unmark %s, move position %d px %d px', mark, toks[1], toks[2]-foccon.deco_rect.height),
(err, resp) => {
if (err) {
console.error(err)
}
}
)
}
}
}
}
}
module.exports = Shade