Desktop notifications for linux using Tkinter and ZeroMQ

Linux_Tkinter_ZeroMQ_Python_

Custom desktop notifications for Linux using Tkinter and ZeroMQ

Let's face it, the desktop notifications on most linux window managers sucks, at least based on my experience using notify-send,it's complicated or sometimes impossible to customize, at least that's what I've faced on Openbox, a window manager that I actually love.

So I started looking for alternatives, then I came across the so known GUI library Tkinter aka TK, using it you can actually build a sort of splash screen, which will make a nice notification window in my opinion .

So i went ahead and installed the python-tk library for python 2.x.x as well as some dependencies on my Ubuntu 18.04 distribution:

apt install virtualenv tk python-tk blt

Or if you want to use python 3.x.x :

apt install python3-tk

Then i created a python virtual environment and activated it :

virtualenv venv
source venv/bin/activate
python --version
Python 2.7.17

 Or for python 3.x.x with :

python3 -m venv env3
python --version
Python 3.6.9

And i started tinkering a bit with the library till I reached this stage :

That is good enough for what i need. The code is quiet basic :

# -*- coding: utf-8 -*-

# Python 2.x
from Tkinter import *

# Python 3.x
#from tkinter import *

import os

# Current script file path
script_path = os.path.dirname(os.path.realpath(__file__))

# Notification window Width and height
splash_width=445
splash_height=200


def notification(message,urgentcy="info"):

background_color='#438bd3'
border_color='#004e99'
text_color='#ddded9'
icon = os.path.join(script_path,"info.png")

if urgentcy == 'danger':
background_color='#d34343'
border_color='#990000'
text_color='#ddded9'
icon = os.path.join(script_path,"danger.png")

if urgentcy == 'warning':
background_color='#d3a243'
border_color='#994400'
text_color='#ddded9'
icon = os.path.join(script_path,"danger.png")

# Create a TK instance .
splash_root = Tk()

# Get the screen resulution
screen_width = splash_root.winfo_screenwidth()
screen_height = splash_root.winfo_screenheight()

# Vertical padding
ypos=10

# If dual monitos are used, then we take half of the width of the screen resultion
# so that the notification is displayed on the first monitor
if screen_width > 2560 :
xpos=int(screen_width/2-splash_width)-10
else :
xpos=screen_width-splash_width-10

# Set a title for the window
splash_root.title("Notification")

# Set the position and size of the window
splash_root.geometry("{}x{}+{}+{}".format(splash_width,splash_height,xpos,ypos))

# Make the window as a splash screen
splash_root.wm_attributes('-type','splash')

# Force the window to be at the top
splash_root.overrideredirect(True)

# Set the background and border color and the order thickness
splash_root.configure(background=background_color,highlightthickness=2,highlightbackground=border_color)

# Create a label
splash_label_message = Label(splash_root, text=message,font=("Helvetica",18),bg=background_color,fg=text_color,wraplength=400)

# Position the label at the center of the window
splash_label_message.place(relx=0.5,rely=0.5,anchor='center')

# Load the the icon image from the file
img = PhotoImage(file=icon)
img.config(file=icon)

# Attach the image to a label and place it in the window
image_label=Label(splash_root, image=img,bg=background_color)
image_label.image=img

# Not sure how the next 2 lines works exactly, but this places the icon at the left top corner of the window
image_label.pack()
image_label.grid(column=2,row=2)

# Destroy the window after 6seconds
splash_root.after(6000,splash_root.destroy)


if __name__ == "__main__" :

#Show the notification
notification("Test Message","info")
mainloop()

Now everything was going well, till I tried to integrate it with a script that is running as a systemd service as root and then I got this :

_tkinter.TclError: no display name and no $DISPLAY environment variable


Classic !, It totally slipped my mind, the library needs to have access to a display session. I had a similar case before and I found a work around using a bash script that finds the display id session for a specific user, then you simply run your script as :

DISPLAY=:0 python notif.py  # where 0 is the display id

But i don't want to go that road again ...

What I've decided to do is to use some kind of queuing message protocol to communicate between my script and the notification script that will be automatically running at the start up session of the window manager.

I decided to try out ZeroMQ or zmq "it gives you sockets that carry atomic messages across various transports like in-process, inter-process, TCP, and multicast." exactly what I need !.

I installed it using pip :

pip install pyzmq

And to transform the notification script to listen for zmq message was pretty straight forward :

# -*- coding: utf-8 -*-

# Python 2.x
from Tkinter import *

# Python 3.x
#from tkinter import *

import zmq
import os

# Current script file path
script_path = os.path.dirname(os.path.realpath(__file__))

# ZMQ initialization
context = zmq.Context()
socket = context.socket(zmq.PULL)


# Listening on port 5555
socket.bind("tcp://*:5555")


# Notification window Width and height
splash_width=445
splash_height=200


def notification(message,urgentcy="info"):


background_color='#438bd3'
border_color='#004e99'
text_color='#ddded9'
icon = os.path.join(script_path,"info.png")

if urgentcy == 'danger':
background_color='#d34343'
border_color='#990000'
text_color='#ddded9'
icon = os.path.join(script_path,"danger.png")

if urgentcy == 'warning':
background_color='#d3a243'
border_color='#994400'
text_color='#ddded9'
icon = os.path.join(script_path,"danger.png")

# Create a TK instance .
splash_root = Tk()

# Get the screen resulution
screen_width = splash_root.winfo_screenwidth()
screen_height = splash_root.winfo_screenheight()

# Vertical padding
ypos=10

# If dual monitos are used, then we take half of the width of the screen resultion
# so that the notification is displayed on the first monitor
if screen_width > 2560 :
xpos=int(screen_width/2-splash_width)-10
else :
xpos=screen_width-splash_width-10

# Set a title for the window
splash_root.title("Notification")

# Set the position and size of the window
splash_root.geometry("{}x{}+{}+{}".format(splash_width,splash_height,xpos,ypos))

# Make the window as a splash screen
splash_root.wm_attributes('-type','splash')

# Force the window to be at the top
splash_root.overrideredirect(True)

# Set the background and border color and the order thickness
splash_root.configure(background=background_color,highlightthickness=2,highlightbackground=border_color)

# Create a label
splash_label_message = Label(splash_root, text=message,font=("Helvetica",18),bg=background_color,fg=text_color,wraplength=400)

# Position the label at the center of the window
splash_label_message.place(relx=0.5,rely=0.5,anchor='center')

# Load the the icon image from the file
img = PhotoImage(file=icon)
img.config(file=icon)

# Attach the image to a label and place it in the window
image_label=Label(splash_root, image=img,bg=background_color)
image_label.image=img

# Not sure how the next 2 lines works exactly, but this places the icon at the left top corner of the window
image_label.pack()
image_label.grid(column=2,row=2)

# Destroy the window after 6seconds
splash_root.after(6000,splash_root.destroy)


if __name__ == "__main__" :

while True :
try:

# Listen for coming messages
message = socket.recv()

# Get the string message and split it to get the message and the urgency.
data=message.split(':')


#Show the notification
notification(data[0],data[1])
mainloop()
except Exception as e :
# Python 2.x
print e

# Python 3.x
#print(e)
pass

And finally in my script I will just do something like :

import zmq

context = zmq.Context()
zmq_socket = context.socket(zmq.PUSH)
zmq_socket.connect("tcp://localhost:5555")
zmq_socket.send("{}:{}".format("Message message !!","danger"),zmq.NOBLOCK)

The complete source code can be found in github repository .

Comments :