Mysore Linux Users' Group

A website for Linux users and geeks in general to express their thoughts and spread information


Writing a GNOME Extension

By Prithvi Vishak

Created June 16th, 2021


Unfortunately, there is very little documentation regarding how to write extensions for the GNOME desktop environment. The GNOME libraries seem to be fairly well documented, but it can be exhausting to read through it all. Often, people, including me when I wrote my first extension, just got code snippets from other peoples' extensions. This article aims to document how I built mine.

GNOME Logo

Guh-NOME

What It Should Do

The extension is called MacBook Battery Percentage Corrector. It exists because MacBooks (Intel-based ones, at least) have a custom SMC, or System Management Controller. According to Wikipedia, it is supposed to be an equivalent to a PMU, or Power Management Unit, on regular PC laptops.

PMUs report a number of different metrics.

~$ upower -i /org/freedesktop/UPower/devices/battery_BAT1 native-path: BAT1 vendor: सेब Inc. model: A1A4492 serial: 2345 power supply: yes updated: Thu 17 Jun 2021 01:48:50 PM IST (32 seconds ago) has history: yes has statistics: yes battery present: yes rechargeable: yes state: fully-charged warning-level: none energy: 56.66 Wh energy-empty: 0 Wh energy-full: 57.33 Wh energy-full-design: 71.1 Wh energy-rate: 0.00591272 W voltage: 12.728 V percentage: 98% capacity: 80.6329% technology: lithium-ion icon-name: 'battery-full-charged-symbolic'

Your desktop gets these metrics through a power subsystem such as UPower and displays it on your desktop. The thing with Apple's SMC, though, is that the metrics it reports don't seem to follow the convention that PC laptop PMUs do. On MacBooks, Linux seems to read battery percentage as the percentage of capacity by design (energy-full-design above). Batteries degrade, or lose some capacity, with every charge-discharge cycle. This means that the more a battery is used, the smaller its maximum capacity becomes. Normally, operating systems consider battery degradation while reporting percentage. That doesn't seem to be the case with Linux on a MacBook.

Long story short, as indicated by their motto, Apple has a pathological need to do their own nonsense and not follow standards. That's probably the reason Linux considers the wrong values while reporting battery percentage.

How It Should Work

These battery metrics can be found in the directory /sys/class/power_supply/BAT0. We need to do the following:

  1. Read the charge_now and charge_full files
  2. Calculate percentage from these numbers
  3. Replace the indicator in the top-right corner with what we calculated

Code

A good place to start would be GNOME's official extension guide. Extensions follow certain guidelines. Other useful documentation is listed on the world's most useful website. Here's a look at my code:

const Panel = imports.ui.main.panel; const { GObject } = imports.gi; const GLib = imports.gi.GLib; const BaseIndicator = imports.ui.status.power.Indicator; let aggregateMenu = Panel.statusArea['aggregateMenu']; let origIndicator; let newIndicator; const BatPath = "/sys/class/power_supply/BAT0" function init() {} function enable() { origIndicator = aggregateMenu._power; newIndicator = new Indicator(); aggregateMenu._indicators.replace_child(origIndicator, newIndicator); } function disable() { aggregateMenu._indicators.replace_child(newIndicator, origIndicator); } function readFile(filepath) { return String(GLib.file_get_contents(filepath)[1]).replace("\n", ""); } var Indicator = GObject.registerClass( class Indicator extends BaseIndicator { _getCorrectPercentage() { let charge_full = parseInt(readFile(BatPath + "/charge_full")); let charge_now = parseInt(readFile(BatPath + "/charge_now")); let percentage = Math.trunc((charge_now/charge_full) * 100); return percentage + "%"; } _sync() { super._sync(); this._percentageLabel.text = this._getCorrectPercentage(); } } );

All right. Let's go through this real quick. At the top there are some imports and global variables. GLib and GObject are some GNOME libraries. Everything else is pretty self-explanatory. function init() is a function required by GNOME in extension.js. My extension doesn't need anything to be done when GNOME Shell starts, so I've left it empty.

At the end of the code, we see a class definition. It inherits from GNOME's battery power indicator base class. The function readFile, well, reads the content of a file and returns it as a string. The _getCorrectPercentage function reads the battery's current charge and full charge values from the files mentioned above, and returns the percentage formatted as a string. The _sync function runs every time battery power status is updated. It sets the text of the percentage label next to the battery icon to the value _getCorrectPercentage calculates. This overwrites the incorrect value.

enable runs when the extension is enabled. origIndicator = aggregateMenu._power; stores the default indicator in a variable, so that it can be restored if the extension is disabled. It then creates a new instance of our custom battery status indicator, and puts that in the old one's place. disable runs when, well, the extension is disabled. It simply replaces our custom indicator with the original we stored away.