mirror of
https://github.com/tomru/switchmon.git
synced 2026-03-03 06:27:23 +01:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06855cee91 | ||
|
|
c71c1629e2 | ||
|
|
5d945c8f66 | ||
|
|
52ba27e092 | ||
|
|
272441dd70 | ||
|
|
fe99d75cb4 | ||
|
|
90ad436396 | ||
|
|
a64a14419b | ||
|
|
ef0b8de4e1 | ||
|
|
03f11a88d5 | ||
|
|
a82891c9e8 | ||
|
|
e881214463 | ||
|
|
f6f0de0e04 | ||
|
|
9d25ebfc33 | ||
|
|
adaa1ed68a | ||
|
|
c1c0f75c0a | ||
|
|
41e85ebb68 | ||
|
|
372eaab45e | ||
|
|
fa06e7914b | ||
|
|
412f26a9dd | ||
|
|
0adc89d2d9 | ||
|
|
c1e651245e | ||
|
|
77c1f3c674 | ||
|
|
15faee38c9 | ||
|
|
67695abfab | ||
|
|
128724bbc8 | ||
|
|
029b12332a | ||
|
|
067471dbc5 | ||
|
|
85cb7ba3f8 | ||
|
|
dc0552c10d | ||
|
|
196b9cb8c8 | ||
|
|
e3054967b4 | ||
|
|
3b0ee78cf4 | ||
|
|
a2a47cdf8d | ||
|
|
242a12d12a | ||
|
|
511b972c22 | ||
|
|
7d391334f1 | ||
|
|
31711c7191 | ||
|
|
0cff05484f | ||
|
|
d2e0e172f9 | ||
|
|
610fd07bec | ||
|
|
985d394872 | ||
|
|
04a8bf17eb | ||
|
|
d452fefc9c | ||
|
|
6dca960958 | ||
|
|
cbe0c518d8 | ||
|
|
919735bf5c |
14
.editorconfig
Normal file
14
.editorconfig
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# This file is for unifying the coding style for different editors and IDEs
|
||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[**.{js,json,md}]
|
||||||
|
insert_final_newline = true
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
switchmon-*.tgz
|
||||||
|
coverage
|
||||||
|
.lvimrc
|
||||||
22
.jshintrc
Normal file
22
.jshintrc
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"node": true,
|
||||||
|
"browser": false,
|
||||||
|
"esnext": true,
|
||||||
|
"bitwise": true,
|
||||||
|
"camelcase": true,
|
||||||
|
"curly": true,
|
||||||
|
"eqeqeq": true,
|
||||||
|
"immed": true,
|
||||||
|
"indent": 4,
|
||||||
|
"latedef": true,
|
||||||
|
"newcap": true,
|
||||||
|
"noarg": true,
|
||||||
|
"quotmark": "single",
|
||||||
|
"regexp": true,
|
||||||
|
"undef": true,
|
||||||
|
"unused": true,
|
||||||
|
"strict": true,
|
||||||
|
"trailing": true,
|
||||||
|
"smarttabs": true,
|
||||||
|
"globals" : {}
|
||||||
|
}
|
||||||
2
.npmignore
Normal file
2
.npmignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.editorconfig
|
||||||
|
.jshintrc
|
||||||
4
.travis.yml
Normal file
4
.travis.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- "5"
|
||||||
|
install: npm install
|
||||||
60
README.md
60
README.md
@@ -1,14 +1,58 @@
|
|||||||
|
[](https://travis-ci.org/tomru/switchmon) [](https://coveralls.io/github/tomru/switchmon?branch=master)
|
||||||
|
|
||||||
# SWM - Switch Monitors
|
# SWM - Switch Monitors
|
||||||
|
|
||||||
Simple helper for turning on/off connected/disconnected monitors with `xrandr`.
|
Simple helper for turning on/off connected/disconnected monitors with `xrandr`.
|
||||||
|
|
||||||
It uses `xrandr` to detect on which connectors there are monitors connected. It
|
## Synopsis
|
||||||
then runs `xrandr` and sets all disconnected monitors to `off` and all connected
|
`swm`
|
||||||
monitors to `on`.
|
|
||||||
|
|
||||||
Monitors will be turned on with xrandr `--auto` option which tries to detect
|
`swm [monitor-1..montior-n]`
|
||||||
the optimal video setting.
|
|
||||||
|
|
||||||
If there are multiple monitors connected, each monitor is placed to the right
|
`swm --profile external`
|
||||||
of the previous monitor. This order is currently driven as xrandr lists the
|
|
||||||
connectors.
|
`swm --list`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
To turn on/off connected/disconnected monitors.
|
||||||
|
|
||||||
|
If no monitors are specified all connected monitors will be turned on and
|
||||||
|
placed from left to right in alphabetical order of their name.
|
||||||
|
|
||||||
|
If monitors `monitor-1..monitor-n` are specified these monitors will be turned
|
||||||
|
on and place them from left to right in the order given.
|
||||||
|
|
||||||
|
* `--profile profilename` or `-p profilename`
|
||||||
|
|
||||||
|
If a profile is specified, the configured monitors will be turned on.
|
||||||
|
|
||||||
|
* `--postCmd "some cmd"`
|
||||||
|
|
||||||
|
A post command is executed after switching the monitors. This is usefull to
|
||||||
|
tell your window manager to re-detect monitors, e.g. for herbstluftwm
|
||||||
|
`herbstclient reload`.
|
||||||
|
|
||||||
|
* `-l` or `swm --list`
|
||||||
|
|
||||||
|
List all devices with the connectivity status.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The configuration can be placed in `$XDG_CONFIG_HOME/switchmon/config.json` in
|
||||||
|
the form of
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"postCmd": "some command",
|
||||||
|
"profiles": {
|
||||||
|
"internal": ["LVDS1"],
|
||||||
|
"external": ["HDMI1"],
|
||||||
|
"dual": ["LVDS1", "HDMI1"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
Node.js > v4 on your PATH.
|
||||||
|
|||||||
52
cli.js
Executable file
52
cli.js
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const argv = require('minimist')(process.argv.slice(2));
|
||||||
|
|
||||||
|
const swm = require('./swm.js');
|
||||||
|
const config = require('./config.js');
|
||||||
|
const usage = require('./usage.js');
|
||||||
|
|
||||||
|
const postCmd = argv.postCmd || config.postCmd;
|
||||||
|
const profile = argv.profile || argv.p;
|
||||||
|
|
||||||
|
function connectionStatus(device) {
|
||||||
|
return device.connected ? 'Connected' : 'Disconnected';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argv.help || argv.h) {
|
||||||
|
console.log(usage);
|
||||||
|
return;
|
||||||
|
} else if (argv.list || argv.l) {
|
||||||
|
const devices = swm.getDevices((err, devices) => {
|
||||||
|
if (err) {
|
||||||
|
throw new Error(err);
|
||||||
|
}
|
||||||
|
console.log('Detected devices:\n');
|
||||||
|
Object.keys(devices)
|
||||||
|
.sort(key => !devices[key].connected)
|
||||||
|
.forEach(key => console.log(key + ':', connectionStatus(devices[key])));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let selectedMonitors = argv._;
|
||||||
|
|
||||||
|
if (profile) {
|
||||||
|
if (!config.profiles[profile]) {
|
||||||
|
console.error('profile', profile, 'not found in config');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
selectedMonitors = config.profiles[profile];
|
||||||
|
console.log('Using profile', profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Switching on', selectedMonitors.length ? selectedMonitors : 'all connected monitors');
|
||||||
|
|
||||||
|
swm.getDevices((err, devices) => {
|
||||||
|
if (err) {
|
||||||
|
throw new Error(err);
|
||||||
|
}
|
||||||
|
const xrandrOptions = swm.generateXrandrOptions(selectedMonitors, devices);
|
||||||
|
swm.switchDevices(xrandrOptions);
|
||||||
|
swm.executePostCmd(postCmd);
|
||||||
|
});
|
||||||
|
}
|
||||||
21
config.js
Normal file
21
config.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const configPath = require('xdg').basedir.configPath('switchmon/config.json');
|
||||||
|
|
||||||
|
const defaults = {
|
||||||
|
postCmd: undefined,
|
||||||
|
profiles: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = Object.assign({}, defaults);
|
||||||
|
|
||||||
|
try {
|
||||||
|
config = Object.assign(config, require(configPath));
|
||||||
|
} catch(err) {
|
||||||
|
if (err.code !== 'MODULE_NOT_FOUND') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
// No config found in "${configPath}". Using defaults...
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
40
package.json
Normal file
40
package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "switchmon",
|
||||||
|
"version": "1.2.0",
|
||||||
|
"description": "Simple helper for turning on/off connected/disconnected monitors with xrandr",
|
||||||
|
"main": "cli.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage",
|
||||||
|
"testwatch": "mocha -w"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"swm": "./cli.js"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/tomru/switchmon.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"xrandr",
|
||||||
|
"monitor"
|
||||||
|
],
|
||||||
|
"author": "Thomas Ruoff",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/tomru/switchmon/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/tomru/switchmon#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": "^1.2.0",
|
||||||
|
"xdg": "^0.1.1",
|
||||||
|
"xrandr-parse": "^0.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"coveralls": "^2.11.8",
|
||||||
|
"istanbul": "^0.4.2",
|
||||||
|
"mocha": "^2.4.5",
|
||||||
|
"mocha-lcov-reporter": "^1.2.0",
|
||||||
|
"proxyquire": "^1.7.4",
|
||||||
|
"sinon": "^1.17.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
37
swm
37
swm
@@ -1,37 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
#
|
|
||||||
# Enable all connected montiors, disable all disconnected ones.
|
|
||||||
# Multiple monitors will be added to the right of previous ones.
|
|
||||||
#
|
|
||||||
# Author Thomas Ruoff <Thomas.Ruoff@gmail.com>
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
CONNECTED=$( xrandr | grep " connected" | awk '{print $1}')
|
|
||||||
DISCONNECTED=$( xrandr | grep "disconnected" | awk '{print $1}')
|
|
||||||
|
|
||||||
POSTCMD="herbstclient reload"
|
|
||||||
|
|
||||||
|
|
||||||
# turn off all disconnected monitors
|
|
||||||
XRANDR_OFF_OPTIONS=""
|
|
||||||
for mon in $DISCONNECTED; do
|
|
||||||
XRANDR_OFF_OPTIONS+=" --output $mon --off"
|
|
||||||
done
|
|
||||||
|
|
||||||
# turn on all connected monitors
|
|
||||||
XRANDR_ON_OPTIONS=""
|
|
||||||
LAST=""
|
|
||||||
for mon in $CONNECTED; do
|
|
||||||
XRANDR_ON_OPTIONS+=" --output $mon --auto"
|
|
||||||
if [ ! -z $LAST ]; then
|
|
||||||
XRANDR_ON_OPTIONS+=" --right-of $LAST"
|
|
||||||
fi
|
|
||||||
LAST=$mon
|
|
||||||
done
|
|
||||||
|
|
||||||
xrandr $XRANDR_ON_OPTIONS $XRANDR_OFF_OPTIONS &&\
|
|
||||||
echo Activated monitors: ${CONNECTED} &&\
|
|
||||||
$POSTCMD
|
|
||||||
|
|
||||||
exit $?
|
|
||||||
94
swm.js
Normal file
94
swm.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const xrandrParse = require('xrandr-parse');
|
||||||
|
const exec = require('child_process').exec;
|
||||||
|
|
||||||
|
function executeCmd(cmd, callback) {
|
||||||
|
exec(cmd, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDevices(callback) {
|
||||||
|
executeCmd('xrandr', (err, stdout) => callback(err, err ? null : xrandrParse(stdout)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchDevices(xrandrOptions, callback) {
|
||||||
|
executeCmd('xrandr ' + xrandrOptions, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function executePostCmd(postCmd, callback) {
|
||||||
|
executeCmd(postCmd, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function orderDeviceKeys(selectedDevices, devices) {
|
||||||
|
let orderedDeviceKeys = Object.keys(devices).sort();
|
||||||
|
|
||||||
|
// fix the sort order if monitors were explicitly selected
|
||||||
|
selectedDevices.reverse().forEach((monitor) => {
|
||||||
|
const index = orderedDeviceKeys.indexOf(monitor);
|
||||||
|
if (index < 0) {
|
||||||
|
console.error('Unkown monitor', monitor, '(ignored)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
orderedDeviceKeys.splice(index, 1);
|
||||||
|
orderedDeviceKeys.unshift(monitor);
|
||||||
|
});
|
||||||
|
|
||||||
|
return orderedDeviceKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActivationFlag(selectedDevices, devices) {
|
||||||
|
const result = {};
|
||||||
|
const selectByDefault = selectedDevices.length === 0;
|
||||||
|
|
||||||
|
Object.keys(devices).forEach(deviceKey => {
|
||||||
|
const device = Object.assign({}, devices[deviceKey]);
|
||||||
|
const isSelected = selectedDevices.indexOf(deviceKey) > -1;
|
||||||
|
|
||||||
|
if (isSelected || selectByDefault) {
|
||||||
|
if (device.connected) {
|
||||||
|
device.activate = true;
|
||||||
|
} else if (isSelected) {
|
||||||
|
console.error(deviceKey, 'not connected. Skipping...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result[deviceKey] = device;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateXrandrOptions(selectedDevices, rawDevices) {
|
||||||
|
let xrandrOptions = '';
|
||||||
|
let prevDevice;
|
||||||
|
let devices = setActivationFlag(selectedDevices, rawDevices);
|
||||||
|
|
||||||
|
orderDeviceKeys(selectedDevices, devices).forEach(deviceKey => {
|
||||||
|
const device = devices[deviceKey];
|
||||||
|
const monitorOptions = ['', '--output', deviceKey];
|
||||||
|
|
||||||
|
if (!device.activate) {
|
||||||
|
monitorOptions.push('--off');
|
||||||
|
} else {
|
||||||
|
monitorOptions.push('--auto');
|
||||||
|
|
||||||
|
if (prevDevice) {
|
||||||
|
monitorOptions.push(['--right-of', prevDevice].join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
prevDevice = deviceKey;
|
||||||
|
}
|
||||||
|
xrandrOptions += monitorOptions.join(' ');
|
||||||
|
});
|
||||||
|
|
||||||
|
// sanity check if at least one monitor is on
|
||||||
|
if (xrandrOptions.indexOf('--auto') === -1) {
|
||||||
|
throw new Error('Non of the given monitors are connected, aborting...');
|
||||||
|
}
|
||||||
|
|
||||||
|
return xrandrOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.getDevices = getDevices;
|
||||||
|
module.exports.generateXrandrOptions = generateXrandrOptions;
|
||||||
|
module.exports.switchDevices = switchDevices;
|
||||||
|
module.exports.executePostCmd = executePostCmd;
|
||||||
184
test/cli.tests.js
Normal file
184
test/cli.tests.js
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const sinon = require('sinon');
|
||||||
|
const assert = require('assert');
|
||||||
|
const proxyquire = require('proxyquire');
|
||||||
|
|
||||||
|
describe('cli', () => {
|
||||||
|
let sandbox;
|
||||||
|
let consoleLogSpy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.sandbox.create();
|
||||||
|
consoleLogSpy = sandbox.spy(console, 'log');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows help', () => {
|
||||||
|
const minimistStub = sandbox.stub();
|
||||||
|
minimistStub.returns({h: true});
|
||||||
|
|
||||||
|
const cli = proxyquire('../cli.js', {
|
||||||
|
'minimist': minimistStub,
|
||||||
|
'./usage.js': '[usage]'
|
||||||
|
});
|
||||||
|
assert.equal(consoleLogSpy.callCount, 1);
|
||||||
|
assert.equal(consoleLogSpy.args[0][0], '[usage]');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('device list', () => {
|
||||||
|
let minimistStub, getDevicesStub;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
minimistStub = sandbox.stub();
|
||||||
|
minimistStub.returns({l: true});
|
||||||
|
getDevicesStub = sandbox.stub();
|
||||||
|
|
||||||
|
proxyquire('../cli.js', {
|
||||||
|
'minimist': minimistStub,
|
||||||
|
'./swm.js': {
|
||||||
|
getDevices: getDevicesStub
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lists devices', () => {
|
||||||
|
getDevicesStub.args[0][0](null, {
|
||||||
|
LVDS1: {connected: true},
|
||||||
|
HDMI2: {connected: false}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(getDevicesStub.callCount, 1);
|
||||||
|
assert.equal(consoleLogSpy.callCount, 3);
|
||||||
|
assert.equal(consoleLogSpy.args[0][0], 'Detected devices:\n');
|
||||||
|
assert.equal(consoleLogSpy.args[1].join(' '), 'LVDS1: Connected');
|
||||||
|
assert.equal(consoleLogSpy.args[2].join(' '), 'HDMI2: Disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error when listing devices fails', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
getDevicesStub.args[0][0]('[some err]');
|
||||||
|
}, /some err/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('switching', () => {
|
||||||
|
let minimistStub;
|
||||||
|
let deviceData;
|
||||||
|
let getDevicesStub;
|
||||||
|
let generateXrandrOptionsStub;
|
||||||
|
let switchDevicesStub;
|
||||||
|
let executePostCmdStub;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
minimistStub = sandbox.stub();
|
||||||
|
minimistStub.returns({
|
||||||
|
_: ['LVDS1'],
|
||||||
|
postCmd: '[some post cmd]'
|
||||||
|
});
|
||||||
|
|
||||||
|
deviceData = {
|
||||||
|
LVDS1: {connected: true},
|
||||||
|
HDMI2: {connected: false}
|
||||||
|
};
|
||||||
|
|
||||||
|
getDevicesStub = sandbox.stub();
|
||||||
|
generateXrandrOptionsStub = sandbox.stub().returns('[some xrandr options]');
|
||||||
|
switchDevicesStub = sandbox.stub();
|
||||||
|
executePostCmdStub = sandbox.stub();
|
||||||
|
|
||||||
|
const cli = proxyquire('../cli.js', {
|
||||||
|
'minimist': minimistStub,
|
||||||
|
'./swm.js': {
|
||||||
|
getDevices: getDevicesStub,
|
||||||
|
generateXrandrOptions: generateXrandrOptionsStub,
|
||||||
|
switchDevices: switchDevicesStub,
|
||||||
|
executePostCmd: executePostCmdStub
|
||||||
|
},
|
||||||
|
'./config.js': {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls getDevices', () => {
|
||||||
|
getDevicesStub.args[0][0](null, deviceData);
|
||||||
|
assert.equal(getDevicesStub.callCount, 1, 'calls device stub');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error when getDevices fails', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
getDevicesStub.args[0][0]('[some err]');
|
||||||
|
}, /some err/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls generateXrandrOptions', () => {
|
||||||
|
getDevicesStub.args[0][0](null, deviceData);
|
||||||
|
assert.equal(generateXrandrOptionsStub.callCount, 1, 'calls generateXrandrOptions');
|
||||||
|
assert.deepEqual(generateXrandrOptionsStub.args[0][0], ['LVDS1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls switchDevices', () => {
|
||||||
|
getDevicesStub.args[0][0](null, deviceData);
|
||||||
|
assert.equal(switchDevicesStub.callCount, 1);
|
||||||
|
assert.equal(switchDevicesStub.args[0][0], '[some xrandr options]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls executePostCmd', () => {
|
||||||
|
getDevicesStub.args[0][0](null, deviceData);
|
||||||
|
assert.equal(executePostCmdStub.callCount, 1);
|
||||||
|
assert.equal(executePostCmdStub.args[0][0], '[some post cmd]');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('switching with profiles', () => {
|
||||||
|
let minimistStub;
|
||||||
|
let deviceData;
|
||||||
|
let getDevicesStub;
|
||||||
|
let generateXrandrOptionsStub;
|
||||||
|
let switchDevicesStub;
|
||||||
|
let executePostCmdStub;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
minimistStub = sandbox.stub();
|
||||||
|
minimistStub.returns({
|
||||||
|
_: ['LVDS1'],
|
||||||
|
postCmd: '[some post cmd]',
|
||||||
|
profile: 'profile1'
|
||||||
|
});
|
||||||
|
|
||||||
|
deviceData = {
|
||||||
|
LVDS1: {connected: true},
|
||||||
|
HDMI2: {connected: false}
|
||||||
|
};
|
||||||
|
|
||||||
|
getDevicesStub = sandbox.stub();
|
||||||
|
generateXrandrOptionsStub = sandbox.stub().returns('[some xrandr options]');
|
||||||
|
switchDevicesStub = sandbox.stub();
|
||||||
|
executePostCmdStub = sandbox.stub();
|
||||||
|
|
||||||
|
proxyquire('../cli.js', {
|
||||||
|
'minimist': minimistStub,
|
||||||
|
'./swm.js': {
|
||||||
|
getDevices: getDevicesStub,
|
||||||
|
generateXrandrOptions: generateXrandrOptionsStub,
|
||||||
|
switchDevices: switchDevicesStub,
|
||||||
|
executePostCmd: executePostCmdStub
|
||||||
|
},
|
||||||
|
'./config.js': {
|
||||||
|
profiles: {
|
||||||
|
profile1: ['HDMI1', 'HDMI2']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls generateXrandrOptions with profile settings', () => {
|
||||||
|
getDevicesStub.args[0][0](null, deviceData);
|
||||||
|
assert.equal(generateXrandrOptionsStub.callCount, 1, 'calls generateXrandrOptions');
|
||||||
|
assert.deepEqual(generateXrandrOptionsStub.args[0][0], ['HDMI1', 'HDMI2']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
57
test/config.tests.js
Normal file
57
test/config.tests.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const sinon = require('sinon');
|
||||||
|
const assert = require('assert');
|
||||||
|
const proxyquire = require('proxyquire').noCallThru();
|
||||||
|
|
||||||
|
describe('config', () => {
|
||||||
|
let sandbox;
|
||||||
|
let consoleLogSpy;
|
||||||
|
let xdgBasedirConfigStub;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.sandbox.create();
|
||||||
|
consoleLogSpy = sandbox.spy(console, 'log');
|
||||||
|
xdgBasedirConfigStub = sandbox.stub();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads config if existent', () => {
|
||||||
|
xdgBasedirConfigStub.returns('./some_config.json');
|
||||||
|
|
||||||
|
const config = proxyquire('../config.js', {
|
||||||
|
'xdg': {
|
||||||
|
basedir: {
|
||||||
|
configPath: xdgBasedirConfigStub
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'./some_config.json': {
|
||||||
|
postCmd: '[some postCmd]',
|
||||||
|
profiles: {
|
||||||
|
profile1: ['HDMI1', 'HDMI2'],
|
||||||
|
profile2: ['LVDS1']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert.equal(config.postCmd, '[some postCmd]');
|
||||||
|
assert.deepEqual(config.profiles.profile1, ['HDMI1', 'HDMI2']);
|
||||||
|
assert.deepEqual(config.profiles.profile2, ['LVDS1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses defaults if not existent', () => {
|
||||||
|
xdgBasedirConfigStub.returns('./not_existing_config.json');
|
||||||
|
|
||||||
|
const config = proxyquire('../config.js', {
|
||||||
|
'xdg': {
|
||||||
|
basedir: {
|
||||||
|
configPath: xdgBasedirConfigStub
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert.equal(config.postCmd, undefined);
|
||||||
|
assert.equal(Object.keys(config.profiles).length, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
182
test/swm.tests.js
Normal file
182
test/swm.tests.js
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const sinon = require('sinon');
|
||||||
|
const assert = require('assert');
|
||||||
|
const proxyquire = require('proxyquire');
|
||||||
|
|
||||||
|
describe('swm', () => {
|
||||||
|
let sandbox;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.sandbox.create();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getDevices', () => {
|
||||||
|
let execStub;
|
||||||
|
let xrandrParseStub;
|
||||||
|
let swm;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
execStub = sandbox.stub();
|
||||||
|
xrandrParseStub = sandbox.stub();
|
||||||
|
swm = proxyquire('../swm.js', {
|
||||||
|
child_process: {
|
||||||
|
exec: execStub
|
||||||
|
},
|
||||||
|
'xrandr-parse': xrandrParseStub
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls exec', () => {
|
||||||
|
swm.getDevices(sandbox.stub());
|
||||||
|
|
||||||
|
assert.equal(execStub.callCount, 1);
|
||||||
|
assert.equal(execStub.args[0][0], 'xrandr');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes error when exec passes error', (done) => {
|
||||||
|
const devices = swm.getDevices((err, devices) => {
|
||||||
|
assert.equal(xrandrParseStub.callCount, 0);
|
||||||
|
assert.equal(err, 'some error');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
const cb = execStub.args[0][1];
|
||||||
|
cb('some error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses result', (done) => {
|
||||||
|
xrandrParseStub.returns('[some result]');
|
||||||
|
swm.getDevices((err, devices) => {
|
||||||
|
assert.equal(xrandrParseStub.args[0][0], '[some stdout]');
|
||||||
|
assert.equal(xrandrParseStub.callCount, 1);
|
||||||
|
assert.equal(devices, '[some result]');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
const cb = execStub.args[0][1];
|
||||||
|
cb(null, '[some stdout]');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generateXrandrOptions', () => {
|
||||||
|
let swm;
|
||||||
|
let devices;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
devices = {
|
||||||
|
'HDMI1': {
|
||||||
|
connected: true
|
||||||
|
},
|
||||||
|
'HDMI2': {
|
||||||
|
connected: true,
|
||||||
|
},
|
||||||
|
'LVDS1': {
|
||||||
|
connected: false
|
||||||
|
},
|
||||||
|
'DP1': {
|
||||||
|
connected: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
swm = require('../swm.js');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('activation of monitor', () => {
|
||||||
|
it('for connected monitors if non is selected', () => {
|
||||||
|
const result = swm.generateXrandrOptions([], devices);
|
||||||
|
assert(result.indexOf('--output HDMI1 --auto') > -1);
|
||||||
|
assert(result.indexOf('--output HDMI2 --auto') > -1);
|
||||||
|
assert(result.indexOf('--output LVDS1 --off') > -1);
|
||||||
|
});
|
||||||
|
it('for connected selected monitors', () => {
|
||||||
|
const result = swm.generateXrandrOptions(['HDMI2'], devices);
|
||||||
|
assert(result.indexOf('--output HDMI1 --off') > -1);
|
||||||
|
assert(result.indexOf('--output HDMI2 --auto') > -1);
|
||||||
|
assert(result.indexOf('--output LVDS1 --off') > -1);
|
||||||
|
});
|
||||||
|
it('skipps selected monitors that are not connected', () => {
|
||||||
|
const result = swm.generateXrandrOptions(['LVDS1', 'HDMI2'], devices);
|
||||||
|
assert(result.indexOf('--output HDMI2 --auto') > -1);
|
||||||
|
assert(result.indexOf('--output LVDS1 --off') > -1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('aligns monitor n to the right of monitor n-1', () => {
|
||||||
|
const result = swm.generateXrandrOptions(['HDMI1', 'HDMI2', 'DP1'], devices);
|
||||||
|
assert(result.indexOf('--output HDMI1 --auto') > -1);
|
||||||
|
assert(result.indexOf('--output HDMI2 --auto --right-of HDMI1') > -1);
|
||||||
|
assert(result.indexOf('--output DP1 --auto --right-of HDMI2') > -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when no selected monitor is connected', () => {
|
||||||
|
assert.throws(() => {swm.generateXrandrOptions(['LVDS1'], devices)});
|
||||||
|
assert.throws(() => {swm.generateXrandrOptions(['BOGUS'], devices)});
|
||||||
|
devices = {};
|
||||||
|
assert.throws(() => {swm.generateXrandrOptions([], devices)});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('switchDevices', () => {
|
||||||
|
let execStub;
|
||||||
|
let swm;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
execStub = sandbox.stub();
|
||||||
|
swm = proxyquire('../swm.js', {
|
||||||
|
child_process: {
|
||||||
|
exec: execStub
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls exec with xrandr and opitons', () => {
|
||||||
|
swm.switchDevices('[some options]', sandbox.stub());
|
||||||
|
|
||||||
|
assert.equal(execStub.callCount, 1);
|
||||||
|
assert.equal(execStub.args[0][0], 'xrandr [some options]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes error when exec passes error', (done) => {
|
||||||
|
swm.switchDevices('[some options]', (err, devices) => {
|
||||||
|
assert.equal(err, 'some error');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
const cb = execStub.args[0][1];
|
||||||
|
cb('some error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('executePostCmd', () => {
|
||||||
|
let execStub;
|
||||||
|
let swm;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
execStub = sandbox.stub();
|
||||||
|
swm = proxyquire('../swm.js', {
|
||||||
|
child_process: {
|
||||||
|
exec: execStub
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls postCmd', () => {
|
||||||
|
swm.executePostCmd('[some cmd]');
|
||||||
|
|
||||||
|
assert.equal(execStub.callCount, 1);
|
||||||
|
assert.equal(execStub.args[0][0], '[some cmd]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes error when exec passes error', (done) => {
|
||||||
|
swm.executePostCmd('[some cmd]', (err, devices) => {
|
||||||
|
assert.equal(err, '[some error]');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
const cb = execStub.args[0][1];
|
||||||
|
cb('[some error]');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
47
usage.js
Normal file
47
usage.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
const usage = `Synopsis
|
||||||
|
|
||||||
|
swm
|
||||||
|
swm [monitor-1..montior-n]
|
||||||
|
swm --profile external
|
||||||
|
swm --list
|
||||||
|
|
||||||
|
Description
|
||||||
|
|
||||||
|
To turn on/off connected/disconnected monitors.
|
||||||
|
|
||||||
|
If no monitors are specified all connected monitors will be turned on and
|
||||||
|
placed from left to right in alphabetical order of their name.
|
||||||
|
|
||||||
|
If monitors monitor-1..monitor-n are specified these monitors will be turned
|
||||||
|
on and place them from left to right in the order given.
|
||||||
|
|
||||||
|
--profile profilename or -p profilename
|
||||||
|
If a profile is specified, the configured monitors will be turned on.
|
||||||
|
|
||||||
|
--postCmd "some cmd"
|
||||||
|
|
||||||
|
A post command is executed after switching the monitors. This is usefull to
|
||||||
|
tell your window manager to re-detect monitors, e.g. for herbstluftwm
|
||||||
|
herbstclient reload.
|
||||||
|
|
||||||
|
-l or swm --list
|
||||||
|
|
||||||
|
List all devices with the connectivity status.
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
|
||||||
|
The configuration can be placed in $XDG_CONFIG_HOME/switchmon/config.json in
|
||||||
|
the form of
|
||||||
|
|
||||||
|
{
|
||||||
|
"postCmd": "some command",
|
||||||
|
"profiles": {
|
||||||
|
"internal": ["LVDS1"],
|
||||||
|
"external": ["HDMI1"],
|
||||||
|
"dual": ["LVDS1", "HDMI1"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
module.exports = usage;
|
||||||
Reference in New Issue
Block a user