neděle 7. října 2012

Sessions Selector - Python + GTK3 + komentovaný skript

Na popud jednoho uživatele jsem pro Ubuntu (a příbuzné systémy) napsal krátký skript, který sice nedělá nic zásadního a jeho využití nebude pro každého, ale proč to nevyužít jako příklad, jak jednoduše můžete v Pythonu vyrábět funkční okénka. Python je velmi intuitivní a srozumitelný interpretovaný jazyk, ve kterém je radost tvořit a základy Gtk toolkitu se dají také pobrat rychle. Je to podobné jako Lego, berete hotové kostičky a skládáte je dohromady. Nebudu rozebírat nějaké základy, jen sem vlepím ten kód, jednou čistý, podruhé komentovaný, a třeba to někomu pomůže. Třeba mně, až zas všechno zapomenu.




Skript nedělá nic jiného, než že vám umožní naklikat si v seznamu sezení dostupných na vašem systému, která z nich se vám v nabídce přihlašovací obrazovky zobrazí a která nikoli. Není to nijak ošetřeno proti blbosti, takže vězte, že když si zakážete všechna sezení, nikam se příště nepřihlásíte. Což nebude problém opravit z konzole. Funguje pro LightDM a GDM.

Jak to funguje

Skript provádí manipulaci s názvy spouštěčů xsessions v adresáři /usr/share/xsessions/ a když položku v okně odškrtnete, prostě k názvu spouštěče přidá koncovku .disabled, čímž ho znefunkční a systém ho neuvidí. A samozřejmě to umí zas vrátit zpět. Pokud se stane, že se po aktualizaci v xsessions objeví duplicitní spouštěč spouštěče dříve zakázaného, bude ten původní (s koncovkou .disabled) po startu skriptu automaticky smazán a uživatel si může sezení zakázat znovu, pokud bude chtít.
Níže uvedený skript zkopírujete do souboru pojmenovaného třeba sselector.py a pak ho budete spouštět z terminálu jako root příkazem:

# python sselector.py

z adresáře, kde se skript bude nacházet, jinak zadáte celou cestu a samozřejmě můžete použít sudo pro spuštění s právy roota. Kód je funkční jak pro Python 2, tak i 3

Výsledek bude vypadat podobně jako na obrázku z úvodu (podle toho, jaká prostředí jste si do systému nainstalovali).


sselector.py
#!/usr/bin/env python
#-*- coding: UTF-8 -*-

# Select sessions which will be available in system login screen

import os, sys

if not os.geteuid()==0:
    print('Error: Only root can use this script')
    sys.exit(1)

from gi.repository import Gtk
import re

path = '/usr/share/xsessions/'
regn = re.compile(r'^Name=')
regd = re.compile(r'\.disabled$')

class SS(Gtk.Window):
    def __init__(self):
        super(SS, self).__init__()

        self.set_position(Gtk.WindowPosition.CENTER)
        self.set_title('Sessions Selector')
        self.set_border_width(10)
        self.connect('destroy', self.quit)

        vb = Gtk.VBox(False, 0)
        self.add(vb)
        lt = Gtk.Label()
        lt.set_markup('<b>Select Sessions:</b>\n')
        lt.set_alignment(xalign=0, yalign=0.5)
        vb.add(lt)

        lf = self.filter_duplicates([f for f in os.listdir(path) if '.desktop' in f])

        lh = []
        for f in lf:
            with open('%s%s' %(path,f), 'r') as d:
                for l in d:
                    if regn.match(l):
                        nm = l[5:-1]
                        break
            ln = Gtk.Label(nm)
            sw = Gtk.CheckButton()
            if not regd.search(f):
                sw.set_active(True)
            sw.connect('notify::active', self.switch, regd.sub('', f))
            hb = Gtk.HBox(False, 20)
            hb.pack_start(ln, False, False, 0)
            hb.pack_end(sw, False, False, 0)
            lh.append([nm, hb])

        for i in sorted(lh, key=lambda i:i[0].lower()):
            vb.pack_start(i[1], False, False, 3)

        self.show_all()

    def filter_duplicates(self, lf):
        l = []
        for f in [regd.sub('', f) for f in lf]:
            if not f in l:
                l.append(f)
            else:
                try: os.remove('%s%s.disabled'%(path,f))
                except: pass
                else: lf.remove('%s.disabled'%f)
        return lf

    def switch(self, w, e, f):
        p = '%s/%s' %(path, f)
        if w.get_active():
            os.rename('%s.disabled'%p, p)
        else:
            os.rename(p, '%s.disabled'%p)
    
    def quit(self, *args):
        Gtk.main_quit()

if __name__ == '__main__': 
    SS()
    Gtk.main()



sselector.py  komentovaný
#!/usr/bin/env python
#-*- coding: UTF-8 -*-

# Select sessions available in system login screen

import os, sys

if not os.geteuid()==0: # root má ID 0, pokud skript spustil jiný uživatel,
    print('Error: Only root can use this script')  #  vypíšeme chybu
    sys.exit(1)   # a ukončíme program

from gi.repository import Gtk
import re

path = '/usr/share/xsessions/' # adresář, kde jsou umístěné spouštěče jednotlivých sezení
regn = re.compile(r'^Name=')      # předkompilujeme si regulární výrazy,
regd = re.compile(r'\.disabled$') # které budeme potřebovat hledat

class SS(Gtk.Window):  # třída bude odvozena od Gtk.Window
    def __init__(self):
        super(SS, self).__init__() # inicializace
        # nastavení vlastností okna:
        self.set_position(Gtk.WindowPosition.CENTER) # Okno se zobrazí uprostřed obrazovky
        self.set_title('Sessions Selector') # Text, který se zobrazí v záhlaví okna
        self.set_border_width(10) # Okno bude mít okraj silný 10 bodů
        self.connect('destroy', self.quit) # pokus o zavření okna vyvolá metodu quit definovanou níže
        # výrova součástí okna:
        vb = Gtk.VBox(False, 0) # vytvoří nehomogenní (argument False,vložené objekty nebudou mít stejnou šířku)
        # vertikální box, do kterého se další grafické objekty skládají na výšku zhora dolu
        self.add(vb) # box přidáme do okna
        lt = Gtk.Label() # vytvoří štítek, do kterého budeme moct napsat nadpis
        lt.set_markup('<b>Select Sessions:</b>\n') # na štítek napíšeme tučný text pomocí značkovacího jazyka pango
        lt.set_alignment(xalign=0, yalign=0.5) # text se normálně umisťuje doprostřed štítku a my chceme hned od leva
        vb.add(lt) # štítek přidáme do boxu, který jsme si vytvořili výše

        lf = self.filter_duplicates([f for f in os.listdir(path) if '.desktop' in f])
        # načteme seznam souborů obsahujících koncovku '.desktop' a zbavený duplicit

        lh = [] # prázdný seznam, do kterého budeme přidávat nalezená sezení
        for f in lf:    # projdeme seznam souborů
            with open('%s%s' %(path,f), 'r') as d:  # každý soubor otevřeme
                for l in d:           # a na jednotlivých řádcích
                    if regn.match(l): # budeme hledat řetězec 'Name=' na začátku (viz kompilace regulárního výrazu výše)
                        nm = l[5:-1]  # když najdeme, vyřízneme jen samotné jméno (poslední znak je konec řádku)
                        break         # a přerušíme smyčku. tím se zároveň ukončí blok 'with' a soubor se automaticky zavře
            ln = Gtk.Label(nm)        # přpravíme si štítek, na který napíšeme získané jméno sezení
            sw = Gtk.CheckButton()    # vytvoříme zaškrtávátko
          # sw = Gtk.Switch()         # nebo můžeme místo zaškrtávátka vytvořit přepínač pokud se nám líbí víc
            if not regd.search(f):    # pokud jméno souboru nekončí na ".disabled" (viz kompilace regulárního výrazu výše)
                sw.set_active(True)   # přepneme přepínač do aktivní polohy
            sw.connect('notify::active', self.switch, regd.sub('', f)) # na signál vyvolávaný aktivací přepínače
                    # napojíme metodu switch (definujeme níž), které bude při volání předáno jméno souboru,
                    # ze kterého rovnou odstraníme případnou koncovku ".disabled"
            hb = Gtk.HBox(False, 20) # dále vytvoříme horizontální box, který nebude homogenní (vložené objekty nebudou mít stejnou šířku)
                                     # jednotlivé objekty budou mít mezi sebou 20 bodů
            hb.pack_start(ln, False, False, 0) # do boxu přidáme štítek se jménem
            hb.pack_end(sw, False, False, 0)  # a vedle přepínač
            # pack_start()/pack_end() určují, k jakému konci boxu se bude přidaný objekt zarovnávat
            lh.append([nm, hb]) # do seznamu sezení si přidáme (v seznamu) jméno a vytvořený box
            # a pokračuje se na další zezení, pokud ještě nějaké zbývá

        for i in sorted(lh, key=lambda i:i[0].lower()): # nakonec projedeme vytvořený seznam sezení setříděný podle abecedy,
            # nezávisle na malých/velkých písmenech, položku po položce
            # key se přiřazuje funkce, jejíž výstupem je klíčový řetězec, podle kterého se bude řadit
            # protože chceme setřídit seznam, který obsahuje seznamy složené nejen z řeťězců (jsou tam krom jmen i objekty Gtk.HBox)
            # nelze použít univerzální str.lower(), která by se snažila převést na malá písmena vše (a samozřejmě by řvala).
            # místo toho použijeme lambda funkci, která vezme aktuální položku seznamu, oddělí jen její prní část obsahující řetězec se jménem
            # a převede ho na malá písmena. seznam se tedy řadí čistě podle jmen
            vb.pack_start(i[1], False, False, 3) # pro každou položku seznamu vyjmeme jen box obsahující jméno sezení a přepínač
            # a přidáme do hlavního vertikálního boxu okna
        self.show_all() # nakonec poskládané okno necháme zobrazit na monitoru

    def switch(self, w, e, f): # metoda, která je volána při přepnutí každého přepínače v okně
        # předávány jsou argumenty 'w' je ukazatel na objekt (přepínač), který volání inicioval,
        # v 'e' jsou podrobnosti události a v 'f' je jméno souboru, který patří sezení, jehož přepínač volání způsobil
        p = '%s/%s' %(path, f) # zkompletujeme si celou cestu k souboru
        if w.get_active():  # zjistíme, jak v jaké poloze je přepínač
            os.rename('%s.disabled'%p, p) # pokud je zapnutý, přejmenujeme soubor se spouštěčem tak, že mu přidáme koncovku '.disabled'
        else:
            os.rename(p, '%s.disabled'%p) # pokud je vypnutý, naopak z názvu koncovku '.disabled' odebereme
 
    def filter_duplicates(self, lf):
        l = []
        for f in [regd.sub('', f) for f in lf]: # projdeme seznam souborů zbavených případné koncovky .disable
            if not f in l: # pokud již položku nemáme v seznamu,
                l.append(f) # přidáme jí tam
            else:
                try: os.remove('%s%s.disabled'%(path,f)) # jinak zkusíme zakázanou variantu duplicitního spouštěče odstranit z disku
                except: pass
                else: lf.remove('%s.disabled'%f) # a pokus se to povede, tak i z původního seznamu
        return lf # upravený seznam vrátíme zpět
   
    def quit(self, *args):  # tato metoda se volá, když uživatel klikne na křížek pro zavření okna, nebo ostatních pokusech o zavření okna
        Gtk.main_quit()     # jednoduše se přeruší hlavní smyčka Gtk

if __name__ == '__main__': # pokud se skript spouští samostatně, ne jako součást jiného skriptu
    SS()        # inicializuje se třída SS
    Gtk.main()  # a spustí se hlavní smyčka Gtk, která čeká na události v aplikaci, aby je zpracovala. bez ní by se okno ani nevykreslilo.

Doufám, že jsou ty komentáře alespoň tak srozumitelné, původní kód jsem doplňoval o eliminaci duplicit, které mohou vzniknout po aktualizaci, kdy se zapíše nový spouštěč, když je ten původní zakázaný a pak to dopisoval do komentované části. Každopádně jsem na svých systémech otestoval funkčnost obou verzí.

Jistě je toho více, co by taková aplikace mohla umět řešit, ale mám zásadnější nedodělky..

Nějaká dokumentace:
http://python-gtk-3-tutorial.readthedocs.org/en/latest/index.html



5 komentářů:

  1. File "./sselector.py", line 39
    with open('%s%s' %(path,f), 'r') as d:
    ^

    OdpovědětSmazat
    Odpovědi
    1. Hází ti to chybu syntaxe? Pokud máš dostatečně starou verzi Pythonu, myslím, že starší než 2.5, tak musíš použít klasickou konstrukci open/close.. Nebo použít novější Python.

      Smazat
    2. python --version
      Python 2.7.3

      Smazat
    3. Na stejné verzi na Ubuntu 12.04 jsem to psal a násobné přezkoušení kódu zkopírovaného odsud potvrzuje funkčnost. Zajímalo by mě celé znění toho chybového hlášení, na chybu syntaxe to nevypadá.
      Pokud tedy nekopíruješ ten komentovaný, tam se mi koukám nějak pomrvilo formátování..

      Smazat
    4. Tak už jsem opravil i tu komentovanou verzi. Díky za upozornění.

      Smazat

Zkuste prosím při komentováni používat místo volby Anonymní volbu Název/adresa URL, kde vyplníte nějakou přezdívku, adresu zadávat nemusíte. Vědět, které příspěvky jsou od jednoho člověka, je fajn. Díky.

Pokud by se vám náhodou odeslaný komentář na stránce nezobrazil, vytáhnu ho z koše hned jak si toho všimnu. I Google spam filter se občas sekne.