DEV Community

mkuts12
mkuts12

Posted on

Linux simple status bar with mouse controls

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 protocol 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

bar {
    ...
    status_command while date +'%Y-%m-%d %l:%M:%S %p'; do sleep 1; done
Enter fullscreen mode Exit fullscreen mode

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.

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)
Enter fullscreen mode Exit fullscreen mode

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:

bar {
    ....
    status_command node ~/projects/status_bar/index.js
Enter fullscreen mode Exit fullscreen mode

which results in a simple status bar for battery information
Image description

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 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:

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)
Enter fullscreen mode Exit fullscreen mode

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:

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)
Enter fullscreen mode Exit fullscreen mode

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

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';
    }

})
Enter fullscreen mode Exit fullscreen mode

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

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())

})
Enter fullscreen mode Exit fullscreen mode

and here's the config in the sway config

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
}

Enter fullscreen mode Exit fullscreen mode

And here's the final result
Image description

Discussion (0)