'cool python cairo screen widget needs to be ported from PyGTK to PyGI

I am running Ubuntu 20.04 Mate Desktop. I found a cool old screen widget which shows clock, cpu, memory, Wi-Fi strength. It doesn't have any dependency to screenlet package. Though it's very old I managed to run it by installing only http://mirrors.kernel.org/ubuntu/pool/universe/g/gnome-python/python-gconf_2.28.1+dfsg-1.2_amd64.deb with python2.7.

enter image description here

Original code which I found on webarchive: https://web.archive.org/web/20130323064138if_/http://kanslozebagger.org/sphinX/sphinX-0.2.tar.bz2

My stab at it:

I used pygi-convert.sh for basic convertion.

#!/usr/bin/env python
# vim: ts=4 sts=4 sw=4 ai et
# Copyright (C) 2006 Wander Boessenkool
#
#    sphinX is a graphical system-monitor using pycairo
#
#`   Author: Wander Boessenkool <[email protected]>
#
#    This program is free software; you can redistribute it and/or
#    modify it under the terms of the GNU General Public License as
#    published by the Free Software Foundation; either version 2 of
#    the License, or (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA     
#                                        02111-1307  USA
#

NAME = 'sphinX'
VERSION = '0.2'
AUTHORS = ['Wander Boessenkool <[email protected]>']


import os
import sys

import gi
from gi.repository import GObject
import math
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
#import Gtk.gdk
import cairo
from datetime import datetime
from gi.repository import GConf

class cpustat(object):
    def __init__(self, u=0.0, n=0.0, s=0.0, id=0.0, io=0.0, ir=0.0, si=0.0):
        self.user = u
        self.nice = n
        self.system = s
        self.idle = id
        self.iowait = io
        self.irq = ir
        self.softirq = si
        self.cur_freq = 0.0
        self.max_freq = 1.0
        self.scale = self.cur_freq / self.max_freq

class memstat(object):
    def __init__(self, total=0 , free=0 , buffers=0, cached=0):
        self.total = total
        self.free = free
        self.buffers = buffers
        self.cached = cached
        self.used = total - free - buffers - cached

class wifistat(object):
    def __init__(self):
        self.quality = 0.0

def hr(i):
    sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']
    i = float(i)
    index = 0
    while (i > 1024 and index < len(sizes) - 1):
        i = i / 1024.0
        index += 1
    return '%.2f %s' % (i, sizes[index])


class stats(object):
    def __init__(self):
        self.rawcpustats = [0 for i in range(8)]
        self.cpu = cpustat()
        self.mem = memstat()
        self.swap = memstat()
        self.wifi = wifistat()
        self.update_all()

    def update_mem(self):
        statlines = open('/proc/meminfo', 'r').readlines()
        meminfo = {}
        for line in statlines:
            splitline = line.split()
            meminfo[splitline[0]] = float(splitline[1])
        self.mem.total = meminfo['MemTotal:']
        self.mem.free = meminfo['MemFree:'] / self.mem.total
        self.mem.buffers = meminfo['Buffers:'] / self.mem.total
        self.mem.cached = meminfo['Cached:'] / self.mem.total
        self.mem.used = 1.0 \
                      - self.mem.free \
                      - self.mem.buffers \
                      - self.mem.cached
        self.swap.total = meminfo['SwapTotal:']
        if self.swap.total > 0:
            self.swap.free = meminfo['SwapFree:'] / self.swap.total
        else:
            self.swap.free = -1.0
        self.swap.used = 1.0 - self.swap.free

    def update_cpu(self):
        statlines = open('/proc/stat', 'r').readlines()
        for line in statlines:
            splitline = line.split()
            if splitline[0] == 'cpu':
                rawcpu = [int(i) for i in splitline[1:]]
        diff = [rawcpu[i] - self.rawcpustats[i] for i in range(8)]
        totaldiff = float(sum(diff))
        if totaldiff != 0:
            self.cpu.user = diff[0] / totaldiff
            self.cpu.nice = diff[1] / totaldiff
            self.cpu.system = diff[2] / totaldiff
            self.cpu.idle = diff[3] / totaldiff
            self.cpu.iowait = diff[4] / totaldiff
            self.cpu.irq = diff[5] / totaldiff
            self.cpu.softirq = diff[6] / totaldiff
        self.rawcpustats = rawcpu
        try:
            self.cpu.max_freq = float(file(
               '/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq', 'r').read())
            self.cpu.cur_freq = float(file(
               '/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq', 'r').read())
            self.cpu.scale = self.cpu.cur_freq / self.cpu.max_freq
        except:
            self.cpu.max_freq = 1.0
            self.cpu.cur_freq = 1.0
            self.cpu.scale = 0.0

    def update_wifi(self):
        try:
            statlines = file('/proc/net/wireless', 'r').readlines()
        except:
            statlines = ["", "", "0 0 0"]
        while len(statlines) < 3:
            statlines.append("0 0 0")
        self.wifi.quality = float(statlines[2].split()[2]) / 100.0

    def update_all(self):
        self.update_cpu()
        self.update_mem()
        self.update_wifi()

class Witsjet(Gdk.Window):
    '''
    __gsignals__ = {
    'draw':   'override',
    'screen-changed': 'override',
    }
    '''    
    #window = Gdk.Window
    def __init__(self, stats):
        GObject.GObject.__init__(self)
        #Gtk.Window.__init__(self, title="clock")
        #super().__init__(title="Hello World")
        self.gconfclient = GConf.Client.get_default()
        sw = self.gconfclient.get_int('/apps/sphinX/width')
        if sw is 0:
            sw = 200
        sh = self.gconfclient.get_int('/apps/sphinX/height')
        if sh is 0:
            sh = 200
        sx = self.gconfclient.get_int('/apps/sphinX/xpos')
        sy = self.gconfclient.get_int('/apps/sphinX/ypos')
        self.set_size_request(32, 32)
        self.set_default_size(sw, sh)
        self.move(sx, sy)
        self.stats = stats
        self.stick()
        self.set_keep_below(True)
        self.set_property('accept-focus', False)
        self.set_property('skip-pager-hint', True)
        self.set_property('skip-taskbar-hint', True)
        self.set_property('resizable', True)
        self.drawing_stack = []
        self.set_app_paintable(True)
        self.set_decorated(False)
        self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
        self.connect('button-press-event', self.buttonpress)
        self.connect('configure-event', self.configure_handler)
        self.do_screen_changed()
        self.swapgradient = cairo.LinearGradient(0.2, 0.8, 1.0, 0.5)
        self.swapgradient.add_color_stop_rgba(0.0, 1.0, 0.0, 0.0, 0.6)
        self.swapgradient.add_color_stop_rgba(1.0, 1.0, 0.0, 0.0, 1.0)
        self.memgradient = cairo.LinearGradient(0.8, 0.2, 0.0, 0.5)
        self.memgradient.add_color_stop_rgba(0.0, 0.0, 1.0, 0.0, 0.6)
        self.memgradient.add_color_stop_rgba(1.0, 0.0, 1.0, 0.0, 1.0)
        self.scalegradient = cairo.LinearGradient(0.2, 0.8, 1.0, 0.5)
        self.scalegradient.add_color_stop_rgba(0.0, 0.0, 0.0, 1.0, 0.3)
        self.scalegradient.add_color_stop_rgba(1.0, 0.0, 0.0, 1.0, 1.0)
        self.menu = Gtk.Menu()
        '''
        about = Gtk.Action('About', None, None, Gtk.STOCK_ABOUT)
        about.connect('activate', self.about)
        aboutmenu = Gtk.ImageMenuItem()
        about.connect_proxy(aboutmenu)
        self.menu.add(aboutmenu)
        quit = Gtk.Action('Quit', None, None, Gtk.STOCK_QUIT)
        quit.connect('activate', Gtk.main_quit)
        quitmenu = Gtk.ImageMenuItem()
        quit.connect_proxy(quitmenu)
        self.menu.add(quitmenu)
        '''

    def about(self, *args):
        dialog = Gtk.AboutDialog()
        dialog.set_name('sphinX')
        dialog.set_logo_icon_name('system')
        dialog.set_copyright('(C) 2006 Red Hat, Inc.')
        dialog.set_authors(AUTHORS)
        dialog.set_version(VERSION)
        dialog.connect('response', lambda d, r: d.destroy())
        dialog.show()
        

    def configure_handler(self, window, event):
        w,h = self.get_size()
        x, y = self.get_position()
        self.gconfclient.set_int('/apps/sphinX/width', w)
        self.gconfclient.set_int('/apps/sphinX/height', h)
        self.gconfclient.set_int('/apps/sphinX/xpos', x)
        self.gconfclient.set_int('/apps/sphinX/ypos', y)
        return False

    def buttonpress(self, window, event):
        if event.button == 1:
            self.begin_move_drag(event.button,
                                 int(event.x_root),
                                 int(event.y_root),
                                 event.time)
            return True
        if event.button == 2:
            self.begin_resize_drag(Gdk.WINDOW_EDGE_SOUTH_EAST,
                                   event.button,
                                   int(event.x_root),
                                   int(event.y_root),
                                   event.time)
            return True
        if event.button == 3:
            self.menu.popup(None, None, None, event.button, event.time)
            return True

    def do_expose_event(self, event):
        window = self.get_window()
        (width, height) = self.get_size()
        self.Gtk.Window.begin_paint_rect(width, height)
        bmp = Gdk.Pixmap(None, width, height, 1)
        cm = bmp.cairo_create()
        cm.translate(5, 5)
        cm.scale(width - 10, height -10)
        cm.set_source_rgb(0, 0, 0)
        cm.set_operator(cairo.OPERATOR_DEST_OUT)
        cm.paint()
        cm.set_operator(cairo.OPERATOR_OVER)
        cm.arc(0.5, 0.5, 0.51, 0, 2 * math.pi)
        cm.fill()
        if not self.supports_alpha:
            self.window.shape_combine_mask(bmp, 0, 0)
        else:
            self.window.input_shape_combine_mask(bmp, 0, 0)
        cr = self.window.cairo_create()
        self.draw(cr, width, height)
        self.window.end_paint()

    def draw(self, cr, width, height):
        if self.supports_alpha:
            cr.set_source_rgba(1.0, 1.0, 1.0, 0.0)
        else:
            cr.set_source_rgb(0.5, 0.5, 0.5)
        # Draw the background
        cr.set_operator(cairo.OPERATOR_SOURCE)
        cr.paint()
        cr.set_operator(cairo.OPERATOR_OVER)
        cr.translate(5, 5)
        cr.scale(width - 10 , height -10)
        #Inner circle - wifi
        cr.move_to(0.8, 0.5)
        cr.arc(0.5, 0.5, 0.3, 0, 2 * math.pi)
        wifigrad = cairo.RadialGradient(0.5, 0.5, 0.0, 0.5, 0.5, 0.3)
        wifigrad.add_color_stop_rgba(0.0, 0.0, 0.0, 1.0, 1.0)
        interval = 0.02
        while interval < self.stats.wifi.quality:
            wifigrad.add_color_stop_rgba(interval, 0.0, 0.0, 1.0, 0.8)
            interval += 0.04
            wifigrad.add_color_stop_rgba(interval, 0.0, 0.0, 1.0, 0.4)
            interval += 0.04
        wifigrad.add_color_stop_rgba(1.0, 0.0, 0.0, 1.0, 0.0)
        cr.set_source(wifigrad)
        cr.fill()
        if self.stats.swap.free >= 0:
            #Lower small bar - swap
            cr.move_to(0.2, 0.5)
            cr.arc_negative(0.6, 0.5, 0.4, math.pi, self.stats.swap.free * math.pi)
            cr.arc(0.5, 0.5, 0.3, self.stats.swap.free * math.pi, math.pi)
            cr.close_path()
            cr.set_source(self.swapgradient)
            cr.fill()
            #Lower small outline
            cr.move_to(0.2, 0.5)
            cr.arc_negative(0.6, 0.5, 0.4, math.pi, 0)
            cr.arc(0.5, 0.5, 0.3, 0, math.pi)
            cr.close_path()
            cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
            cr.set_line_width(0.01)
            cr.stroke()
        #Lower large bar - cpu
        cr.move_to(1.0, 0.5)
        cr.arc(0.5, 0.5, 0.5, 0, (1.0 - self.stats.cpu.idle) * math.pi)
        cr.arc_negative(0.6, 0.5, 0.4, (1.0 - self.stats.cpu.idle) * math.pi, 0)
        cr.close_path()
        gradient = cairo.LinearGradient(1.0, 1.0, 0.0, 0.5)
        gradient.add_color_stop_rgba(0.0, 0.0, 1.0, 0.0, 0.6)
        u = self.stats.cpu.user + self.stats.cpu.nice
        s = self.stats.cpu.system + u
        i = self.stats.cpu.irq + self.stats.cpu.softirq + \
            self.stats.cpu.iowait + s
        gradient.add_color_stop_rgba(u, 0.0, 1.0, 0.0, 0.8)
        gradient.add_color_stop_rgba(s, 1.0, 1.0, 0.0, 0.8)
        gradient.add_color_stop_rgba(i, 1.0, 0.0, 0.0, 1.0)
        gradient.add_color_stop_rgba(1.0, 1.0, 0.0, 0.0, 1.0)
        cr.set_source(gradient)
        cr.fill()
        #Lower large outline
        cr.move_to(1.0, 0.5)
        cr.arc(0.5, 0.5, 0.5, 0, math.pi)
        cr.arc_negative(0.6, 0.5, 0.4, math.pi, 0)
        cr.close_path()
        cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
        cr.set_line_width(0.01)
        cr.stroke()
        #Upper small bar - mem
        cr.move_to(0.8, 0.5)
        cr.arc_negative(0.4, 0.5, 0.4, 0, -self.stats.mem.used * math.pi)
        cr.arc(0.5, 0.5, 0.3, -self.stats.mem.used * math.pi, 0)
        cr.close_path()
        cr.set_source(self.memgradient)
        cr.fill()
        #Upper small outline
        cr.move_to(0.8, 0.5)
        cr.arc_negative(0.4, 0.5, 0.4, 0, math.pi)
        cr.arc(0.5, 0.5, 0.3, math.pi, 0)
        cr.close_path()
        cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
        cr.set_line_width(0.01)
        cr.stroke()
        if self.stats.cpu.scale > 0.0:
            #Upper large bar - cpu-scale
            cr.move_to(0.0, 0.5)
            cr.arc(0.4, 0.5, 0.4, math.pi, math.pi * -(1 - self.stats.cpu.scale))
            cr.arc_negative(0.5, 0.5, 0.5, math.pi * -(1 - self.stats.cpu.scale),
                            math.pi)
            cr.close_path()
            cr.set_source(self.scalegradient)
            cr.fill()
            #Upper large outline
            cr.move_to(0.0, 0.5)
            cr.arc(0.4, 0.5, 0.4, math.pi, 0)
            cr.arc_negative(0.5, 0.5, 0.5, 0, math.pi)
            cr.close_path()
            cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
            cr.set_line_width(0.01)
            cr.stroke()
        #Clock
        curtime = datetime.now()
        h = float(curtime.hour)
        m = float(curtime.minute)
        s = float(curtime.second)
        m += s / 60
        h += m / 60
        h = (h % 12) / 6 * math.pi
        m = m / 30 * math.pi
        s = s / 30 * math.pi
        try:
            cr.push_group()
        except:
            pass
        cr.set_line_cap(cairo.LINE_CAP_ROUND)
        cr.move_to(0.5, 0.5)
        cr.line_to(0.5 + 0.3 * math.sin(h), 0.5 - 0.3 * math.cos(h))
        cr.set_source_rgba(0.0, 0.0, 0.0, 0.8)
        cr.set_line_width(0.075)
        cr.stroke_preserve()
        cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
        cr.set_line_width(0.05)
        cr.stroke()
        cr.move_to(0.5, 0.5)
        cr.line_to(0.5 + 0.35 * math.sin(m), 0.5 - 0.35 * math.cos(m))
        cr.set_source_rgba(0.0, 0.0, 0.0, 0.8)
        cr.set_line_width(0.065)
        cr.stroke_preserve()
        cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
        cr.set_line_width(0.04)
        cr.stroke()
        cr.move_to(0.5, 0.5)
        cr.line_to(0.5 + 0.4 * math.sin(s), 0.5 - 0.4 * math.cos(s))
        cr.set_source_rgba(0.0, 0.0, 0.0, 0.8)
        cr.set_line_width(0.055)
        cr.stroke_preserve()
        cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
        cr.set_line_width(0.03)
        cr.stroke()
        cr.move_to(0.55, 0.5)
        cr.arc(0.5, 0.5, 0.05, 0, 2 * math.pi)
        cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
        cr.fill_preserve()
        cr.set_line_width(0.015)
        cr.set_source_rgba(0.0, 0.0, 0.0, 1.0)
        cr.stroke()
        try:
            cr.pop_group_to_source()
            cr.paint_with_alpha(0.5)
        except:
            pass

    def do_screen_changed(self, old_screen=None):
        screen = self.get_screen()
        try:
            colormap = screen.get_rgba_visual()
            if colormap:
                self.supports_alpha = True
            else:
                self.supports_alpha = False
        except:
            colormap = screen.get_rgb_visual()
            self.supports_alpha = False
        if colormap:
            self.set_visual(colormap)

    def update(self):
        self.stats.update_all()
        self.do_expose_event('update')
        return True

if __name__ == '__main__':
    mystat = stats()
    mystat.update_all()
    window = Witsjet(mystat)
    window.set_title('Witsjet Demo')
    window.connect('delete-event', Gtk.main_quit)
    window.show()
    GObject.timeout_add(1000, window.update)
    try:
        Gtk.main()
    except KeyboardInterrupt:
        pass

Would you please help me with porting it to python3?

EDIT: I found this PyGobject cairo example which looks instructive https://pygobject.readthedocs.io/en/latest/guide/cairo_integration.html



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source