1# Terminator.util - misc utility functions
2# Copyright (C) 2006-2010  cmsj@tenshu.net
3#
4#  This program is free software; you can redistribute it and/or modify
5#  it under the terms of the GNU General Public License as published by
6#  the Free Software Foundation, version 2 only.
7#
8#  This program is distributed in the hope that it will be useful,
9#  but WITHOUT ANY WARRANTY; without even the implied warranty of
10#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11#  GNU General Public License for more details.
12#
13#  You should have received a copy of the GNU General Public License
14#  along with this program; if not, write to the Free Software
15#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16"""Terminator.util - misc utility functions"""
17
18from __future__ import print_function
19
20import sys
21import cairo
22import os
23import pwd
24import inspect
25import uuid
26import subprocess
27import gi
28
29
30try:
31    gi.require_version('Gtk','3.0')
32    from gi.repository import Gtk, Gdk
33except ImportError:
34    print('You need Gtk 3.0+ to run Remotinator.')
35    sys.exit(1)
36
37# set this to true to enable debugging output
38DEBUG = False
39# set this to true to additionally list filenames in debugging
40DEBUGFILES = False
41# list of classes to show debugging for. empty list means show all classes
42DEBUGCLASSES = []
43# list of methods to show debugging for. empty list means show all methods
44DEBUGMETHODS = []
45
46def dbg(log = ""):
47    """Print a message if debugging is enabled"""
48    if DEBUG:
49        stackitem = inspect.stack()[1]
50        parent_frame = stackitem[0]
51        method = parent_frame.f_code.co_name
52        names, varargs, keywords, local_vars = inspect.getargvalues(parent_frame)
53        try:
54            self_name = names[0]
55            classname = local_vars[self_name].__class__.__name__
56        except IndexError:
57            classname = "noclass"
58        if DEBUGFILES:
59            line = stackitem[2]
60            filename = parent_frame.f_code.co_filename
61            extra = " (%s:%s)" % (filename, line)
62        else:
63            extra = ""
64        if DEBUGCLASSES != [] and classname not in DEBUGCLASSES:
65            return
66        if DEBUGMETHODS != [] and method not in DEBUGMETHODS:
67            return
68        try:
69            print("%s::%s: %s%s" % (classname, method, log, extra), file=sys.stderr)
70        except IOError:
71            pass
72
73def err(log = ""):
74    """Print an error message"""
75    try:
76        print(log, file=sys.stderr)
77    except IOError:
78        pass
79
80def gerr(message = None):
81    """Display a graphical error. This should only be used for serious
82    errors as it will halt execution"""
83
84    dialog = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL,
85            Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, message)
86    dialog.run()
87    dialog.destroy()
88
89def has_ancestor(widget, wtype):
90    """Walk up the family tree of widget to see if any ancestors are of type"""
91    while widget:
92        widget = widget.get_parent()
93        if isinstance(widget, wtype):
94            return(True)
95    return(False)
96
97def manual_lookup():
98    '''Choose the manual to open based on LANGUAGE'''
99    available_languages = ['en']
100    base_url = 'http://gnome-terminator.readthedocs.io/%s/latest/'
101    target = 'en'   # default to English
102    if 'LANGUAGE' in os.environ:
103        languages = os.environ['LANGUAGE'].split(':')
104        for language in languages:
105            if language in available_languages:
106                target = language
107                break
108
109    return base_url % target
110
111def path_lookup(command):
112    '''Find a command in our path'''
113    if os.path.isabs(command):
114        if os.path.isfile(command):
115            return(command)
116        else:
117            return(None)
118    elif command[:2] == './' and os.path.isfile(command):
119        dbg('path_lookup: Relative filename %s found in cwd' % command)
120        return(command)
121
122    try:
123        paths = os.environ['PATH'].split(':')
124        if len(paths[0]) == 0:
125            raise(ValueError)
126    except (ValueError, NameError):
127        dbg('path_lookup: PATH not set in environment, using fallbacks')
128        paths = ['/usr/local/bin', '/usr/bin', '/bin']
129
130    dbg('path_lookup: Using %d paths: %s' % (len(paths), paths))
131
132    for path in paths:
133        target = os.path.join(path, command)
134        if os.path.isfile(target):
135            dbg('path_lookup: found %s' % target)
136            return(target)
137
138    dbg('path_lookup: Unable to locate %s' % command)
139
140def shell_lookup():
141    """Find an appropriate shell for the user"""
142    try:
143        usershell = pwd.getpwuid(os.getuid())[6]
144    except KeyError:
145        usershell = None
146    shells = [usershell, 'bash', 'zsh', 'tcsh', 'ksh', 'csh', 'sh']
147
148    for shell in shells:
149        if shell is None:
150            continue
151        elif os.path.isfile(shell):
152            return(shell)
153        else:
154            rshell = path_lookup(shell)
155            if rshell is not None:
156                dbg('shell_lookup: Found %s at %s' % (shell, rshell))
157                return(rshell)
158    dbg('shell_lookup: Unable to locate a shell')
159
160def widget_pixbuf(widget, maxsize=None):
161    """Generate a pixbuf of a widget"""
162    # FIXME: Can this be changed from using "import cairo" to "from gi.repository import cairo"?
163    window = widget.get_window()
164    width, height = window.get_width(), window.get_height()
165
166    longest = max(width, height)
167
168    if maxsize is not None:
169        factor = float(maxsize) / float(longest)
170
171    if not maxsize or (width * factor) > width or (height * factor) > height:
172        factor = 1
173
174    preview_width, preview_height = int(width * factor), int(height * factor)
175
176    preview_surface = Gdk.Window.create_similar_surface(window,
177        cairo.CONTENT_COLOR, preview_width, preview_height)
178
179    cairo_context = cairo.Context(preview_surface)
180    cairo_context.scale(factor, factor)
181    Gdk.cairo_set_source_window(cairo_context, window, 0, 0)
182    cairo_context.paint()
183
184    scaledpixbuf = Gdk.pixbuf_get_from_surface(preview_surface, 0, 0, preview_width, preview_height);
185
186    return(scaledpixbuf)
187
188def get_system_config_dir():
189    system_config_dir = '/etc/xdg'
190    if 'XDG_CONFIG_DIRS' in os.environ.keys():
191        for sysconfdir in os.environ['XDG_CONFIG_DIRS'].split(":"):
192                if os.path.isdir(sysconfdir):
193                    system_config_dir = sysconfdir
194                    break
195    return(os.path.join(system_config_dir,'terminator'))
196
197def get_config_dir():
198    """Expand all the messy nonsense for finding where ~/.config/terminator
199    really is"""
200    try:
201        configdir = os.environ['XDG_CONFIG_HOME']
202    except KeyError:
203        configdir = os.path.join(os.path.expanduser('~'), '.config')
204
205    dbg('Found config dir: %s' % configdir)
206    return(os.path.join(configdir, 'terminator'))
207
208def dict_diff(reference, working):
209    """Examine the values in the supplied working set and return a new dict
210    that only contains those values which are different from those in the
211    reference dictionary
212
213    >>> a = {'foo': 'bar', 'baz': 'bjonk'}
214    >>> b = {'foo': 'far', 'baz': 'bjonk'}
215    >>> dict_diff(a, b)
216    {'foo': 'far'}
217    """
218
219    result = {}
220
221    for key in reference:
222        if reference[key] != working[key]:
223            result[key] = working[key]
224
225    return(result)
226
227# Helper functions for directional navigation
228def get_edge(allocation, direction):
229    """Return the edge of the supplied allocation that we will care about for
230    directional navigation"""
231    if direction == 'left':
232        edge = allocation.x
233        p1, p2 = allocation.y, allocation.y + allocation.height
234    elif direction == 'up':
235        edge = allocation.y
236        p1, p2 = allocation.x, allocation.x + allocation.width
237    elif direction == 'right':
238        edge = allocation.x + allocation.width
239        p1, p2 = allocation.y, allocation.y + allocation.height
240    elif direction == 'down':
241        edge = allocation.y + allocation.height
242        p1, p2 = allocation.x, allocation.x + allocation.width
243    else:
244        raise ValueError('unknown direction %s' % direction)
245
246    return(edge, p1, p2)
247
248def get_nav_possible(edge, allocation, direction, p1, p2):
249    """Check if the supplied allocation is in the right direction of the
250    supplied edge"""
251    x1, x2 = allocation.x, allocation.x + allocation.width
252    y1, y2 = allocation.y, allocation.y + allocation.height
253    if direction == 'left':
254        return(x2 <= edge and y1 <= p2 and y2 >= p1)
255    elif direction == 'right':
256        return(x1 >= edge and y1 <= p2 and y2 >= p1)
257    elif direction == 'up':
258        return(y2 <= edge and x1 <= p2 and x2 >= p1)
259    elif direction == 'down':
260        return(y1 >= edge and x1 <= p2 and x2 >= p1)
261    else:
262        raise ValueError('Unknown direction: %s' % direction)
263
264def get_nav_offset(edge, allocation, direction):
265    """Work out how far edge is from a particular point on the allocation
266    rectangle, in the given direction"""
267    if direction == 'left':
268        return(edge - (allocation.x + allocation.width))
269    elif direction == 'right':
270        return(allocation.x - edge)
271    elif direction == 'up':
272        return(edge - (allocation.y + allocation.height))
273    elif direction == 'down':
274        return(allocation.y - edge)
275    else:
276        raise ValueError('Unknown direction: %s' % direction)
277
278def get_nav_tiebreak(direction, cursor_x, cursor_y, rect):
279    """We have multiple candidate terminals. Pick the closest by cursor
280    position"""
281    if direction in ['left', 'right']:
282        return(cursor_y >= rect.y and cursor_y <= (rect.y + rect.height))
283    elif direction in ['up', 'down']:
284        return(cursor_x >= rect.x and cursor_x <= (rect.x + rect.width))
285    else:
286        raise ValueError('Unknown direction: %s' % direction)
287
288def enumerate_descendants(parent):
289    """Walk all our children and build up a list of containers and
290    terminals"""
291    # FIXME: Does having to import this here mean we should move this function
292    # back to Container?
293    from .factory import Factory
294
295    containerstmp = []
296    containers = []
297    terminals = []
298    maker = Factory()
299
300    if parent is None:
301        err('no parent widget specified')
302        return
303
304    for descendant in parent.get_children():
305        if maker.isinstance(descendant, 'Container'):
306            containerstmp.append(descendant)
307        elif maker.isinstance(descendant, 'Terminal'):
308            terminals.append(descendant)
309
310        while len(containerstmp) > 0:
311            child = containerstmp.pop(0)
312            for descendant in child.get_children():
313                if maker.isinstance(descendant, 'Container'):
314                    containerstmp.append(descendant)
315                elif maker.isinstance(descendant, 'Terminal'):
316                    terminals.append(descendant)
317            containers.append(child)
318
319    dbg('%d containers and %d terminals fall beneath %s' % (len(containers),
320        len(terminals), parent))
321    return(containers, terminals)
322
323def make_uuid(str_uuid=None):
324    """Generate a UUID for an object"""
325    if str_uuid:
326        return uuid.UUID(str_uuid)
327    return uuid.uuid4()
328
329def inject_uuid(target):
330    """Inject a UUID into an existing object"""
331    uuid = make_uuid()
332    if not hasattr(target, "uuid") or target.uuid == None:
333        dbg("Injecting UUID %s into: %s" % (uuid, target))
334        target.uuid = uuid
335    else:
336        dbg("Object already has a UUID: %s" % target)
337
338def spawn_new_terminator(cwd, args):
339    """Start a new terminator instance with the given arguments"""
340    cmd = sys.argv[0]
341
342    if not os.path.isabs(cmd):
343        # Command is not an absolute path. Figure out where we are
344        cmd = os.path.join (cwd, sys.argv[0])
345        if not os.path.isfile(cmd):
346            # we weren't started as ./terminator in a path. Give up
347            err('Unable to locate Terminator')
348            return False
349
350    dbg("Spawning: %s" % cmd)
351    subprocess.Popen([cmd]+args)
352
353def display_manager():
354    """Try to detect which display manager we run under"""
355    if os.environ.get('WAYLAND_DISPLAY'):
356        return 'WAYLAND'
357    # Fallback assumption of X11
358    return 'X11'
359