aboutsummaryrefslogblamecommitdiffstats
path: root/src/__main__.py
blob: 2f786d70fff58e9501935f580ecf25db3f484e24 (plain) (tree)
1
2
3
4
5
6
7
8
9
                      
               

                                                            
 



                                                                    
 



                                                               
 


                                                                     
                                                       
                       
 
               
                  
 
 



                       

                                                              
                                     

                                          



                                                   

             

                                        






                                                          
                                        







                                                                      
                                        



               








                                                
 



















                                                                                                                     
                                                               
























                                                                                                                                                  



                                                                                                        






























                                                                  

                                                                

                                    



                                 
                                                              














                                                                       


                                                                                            
                                                            
                              

                        
    









































































                                                                                            
    





































                                                                              








                                                                                
 
 































































                                                                                             

 
        
                       
                        
       
 




                                          

                              

                             
            
                   
 
      
         
 
#!/usr/bin/env python3
copyright = '''
xpybar – xmobar replacement written in python
Copyright © 2014  Mattias Andrée (maandree@member.fsf.org)

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 3 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, see <http://www.gnu.org/licenses/>.
'''
import Xlib.display, Xlib.Xatom, Xlib.ext.randr, Xlib.X
from argparser import *

from x import *
from util import *


PROGRAM_NAME = 'xpybar'
PROGRAM_VERSION = '1.0'


global OUTPUT, HEIGHT, YPOS, TOP, FONT, BACKGROUND, FOREGROUND
global dislay, outputs, redraw, Bar, start, stop
global conf_opts, config_file, parser

OUTPUT, HEIGHT, YPOS, TOP = 0, 12, 0, True
FONT = '-misc-fixed-*-*-*-*-10-*-*-*-*-*-*-*'
BACKGROUND, FOREGROUND = (0, 0, 0), (192, 192, 192)


def redraw():
    '''
    Invoked when redraw is needed,
    feel free to replace this completely
    '''
    global bar
    bar.clear()

def start():
    '''
    Invoked when it is time to create panels and map them,
    feel free to replace this completely
    '''
    global bar
    bar = Bar(OUTPUT, HEIGHT, YPOS, TOP, FONT, BACKGROUND, FOREGROUND)
    bar.map()

def stop():
    '''
    Invoked when it is time to unmap the panels,
    feel free to replace this completely
    '''
    global bar
    bar.unmap()

def unhandled_event(e):
    '''
    Invoked when an unrecognised even is polled,
    feel free to replace this completely
    
    @param  e  The event
    '''
    pass


class Bar:
    '''
    Docked panel
    
    @variable  window            The X window
    @variable  gc                The window's graphics context
    @variable  cmap              The window's colour map
    
    @variable  width:int         The output's pixel width
    @variable  height:int        The output's pixel height
    @variable  left:int          The output's left position
    @variable  ypos:int          The position of the panel in relation to either the top or bottom edge of the output
    @variable  panel_height:int  The panel's height
    @variable  at_top:bool       Whether the panel is to be docked to the top of the output, otherwise to the bottom
    
    @variable  background        The default background
    @variable  foreground        The default foreground
    @variable  font              The default font
    @variable  font_metrics      The default font's metrics
    @variable  font_height:int   The height of the default font
    @variable  palette           A 16-array of standard colours
    '''
    
    def __init__(self, output, height, ypos, top, font, background, foreground):
        '''
        Constructor
        
        @param  output:int                                 The index of the output within the screen as printed by xrandr, except primary is first
        @param  height:int                                 The height of the panel
        @param  ypos:int                                   The position of the panel in relation the either the top or bottom edge of the output
        @param  top:int                                    Whether the panel is to be docked to the top of the output, otherwise to the bottom
        @param  font:str                                   The default font
        @param  background:(red:int, green:int, blue:int)  The default background
        @param  foreground:(red:int, green:int, blue:int)  The default foreground
        '''
        ## Panel position
        pos = outputs[output][:3] + [ypos, height, top]
        self.width, self.height, self.left, self.ypos, self.panel_height, self.at_top = pos
        ## Create window and create/fetch resources
        self.window = create_panel(*pos)
        self.gc = self.window.create_gc()
        self.cmap = self.window.get_attributes().colormap
        ## Graphics variables
        self.background = self.create_colour(*background)
        self.foreground = self.create_colour(*foreground)
        (self.font, self.font_metrics, self.font_height) = self.create_font(font)
        self.palette = [0x000000, 0xCD656C, 0x32A679, 0xCCAD47, 0x2495BE, 0xA46EB0, 0x00A09F, 0xD8D8D8]
        self.palette += [0x555555, 0xEB5E6A, 0x0EC287, 0xF2CA38, 0x00ACE0, 0xC473D1, 0x00C3C7, 0xEEEEEE]
        self.palette = [((p >> 16) & 255, (p >> 8) & 255, p & 255) for p in self.palette]
        self.palette = [self.create_colour(*p) for p in self.palette]
    
    def map(self):
        '''
        Map the window
        '''
        self.window.map()
        display.flush()
    
    def unmap(self):
        '''
        Unmap the window
        '''
        self.window.unmap()
    
    def text_width(self, text):
        '''
        Get the width of a text
        
        @param   text:str  The text
        @return  :int      The width of the text
        '''
        return self.font.query_text_extents(text).overall_width
    
    def draw_text(self, x, y, text):
        '''
        Draw a text
        
        @param  x:int     The left position of the text
        @param  y:int     The Y position of the bottom of the text
        @param  text:str  The text to draw
        '''
        special = '─│┌┐└┘├┤┬┴┼╱╲╳\0'
        buf = ''
        w = self.text_width('X') - 1
        h = self.font_height - 1
        y_ = y - self.font_height
        for c in text + '\0':
            if c in special:
                if not buf == '':
                    draw_text(self.window, self.gc, x, y, buf)
                    x += self.text_width(buf)
                    buf = ''
                if not c == '\0':
                    segs = []
                    if c in '─┼┬┴':  segs.append((0, 1,  2, 1))
                    if c in '│┼├┤':  segs.append((1, 0,  1, 2))
                    if c in '├┌└':   segs.append((1, 1,  2, 1))
                    if c in '┤┐┘':   segs.append((0, 1,  1, 1))
                    if c in '┬┌┐':   segs.append((1, 1,  1, 2))
                    if c in '┴└┘':   segs.append((1, 0,  1, 1))
                    if c in '╱╳':    segs.append((0, 2,  2, 0))
                    if c in '╲╳':    segs.append((0, 0,  2, 2))
                    segs_ = []
                    for seg in segs:
                        (x1, y1, x2, y2) = [c / 2 for c in seg]
                        x1, x2 = x1 * w + 0.5, x2 * w + 0.5
                        y1, y2 = y1 * h + 0.5, y2 * h + 0.5
                        segs_.append((int(x1) + x, int(y1) + y_, int(x2) + x, int(y2) + y_))
                    self.window.poly_segment(self.gc, segs_)
                    x += w + 1
            else:
                buf += c
    
    def draw_coloured_text(self, x, y, ascent, descent, text):
        '''
        Draw a coloured multi-line text
        
        @param  x:int        The left position of the text
        @param  y:int        The Y position of the bottom of the text
        @param  ascent:int   Extra height above the text on each line
        @param  descent:int  Extra height under the text on each line
        @param  text:str     The text to draw
        '''
        buf, bc, fc, xx = '', self.background, self.foreground, x
        line_height = ascent + self.font_height + descent
        esc = False
        for c in text + '\033[m':
            if esc:
                buf += c
                if ('a' <= c <= 'z') or ('A' <= c <= 'Z') or (c == '~'):
                    if (buf[0] == '[') and (buf[-1] == 'm'):
                        buf = buf[1 : -1].split(';')
                        buf = [int('0' + x) for x in buf]
                        bci, fci = 0, 0
                        for b in buf:
                            if bci != 0:
                                if bci == 1:
                                    if not b == 2:
                                        bci = -2
                                    bc = 0
                                elif bci > 1:
                                    bc = (bc << 8) + b
                                    if bci == 4:
                                        bci = -1
                                        bc = self.create_colour(*bc)
                                bci += 1
                            elif fci != 0:
                                if fci == 1:
                                    if not b == 2:
                                        fci = -2
                                    fc = 0
                                elif fci > 1:
                                    fc = (fc << 8) + b
                                    if fci == 4:
                                        fci = -1
                                        fc = ((fc >> 16) & 255, (fc >> 8) & 255, fc & 255)
                                        fc = self.create_colour(*fc)
                                fci += 1
                            elif b == 0:           bc, fc = self.background, self.foreground
                            elif b == 39:          fc = self.foreground
                            elif b == 49:          bc = self.background
                            elif 30 <= b <= 37:    fc = self.palette[b - 30]
                            elif 40 <= b <= 47:    bc = self.palette[b - 40]
                            elif 90 <= b <= 97:    fc = self.palette[b - 90 + 8]
                            elif 100 <= b <= 107:  bc = self.palette[b - 100 + 8]
                            elif b == 38:          fci = 1
                            elif b == 48:          bci = 1
                    buf = ''
                    esc = False
            elif c in ('\033', '\n'):
                if not buf == '':
                    self.change_colour(bc)
                    h = self.font_height + ascent
                    w = self.text_width(buf)
                    self.window.fill_rectangle(self.gc, x, y - h, w, line_height)
                    self.gc.change(foreground = fc, background = bc)
                    self.draw_text(x, y + ascent, buf)
                    x += w
                    buf = ''
                if c == '\n':
                    y += line_height
                    x = xx
                else:
                    esc = True
            else:
                buf += c
        self.change_colour(self.foreground)
    
    def create_colour(self, red, green, blue):
        '''
        Create a colour instance
        
        @param   red:int    The red component [0, 255]
        @param   green:int  The green component [0, 255]
        @param   blue:int   The blue component [0, 255]
        @return             The colour
        '''
        return self.cmap.alloc_color(red * 257, green * 257, blue * 257).pixel
    
    def create_font(self, font):
        '''
        Create a font
        
        @param   font:str  The font
        @return            The font, font metrics, and font height
        '''
        font = display.open_font(font)
        font_metrics = font.query()
        font_height = font_metrics.font_ascent + font_metrics.font_descent
        return (font, font_metrics, font_height)
    
    def change_colour(self, colour):
        '''
        Change the current colour
        
        @param  colour  The colour
        '''
        self.gc.change(foreground = colour)
    
    def change_font(self, font):
        '''
        Change the current font
        
        @param  font  The font
        '''
        self.gc.change(font = font)
    
    def clear(self):
        '''
        Fill the panel with its background colour and reset the colour and font
        '''
        self.change_colour(self.background)
        self.window.fill_rectangle(self.gc, 0, 0, self.width, self.panel_height)
        self.change_colour(self.foreground)
        self.change_font(self.font)


## Read command line arguments
parser = ArgParser('A highly extensible minimalistic dock panel',
                   sys.argv[0] + ' [options] [-- configuration-options]',
                   None, None, True, ArgParser.standard_abbreviations())

parser.add_argumented(['-c', '--configurations'], 0, 'FILE', 'Select configuration file')
parser.add_argumentless(['-h', '-?', '--help'], 0, 'Print this help information')
parser.add_argumentless(['-C', '--copying', '--copyright'], 0, 'Print copyright information')
parser.add_argumentless(['-W', '--warranty'], 0, 'Print non-warranty information')
parser.add_argumentless(['-v', '--version'], 0, 'Print program name and version')

parser.parse()
parser.support_alternatives()

if parser.opts['--help'] is not None:
    parser.help()
    sys.exit(0)
elif parser.opts['--copyright'] is not None:
    print(copyright[1 : -1])
    sys.exit(0)
elif parser.opts['--warranty'] is not None:
    print('This program is distributed in the hope that it will be useful,')
    print('but WITHOUT ANY WARRANTY; without even the implied warranty of')
    print('MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the')
    print('GNU Affero General Public License for more details.')
    sys.exit(0)
elif parser.opts['--version'] is not None:
    print('%s %s' % (PROGRAM_NAME, PROGRAM_VERSION))
    sys.exit(0)

a = lambda opt : opt[0] if opt is not None else None
config_file = a(parser.opts['--configurations'])


## Load extension and configurations via xpybarrc
if config_file is None:
    for file in ('$XDG_CONFIG_HOME/%/%rc', '$HOME/.config/%/%rc', '$HOME/.%rc', '/etc/%rc'):
        file = file.replace('%', 'xpybarrc')
        for arg in ('XDG_CONFIG_HOME', 'HOME'):
            if '$' + arg in file:
                if arg in os.environ:
                    file = file.replace('$' + arg, os.environ[arg].replace('$', '\0'))
                else:
                    file = None
                    break
        if file is not None:
            file = file.replace('\0', '$')
            if os.path.exists(file):
                config_file = file
                break
conf_opts = [config_file] + parser.files
if config_file is not None:
    code = None
    with open(config_file, 'rb') as script:
        code = script.read()
    code = code.decode('utf-8', 'error') + '\n'
    code = compile(code, config_file, 'exec')
    g, l = globals(), dict(locals())
    for key in l:
        g[key] = l[key]
    exec(code, g)
else:
    print('No configuration file found')
    sys.exit(1)


open_x()
display = get_display()
outputs = get_monitors()
start()

while True:
    try:
        e = display.next_event()
        if e.type == Xlib.X.DestroyNotify:
            break
        else:
            unhandled_event(e)
    except KeyboardInterrupt:
        break
    redraw()
    display.flush()

stop()
close_x()