i3-status
Version:
A highly customizable i3wm status bar
311 lines (250 loc) • 9.1 kB
JavaScript
;
/** @module i3Status */
import yaml from 'read-yaml';
import buildin from './buildin/index.js';
import logger from 'winston';
import { exec } from 'child_process';
import Crypto from './crypto.js';
import NoReporter from './noReporter.js';
/** button mapping from i3bar number to button name */
const named_buttons = {
1: 'left',
2: 'middle',
3: 'right',
4: 'up',
5: 'down'
}
export default class i3Status {
/**
* @param {Object} options - options from the commandline
* @param {Stream} output - output stream i3Status will write to
*/
constructor(options, output) {
//check id config file is present
var config = yaml.sync(options.config);
config.main = config.main || {};
config.blocks = config.blocks || [];
//store as members
this.config = config;
this.output = output;
// set configured interval, fallback to 30 seconds
config.main.interval = config.main.interval || 30;
// set configured color, fallback to white
config.main.color = config.main.color || '#FFFFFF';
//values for output
this.lines = new Array(config.blocks.length);
//crypto
this.crypto = new Crypto(options.secret);
this.reporter = new NoReporter();
}
/**
* start i3Status. Will initialize all blocks and update them according to the configured interval.
*/
async run() {
//print header for i3
this.output.write('{"version":1,"click_events":true}\n[[]\n');
//init repoter
this.reporter = await this.initReporter(this.config.reporter);
//load and update all blocks
await this.initializeBlocks();
this.config.blocks.forEach(config => {
var block = this.blocks[config.name];
logger.debug('starting %s with interval %d ms', block.__name, config.interval);
//remember interval time
block.__interval_time = config.interval;
//start timer on block
block.__interval = setInterval(() => {
block.update()
}, config.interval);
//update block for the first time;
block.update();
});
}
/**
* initialize all blocks, sets common properties and adds listeners for updated, pause and resume.
* @private
*/
async initializeBlocks() {
const crypto = this.crypto;
//for all blocks from the config
//check config to be valid
this.checkConfig();
const initBlocks = await Promise.all(this.config.blocks.map(async config => {
//decrypt encrypted values
config = crypto.decrypt(config);
//use configured interval or fallback to main interval, convert to ms
config.interval = (config.interval || this.config.main.interval) * 1000;
//prepare default output
var output = {
name: config.name,
color: config.color || this.config.main.color,
background: config.background || this.config.main.background
};
//load block
if (config.type) {
var type = buildin[config.type];
if (type == null) {
throw new Error('block of type ' + config.type + ' is unknown');
}
var block = new type(config, output);
} else if (config.module) {
//load module
try {
const module = await import(config.module);
var moduleConstructor = module.default;
//legacy modules
if(module.default.default){
moduleConstructor=module.default.default;
}
var block = new moduleConstructor(config, output);
} catch (e) {
var block = new buildin.text({
text: `unable to load module '${config.module}': `+e
});
block.output.color = '#FF0000';
}
}
//set name
block.__name = config.name;
//add click
block.__click = config.click;
//add label
block.__label = config.label;
//add logger
block.__logger = logger;
//add reporter
block.__reporter = Object.assign(this.reporter,{});
//add listener for updated
block.on('updated', ((block, data) => {
this.update(block.__name, data);
}));
//add listener for pause
block.on('pause', (block) => {
this.pauseBlock(block);
});
//add listener for resume
block.on('resume', (block) => {
this.resumeBlock(block);
});
//add default action code if no other exists
if (!block.action)
block.action = defaultAction;
return block;
}));
const initBlocksMap = initBlocks.reduce((map, block)=> {
map[block['__name']] = block;
return map;
}, {});
var index = 0;
this.blocks = this.config.blocks.reduce( (map, config)=> {
var block=initBlocksMap[config.name];
block.__index= index++;
map[block['__name']] = block;
return map;
},{});
}
/**
* clear all intervals to shutdown i3Status
*/
close() {
//clear all intervals
for (const name in this.blocks) {
let block = this.blocks[name];
block.interval = clearInterval(block.__interval);
}
}
/**
* update the output of a block with the given data
*/
update(name, data) {
var block = this.blocks[name];
var index = block.__index;
var label = block.__label;
var line = this.lines[index] = data;
//add label
if (label && label.length > 0) {
line.short_text = label + ' ' + line.short_text;
line.full_text = label + ' ' + line.full_text;
}
this.print();
}
/**
* print all blocks to output stream.
* @private
*/
print() {
this.output.write(',' + JSON.stringify(this.lines) + '\n');
}
/**
* dispatch action to the target block
*/
action(data) {
//dispatch action to block
var block = this.blocks[data.name];
if (block) block.action(data);
}
/**
* pause (remove) the interval for the given block to prevent further calls to update.
* @private
*/
pauseBlock(block) {
logger.debug('pause interval for ', block.__name);
block.__interval = clearInterval(block.__interval);
}
/**
* resume (set new) interval for the given block to execute calls to update.
* @private
*/
resumeBlock(block) {
logger.debug(`resume interval (${block.__interval_time}) for ${block.__name}`);
block.__interval = setInterval(() => {
block.update();
}, block.__interval_time);
}
/**
* check if the config is valid.
* @param {Object} - config for the block to test
* @private
*/
checkConfig() {
//check for duplicate names
if(this.config.blocks.length != new Set(this.config.blocks.map(it=>it.name)).size){
throw new Error('config error: duplicate block name found');
}
this.config.blocks.forEach(config=> {
if (!config.name)
throw new Error('config error: block needs a name');
if (!config.type && !config.module)
throw new Error('config error: block ' + config.name + ' has no type/module');
});
}
async initReporter(config){
if(!config || !config.module) return new NoReporter();
const module = await import(config.module);
var moduleConstructor = module.default;
//legacy modules
if(module.default.default){
moduleConstructor=module.default.default;
}
return new moduleConstructor(config);
}
}
/**
* Default action handler for actions from the i3bar.
* @param {Object} - action from i3bar, see http://i3wm.org/docs/i3bar-protocol.html#_click_events
*/
function defaultAction(action) {
logger.debug('button pressed on %s:', this.__name, action);
if (typeof this.__click === 'string') {
//config is click: command
//all button clicks have the same action
exec(this.__click);
} else if (typeof this.__click === 'object') {
//config has click action per mouse button
//get command for the action button
var command = this.__click[named_buttons[action.button] || action.button];
//if command exists, execute it
if (command) exec(command);
}
}