# 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 is_flatpak(): return os.path.exists("/.flatpak-info") 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""" if is_flatpak(): getent = subprocess.check_output([ 'flatpak-spawn', '--host', 'getent', 'passwd', pwd.getpwuid(os.getuid())[0] ]).decode(encoding='UTF-8').rstrip('\n') shell = getent.split(':')[6] return shell 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 writing.\n(%s)' % (filename, ex)) def get_flatpak_args(args, envv, cwd): """Construct args to be executed via flatpak-spawn""" flatpak_args = None env_args = ['--env={}'.format(env) for env in envv] flatpak_spawn = [ "flatpak-spawn", "--host", "--watch-bus", "--forward-fd=1", "--forward-fd=2", "--directory={}".format(cwd) ] # Detect and remove duplicate shell in args # to work around vte.spawn_sync() requirement. if len(set([args[0], args[1]])) == 1: del args[0] flatpak_args = flatpak_spawn + env_args + args dbg('returned flatpak args: %s' % flatpak_args) return flatpak_args