1
0
mirror of https://github.com/gryf/urxvt-wrapper.git synced 2025-12-17 19:40:25 +01:00
Files
urxvt-wrapper/urxvt.py

350 lines
11 KiB
Python
Executable File

#!/usr/bin/env python
"""
Wrapper for launching URXVT terminal emulator. It supports automatically
generated font list for normal and bold typefaces, setting up bitmap font as a
main one, selecting an icon and so on.
Consult options below.
"""
import argparse
import os
import subprocess
import sys
import logging
SIZE = os.environ.get('URXVT_SIZE', 14)
FIXED_SIZE = os.environ.get('URXVT_FIXED_SIZE', 16)
ICON = os.environ.get('URXVT_ICON', 'tilda.png')
ICON_PATH = os.environ.get('URXVT_ICON_PATH',
os.path.expanduser('~/.urxvt/icons'))
DEFAULT_FONT = os.environ.get('URXVT_TTF', 'DejaVuSansMono Nerd Font Mono')
DEFAULT_BITMAP = os.environ.get('URXVT_BMP', 'Misc Fixed')
PERLEXT = os.environ.get('URXVT_PERL_EXT',
"url-select,keyboard-select,font-size,color-themes")
LOG = None
class Logger:
"""
Simple logger class with output on console only
"""
def __init__(self, logger_name):
"""
Initialize named logger
"""
self._log = logging.getLogger(logger_name)
self.setup_logger()
self._log.set_verbose = self.set_verbose
def __call__(self):
"""
Calling this object will return configured logging.Logger object with
additional set_verbose() method.
"""
return self._log
def set_verbose(self, verbose_level):
"""
Change verbosity level. Default level is warning.
"""
ver_map = {0: logging.CRITICAL,
1: logging.ERROR,
2: logging.WARNING,
3: logging.INFO,
4: logging.DEBUG}
self._log.setLevel(ver_map.get(verbose_level, ver_map[4]))
def setup_logger(self):
"""
Create setup instance and make output meaningful :)
"""
if self._log.handlers:
# need only one handler
return
console_handler = logging.StreamHandler(sys.stderr)
console_handler.set_name("console")
console_formatter = logging.Formatter("%(levelname)s: %(message)s")
console_handler.setFormatter(console_formatter)
self._log.addHandler(console_handler)
self._log.setLevel(logging.WARNING)
LOG = Logger(__name__)()
class Font:
"""
Represents font object, which can produce valid XFT strings for provided
fonts as it's attributes - bold and regular.
xft:font name:style=somestyle:pixelsize=12
"""
XFT_TEMPLATE = 'xft:%s:style=%s:pixelsize=%d'
_REGULAR = ['regular', 'normal', 'book', 'medium']
_BOLD = ['bold']
_AVAILABLE_FONTS = {}
def __init__(self, name, size):
self.size = size
self.name = name
self._regular = None
self._bold = None
self._get_all_suitable_fonts()
@property
def bold(self):
"""
Return full string font to use for xft definition for urxvt, i.e.
xft:font name:style=Bold:pixelsize=20
It may happen, that a font doesn't provide bold face. Function will
than return empty string.
"""
if self._bold is not None:
return self._bold
style = Font._AVAILABLE_FONTS['bold'].get(self.name)
if not style:
LOG.warning(f'Bold style not found for {self.name}')
self._bold = ''
else:
self._bold = Font.XFT_TEMPLATE % (self.name, style, self.size)
return self._bold
@property
def regular(self):
"""
Return full string font to use for xft definition for urxvt, i.e.
xft:font name:style=Regular:pixelsize=20
Font style depends on the particular font, and can be one of case
insensitive: regular, normal, book, medium.
Technically, medium style should be semi-bold or bold, but there is at
least one font face which treats medium style as regular one. It is
placed as a last resort, and choice for particular font face is left
to the user.
"""
if self._regular is not None:
return self._regular
style = Font._AVAILABLE_FONTS['regular'].get(self.name)
if not style:
LOG.warning(f'Regular style not found for {self.name}')
self._regular = ''
else:
self._regular = Font.XFT_TEMPLATE % (self.name, style, self.size)
return self._regular
def _get_all_suitable_fonts(self):
"""
Scan all available in the system fonts, where every line have format:
font_filename: font_name1[,font_name2,…]:style=style1[,style2,…]
Font can have several names and several styles. Styles can be either
single defined style or comma separated list of aliases or
internationalized names for style, i.e.:
filename1: font_name1:style=style
filename2: font_name2,font_name3:style=style1,style2
filename3: font_name4:style=style3,style4
Information about fonts will be stored as a class variable
_AVAILABLE_FONTS within two dictionaries: bold and regular, stored in
corresponding keys in mentioned dict. _AVAILABLE_FONTS will have a
format:
{'bold': {'font name': 'Bold',
'font name3': 'bold'},
'regular': {'font_name1': 'Regular',
'font_name2': 'Medium',
'font_name3': 'Normal'}}
As for regular/normal/book/medium styles, whatever style is available
for given font, it will be chosen, as ordered in _REGULAR list
whichever matches first.
Note, that fc-list and all the parsing is done once, per running this
script, regardless of font faces passed by -f option or those defined
in ADDITIONAL_FONTS.
"""
if Font._AVAILABLE_FONTS:
return
regular = {}
bold = {}
out = subprocess.check_output(['fc-list']).decode('utf-8')
out = sorted(out.split('\n')) # let's have it in order
for line in out:
if not line:
continue
if ': ' not in line:
continue
if ':style=' not in line:
continue
line = line.split(': ')[1]
font_names = [n.strip() for n in line.split(':')[0].split(',')]
styles = [s.strip() for s in line.split(':style=')[1].split(',')]
style = self._parse_style(styles)
if not style:
LOG.debug('No suitable styles found for font in line: %s',
line)
continue
for name in font_names:
if style.lower() == 'bold' and not bold.get(name):
LOG.info('Adding bold font for name: %s', name)
bold[name] = style
elif style.lower() != 'bold' and not regular.get(name):
LOG.info('Adding regular font for name: %s', name)
regular[name] = style
else:
LOG.debug('Font %s probably already exists in dict', name)
Font._AVAILABLE_FONTS = {'regular': regular, 'bold': bold}
def _parse_style(self, styles):
for reg_style in Font._REGULAR:
if reg_style in ''.join(styles).lower():
for style in styles:
if style.lower() == reg_style:
return style
if 'bold' in ''.join(styles).lower():
for style in styles:
if style.lower() == 'bold':
return style
class Urxvt:
"""
Runner for the URXVT
"""
# Arbitrary added fonts, that provides symbols, icons, emoji (besides
# those in defualt font)
_ADDITIONAL_FONTS = ['Symbola', 'Unifont Upper', 'DejaVu Sans']
def __init__(self, args):
self.size = args.size
self.icon = args.icon
self.fonts = None
self.perl_extensions = None
self._bitmap = args.bitmap
self._icon_path = ICON_PATH
self._exec = args.execute
self._setup(args)
self._validate()
def run(self):
"""Run terminal emulator"""
args = self._make_command_args()
LOG.info('%s', args)
# subprocess.run(command)
# LOG.info('%s', command)
def _setup(self, args):
# it could be a list or a single font, it will be combined with
# additional fonts
self.fonts = self._parse_fonts(args.default_font)
if args.no_perl:
self.perl_extensions = ''
else:
self.perl_extensions = PERLEXT
if args.tabbedalt:
self.perl_extensions = 'tabbedalt,' + PERLEXT
def _validate(self):
# validate fonts
for font in self.fonts:
if not any((font.regular, font.bold)):
LOG.error('Font %s seems to be unusable or nonexistent.',
font.name)
def _make_command_args(self):
args = []
args.extend(['-pe', self.perl_extensions])
args.extend(['-fn',
','.join([f.regular for f in self.fonts if f.regular])])
args.extend(['-fb', ','.join([f.bold for f in self.fonts if f.bold])])
args.extend(['-icon', os.path.join(ICON_PATH, self.icon)])
if self._exec:
args.extend(['-e', self._exec])
return args
def _parse_fonts(self, font_string):
"""
Parse potentially provided font list, add additional fonts to it,
adjust for possible bitmap font and return a list of Font objects.
"""
# get the copy of additional fonts
font_faces = ADDITIONAL_FONTS[:]
# find out main font/fonts passed by commandline/env/default
if ',' in font_string:
f_f = [f.strip() for f in font_string.split(',')]
f_f.extend(font_faces)
font_faces = f_f
else:
font_faces.insert(0, font_string)
if self._bitmap:
font_faces.insert(0, DEFAULT_BITMAP)
# return list of Font objects
return [Font(f, self.size) for f in font_faces]
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('-f', '--default-font', default=DEFAULT_FONT,
help='use particular (comma separated) font face(s) '
'as default(s) one, should be provided by font name, '
'not file name(s), default is "%s"' % DEFAULT_FONT)
parser.add_argument('-b', '--bitmap', action='store_true', help='use '
'bitmap font prior to scalable defined above')
parser.add_argument('-i', '--icon', default=ICON, help='select icon from '
'%s, default "%s"' % (ICON_PATH, ICON))
parser.add_argument('-t', '--tabbedalt', action='store_true',
help='activate tabbedalt extension')
parser.add_argument('-n', '--no-perl', action='store_true',
help='no perl extensions')
parser.add_argument('-s', '--size', default=SIZE, type=int,
help='set scalable forn size, default %s' % SIZE)
parser.add_argument('-e', '--execute', default=None,
help='pass exec to urxvt')
parser.add_argument("-v", "--verbose", help='be verbose. Adding more "v" '
'will increase verbosity', action="count", default=0)
args = parser.parse_args()
LOG.set_verbose(args.verbose)
urxvt = Urxvt(args)
urxvt.run()
if __name__ == '__main__':
main()