| @@ -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": { | |||