@@ -0,0 +1,20 @@ | |||
# DEDaemon | |||
DEDaemon is a daemon to give some of the perks of a full desktop environment to | |||
those of us running window managers. | |||
## Installation | |||
Run `npm install -g dedaemon` as root. | |||
## Usage | |||
`dedaemon <config file>` | |||
e.g: | |||
`dedaemon ~/.config/dedaemon.hcnf` | |||
You probably want to run that on startup. If you're running i3wm, that means | |||
adding `exec --no-startup-id dedaemon ~/.config/dedaemon.hcnf` to | |||
`~/.i3/config`. |
@@ -1,55 +1,23 @@ | |||
#!/usr/bin/env node | |||
var syscheck = require("./js/syscheck"); | |||
var parseConf = require("./js/parse-conf"); | |||
var async = require("./js/async"); | |||
var modules = { | |||
display: require("./modules/display"), | |||
// display: require("./modules/display"), | |||
input: require("./modules/input"), | |||
wallpaper: require("./modules/wallpaper") | |||
wallpaper: require("./modules/wallpaper"), | |||
process: require("./modules/process"), | |||
}; | |||
var config = { | |||
display: [ | |||
{ | |||
name: "*", | |||
resolution: "max", | |||
rate: "max", | |||
where: { left_of: "primary" }, | |||
}, | |||
], | |||
input: [ | |||
{ | |||
type: "pointer", | |||
name: "*", | |||
options: [, | |||
[ "libinput Tapping Enabled", 1 ], | |||
], | |||
}, | |||
{ | |||
type: "pointer", | |||
name: "Razer Razer Naga", | |||
options: [ | |||
[ "libinput Accel Speed", "-0.8" ], | |||
], | |||
}, | |||
{ | |||
type: "keyboard", | |||
name: "*", | |||
commands: [ | |||
"xset r rate 200 60", | |||
"setxkbmap dvorak -option ctrl:swapcaps -option altwin:swap_alt_win", | |||
], | |||
}, | |||
], | |||
wallpaper: { | |||
path: "/home/martin/background.jpg", | |||
}, | |||
if (!process.argv[2]) { | |||
console.log("Usage:", process.argv[1], "<config>"); | |||
process.exit(1); | |||
} | |||
var config = parseConf(process.argv[2]); | |||
function createLogger(name) { | |||
function log(pre, msg) { | |||
console.error(pre+msg.join(" ")); | |||
@@ -67,21 +35,21 @@ function startAll() { | |||
var mod = modules[i]; | |||
var conf = config[i] || {}; | |||
if (conf instanceof Array && conf.length === 0) | |||
return; | |||
mod.start(conf, createLogger(i), modules); | |||
}); | |||
} | |||
function stopAll(cb) { | |||
var keys = Object.keys(modules); | |||
var next = async(keys.length, cb); | |||
keys.forEach(i => modules[i].stop(next)); | |||
} | |||
var cbs = keys.length; | |||
function next() { | |||
cbs -= 1; | |||
if (cbs === 0) | |||
cb(); | |||
} | |||
keys.forEach(i => modules.stop(next)); | |||
function onTerm() { | |||
stopAll(() => process.exit(1)); | |||
} | |||
syscheck(ok => { | |||
@@ -90,3 +58,6 @@ syscheck(ok => { | |||
else | |||
console.error("Missing binaries, exiting."); | |||
}); | |||
process.on("SIGTERM", onTerm); | |||
process.on("SIGINT", onTerm); |
@@ -0,0 +1,9 @@ | |||
module.exports = async; | |||
function async(num, cb) { | |||
return function() { | |||
num -= 1; | |||
if (num === 0) | |||
cb(); | |||
} | |||
} |
@@ -0,0 +1,56 @@ | |||
var hconfig = require("hconfig"); | |||
module.exports = parse; | |||
var configStructure = { | |||
display: { | |||
count: "many", | |||
props: { | |||
name: "string", | |||
resolution: "string", | |||
rate: "string", | |||
where: "object", | |||
}, | |||
}, | |||
input: { | |||
count: "many", | |||
props: { | |||
name: "string", | |||
type: "string", | |||
commands: "array", | |||
options: "array", | |||
}, | |||
}, | |||
wallpaper: { | |||
count: "once", | |||
props: { | |||
name: "null", | |||
path: "string", | |||
}, | |||
}, | |||
process: { | |||
count: "many", | |||
props: { | |||
name: "string", | |||
run: "array", | |||
"in": [ "string", "null" ], | |||
env: [ "object", "null" ], | |||
restart: "bool", | |||
as: [ "string", "null" ], | |||
}, | |||
}, | |||
} | |||
function parse(file) { | |||
try { | |||
return hconfig.parseConfFile( | |||
file, configStructure); | |||
} catch (err) { | |||
if (err.hconfigParseError) { | |||
console.error(err.message); | |||
process.exit(1); | |||
} else { | |||
throw err; | |||
} | |||
} | |||
} |
@@ -1,3 +1,5 @@ | |||
var udev = require("udev"); | |||
exports.start = start; | |||
exports.stop = stop; | |||
exports.event = event; | |||
@@ -6,10 +8,27 @@ var conf; | |||
var logger; | |||
var modules; | |||
var monitor; | |||
function onchange(dev, evt) { | |||
if (evt === "add") | |||
logger.info("display added"); | |||
else if (evt === "change") | |||
logger.info("display changed"); | |||
else | |||
logger.info(dev); | |||
//modules.wallpaper.event("reload"); | |||
} | |||
function start(conf_, logger_, modules_) { | |||
conf = conf_ || conf; | |||
logger = logger_ || logger; | |||
modules = modules_ || modules; | |||
monitor = udev.monitor("drm"); | |||
monitor.on("add", dev => onchange(dev, "add")); | |||
monitor.on("change", dev => onchange(dev, "change")); | |||
} | |||
function stop(cb) { | |||
@@ -17,5 +36,8 @@ function stop(cb) { | |||
} | |||
function event(name, ...params) { | |||
logger.info("Event", name, params.toString()); | |||
switch (name) { | |||
default: | |||
logger.warn("Unknown event:", name); | |||
} | |||
} |
@@ -59,8 +59,8 @@ var runCmds = debounce(function() { | |||
// Devices which aren't keyboards or mice with names aren't interesting | |||
function filter(dev) { | |||
return dev.NAME && dev.SUBSYSTEM === "input" && | |||
(dev.ID_INPUT_KEYBOARD || dev.ID_INPUT_MOUSE); | |||
return dev.NAME && | |||
(dev.ID_INPUT_KEYBOARD || dev.ID_INPUT_MOUSE || dev.ID_INPUT_TOUCHPAD); | |||
} | |||
// name can be either an array or a string | |||
@@ -94,13 +94,16 @@ function onchange(dev, evt) { | |||
if (!filter(dev)) | |||
return; | |||
var isKeyboard = !!dev.ID_INPUT_KEYBOARD; | |||
var isPointer = !!(dev.ID_INPUT_MOUSE || dev.ID_INPUT_TOUCHPAD); | |||
// Find out what to log | |||
var inputType; | |||
if (dev.ID_INPUT_KEYBOARD && dev.ID_INPUT_MOUSE) | |||
inputType = "keyboard/mouse"; | |||
else if (dev.ID_INPUT_KEYBOARD) | |||
if (isKeyboard && isPointer) | |||
inputType = "keyboard/pointer"; | |||
else if (isKeyboard) | |||
inputType = "keyboard"; | |||
else if (dev.ID_INPUT_MOUSE) | |||
else if (isPointer) | |||
inputType = "mouse"; | |||
// Log add/change | |||
@@ -111,9 +114,9 @@ function onchange(dev, evt) { | |||
// Run through and apply relevant rules | |||
conf.forEach(entry => { | |||
if (entry.type === "pointer" && !dev.ID_INPUT_MOUSE) | |||
if (entry.type === "pointer" && !isPointer) | |||
return; | |||
if (entry.type === "keyboard" && !dev.ID_INPUT_KEYBOARD) | |||
if (entry.type === "keyboard" && !isKeyboard) | |||
return; | |||
if (!nameMatches(dev, entry.name)) | |||
return; | |||
@@ -148,9 +151,9 @@ function start(conf_, logger_, modules_) { | |||
logger = logger_ || logger; | |||
modules = modules_ || modules; | |||
udev.list().forEach(dev => onchange(dev, "init")); | |||
udev.list("input").forEach(dev => onchange(dev, "init")); | |||
monitor = udev.monitor(); | |||
monitor = udev.monitor("input"); | |||
monitor.on("add", dev => onchange(dev, "add")); | |||
monitor.on("change", dev => onchange(dev, "change")); | |||
} | |||
@@ -161,5 +164,8 @@ function stop(cb) { | |||
} | |||
function event(name, ...params) { | |||
logger.info("Event", name, params.toString()); | |||
switch (name) { | |||
default: | |||
logger.warn("Unknown event: "+name); | |||
} | |||
} |
@@ -0,0 +1,183 @@ | |||
var spawn = require("child_process").spawn; | |||
var async = require("../../js/async"); | |||
exports.start = start; | |||
exports.stop = stop; | |||
exports.event = event; | |||
var conf; | |||
var logger; | |||
var modules; | |||
class Process { | |||
constructor(id, cmd, options) { | |||
this.id = id; | |||
this.restart = options.restart; | |||
this.stopping = false; | |||
this.running = false; | |||
this.restarts = 0; | |||
this.name = cmd[0]; | |||
cmd.shift(); | |||
this.args = cmd; | |||
this.childOpts = { | |||
env: options.env, | |||
cwd: options.cwd, | |||
}; | |||
this.info = logger.info.bind(logger, this.id+":"); | |||
this.warn = logger.warn.bind(logger, this.id+":"); | |||
} | |||
logOutput(stream, data) { | |||
data.toString() | |||
.split("\n") | |||
.map(line => line.trim()) | |||
.forEach(line => { | |||
if (line === "") return; | |||
this.info(stream+":", line); | |||
}); | |||
} | |||
onexit() { | |||
if (!this.restart) | |||
return; | |||
if (this.restarts < 2) { | |||
this.restarts += 1; | |||
var restarts = this.restarts; | |||
this.info("Restarting in 2 seconds."); | |||
setTimeout(() => { | |||
this.start(); | |||
this.restarts = restarts; | |||
}, 2000); | |||
} else { | |||
this.warn("Not restarting anymore after 2 restarts."); | |||
} | |||
} | |||
start() { | |||
this.stopping = false; | |||
this.running = true; | |||
this.restarts = 0; | |||
this.child = spawn(this.name, this.args, this.childOpts); | |||
this.child.stdout.on("data", | |||
d => this.logOutput("stdout", d)); | |||
this.child.stderr.on("data", | |||
d => this.logOutput("stderr", d)); | |||
this.child.once("error", err => { | |||
if (!this.stopping) | |||
this.warn("Failed to start:", err); | |||
this.onexit(); | |||
}); | |||
this.child.once("close", code => { | |||
this.running = false; | |||
if (this.stopping) | |||
return; | |||
if (code === 0) | |||
this.info("Exited with status code 0"); | |||
else | |||
this.warn("Exited with status code", code); | |||
this.onexit(); | |||
}); | |||
} | |||
stop(cb) { | |||
this.stopping = true; | |||
if (!this.running) | |||
return cb(); | |||
this.info("Sending SIGTERM."); | |||
this.child.kill("SIGTERM"); | |||
setTimeout(() => { | |||
if (this.running) { | |||
this.info("Sending SIGKILL."); | |||
this.child.kill("SIGKILL"); | |||
} | |||
cb(); | |||
}, 1000); | |||
} | |||
} | |||
class ProcessGroup { | |||
constructor(id, cmds, options) { | |||
this.procs = []; | |||
cmds.forEach(cmd => { | |||
var name = cmd[0]; | |||
this.procs.push(new Process(id+"("+name+")", cmd, options)); | |||
}); | |||
} | |||
start() { | |||
this.procs.forEach(p => p.start()); | |||
} | |||
stop(cb) { | |||
var next = async(this.procs.length, cb); | |||
this.procs.forEach(p => p.stop(next)); | |||
} | |||
} | |||
var procs = {}; | |||
function start(conf_, logger_, modules_) { | |||
conf = conf_ || conf; | |||
logger = logger_ || logger; | |||
modules = modules_ || modules; | |||
conf.forEach(proc => { | |||
if (procs[proc.name]) | |||
return logger.warn("Igonring duplicate process: "+proc.name); | |||
var env = null; | |||
if (proc.env) { | |||
env = {}; | |||
for (var i in process.env) { | |||
env[i] = process.env[i]; | |||
} | |||
for (var i in proc.env) { | |||
env[i] = proc.env[i]; | |||
} | |||
} | |||
var opts = { | |||
cwd: proc.in, | |||
env: env, | |||
restart: !!proc.restart, | |||
}; | |||
var p; | |||
if (!proc.as || proc.as === "process") { | |||
p = new Process(proc.name, proc.run, opts); | |||
} else if (proc.as === "group") { | |||
p = new ProcessGroup(proc.name, proc.run, opts); | |||
} else { | |||
return logger.warn( | |||
proc.name+":", | |||
"Invalid 'as' attribute:", | |||
proc.as); | |||
} | |||
p.start(); | |||
procs[proc.name] = p; | |||
}); | |||
} | |||
function stop(cb) { | |||
var keys = Object.keys(procs); | |||
var next = async(keys.length, cb); | |||
keys.forEach(i => procs[i].stop(next)); | |||
} | |||
function event(name, ...params) { | |||
switch (name) { | |||
default: | |||
logger.warn("Unknown event: "+name); | |||
} | |||
} |
@@ -78,11 +78,12 @@ function stop(cb) { | |||
} | |||
function event(name, ...params) { | |||
logger.info("Event", name, params.toString()); | |||
switch (name) { | |||
case "reload": | |||
runFeh(); | |||
break; | |||
default: | |||
logger.warn("Unknown event:", name); | |||
} | |||
} |
@@ -13,6 +13,7 @@ | |||
"author": "Martin Dørum Nygaard <martid0311@gmail.com> (http://mort.coffee)", | |||
"license": "ISC", | |||
"dependencies": { | |||
"hconfig": "^0.4.0", | |||
"udev": "^0.4.0" | |||
}, | |||
"bin": { |