421 lines
14 KiB
Python
421 lines
14 KiB
Python
# 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 writting.\n(%s)'
|
||
% (filename, ex))
|
||
|
||
def get_flatpak_args(args, envv, cwd):
|
||
"""Contruct 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)
|
||
]
|
||
flatpak_args = flatpak_spawn + env_args + args
|
||
|
||
dbg('returned flatpak args: %s' % flatpak_args)
|
||
|
||
return flatpak_args
|