Linux simple status bar with mouse controls
Tags: linux, archlinux, i3wm, sway
Published: 2021-10-23 13:49:26

Recently I've grown tired of Manjaro and decided once again delve into ricing world and setup Archlinux from "scratch".
There are things I've never done before like auto-login, setting custom status bar with things I need, auto-configuring screens and sound over HDMI etc. But today I work on the status bar.

I've always been using i3wm, but by my friend's suggestion, decided to try Wayland. Sway is a WM for Wayland that is similar to i3.

It uses a similar tool to i3bar called swaybar and both have a [JSON](https://man.archlinux.org/man/swaybar-protocol.7.en) [protocol](https://i3wm.org/docs/i3bar-protocol.html) that allows some more advanced configurations. I aim to set something with mouse control.

## Basic script
Online I've seen some people creating a script that prints to stdout some string and that would be their status line.
In the Sway's config, in `~/.config/sway/config`, we have this section

```javascript
bar {
    ...
    status_command while date +'%Y-%m-%d %l:%M:%S %p'; do sleep 1; done
```

The breakdown:
`status_command` means that it would run a script to get the status line. After that comes the command
`while date +'%Y-%m-%d %l:%M:%S %p'; do sleep 1; done` is the command, which is a while loop with a sleep of 1 second between executions and the result is the current date, calculated by the command `date`.

Perfect basic example

## Intermediate script
I've created a NodeJS script that bring the Battery status (I have a Thinkpad so 2 batteries for me). The script can get 2 length modes, short and long. The short is taking the percentage of all betteries and calculated the average. The long mode is giving the full breakdown per battery.

```javascript
const fs = require('fs');
const path = require('path');

let battLogMode = 'short';
const sum = (a, b) => a+b;

function batteryStatus(mode) {
	const batteryPath = '/sys/class/power_supply'
	const batteries = fs.readdirSync(batteryPath).filter(a=>a.indexOf('BAT')>-1);

	if (!batteries.length) {
		return 'no baterries';
	}

	const percentages = batteries.map(batName => {
		const current =  fs.readFileSync(path.join(batteryPath, batName, 'energy_now'));
		const full =  fs.readFileSync(path.join(batteryPath, batName, 'energy_full'));
		return current/full*100;
	})
	if( mode === 'short') {
		return parseInt(percentages.reduce(sum) / percentages.length, 10)
	}
	return batteries.map((batName, i) => batName.concat(': ', parseInt(percentages[i], 10))).join(' ')
}

function isCharging () {
	const status = parseInt(fs.readFileSync('/sys/class/power_supply/AC/online'), 10)
	return status ? 'Charging' : 'Discharging';
}

console.log(batteryStatus(battLogMode), ' ', isCharging())
setInterval(() => {
	console.log(batteryStatus(battLogMode), ' ', isCharging())
}, 1000)
```

There's some linux wizardry and not so clean code but I can live with that.
Note that the script simply `console.log` a string

In the config file 
We would replace the `status_command` line with this:

```javascript
bar {
    ....
    status_command node ~/projects/status_bar/index.js
```

which results in a simple status bar for battery information
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d5crqgfzlxyqbjhz7ccb.png)

Now we will try to change it to use the JSON protocol instead. 

## Refactor to use JSON protocol

This one was a bit tricky because of the way of thinking in JS, or maybe I'm a bit slow. [The protocol](https://i3wm.org/docs/i3bar-protocol.html) is along these lines:

first message to stdout is a header JSON, basically the version
The body of the communication should be an endless array, in which each element is the whole status line, i.e. it's an array of objects, and each object is a part of the status line.

In practice, you have a stream of the stdout and you send these messages:
1. send the header JSON
2. send the opening '[' of the endless array
3. send a JSON of an array whose elements are objects that have to have 'full_text'.
4. repeat 3 to update the status line with new data

Here's an example:

```javascript
console.log(JSON.stringify({version:1})) // header JSON
console.log('[') // the beginning of the endless array
console.log(JSON.stringify([{full_text:'beginning'}, {full_text:'second section'}])) // the first line to be printed

let i = 1;
setInterval(() => {
	console.log(',',JSON.stringify([{full_text:i}, {full_text:'second section'}]))
	i++;
}, 1000)
```

If you reload the config you will see now that the status line is updating with the counter, yay.

This way we will update it to print the battery status, the script is the same but with the battery information from the beginning:

```javascript
console.log(JSON.stringify({version:1, click_events: true}))
console.log('[')
console.log(JSON.stringify([{
	name: 'battery',
	full_text: batteryStatus(battLogMode).concat(' ', isCharging())
}]))

setInterval(() => {
	console.log(',',JSON.stringify([{
	name: 'battery',
	full_text: batteryStatus(battLogMode).concat(' ', isCharging())
}]))
}, 1000)
```


### handle mouse events

So the events coming from the sway-bar are the same as the events you are supposed to send, apart from the header object. Therefore you have to skip the '[' message and then skip the comma, ',' at the beginning of each line after the first. Here's the simplest parser of messages

```javascript
process.stdin.on('data', rawEvent => {
	let e = rawEvent.toString()
	if(e.length <= 2) {
		return;
	}
	if(e[0] !== '{') {
		e = e.slice(1)
	}
	const {name} = JSON.parse(e);
	if(name === 'battery') {
		battLogMode = battLogMode === 'short' ? 'long' : 'short';
	}
	
})
```

see that we know the part clicked on by looking at the name in the event and comparing it to the name given to the name property in the event we're sending.

## Full script
I've added also a date section and added a `console.log` to the event handler, so we don't have to wait for a second to pass until a refresh

```javascript
const fs = require('fs');
const path = require('path');

let battLogMode = 'short';
const sum = (a, b) => a+b;

function batteryStatus(mode) {
	const batteryPath = '/sys/class/power_supply'
	const batteries = fs.readdirSync(batteryPath).filter(a=>a.indexOf('BAT')>-1);

	if (!batteries.length) {
		return 'no baterries';
	}

	const percentages = batteries.map(batName => {
		const current =  fs.readFileSync(path.join(batteryPath, batName, 'energy_now'));
		const full =  fs.readFileSync(path.join(batteryPath, batName, 'energy_full'));
		return current/full*100;
	})
	if( mode === 'short') {
		return '' + parseInt(percentages.reduce(sum) / percentages.length, 10)
	}
	return batteries.map((batName, i) => batName.concat(': ', parseInt(percentages[i], 10))).join(' ')
}

function isCharging () {
	const status = parseInt(fs.readFileSync('/sys/class/power_supply/AC/online'), 10)
	return status ? 'Charging' : 'Discharging';
}

function generateLine() {
	return JSON.stringify([{
		name: 'battery',
		full_text: batteryStatus(battLogMode).concat(' ', isCharging())
	}, {
		name: 'date',
		full_text: (new Date()).toDateString()
	}]);
}

console.log(JSON.stringify({version:1, click_events: true}))
console.log('[')
console.log(generateLine())

setInterval(() => {
	console.log(',', generateLine())
}, 2000)

process.stdin.on('data', rawEvent => {
	let e = rawEvent.toString()
	if(e.length <= 2) {
		return;
	}
	if(e[0] !== '{') {
		e = e.slice(1)
	}
	const {name} = JSON.parse(e);
	if(name === 'battery') {
		battLogMode = battLogMode === 'short' ? 'long' : 'short';
	}
	console.log(',', generateLine())
	
})
```

and here's the config in the sway config

```javascript
bar {

    status_command node ~/projects/status_bar/index.js
    separator_symbol "::"

    colors {
	separator #ffffff
        statusline #ffffff
        background #323232
        inactive_workspace #32323200 #32323200 #5c5c5c
    }
    position bottom
}

```

And here's the final result
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9e7vwlegarcf6597td5z.png)