terminator/terminatorlib/util.py

426 lines
14 KiB
Python
Raw Normal View History

# Terminator.util - misc utility functions
# Copyright (C) 2006-2010 cmsj@tenshu.net
2009-08-07 09:21:37 +00:00
#
# 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"""
2009-08-07 09:21:37 +00:00
2020-04-17 15:53:38 +00:00
from __future__ import print_function
2009-08-07 09:21:37 +00:00
import sys
import cairo
import os
import pwd
import inspect
import uuid
import subprocess
import gi
2020-04-17 15:53:38 +00:00
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)
2009-08-07 09:21:37 +00:00
# set this to true to enable debugging output
2010-01-11 10:10:19 +00:00
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 = []
2009-08-07 09:21:37 +00:00
2022-01-31 19:10:10 +00:00
def is_flatpak():
return os.path.exists("/.flatpak-info")
def dbg(log = ""):
2009-08-09 23:10:08 +00:00
"""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
2009-08-07 09:21:37 +00:00
def err(log = ""):
2009-08-09 23:10:08 +00:00
"""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"""
2014-09-19 14:08:08 +00:00
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']
2020-11-09 14:46:07 +00:00
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:
2021-12-01 16:01:39 +00:00
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']
2010-01-18 22:56:43 +00:00
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"""
2022-01-31 19:10:10 +00:00
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()
2022-08-15 06:33:01 +00:00
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))
2022-01-31 19:10:10 +00:00
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)
]
# 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]
2022-01-31 19:59:34 +00:00
flatpak_args = flatpak_spawn + env_args + args
2022-01-31 19:10:10 +00:00
dbg('returned flatpak args: %s' % flatpak_args)
return flatpak_args