terminator/terminatorlib/util.py
Fernando Basso bd5dba5b08 Refactor line height to cell height
After the previous commit [1], which implements ‘cell width’, it makes
sense to rename ‘line height’ to ‘cell height’, especially because it is
the terminology used by VTE itself [2].

1. ef1768505c Add cell width configuration in preferences
2. https://lazka.github.io/pgi-docs/Vte-2.91/classes/Terminal.html#Vte.Terminal.set_cell_height_scale
2021-12-14 06:30:58 -03:00

397 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Terminator.util - misc utility functions
# Copyright (C) 2006-2010 cmsj@tenshu.net
#
# 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, version 2 only.
#
# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
"""Terminator.util - misc utility functions"""
from __future__ import print_function
import sys
import cairo
import os
import pwd
import inspect
import uuid
import subprocess
import gi
try:
gi.require_version('Gtk','3.0')
from gi.repository import Gtk, Gdk
except ImportError:
print('You need Gtk 3.0+ to run Remotinator.')
sys.exit(1)
# set this to true to enable debugging output
DEBUG = False
# set this to true to additionally list filenames in debugging
DEBUGFILES = False
# list of classes to show debugging for. empty list means show all classes
DEBUGCLASSES = []
# list of methods to show debugging for. empty list means show all methods
DEBUGMETHODS = []
def dbg(log = ""):
"""Print a message if debugging is enabled"""
if DEBUG:
stackitem = inspect.stack()[1]
parent_frame = stackitem[0]
method = parent_frame.f_code.co_name
names, varargs, keywords, local_vars = inspect.getargvalues(parent_frame)
try:
self_name = names[0]
classname = local_vars[self_name].__class__.__name__
except IndexError:
classname = "noclass"
if DEBUGFILES:
line = stackitem[2]
filename = parent_frame.f_code.co_filename
extra = " (%s:%s)" % (filename, line)
else:
extra = ""
if DEBUGCLASSES != [] and classname not in DEBUGCLASSES:
return
if DEBUGMETHODS != [] and method not in DEBUGMETHODS:
return
try:
print("%s::%s: %s%s" % (classname, method, log, extra), file=sys.stderr)
except IOError:
pass
def err(log = ""):
"""Print an error message"""
try:
print(log, file=sys.stderr)
except IOError:
pass
def gerr(message = None):
"""Display a graphical error. This should only be used for serious
errors as it will halt execution"""
dialog = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL,
Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, message)
dialog.run()
dialog.destroy()
def has_ancestor(widget, wtype):
"""Walk up the family tree of widget to see if any ancestors are of type"""
while widget:
widget = widget.get_parent()
if isinstance(widget, wtype):
return(True)
return(False)
def manual_lookup():
'''Choose the manual to open based on LANGUAGE'''
available_languages = ['en']
base_url = 'http://gnome-terminator.readthedocs.io/%s/latest/'
target = 'en' # default to English
if 'LANGUAGE' in os.environ:
languages = os.environ['LANGUAGE'].split(':')
for language in languages:
if language in available_languages:
target = language
break
elif 'LANG' in os.environ:
language = os.environ['LANG'].split('.')[0]
for i in range(len(available_languages)):
if language == available_languages[i]:
target = language
break
return base_url % target
def path_lookup(command):
'''Find a command in our path'''
if os.path.isabs(command):
if os.path.isfile(command):
return(command)
else:
return(None)
elif command[:2] == './' and os.path.isfile(command):
dbg('path_lookup: Relative filename %s found in cwd' % command)
return(command)
try:
paths = os.environ['PATH'].split(':')
if len(paths[0]) == 0:
raise(ValueError)
except (ValueError, NameError):
dbg('path_lookup: PATH not set in environment, using fallbacks')
paths = ['/usr/local/bin', '/usr/bin', '/bin']
dbg('path_lookup: Using %d paths: %s' % (len(paths), paths))
for path in paths:
target = os.path.join(path, command)
if os.path.isfile(target):
dbg('path_lookup: found %s' % target)
return(target)
dbg('path_lookup: Unable to locate %s' % command)
def shell_lookup():
"""Find an appropriate shell for the user"""
try:
usershell = pwd.getpwuid(os.getuid())[6]
except KeyError:
usershell = None
shells = [usershell, 'bash', 'zsh', 'tcsh', 'ksh', 'csh', 'sh']
for shell in shells:
if shell is None:
continue
elif os.path.isfile(shell):
return(shell)
else:
rshell = path_lookup(shell)
if rshell is not None:
dbg('shell_lookup: Found %s at %s' % (shell, rshell))
return(rshell)
dbg('shell_lookup: Unable to locate a shell')
def widget_pixbuf(widget, maxsize=None):
"""Generate a pixbuf of a widget"""
# FIXME: Can this be changed from using "import cairo" to "from gi.repository import cairo"?
window = widget.get_window()
width, height = window.get_width(), window.get_height()
longest = max(width, height)
if maxsize is not None:
factor = float(maxsize) / float(longest)
if not maxsize or (width * factor) > width or (height * factor) > height:
factor = 1
preview_width, preview_height = int(width * factor), int(height * factor)
preview_surface = Gdk.Window.create_similar_surface(window,
cairo.CONTENT_COLOR, preview_width, preview_height)
cairo_context = cairo.Context(preview_surface)
cairo_context.scale(factor, factor)
Gdk.cairo_set_source_window(cairo_context, window, 0, 0)
cairo_context.paint()
scaledpixbuf = Gdk.pixbuf_get_from_surface(preview_surface, 0, 0, preview_width, preview_height);
return(scaledpixbuf)
def get_system_config_dir():
system_config_dir = '/etc/xdg'
if 'XDG_CONFIG_DIRS' in os.environ.keys():
for sysconfdir in os.environ['XDG_CONFIG_DIRS'].split(":"):
if os.path.isdir(sysconfdir):
system_config_dir = sysconfdir
break
return(os.path.join(system_config_dir,'terminator'))
def get_config_dir():
"""Expand all the messy nonsense for finding where ~/.config/terminator
really is"""
try:
configdir = os.environ['XDG_CONFIG_HOME']
except KeyError:
configdir = os.path.join(os.path.expanduser('~'), '.config')
dbg('Found config dir: %s' % configdir)
return(os.path.join(configdir, 'terminator'))
def dict_diff(reference, working):
"""Examine the values in the supplied working set and return a new dict
that only contains those values which are different from those in the
reference dictionary
>>> a = {'foo': 'bar', 'baz': 'bjonk'}
>>> b = {'foo': 'far', 'baz': 'bjonk'}
>>> dict_diff(a, b)
{'foo': 'far'}
"""
result = {}
for key in reference:
if reference[key] != working[key]:
result[key] = working[key]
return(result)
# Helper functions for directional navigation
def get_edge(allocation, direction):
"""Return the edge of the supplied allocation that we will care about for
directional navigation"""
if direction == 'left':
edge = allocation.x
p1, p2 = allocation.y, allocation.y + allocation.height
elif direction == 'up':
edge = allocation.y
p1, p2 = allocation.x, allocation.x + allocation.width
elif direction == 'right':
edge = allocation.x + allocation.width
p1, p2 = allocation.y, allocation.y + allocation.height
elif direction == 'down':
edge = allocation.y + allocation.height
p1, p2 = allocation.x, allocation.x + allocation.width
else:
raise ValueError('unknown direction %s' % direction)
return(edge, p1, p2)
def get_nav_possible(edge, allocation, direction, p1, p2):
"""Check if the supplied allocation is in the right direction of the
supplied edge"""
x1, x2 = allocation.x, allocation.x + allocation.width
y1, y2 = allocation.y, allocation.y + allocation.height
if direction == 'left':
return(x2 <= edge and y1 <= p2 and y2 >= p1)
elif direction == 'right':
return(x1 >= edge and y1 <= p2 and y2 >= p1)
elif direction == 'up':
return(y2 <= edge and x1 <= p2 and x2 >= p1)
elif direction == 'down':
return(y1 >= edge and x1 <= p2 and x2 >= p1)
else:
raise ValueError('Unknown direction: %s' % direction)
def get_nav_offset(edge, allocation, direction):
"""Work out how far edge is from a particular point on the allocation
rectangle, in the given direction"""
if direction == 'left':
return(edge - (allocation.x + allocation.width))
elif direction == 'right':
return(allocation.x - edge)
elif direction == 'up':
return(edge - (allocation.y + allocation.height))
elif direction == 'down':
return(allocation.y - edge)
else:
raise ValueError('Unknown direction: %s' % direction)
def get_nav_tiebreak(direction, cursor_x, cursor_y, rect):
"""We have multiple candidate terminals. Pick the closest by cursor
position"""
if direction in ['left', 'right']:
return(cursor_y >= rect.y and cursor_y <= (rect.y + rect.height))
elif direction in ['up', 'down']:
return(cursor_x >= rect.x and cursor_x <= (rect.x + rect.width))
else:
raise ValueError('Unknown direction: %s' % direction)
def enumerate_descendants(parent):
"""Walk all our children and build up a list of containers and
terminals"""
# FIXME: Does having to import this here mean we should move this function
# back to Container?
from .factory import Factory
containerstmp = []
containers = []
terminals = []
maker = Factory()
if parent is None:
err('no parent widget specified')
return
for descendant in parent.get_children():
if maker.isinstance(descendant, 'Container'):
containerstmp.append(descendant)
elif maker.isinstance(descendant, 'Terminal'):
terminals.append(descendant)
while len(containerstmp) > 0:
child = containerstmp.pop(0)
for descendant in child.get_children():
if maker.isinstance(descendant, 'Container'):
containerstmp.append(descendant)
elif maker.isinstance(descendant, 'Terminal'):
terminals.append(descendant)
containers.append(child)
dbg('%d containers and %d terminals fall beneath %s' % (len(containers),
len(terminals), parent))
return(containers, terminals)
def make_uuid(str_uuid=None):
"""Generate a UUID for an object"""
if str_uuid:
return uuid.UUID(str_uuid)
return uuid.uuid4()
def inject_uuid(target):
"""Inject a UUID into an existing object"""
uuid = make_uuid()
if not hasattr(target, "uuid") or target.uuid == None:
dbg("Injecting UUID %s into: %s" % (uuid, target))
target.uuid = uuid
else:
dbg("Object already has a UUID: %s" % target)
def spawn_new_terminator(cwd, args):
"""Start a new terminator instance with the given arguments"""
cmd = sys.argv[0]
if not os.path.isabs(cmd):
# Command is not an absolute path. Figure out where we are
cmd = os.path.join (cwd, sys.argv[0])
if not os.path.isfile(cmd):
# we weren't started as ./terminator in a path. Give up
err('Unable to locate Terminator')
return False
dbg("Spawning: %s" % cmd)
subprocess.Popen([cmd]+args)
def display_manager():
"""Try to detect which display manager we run under"""
if os.environ.get('WAYLAND_DISPLAY'):
return 'WAYLAND'
# Fallback assumption of X11
return 'X11'
def update_config_to_cell_height(filename):
'''Replace line_height with cell_height in Terminator
config file (usually ~/.config/terminator/config on
Unix-like systems).'''
dbg('update_config_to_cell_height() config filename %s' % filename)
try:
with open(filename, 'r+') as file:
config_text = file.read()
if not 'line_height' in config_text:
#
# It is either a new config, or it is already using the
# new cell_height property instead the old line_height.
#
dbg('No line_height found in %s.' % filename)
file.close()
return
updated_config_text = config_text.replace('line_height', 'cell_height')
file.seek(0)
file.write(updated_config_text)
file.truncate()
file.close()
dbg('Updted line_height to cell_height.')
except Exception as ex:
err('Unable to open %s for reading and/or writting.\n(%s)'
% (filename, ex))