Browse Source

dedaemon can now manage displays

master
mortie 6 years ago
parent
commit
a1df95f57e
11 changed files with 413 additions and 31 deletions
  1. 28
    13
      dedaemon.js
  2. 24
    0
      example.hcnf
  3. 3
    0
      js/async.js
  4. 1
    1
      js/parse-conf.js
  5. 41
    0
      js/table.js
  6. 132
    8
      modules/display/index.js
  7. 149
    0
      modules/display/xrandr.js
  8. 26
    5
      modules/input/index.js
  9. 8
    1
      modules/process/index.js
  10. 0
    2
      modules/wallpaper/index.js
  11. 1
    1
      udev/index.js

+ 28
- 13
dedaemon.js View File

@@ -6,18 +6,19 @@ var async = require("./js/async");
var udev = require("./udev");

var modules = {
// display: require("./modules/display"),
display: require("./modules/display"),
input: require("./modules/input"),
wallpaper: require("./modules/wallpaper"),
process: require("./modules/process"),
};

if (!process.argv[2]) {
console.error("Usage:", process.argv[1], "<config>");
console.error("Usage:", process.argv[1], "<config file>");
console.error(" ", process.argv[1], "list");
process.exit(1);
}

var config = parseConf(process.argv[2]);
var config;

function createLogger(name) {
function log(pre, msg) {
@@ -34,7 +35,7 @@ function createLogger(name) {
function startAll() {
Object.keys(modules).forEach(i => {
var mod = modules[i];
var conf = config[i] || {};
var conf = config[i];

if (conf instanceof Array && conf.length === 0)
return;
@@ -50,16 +51,30 @@ function stopAll(cb) {
}

function onTerm() {
udev.exit();
console.error("Exiting...");
stopAll(() => process.exit(1));
udev.exit();
}

syscheck(ok => {
if (ok)
startAll();
else
console.error("Missing binaries, exiting.");
});
if (process.argv[2] === "list") {
console.error("display:");
modules.display.list(() => {
console.error("input:");
modules.input.list(() => {
udev.exit();
process.exit(0);
});
});
} else {
var config = parseConf(process.argv[2]);

syscheck(ok => {
if (ok)
startAll();
else
console.error("Missing binaries, exiting.");
});

process.on("SIGTERM", onTerm);
process.on("SIGINT", onTerm);
process.on("SIGTERM", onTerm);
process.on("SIGINT", onTerm);
}

+ 24
- 0
example.hcnf View File

@@ -0,0 +1,24 @@
# display * {
# mode max
# rate max
# where { left-of primary }
# }

input * {
type pointer
options [
[ "libinput Tapping Enabled" 1 ]
]
}

input * {
type keyboard
commands [
"xset r rate 200 60"
"setxkbmap dvorak -option ctrl:swapcaps -option altwin:swap_alt_win"
]
}

wallpaper {
path /home/martin/background.jpg
}

+ 3
- 0
js/async.js View File

@@ -1,6 +1,9 @@
module.exports = async;

function async(num, cb) {
if (num === 0)
return cb();

return function() {
num -= 1;
if (num === 0)

+ 1
- 1
js/parse-conf.js View File

@@ -7,7 +7,7 @@ var configStructure = {
count: "many",
props: {
name: "string",
resolution: "string",
mode: "string",
rate: "string",
where: "object",
},

+ 41
- 0
js/table.js View File

@@ -0,0 +1,41 @@
module.exports = table;

function table(log, data, pre, sep) {
var sep = sep || " ";
pre = pre || "";

var lengths = [];

var i = 0;
while (true) {
var longest = -1;
for (var j in data) {
var d = data[j];
if (d[i] == null)
continue;

if (d[i].length > longest)
longest = d[i].length;
}
if (longest === -1)
break;
else
lengths[i] = longest;
i += 1;
}

for (var i in data) {
var d = data[i];
var str = "";
for (var j in d) {
var s = d[j];
var len = lengths[j];

str +=
s +
(j == d.length - 1 ? "" : sep) +
new Array(len - s.length + 1).join(" ");
}
log(pre+str);
}
}

+ 132
- 8
modules/display/index.js View File

@@ -1,22 +1,121 @@
var udev = require("../../udev");
var xrandr = require("./xrandr");
var table = require("../../js/table");
var spawn = require("child_process").spawn;

exports.start = start;
exports.stop = stop;
exports.event = event;
exports.list = list;

var conf;
var logger;
var modules;

function onchange(dev) {
if (dev.ACTION === "add")
logger.info("display added");
else if (dev.ACTION === "change")
logger.info("display changed");
else
logger.info(dev);
function findMode(display, mode) {
if (mode === "max")
return display.modes[0];

//modules.wallpaper.event("reload");
for (var i in display.modes) {
if (display.modes[i].resStr == mode)
return display.modes[i];
}

return null;
}

function findRate(mode, rate) {
if (rate === "max")
return mode.rates[0];

for (var i in mode.rates) {
if (mode.rates[i] == rate)
return mode.rates[i];
}

return null;
}

function applyRule(primary, rule, display) {
var cmd = "xrandr";
var args = [ "--output", display.id ];

if (rule.rate != null && rule.mode == null)
return logger.warn("Display "+rule.name+": 'rate' specified without 'mode'");

var mode;
if (rule.mode != null) {
mode = findMode(display, rule.mode);
if (mode == null)
return logger.warn(
"Display "+rule.name+": Invalid mode: "+rule.res);

args.push("--mode");
args.push(mode.resStr);
}

var rate;
if (rule.rate != null) {
rate = findRate(mode, rule.rate);
if (rate == null)
return logger.warn(
"Display "+rule.name+": Invalid rate: "+rule.rate);

args.push("--rate");
args.push(rate);
}

if (rule.where) {
var w = rule.where;
for (var i in w) {
if (
i !== "left-of" && i !== "right-of" &&
i !== "above" && i !== "below") {
return logger.warn(
"Display "+rule.name+": Invalid 'where' value: "+i);
}

if (w[i] === "primary" && display === primary)
continue;

var id;
if (w[i] === "primary") {
id = primary.id;
} else {
id = w[i];
}

args.push("--"+i);
args.push(id);
}
}

var child = spawn(cmd, args);
child.stderr.on("data", d => logger.warn("xrandr:", d.toString()));
}

function onchange() {
if (conf == null) {
setTimeout(() => modules.wallpaper.event("reload"), 0);
logger.info("No displays configured.");
return;
}

xrandr.list(data => {
var displays = data.displays.filter(d => d.connected);
var primary = displays.filter(d => d.primary)[0];

conf.forEach(entry => {
displays.forEach(disp => {
if (entry.name !== "*" && entry.name !== disp.id)
return;

applyRule(primary, entry, disp);
});
});

setTimeout(() => modules.wallpaper.event("reload"), 400);
});
}

function start(conf_, logger_, modules_) {
@@ -24,6 +123,7 @@ function start(conf_, logger_, modules_) {
logger = logger_ || logger;
modules = modules_ || modules;

onchange();
udev.monitor("drm", onchange);
}

@@ -38,3 +138,27 @@ function event(name, ...params) {
logger.warn("Unknown event:", name);
}
}

function list(cb) {
xrandr.list(devs => {
devs.displays.forEach(dev => {
console.error(
"\tname: "+dev.id+
(dev.connected ? ", connected" : "")+
(dev.primary ? ", primary" : ""));

var data = [];
for (var i in dev.modes) {
var mode = dev.modes[i];
data[i] = [];
var d = data[i];
d.push("mode: "+mode.resStr+",");
d.push("rate:");
mode.rates.forEach(r => d.push(r));
}

table(console.error.bind(console), data, "\t\t");
});
cb();
});
}

+ 149
- 0
modules/display/xrandr.js View File

@@ -0,0 +1,149 @@
var spawn = require("child_process").spawn;

exports.list = list;

function parseScreen(line) {
var rxId = /Screen (\d+)/;
var rxMin = /minimum (\d+) x (\d+)/;
var rxCurr = /current (\d+) x (\d+)/;
var rxMax = /maximum (\d+) x (\d+)/;

var id = line.match(rxId);
var min = line.match(rxMin);
var curr = line.match(rxCurr);
var max = line.match(rxMax);
return {
id: parseInt(id[1]),
minimum: {
w: parseInt(min[1]),
h: parseInt(min[2]),
},
current: {
w: parseInt(curr[1]),
h: parseInt(curr[2]),
},
maximum: {
w: parseInt(max[1]),
h: parseInt(max[2]),
},
};
}

function parseDisplayMode(line) {
var rxResStr = /(\d+x\d+[^\s]+)/;
var rxRes = /(\d+)x(\d+)/;
var rxRate = /(\d+\.\d+)/;

var obj = {};

var resStr = line.match(rxResStr);
obj.resStr = resStr[1];
var res = line.match(rxRes);
obj.res = { w: parseInt(res[1]), h: parseInt(res[2]) };

line = line.substr(res.index + res[0].length);
obj.rates = [];

while (true) {
var rate = line.match(rxRate);
if (rate === null)
break;

obj.rates.push(rate[1]);
line = line.substr(rate.index + rate[0].length);
}

return obj;
}

function parseDisplayModes(lines) {
var modes = [];
for (var i = 1; i < lines.length; ++i) {
modes.push(parseDisplayMode(lines[i]));
}
return modes;
}

function parseDisplayStart(line) {
var rxId = /^([^ ]+)/;
var rxPrimary = /primary/;
var rxConnected = /(connected|disconnected)/;
var rxRes = /(\d+)x(\d+)\+(\d+)\+(\d+)/;
var rxDims = /(\d+)mm x (\d+)mm/;

var id = line.match(rxId);
var primary = line.match(rxPrimary);
var connected = line.match(rxConnected)[1] === "connected";
var res = line.match(rxRes);
var dims = line.match(rxDims);

return {
id: id[1],
primary: primary ? true : false,
connected: connected,
res: (res == null ? null : {
w: parseInt(res[1]),
h: parseInt(res[2]),
x: parseInt(res[3]),
y: parseInt(res[4]),
}),
dims: (res == null ? null : {
w: parseInt(dims[1]),
h: parseInt(dims[2]),
}),
}
}

function parseDisplay(lines) {
var obj = parseDisplayStart(lines[0]);
if (obj.connected)
obj.modes = parseDisplayModes(lines);
return obj;
}

function parse(str) {
var strings = [];
var lines = str.split("\n");

var screens = [];
var displays = [];

var currArr;
for (var i in lines) {
var line = lines[i];
if (line === "")
continue;

if (line.indexOf("Screen") === 0) {
screens.push(parseScreen(line));
} else if (line[0] !== " " && line[0] !== "\t") {
if (currArr) {
displays.push(parseDisplay(currArr));
}
currArr = [line];
} else {
currArr.push(line);
}
}

if (currArr) {
displays.push(parseDisplay(currArr));
}

return {
screens: screens,
displays: displays.filter(d => d != null),
}
}

function list(cb) {
var child = spawn("xrandr", [ "--query" ]);

var output = "";
child.stdout.on("data", d => output += d);

child.on("close", () => {
var devs = parse(output);
cb(devs);
});
}

+ 26
- 5
modules/input/index.js View File

@@ -3,10 +3,12 @@ var spawn = require("child_process").spawn;
var exec = require("child_process").exec;

var debounce = require("../../js/debounce");
var table = require("../../js/table");

exports.start = start;
exports.stop = stop;
exports.event = event;
exports.list = list;

var conf;
var logger;
@@ -88,10 +90,7 @@ function nameMatches(dev, name) {
return true;
}

function onchange(dev) {
if (!filter(dev))
return;

function gettype(dev) {
var isKeyboard = !!dev.ID_INPUT_KEYBOARD;
var isPointer = !!(dev.ID_INPUT_MOUSE || dev.ID_INPUT_TOUCHPAD);

@@ -102,7 +101,16 @@ function onchange(dev) {
else if (isKeyboard)
inputType = "keyboard";
else if (isPointer)
inputType = "mouse";
inputType = "pointer";

return { isKeyboard, isPointer, inputType };
}

function onchange(dev) {
if (!filter(dev))
return;

var { isKeyboard, isPointer, inputType } = gettype(dev);

// Log add/change
if (dev.ACTION === "add")
@@ -164,3 +172,16 @@ function event(name, ...params) {
logger.warn("Unknown event: "+name);
}
}

function list(cb) {
udev.list("input", devs => {
var data = [];
devs.filter(filter).forEach((dev, i) => {
var { inputType } = gettype(dev);

data[i] = [ "name: "+dev.NAME+",", "type: "+inputType ];
});
table(console.error.bind(console), data, "\t");
cb();
});
}

+ 8
- 1
modules/process/index.js View File

@@ -130,13 +130,20 @@ class ProcessGroup {
}
}

var procs = {};
var procs;

function start(conf_, logger_, modules_) {
conf = conf_ || conf;
logger = logger_ || logger;
modules = modules_ || modules;

procs = {};

if (conf == null) {
logger.info("No processes.");
return;
}

conf.forEach(proc => {
if (procs[proc.name])
return logger.warn("Igonring duplicate process: "+proc.name);

+ 0
- 2
modules/wallpaper/index.js View File

@@ -58,8 +58,6 @@ function start(conf_, logger_, modules_) {
if (!conf.path)
return logger.error("Expected conf.path");

runFeh();

var dirname = pathlib.dirname(conf.path);
var basename = pathlib.basename(conf.path);


+ 1
- 1
udev/index.js View File

@@ -24,7 +24,7 @@ child.stdout.on("data", d => {

currstr = lines[lines.length - 1];
});
child.stderr.on("data", d => console.error(d.toString()));
child.stderr.on("data", d => console.error("udev error:", d.toString()));

var listq = [];
var monitors = {};

Loading…
Cancel
Save