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