| # 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`. |
| #!/usr/bin/env node | #!/usr/bin/env node | ||||
| var syscheck = require("./js/syscheck"); | var syscheck = require("./js/syscheck"); | ||||
| var parseConf = require("./js/parse-conf"); | |||||
| var async = require("./js/async"); | |||||
| var modules = { | var modules = { | ||||
| display: require("./modules/display"), | |||||
| // display: require("./modules/display"), | |||||
| input: require("./modules/input"), | 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 createLogger(name) { | ||||
| function log(pre, msg) { | function log(pre, msg) { | ||||
| console.error(pre+msg.join(" ")); | console.error(pre+msg.join(" ")); | ||||
| var mod = modules[i]; | var mod = modules[i]; | ||||
| var conf = config[i] || {}; | var conf = config[i] || {}; | ||||
| if (conf instanceof Array && conf.length === 0) | |||||
| return; | |||||
| mod.start(conf, createLogger(i), modules); | mod.start(conf, createLogger(i), modules); | ||||
| }); | }); | ||||
| } | } | ||||
| function stopAll(cb) { | function stopAll(cb) { | ||||
| var keys = Object.keys(modules); | 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 => { | syscheck(ok => { | ||||
| else | else | ||||
| console.error("Missing binaries, exiting."); | console.error("Missing binaries, exiting."); | ||||
| }); | }); | ||||
| process.on("SIGTERM", onTerm); | |||||
| process.on("SIGINT", onTerm); |
| module.exports = async; | |||||
| function async(num, cb) { | |||||
| return function() { | |||||
| num -= 1; | |||||
| if (num === 0) | |||||
| cb(); | |||||
| } | |||||
| } |
| 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; | |||||
| } | |||||
| } | |||||
| } |
| var udev = require("udev"); | |||||
| exports.start = start; | exports.start = start; | ||||
| exports.stop = stop; | exports.stop = stop; | ||||
| exports.event = event; | exports.event = event; | ||||
| var logger; | var logger; | ||||
| var modules; | 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_) { | function start(conf_, logger_, modules_) { | ||||
| conf = conf_ || conf; | conf = conf_ || conf; | ||||
| logger = logger_ || logger; | logger = logger_ || logger; | ||||
| modules = modules_ || modules; | modules = modules_ || modules; | ||||
| monitor = udev.monitor("drm"); | |||||
| monitor.on("add", dev => onchange(dev, "add")); | |||||
| monitor.on("change", dev => onchange(dev, "change")); | |||||
| } | } | ||||
| function stop(cb) { | function stop(cb) { | ||||
| } | } | ||||
| function event(name, ...params) { | function event(name, ...params) { | ||||
| logger.info("Event", name, params.toString()); | |||||
| switch (name) { | |||||
| default: | |||||
| logger.warn("Unknown event:", name); | |||||
| } | |||||
| } | } |
| // Devices which aren't keyboards or mice with names aren't interesting | // Devices which aren't keyboards or mice with names aren't interesting | ||||
| function filter(dev) { | 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 | // name can be either an array or a string | ||||
| if (!filter(dev)) | if (!filter(dev)) | ||||
| return; | return; | ||||
| var isKeyboard = !!dev.ID_INPUT_KEYBOARD; | |||||
| var isPointer = !!(dev.ID_INPUT_MOUSE || dev.ID_INPUT_TOUCHPAD); | |||||
| // Find out what to log | // Find out what to log | ||||
| var inputType; | 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"; | inputType = "keyboard"; | ||||
| else if (dev.ID_INPUT_MOUSE) | |||||
| else if (isPointer) | |||||
| inputType = "mouse"; | inputType = "mouse"; | ||||
| // Log add/change | // Log add/change | ||||
| // Run through and apply relevant rules | // Run through and apply relevant rules | ||||
| conf.forEach(entry => { | conf.forEach(entry => { | ||||
| if (entry.type === "pointer" && !dev.ID_INPUT_MOUSE) | |||||
| if (entry.type === "pointer" && !isPointer) | |||||
| return; | return; | ||||
| if (entry.type === "keyboard" && !dev.ID_INPUT_KEYBOARD) | |||||
| if (entry.type === "keyboard" && !isKeyboard) | |||||
| return; | return; | ||||
| if (!nameMatches(dev, entry.name)) | if (!nameMatches(dev, entry.name)) | ||||
| return; | return; | ||||
| logger = logger_ || logger; | logger = logger_ || logger; | ||||
| modules = modules_ || modules; | 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("add", dev => onchange(dev, "add")); | ||||
| monitor.on("change", dev => onchange(dev, "change")); | monitor.on("change", dev => onchange(dev, "change")); | ||||
| } | } | ||||
| } | } | ||||
| function event(name, ...params) { | function event(name, ...params) { | ||||
| logger.info("Event", name, params.toString()); | |||||
| switch (name) { | |||||
| default: | |||||
| logger.warn("Unknown event: "+name); | |||||
| } | |||||
| } | } |
| 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); | |||||
| } | |||||
| } |
| } | } | ||||
| function event(name, ...params) { | function event(name, ...params) { | ||||
| logger.info("Event", name, params.toString()); | |||||
| switch (name) { | switch (name) { | ||||
| case "reload": | case "reload": | ||||
| runFeh(); | runFeh(); | ||||
| break; | break; | ||||
| default: | |||||
| logger.warn("Unknown event:", name); | |||||
| } | } | ||||
| } | } |
| "author": "Martin Dørum Nygaard <martid0311@gmail.com> (http://mort.coffee)", | "author": "Martin Dørum Nygaard <martid0311@gmail.com> (http://mort.coffee)", | ||||
| "license": "ISC", | "license": "ISC", | ||||
| "dependencies": { | "dependencies": { | ||||
| "hconfig": "^0.4.0", | |||||
| "udev": "^0.4.0" | "udev": "^0.4.0" | ||||
| }, | }, | ||||
| "bin": { | "bin": { |