Pankaj Tanwar
Published on

I built a gnome shell extension to show how much my day, month, year and life has passed.

––– views

"remember that you must die" ~ memento mori.

Bharat & Sree, my dynamic duo at work, introduced me to this stoicism concept, Memento Mori and ever since, it has stuck with me. While some find it dark and depressing but on the other side, it works as a reminder of the inevitability of death, live life to its fullest as you've got a finite time.

Initially, it made me anxious but overtime I thought of it, I feel grateful to be alive and I'm able to prioritize things much better.

Well, too much philosophy! let's get back to the original topic.

Chilly friday evening, engaged in my routine twitter scrolling. Suddenly, my fingers halted and I found myself looking at a tweet of an internet stranger sharing, 95% of the year has passed. He had this progress bar as a widget on his Mac. Very cool, the next moment, I was challenged by my crooked mind to build an extended version for linux.

End of this writeup, you should be able to build something like this -

Memento Mori

War mode on!

I applied my noob google search expertise to find references on building a desktop widget for linux. After investing 2 hours of my weekend night in the vast ocean of online content, I stumbled upon the software called GNOME shell extension. And It was a perfect fit for my use case allowing me to build one to be visible on my menu bar, instead of desktop.

One down. Next target was to locate good humble resources to teach myself building a gnome extension. Luckily I found Mr Just Perfection .

Oh, hold on, did I even explain what GNOME is? I might be going too fast. Such a novice writer I am.

Gnome is a globally loved, open source and super efficient desktop environment for linux and other unix-like OS. Laymen language, desktop environment is everything you see on your screen, lock screen to home page, app icons, directory etc. Best part, you can customize gnome's user interface as you want.

The coding part.

As advised, I installed Extension Manager, an excellent GUI tool for browsing and installing GNOME shell extension.

sudo snap install gnome-extension-manager

One command, done. Lets open up extension manager.

Extension Manager

All gnome extensions are stored at path /.local/share/gnome-shell/extensions directory.

Extension Directory

If you don't find the folder "extensions", don't panic. Just browse in extension manager and install any extension, it will be automatically created. You can create the folder directly by yourself, no harm.

Lets start building the extension. Let's get into extensions directory and create a new dir named memento-mori.

Tip? gnome-extensions create works beautifully :)

Typically an extension needs 3 files -

metadata.json

As the name yells, It holds the metadata info of the extension like name, description, website, supported version etc.

extension.js

This is the brain. The complete logic goes here and YES, we can code the extension in javascript.

stylesheet.css

This is everything design, color etc. Me, being a backend engineer, would politely avoid it touching. Jokes aside, we won't need any styling for this.

full guide can be found here.

Let's start with metadata.json, very easy peasy -

{
"name": "memento-mori",
"description": "memento mori extension",
"shell-version": ["42"],
"url": "https://github.com/Pankajtanwarbanna/",
"uuid": "memento-mori",
"version": 1
}

My shell version was 42 hence I kept it 42. You can get yours' by command gnome-shell --version. Make sure uuid is lower case and a unique identifier.

Did you notice shameless plug of my github url in metadata.json?

Moving on, next is extension.js.

It asks for 3 methods init, enable, and disable. Pretty self explanatory, init keeps everything you need to initialise for use. enable will have the logic of what to do when this extension is enabled and disable , puts a break on everything, goes and sit in the dark corner.

Lets take a break. Breath.

Feeling better? so far, we have covered basics of building a shell extension.

Next, 3 things left.

  1. a javascript method to calculate % of day, month, year and life passed as per current timestamp.
  2. compute these values every second
  3. and show it on menu bar.

Lets begin with showing a dummy text on menu bar. Calculation part, we will take up post that.

We need a text to show on menu bar and none other than St import comes to the rescue. Basic code to show a text on bar looks like this -

const St = imports.gi.St
const Main = imports.ui.main
let panelButton, panelButtonText
function init() {
panelButton = new St.Bin({})
panelButtonText = new St.Label({
text: 'memento mori.',
})
panelButton.set_child(panelButtonText)
}
function enable() {
Main.panel._rightBox.insert_child_at_index(panelButton, 1)
}
function disable() {
Main.panel._rightBox.remove_child(panelButton)
}

perfect, lets restart the gnome shell. Press Alt + F2 and enter r to restart. You should be seeing your extension in extension manager. Just enable it and yay, you should see the text.

Testing the extension text

Lets break down the code in extension.js file. We initilized a panel button, a panel button label text and appended panel button text to panel button as a child.

Main.panel._rightBox.insert_child_at_index(panelButton, 1)
  • It might seem a bit tricky. Main object provides access to various components of GNOME shell extension and Main.panel specifically represents top panel.
  • GNOME shell is divided into several boxes, and we attach panelButton child at index 1 to _rightBox container.

As promised to GNOME shell, at disable, we remove everything.

Done, looks good so far. Now, lets write logic to calculate how much life has been elapsed.

function getSecondsInYear(year) {
const daysInYear = (year % 4 === 0 && year % 100 > 0) || year % 400 == 0 ? 366 : 365
return daysInYear * 24 * 60 * 60
}
function getSecondsInMonth(month, year) {
const daysInMonth = new Date(year, month, 0).getDate()
return daysInMonth * 24 * 60 * 60
}
function calculatePercentages() {
const currentDate = new Date()
const currentYear = currentDate.getFullYear()
const currentMonth = currentDate.getMonth() + 1
const birthYear = 1998
const ageExpectancy = 80
// Calculate seconds elapsed for the day
const secondsInDay = 24 * 60 * 60
const secondsOfDay = Math.floor(
(currentDate.getHours() * 3600 + currentDate.getMinutes() * 60 + currentDate.getSeconds()) %
secondsInDay
)
const dayPercentage = Math.floor((secondsOfDay / secondsInDay) * 100)
// Calculate seconds elapsed for the month
const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1)
const secondsInMonth = (currentDate - firstDayOfMonth) / 1000
const monthPercentage = Math.floor(
(secondsInMonth / getSecondsInMonth(currentMonth, currentYear)) * 100
)
// Calculate seconds elapsed for the year
const firstDayOfYear = new Date(currentDate.getFullYear(), 0, 1)
const secondsInYear = (currentDate - firstDayOfYear) / 1000
const yearPercentage = Math.floor((secondsInYear / getSecondsInYear(currentYear)) * 100)
// Calculate seconds elapsed for the entire life
const secondsInLife = (currentDate - new Date(birthYear, 0, 1)) / 1000
const lifePercentage = Math.floor((secondsInLife / (ageExpectancy * 31536000)) * 100)
return `Day: ${dayPercentage}%, Month: ${monthPercentage}%, Year: ${yearPercentage}%, Life: ${lifePercentage}%`
}

I'm sure there are better ways of doing this. please don't roast, I wrote this in mid-night lying next to my cats, Kafka and docker.

So the idea is, just calculate the total seconds of each entity (current day, current month, current year and life assuming avg expectancy in India ~80 years). Post that, it calculate how much has been passed as of current timestamp and returns back a string something like -

Day: 21%, Month: 48%, Year: 95%, Life: 32%

Yes, we are close. The last step is to somehow run this calculation every second and replace with memento mori text which is showing up on menu bar right now.

There is a beautiful fundamental concept in gnome shell called mainloop. Wait, remember event loop in Node? its just that.

The main loop is a continuous loop that listens for and dispatches events or messages in an application. It plays a crucial role in handling user input, updating the GUI, and responding to various events.

So, we add a timeout, checking every 1 second, calculating values and replacing it with top bar text and how we do it? very simple -

timeout = Mainloop.timeout_add_seconds(1.0, setButtonText)

setButtonText is a method which sets panel button text to the calculate % values.

function setButtonText() {
panelButtonText.set_text(calculatePercentages())
return true
}

Note : the method which you are specifying in timeout, should always return true. If it returns false or void, main loop will stop.

Well, great job. Final code -

const St = imports.gi.St
const Main = imports.ui.main
const Mainloop = imports.mainloop
let panelButton, panelButtonText, timeout
function getSecondsInYear(year) {
const daysInYear = (year % 4 === 0 && year % 100 > 0) || year % 400 == 0 ? 366 : 365
return daysInYear * 24 * 60 * 60
}
function getSecondsInMonth(month, year) {
const daysInMonth = new Date(year, month, 0).getDate()
return daysInMonth * 24 * 60 * 60
}
function calculatePercentages() {
const currentDate = new Date()
const currentYear = currentDate.getFullYear()
const currentMonth = currentDate.getMonth() + 1
const birthYear = 1998 // replace this with your birth year
const ageExpectancy = 80
// Calculate seconds elapsed for the day
const secondsInDay = 24 * 60 * 60
const secondsOfDay = Math.floor(
(currentDate.getHours() * 3600 + currentDate.getMinutes() * 60 + currentDate.getSeconds()) %
secondsInDay
)
const dayPercentage = Math.floor((secondsOfDay / secondsInDay) * 100)
// Calculate seconds elapsed for the month
const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1)
const secondsInMonth = (currentDate - firstDayOfMonth) / 1000
const monthPercentage = Math.floor(
(secondsInMonth / getSecondsInMonth(currentMonth, currentYear)) * 100
)
// Calculate seconds elapsed for the year
const firstDayOfYear = new Date(currentDate.getFullYear(), 0, 1)
const secondsInYear = (currentDate - firstDayOfYear) / 1000
const yearPercentage = Math.floor((secondsInYear / getSecondsInYear(currentYear)) * 100)
// Calculate seconds elapsed for the entire life
const secondsInLife = (currentDate - new Date(birthYear, 0, 1)) / 1000
const lifePercentage = Math.floor((secondsInLife / (ageExpectancy * 31536000)) * 100)
return `Day: ${dayPercentage}%, Month: ${monthPercentage}%, Year: ${yearPercentage}%, Life: ${lifePercentage}%`
}
function setButtonText() {
panelButtonText.set_text(calculatePercentages())
return true
}
function init() {
panelButton = new St.Bin({})
panelButtonText = new St.Label({
text: 'memento mori.',
})
panelButton.set_child(panelButtonText)
}
function enable() {
Main.panel._rightBox.insert_child_at_index(panelButton, 1)
timeout = Mainloop.timeout_add_seconds(1.0, setButtonText)
}
function disable() {
Mainloop.source_remove(timeout)
Main.panel._rightBox.remove_child(panelButton)
}

And it's done. We have the memento mori extension ready.

Memento Mori

This was a basic version of an extension. This can be extended to may be add some progress bar emoji and taking preferences inputs etc.

Gnome shell has a lot to offer. We can run commands, specify schema to use it like a database and what not.

Now that I end my ramblings, I invite your comment on it. If you find any technical inaccuracies, let me know, please. I'm active on X (twitter) as @the2ndfloorguy and if you are interested in what an unfunny & strange programmer will do next, see you there!