1# Terminator by Chris Jones <cmsj@tenshu.net> 2# GPL v2 only 3"""terminal.py - classes necessary to provide Terminal widgets""" 4 5 6import os 7import signal 8import gi 9from gi.repository import GLib, GObject, Pango, Gtk, Gdk, GdkPixbuf 10gi.require_version('Vte', '2.91') # vte-0.38 (gnome-3.14) 11from gi.repository import Vte 12import subprocess 13try: 14 from urllib.parse import unquote as urlunquote 15except ImportError: 16 from urllib import unquote as urlunquote 17 18from .util import dbg, err, spawn_new_terminator, make_uuid, manual_lookup, display_manager 19from . import util 20from .config import Config 21from .cwd import get_pid_cwd 22from .factory import Factory 23from .terminator import Terminator 24from .titlebar import Titlebar 25from .terminal_popup_menu import TerminalPopupMenu 26from .prefseditor import PrefsEditor 27from .searchbar import Searchbar 28from .translation import _ 29from .signalman import Signalman 30from . import plugin 31from terminatorlib.layoutlauncher import LayoutLauncher 32from . import regex 33 34# pylint: disable-msg=R0904 35class Terminal(Gtk.VBox): 36 """Class implementing the VTE widget and its wrappings""" 37 38 __gsignals__ = { 39 'close-term': (GObject.SignalFlags.RUN_LAST, None, ()), 40 'title-change': (GObject.SignalFlags.RUN_LAST, None, 41 (GObject.TYPE_STRING,)), 42 'enumerate': (GObject.SignalFlags.RUN_LAST, None, 43 (GObject.TYPE_INT,)), 44 'group-tab': (GObject.SignalFlags.RUN_LAST, None, ()), 45 'group-tab-toggle': (GObject.SignalFlags.RUN_LAST, None, ()), 46 'ungroup-tab': (GObject.SignalFlags.RUN_LAST, None, ()), 47 'ungroup-all': (GObject.SignalFlags.RUN_LAST, None, ()), 48 'split-horiz': (GObject.SignalFlags.RUN_LAST, None, 49 (GObject.TYPE_STRING,)), 50 'split-vert': (GObject.SignalFlags.RUN_LAST, None, 51 (GObject.TYPE_STRING,)), 52 'rotate-cw': (GObject.SignalFlags.RUN_LAST, None, ()), 53 'rotate-ccw': (GObject.SignalFlags.RUN_LAST, None, ()), 54 'tab-new': (GObject.SignalFlags.RUN_LAST, None, 55 (GObject.TYPE_BOOLEAN, GObject.TYPE_OBJECT)), 56 'tab-top-new': (GObject.SignalFlags.RUN_LAST, None, ()), 57 'focus-in': (GObject.SignalFlags.RUN_LAST, None, ()), 58 'focus-out': (GObject.SignalFlags.RUN_LAST, None, ()), 59 'zoom': (GObject.SignalFlags.RUN_LAST, None, ()), 60 'maximise': (GObject.SignalFlags.RUN_LAST, None, ()), 61 'unzoom': (GObject.SignalFlags.RUN_LAST, None, ()), 62 'resize-term': (GObject.SignalFlags.RUN_LAST, None, 63 (GObject.TYPE_STRING,)), 64 'navigate': (GObject.SignalFlags.RUN_LAST, None, 65 (GObject.TYPE_STRING,)), 66 'tab-change': (GObject.SignalFlags.RUN_LAST, None, 67 (GObject.TYPE_INT,)), 68 'group-all': (GObject.SignalFlags.RUN_LAST, None, ()), 69 'group-all-toggle': (GObject.SignalFlags.RUN_LAST, None, ()), 70 'move-tab': (GObject.SignalFlags.RUN_LAST, None, 71 (GObject.TYPE_STRING,)), 72 } 73 74 TARGET_TYPE_VTE = 8 75 TARGET_TYPE_MOZ = 9 76 77 MOUSEBUTTON_LEFT = 1 78 MOUSEBUTTON_MIDDLE = 2 79 MOUSEBUTTON_RIGHT = 3 80 81 terminator = None 82 vte = None 83 terminalbox = None 84 scrollbar = None 85 titlebar = None 86 searchbar = None 87 88 group = None 89 cwd = None 90 origcwd = None 91 command = None 92 clipboard = None 93 pid = None 94 95 matches = None 96 regex_flags = None 97 config = None 98 default_encoding = None 99 custom_encoding = None 100 custom_font_size = None 101 layout_command = None 102 relaunch_command = None 103 directory = None 104 105 is_held_open = False 106 107 fgcolor_active = None 108 fgcolor_inactive = None 109 bgcolor = None 110 palette_active = None 111 palette_inactive = None 112 113 composite_support = None 114 115 cnxids = None 116 targets_for_new_group = None 117 118 def __init__(self): 119 """Class initialiser""" 120 GObject.GObject.__init__(self) 121 122 self.terminator = Terminator() 123 self.terminator.register_terminal(self) 124 125 # FIXME: Surely these should happen in Terminator::register_terminal()? 126 self.connect('enumerate', self.terminator.do_enumerate) 127 self.connect('focus-in', self.terminator.focus_changed) 128 self.connect('focus-out', self.terminator.focus_left) 129 130 self.matches = {} 131 self.cnxids = Signalman() 132 133 self.config = Config() 134 135 self.cwd = get_pid_cwd() 136 self.origcwd = self.terminator.origcwd 137 self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) 138 139 self.pending_on_vte_size_allocate = False 140 141 self.vte = Vte.Terminal() 142 self.background_image = None 143 if self.config['background_image'] != '': 144 try: 145 self.background_image = GdkPixbuf.Pixbuf.new_from_file(self.config['background_image']) 146 self.vte.set_clear_background(False) 147 self.vte.connect("draw",self.background_draw) 148 except Exception as e: 149 self.background_image = None 150 self.vte.set_clear_background(True) 151 err('error loading background image: %s' % e) 152 153 self.background_alpha = self.config['background_darkness'] 154 self.vte.set_allow_hyperlink(True) 155 self.vte._draw_data = None 156 if not hasattr(self.vte, "set_opacity") or \ 157 not hasattr(self.vte, "is_composited"): 158 self.composite_support = False 159 else: 160 self.composite_support = True 161 dbg('composite_support: %s' % self.composite_support) 162 163 164 self.vte.show() 165 self.default_encoding = self.vte.get_encoding() 166 self.update_url_matches() 167 168 self.terminalbox = self.create_terminalbox() 169 170 self.titlebar = Titlebar(self) 171 self.titlebar.connect_icon(self.on_group_button_press) 172 self.titlebar.connect('edit-done', self.on_edit_done) 173 self.connect('title-change', self.titlebar.set_terminal_title) 174 self.titlebar.connect('create-group', self.really_create_group) 175 self.titlebar.show_all() 176 177 self.searchbar = Searchbar() 178 self.searchbar.connect('end-search', self.on_search_done) 179 180 self.show() 181 if self.config['title_at_bottom']: 182 self.pack_start(self.terminalbox, True, True, 0) 183 self.pack_start(self.titlebar, False, True, 0) 184 else: 185 self.pack_start(self.titlebar, False, True, 0) 186 self.pack_start(self.terminalbox, True, True, 0) 187 188 self.pack_end(self.searchbar, True, True, 0) 189 190 self.connect_signals() 191 192 os.putenv('TERM', self.config['term']) 193 os.putenv('COLORTERM', self.config['colorterm']) 194 195 env_proxy = os.getenv('http_proxy') 196 if not env_proxy: 197 if self.config['http_proxy'] and self.config['http_proxy'] != '': 198 os.putenv('http_proxy', self.config['http_proxy']) 199 self.reconfigure() 200 self.vte.set_size(80, 24) 201 202 def get_vte(self): 203 """This simply returns the vte widget we are using""" 204 return(self.vte) 205 206 def force_set_profile(self, widget, profile): 207 """Forcibly set our profile""" 208 self.set_profile(widget, profile, True) 209 210 def set_profile(self, _widget, profile, force=False): 211 """Set our profile""" 212 if profile != self.config.get_profile(): 213 self.config.set_profile(profile, force) 214 self.reconfigure() 215 216 def get_profile(self): 217 """Return our profile name""" 218 return(self.config.profile) 219 220 def switch_to_next_profile(self): 221 profilelist = self.config.list_profiles() 222 list_length = len(profilelist) 223 224 if list_length > 1: 225 if profilelist.index(self.get_profile()) + 1 == list_length: 226 self.force_set_profile(False, profilelist[0]) 227 else: 228 self.force_set_profile(False, profilelist[profilelist.index(self.get_profile()) + 1]) 229 230 def switch_to_previous_profile(self): 231 profilelist = self.config.list_profiles() 232 list_length = len(profilelist) 233 234 if list_length > 1: 235 if profilelist.index(self.get_profile()) == 0: 236 self.force_set_profile(False, profilelist[list_length - 1]) 237 else: 238 self.force_set_profile(False, profilelist[profilelist.index(self.get_profile()) - 1]) 239 240 def get_cwd(self): 241 """Return our cwd""" 242 vte_cwd = self.vte.get_current_directory_uri() 243 if vte_cwd: 244 # OSC7 pwd gives an answer 245 return(GLib.filename_from_uri(vte_cwd)[0]) 246 else: 247 # Fall back to old gtk2 method 248 dbg('calling get_pid_cwd') 249 return(get_pid_cwd(self.pid)) 250 251 def close(self): 252 """Close ourselves""" 253 dbg('close: called') 254 self.cnxids.remove_widget(self.vte) 255 self.emit('close-term') 256 if self.pid is not None: 257 try: 258 dbg('close: killing %d' % self.pid) 259 os.kill(self.pid, signal.SIGHUP) 260 except Exception as ex: 261 # We really don't want to care if this failed. Deep OS voodoo is 262 # not what we should be doing. 263 dbg('os.kill failed: %s' % ex) 264 pass 265 266 if self.vte: 267 self.terminalbox.remove(self.vte) 268 del(self.vte) 269 270 def create_terminalbox(self): 271 """Create a GtkHBox containing the terminal and a scrollbar""" 272 273 terminalbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) 274 self.scrollbar = Gtk.Scrollbar.new(Gtk.Orientation.VERTICAL, adjustment=self.vte.get_vadjustment()) 275 self.scrollbar.set_no_show_all(True) 276 277 terminalbox.pack_start(self.vte, True, True, 0) 278 terminalbox.pack_start(self.scrollbar, False, True, 0) 279 terminalbox.show_all() 280 281 return(terminalbox) 282 283 def _add_regex(self, name, re): 284 match = -1 285 if regex.FLAGS_PCRE2: 286 try: 287 reg = Vte.Regex.new_for_match(re, len(re), self.regex_flags or regex.FLAGS_PCRE2) 288 match = self.vte.match_add_regex(reg, 0) 289 except GLib.Error: 290 # happens when PCRE2 support is not builtin (Ubuntu < 19.10) 291 pass 292 293 # try the "old" glib regex 294 if match < 0: 295 reg = GLib.Regex.new(re, self.regex_flags or regex.FLAGS_GLIB, 0) 296 match = self.vte.match_add_gregex(reg, 0) 297 298 self.matches[name] = match 299 self.vte.match_set_cursor_name(self.matches[name], 'pointer') 300 301 def update_url_matches(self): 302 """Update the regexps used to match URLs""" 303 userchars = "-A-Za-z0-9" 304 passchars = "-A-Za-z0-9,?;.:/!%$^*&~\"#'" 305 hostchars = "-A-Za-z0-9:\[\]" 306 pathchars = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%'" 307 schemes = "(news:|telnet:|nntp:|file:/|https?:|ftps?:|webcal:|ssh:)" 308 user = "[" + userchars + "]+(:[" + passchars + "]+)?" 309 urlpath = "/[" + pathchars + "]*[^]'.}>) \t\r\n,\\\"]" 310 311 lboundry = "\\b" 312 rboundry = "\\b" 313 314 re = (lboundry + schemes + 315 "//(" + user + "@)?[" + hostchars +".]+(:[0-9]+)?(" + 316 urlpath + ")?" + rboundry + "/?") 317 self._add_regex('full_uri', re) 318 319 if self.matches['full_uri'] == -1: 320 err ('Terminal::update_url_matches: Failed adding URL matches') 321 else: 322 re = (lboundry + 323 '(callto:|h323:|sip:)' + "[" + userchars + "+][" + 324 userchars + ".]*(:[0-9]+)?@?[" + pathchars + "]+" + 325 rboundry) 326 self._add_regex('voip', re) 327 328 re = (lboundry + 329 "(www|ftp)[" + hostchars + "]*\.[" + hostchars + 330 ".]+(:[0-9]+)?(" + urlpath + ")?" + rboundry + "/?") 331 self._add_regex('addr_only', re) 332 333 re = (lboundry + 334 "(mailto:)?[a-zA-Z0-9][a-zA-Z0-9.+-]*@[a-zA-Z0-9]" + 335 "[a-zA-Z0-9-]*\.[a-zA-Z0-9][a-zA-Z0-9-]+" + 336 "[.a-zA-Z0-9-]*" + rboundry) 337 self._add_regex('email', re) 338 339 re = (lboundry + 340 """news:[-A-Z\^_a-z{|}~!"#$%&'()*+,./0-9;:=?`]+@""" + 341 "[-A-Za-z0-9.]+(:[0-9]+)?" + rboundry) 342 self._add_regex('nntp', re) 343 344 # Now add any matches from plugins 345 try: 346 registry = plugin.PluginRegistry() 347 registry.load_plugins() 348 plugins = registry.get_plugins_by_capability('url_handler') 349 350 for urlplugin in plugins: 351 name = urlplugin.handler_name 352 match = urlplugin.match 353 if name in self.matches: 354 dbg('refusing to add duplicate match %s' % name) 355 continue 356 357 self._add_regex(name, match) 358 359 dbg('added plugin URL handler for %s (%s) as %d' % 360 (name, urlplugin.__class__.__name__, 361 self.matches[name])) 362 except Exception as ex: 363 err('Exception occurred adding plugin URL match: %s' % ex) 364 365 def match_add(self, name, match): 366 """Register a URL match""" 367 if name in self.matches: 368 err('Terminal::match_add: Refusing to create duplicate match %s' % name) 369 return 370 371 self._add_regex(name, match) 372 373 def match_remove(self, name): 374 """Remove a previously registered URL match""" 375 if name not in self.matches: 376 err('Terminal::match_remove: Unable to remove non-existent match %s' % name) 377 return 378 self.vte.match_remove(self.matches[name]) 379 del(self.matches[name]) 380 381 def maybe_copy_clipboard(self): 382 if self.config['copy_on_selection'] and self.vte.get_has_selection(): 383 self.vte.copy_clipboard() 384 385 def connect_signals(self): 386 """Connect all the gtk signals and drag-n-drop mechanics""" 387 388 self.scrollbar.connect('button-press-event', self.on_buttonpress) 389 390 self.cnxids.new(self.vte, 'key-press-event', self.on_keypress) 391 self.cnxids.new(self.vte, 'button-press-event', self.on_buttonpress) 392 self.cnxids.new(self.vte, 'scroll-event', self.on_mousewheel) 393 self.cnxids.new(self.vte, 'popup-menu', self.popup_menu) 394 395 srcvtetargets = [("vte", Gtk.TargetFlags.SAME_APP, self.TARGET_TYPE_VTE)] 396 dsttargets = [("vte", Gtk.TargetFlags.SAME_APP, self.TARGET_TYPE_VTE), 397 ('text/x-moz-url', 0, self.TARGET_TYPE_MOZ), 398 ('_NETSCAPE_URL', 0, 0)] 399 ''' 400 The following should work, but on my system it corrupts the returned 401 TargetEntry's in the newdstargets with binary crap, causing "Segmentation 402 fault (core dumped)" when the later drag_dest_set gets called. 403 404 dsttargetlist = Gtk.TargetList.new([]) 405 dsttargetlist.add_text_targets(0) 406 dsttargetlist.add_uri_targets(0) 407 dsttargetlist.add_table(dsttargets) 408 409 newdsttargets = Gtk.target_table_new_from_list(dsttargetlist) 410 ''' 411 # FIXME: Temporary workaround for the problems with the correct way of doing things 412 dsttargets.extend([('text/plain', 0, 0), 413 ('text/plain;charset=utf-8', 0, 0), 414 ('TEXT', 0, 0), 415 ('STRING', 0, 0), 416 ('UTF8_STRING', 0, 0), 417 ('COMPOUND_TEXT', 0, 0), 418 ('text/uri-list', 0, 0)]) 419 # Convert to target entries 420 srcvtetargets = [Gtk.TargetEntry.new(*tgt) for tgt in srcvtetargets] 421 dsttargets = [Gtk.TargetEntry.new(*tgt) for tgt in dsttargets] 422 423 dbg('Finalised drag targets: %s' % dsttargets) 424 425 for (widget, mask) in [ 426 (self.vte, Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.BUTTON3_MASK), 427 (self.titlebar, Gdk.ModifierType.BUTTON1_MASK)]: 428 widget.drag_source_set(mask, srcvtetargets, Gdk.DragAction.MOVE) 429 430 self.vte.drag_dest_set(Gtk.DestDefaults.MOTION | 431 Gtk.DestDefaults.HIGHLIGHT | Gtk.DestDefaults.DROP, 432 dsttargets, Gdk.DragAction.COPY | Gdk.DragAction.MOVE) 433 434 for widget in [self.vte, self.titlebar]: 435 self.cnxids.new(widget, 'drag-begin', self.on_drag_begin, self) 436 self.cnxids.new(widget, 'drag-data-get', self.on_drag_data_get, 437 self) 438 439 self.cnxids.new(self.vte, 'drag-motion', self.on_drag_motion, self) 440 self.cnxids.new(self.vte, 'drag-data-received', 441 self.on_drag_data_received, self) 442 443 self.cnxids.new(self.vte, 'selection-changed', 444 lambda widget: self.maybe_copy_clipboard()) 445 446 if self.composite_support: 447 self.cnxids.new(self.vte, 'composited-changed', self.reconfigure) 448 449 self.cnxids.new(self.vte, 'window-title-changed', lambda x: 450 self.emit('title-change', self.get_window_title())) 451 self.cnxids.new(self.vte, 'grab-focus', self.on_vte_focus) 452 self.cnxids.new(self.vte, 'focus-in-event', self.on_vte_focus_in) 453 self.cnxids.new(self.vte, 'focus-out-event', self.on_vte_focus_out) 454 self.cnxids.new(self.vte, 'size-allocate', self.deferred_on_vte_size_allocate) 455 456 self.vte.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK) 457 self.cnxids.new(self.vte, 'enter_notify_event', 458 self.on_vte_notify_enter) 459 460 self.cnxids.new(self.vte, 'realize', self.reconfigure) 461 462 def create_popup_group_menu(self, widget, event = None): 463 """Pop up a menu for the group widget""" 464 if event: 465 button = event.button 466 time = event.time 467 else: 468 button = 0 469 time = 0 470 471 menu = self.populate_group_menu() 472 menu.show_all() 473 menu.popup_at_widget(widget,Gdk.Gravity.SOUTH_WEST,Gdk.Gravity.NORTH_WEST,None) 474 return(True) 475 476 def populate_group_menu(self): 477 """Fill out a group menu""" 478 menu = Gtk.Menu() 479 self.group_menu = menu 480 groupitems = [] 481 482 item = Gtk.MenuItem.new_with_mnemonic(_('N_ew group...')) 483 item.connect('activate', self.create_group) 484 menu.append(item) 485 486 if len(self.terminator.groups) > 0: 487 cnxs = [] 488 item = Gtk.RadioMenuItem.new_with_mnemonic(groupitems, _('_None')) 489 groupitems = item.get_group() 490 item.set_active(self.group == None) 491 cnxs.append([item, 'toggled', self.set_group, None]) 492 menu.append(item) 493 494 for group in self.terminator.groups: 495 item = Gtk.RadioMenuItem.new_with_label(groupitems, group) 496 groupitems = item.get_group() 497 item.set_active(self.group == group) 498 cnxs.append([item, 'toggled', self.set_group, group]) 499 menu.append(item) 500 501 for cnx in cnxs: 502 cnx[0].connect(cnx[1], cnx[2], cnx[3]) 503 504 if self.group != None or len(self.terminator.groups) > 0: 505 menu.append(Gtk.SeparatorMenuItem()) 506 507 if self.group != None: 508 item = Gtk.MenuItem(_('Remove group %s') % self.group) 509 item.connect('activate', self.ungroup, self.group) 510 menu.append(item) 511 512 if util.has_ancestor(self, Gtk.Notebook): 513 item = Gtk.MenuItem.new_with_mnemonic(_('G_roup all in tab')) 514 item.connect('activate', lambda x: self.emit('group_tab')) 515 menu.append(item) 516 517 if len(self.terminator.groups) > 0: 518 item = Gtk.MenuItem.new_with_mnemonic(_('Ungro_up all in tab')) 519 item.connect('activate', lambda x: self.emit('ungroup_tab')) 520 menu.append(item) 521 522 if len(self.terminator.groups) > 0: 523 item = Gtk.MenuItem(_('Remove all groups')) 524 item.connect('activate', lambda x: self.emit('ungroup-all')) 525 menu.append(item) 526 527 if self.group != None: 528 menu.append(Gtk.SeparatorMenuItem()) 529 530 item = Gtk.MenuItem(_('Close group %s') % self.group) 531 item.connect('activate', lambda x: 532 self.terminator.closegroupedterms(self.group)) 533 menu.append(item) 534 535 menu.append(Gtk.SeparatorMenuItem()) 536 537 groupitems = [] 538 cnxs = [] 539 540 for key, value in list({_('Broadcast _all'):'all', 541 _('Broadcast _group'):'group', 542 _('Broadcast _off'):'off'}.items()): 543 item = Gtk.RadioMenuItem.new_with_mnemonic(groupitems, key) 544 groupitems = item.get_group() 545 dbg('Terminal::populate_group_menu: %s active: %s' % 546 (key, self.terminator.groupsend == 547 self.terminator.groupsend_type[value])) 548 item.set_active(self.terminator.groupsend == 549 self.terminator.groupsend_type[value]) 550 cnxs.append([item, 'activate', self.set_groupsend, self.terminator.groupsend_type[value]]) 551 menu.append(item) 552 553 for cnx in cnxs: 554 cnx[0].connect(cnx[1], cnx[2], cnx[3]) 555 556 menu.append(Gtk.SeparatorMenuItem()) 557 558 item = Gtk.CheckMenuItem.new_with_mnemonic(_('_Split to this group')) 559 item.set_active(self.config['split_to_group']) 560 item.connect('toggled', lambda x: self.do_splittogroup_toggle()) 561 menu.append(item) 562 563 item = Gtk.CheckMenuItem.new_with_mnemonic(_('Auto_clean groups')) 564 item.set_active(self.config['autoclean_groups']) 565 item.connect('toggled', lambda x: self.do_autocleangroups_toggle()) 566 menu.append(item) 567 568 menu.append(Gtk.SeparatorMenuItem()) 569 570 item = Gtk.MenuItem.new_with_mnemonic(_('_Insert terminal number')) 571 item.connect('activate', lambda x: self.emit('enumerate', False)) 572 menu.append(item) 573 574 item = Gtk.MenuItem.new_with_mnemonic(_('Insert _padded terminal number')) 575 item.connect('activate', lambda x: self.emit('enumerate', True)) 576 menu.append(item) 577 578 return(menu) 579 580 def set_group(self, _item, name): 581 """Set a particular group""" 582 if self.group == name: 583 # already in this group, no action needed 584 return 585 dbg('Terminal::set_group: Setting group to %s' % name) 586 self.group = name 587 self.titlebar.set_group_label(name) 588 self.terminator.group_hoover() 589 590 def create_group(self, _item): 591 """Trigger the creation of a group via the titlebar (because popup 592 windows are really lame)""" 593 self.titlebar.create_group() 594 595 def really_create_group(self, _widget, groupname): 596 """The titlebar has spoken, let a group be created""" 597 self.terminator.create_group(groupname) 598 self.set_group(None, groupname) 599 600 def ungroup(self, _widget, data): 601 """Remove a group""" 602 # FIXME: Could we emit and have Terminator do this? 603 for term in self.terminator.terminals: 604 if term.group == data: 605 term.set_group(None, None) 606 self.terminator.group_hoover() 607 608 def set_groupsend(self, _widget, value): 609 """Set the groupsend mode""" 610 # FIXME: Can we think of a smarter way of doing this than poking? 611 if value in list(self.terminator.groupsend_type.values()): 612 dbg('Terminal::set_groupsend: setting groupsend to %s' % value) 613 self.terminator.groupsend = value 614 615 def do_splittogroup_toggle(self): 616 """Toggle the splittogroup mode""" 617 self.config['split_to_group'] = not self.config['split_to_group'] 618 619 def do_autocleangroups_toggle(self): 620 """Toggle the autocleangroups mode""" 621 self.config['autoclean_groups'] = not self.config['autoclean_groups'] 622 623 def reconfigure(self, _widget=None): 624 """Reconfigure our settings""" 625 dbg('Terminal::reconfigure') 626 self.cnxids.remove_signal(self.vte, 'realize') 627 628 # Handle child command exiting 629 self.cnxids.remove_signal(self.vte, 'child-exited') 630 631 if self.config['exit_action'] == 'restart': 632 self.cnxids.new(self.vte, 'child-exited', self.spawn_child, True) 633 elif self.config['exit_action'] == 'hold': 634 self.cnxids.new(self.vte, 'child-exited', self.held_open, True) 635 elif self.config['exit_action'] in ('close', 'left'): 636 self.cnxids.new(self.vte, 'child-exited', 637 lambda x, y: self.emit('close-term')) 638 639 if self.custom_encoding != True: 640 self.vte.set_encoding(self.config['encoding']) 641 # Word char support was missing from vte 0.38, silently skip this setting 642 if hasattr(self.vte, 'set_word_char_exceptions'): 643 self.vte.set_word_char_exceptions(self.config['word_chars']) 644 self.vte.set_mouse_autohide(self.config['mouse_autohide']) 645 646 backspace = self.config['backspace_binding'] 647 delete = self.config['delete_binding'] 648 649 try: 650 if backspace == 'ascii-del': 651 backbind = Vte.ERASE_ASCII_DELETE 652 elif backspace == 'control-h': 653 backbind = Vte.ERASE_ASCII_BACKSPACE 654 elif backspace == 'escape-sequence': 655 backbind = Vte.ERASE_DELETE_SEQUENCE 656 else: 657 backbind = Vte.ERASE_AUTO 658 except AttributeError: 659 if backspace == 'ascii-del': 660 backbind = 2 661 elif backspace == 'control-h': 662 backbind = 1 663 elif backspace == 'escape-sequence': 664 backbind = 3 665 else: 666 backbind = 0 667 668 try: 669 if delete == 'ascii-del': 670 delbind = Vte.ERASE_ASCII_DELETE 671 elif delete == 'control-h': 672 delbind = Vte.ERASE_ASCII_BACKSPACE 673 elif delete == 'escape-sequence': 674 delbind = Vte.ERASE_DELETE_SEQUENCE 675 else: 676 delbind = Vte.ERASE_AUTO 677 except AttributeError: 678 if delete == 'ascii-del': 679 delbind = 2 680 elif delete == 'control-h': 681 delbind = 1 682 elif delete == 'escape-sequence': 683 delbind = 3 684 else: 685 delbind = 0 686 687 self.vte.set_backspace_binding(backbind) 688 self.vte.set_delete_binding(delbind) 689 690 if not self.custom_font_size: 691 try: 692 if self.config['use_system_font'] == True: 693 font = self.config.get_system_mono_font() 694 else: 695 font = self.config['font'] 696 self.set_font(Pango.FontDescription(font)) 697 except: 698 pass 699 self.vte.set_allow_bold(self.config['allow_bold']) 700 if hasattr(self.vte,'set_cell_height_scale'): 701 self.vte.set_cell_height_scale(self.config['line_height']) 702 if hasattr(self.vte, 'set_bold_is_bright'): 703 self.vte.set_bold_is_bright(self.config['bold_is_bright']) 704 705 if self.config['use_theme_colors']: 706 self.fgcolor_active = self.vte.get_style_context().get_color(Gtk.StateType.NORMAL) # VERIFY FOR GTK3: do these really take the theme colors? 707 self.bgcolor = self.vte.get_style_context().get_background_color(Gtk.StateType.NORMAL) 708 else: 709 self.fgcolor_active = Gdk.RGBA() 710 self.fgcolor_active.parse(self.config['foreground_color']) 711 self.bgcolor = Gdk.RGBA() 712 self.bgcolor.parse(self.config['background_color']) 713 714 if self.config['background_type'] == 'transparent' or self.config['background_type'] == 'image': 715 self.bgcolor.alpha = self.config['background_darkness'] 716 else: 717 self.bgcolor.alpha = 1 718 719 factor = self.config['inactive_color_offset'] 720 if factor > 1.0: 721 factor = 1.0 722 self.fgcolor_inactive = self.fgcolor_active.copy() 723 dbg(("fgcolor_inactive set to: RGB(%s,%s,%s)", getattr(self.fgcolor_inactive, "red"), 724 getattr(self.fgcolor_inactive, "green"), 725 getattr(self.fgcolor_inactive, "blue"))) 726 727 for bit in ['red', 'green', 'blue']: 728 setattr(self.fgcolor_inactive, bit, 729 getattr(self.fgcolor_inactive, bit) * factor) 730 731 dbg(("fgcolor_inactive set to: RGB(%s,%s,%s)", getattr(self.fgcolor_inactive, "red"), 732 getattr(self.fgcolor_inactive, "green"), 733 getattr(self.fgcolor_inactive, "blue"))) 734 colors = self.config['palette'].split(':') 735 self.palette_active = [] 736 for color in colors: 737 if color: 738 newcolor = Gdk.RGBA() 739 newcolor.parse(color) 740 self.palette_active.append(newcolor) 741 if len(colors) == 16: 742 # RGB values for indices 16..255 copied from vte source in order to dim them 743 shades = [0, 95, 135, 175, 215, 255] 744 for r in range(0, 6): 745 for g in range(0, 6): 746 for b in range(0, 6): 747 newcolor = Gdk.RGBA() 748 setattr(newcolor, "red", shades[r] / 255.0) 749 setattr(newcolor, "green", shades[g] / 255.0) 750 setattr(newcolor, "blue", shades[b] / 255.0) 751 self.palette_active.append(newcolor) 752 for y in range(8, 248, 10): 753 newcolor = Gdk.RGBA() 754 setattr(newcolor, "red", y / 255.0) 755 setattr(newcolor, "green", y / 255.0) 756 setattr(newcolor, "blue", y / 255.0) 757 self.palette_active.append(newcolor) 758 self.palette_inactive = [] 759 for color in self.palette_active: 760 newcolor = Gdk.RGBA() 761 for bit in ['red', 'green', 'blue']: 762 setattr(newcolor, bit, 763 getattr(color, bit) * factor) 764 self.palette_inactive.append(newcolor) 765 if self.terminator.last_focused_term == self: 766 self.vte.set_colors(self.fgcolor_active, self.bgcolor, 767 self.palette_active) 768 else: 769 self.vte.set_colors(self.fgcolor_inactive, self.bgcolor, 770 self.palette_inactive) 771 profiles = self.config.base.profiles 772 terminal_box_style_context = self.terminalbox.get_style_context() 773 for profile in list(profiles.keys()): 774 munged_profile = "terminator-profile-%s" % ( 775 "".join([c if c.isalnum() else "-" for c in profile])) 776 if terminal_box_style_context.has_class(munged_profile): 777 terminal_box_style_context.remove_class(munged_profile) 778 munged_profile = "".join([c if c.isalnum() else "-" for c in self.get_profile()]) 779 css_class_name = "terminator-profile-%s" % (munged_profile) 780 terminal_box_style_context.add_class(css_class_name) 781 self.set_cursor_color() 782 self.vte.set_cursor_shape(getattr(Vte.CursorShape, 783 self.config['cursor_shape'].upper())); 784 785 if self.config['cursor_blink'] == True: 786 self.vte.set_cursor_blink_mode(Vte.CursorBlinkMode.ON) 787 else: 788 self.vte.set_cursor_blink_mode(Vte.CursorBlinkMode.OFF) 789 790 if self.config['force_no_bell'] == True: 791 self.vte.set_audible_bell(False) 792 self.cnxids.remove_signal(self.vte, 'bell') 793 else: 794 self.vte.set_audible_bell(self.config['audible_bell']) 795 self.cnxids.remove_signal(self.vte, 'bell') 796 if self.config['urgent_bell'] == True or \ 797 self.config['icon_bell'] == True or \ 798 self.config['visible_bell'] == True: 799 try: 800 self.cnxids.new(self.vte, 'bell', self.on_bell) 801 except TypeError: 802 err('bell signal unavailable with this version of VTE') 803 804 if self.config['scrollback_infinite'] == True: 805 scrollback_lines = -1 806 else: 807 scrollback_lines = self.config['scrollback_lines'] 808 self.vte.set_scrollback_lines(scrollback_lines) 809 self.vte.set_scroll_on_keystroke(self.config['scroll_on_keystroke']) 810 self.vte.set_scroll_on_output(self.config['scroll_on_output']) 811 812 if self.config['scrollbar_position'] in ['disabled', 'hidden']: 813 self.scrollbar.hide() 814 else: 815 self.scrollbar.show() 816 if self.config['scrollbar_position'] == 'left': 817 self.terminalbox.reorder_child(self.scrollbar, 0) 818 elif self.config['scrollbar_position'] == 'right': 819 self.terminalbox.reorder_child(self.vte, 0) 820 821 self.titlebar.update() 822 self.vte.queue_draw() 823 824 def set_cursor_color(self): 825 """Set the cursor color appropriately""" 826 if self.config['cursor_color_fg']: 827 self.vte.set_color_cursor(None) 828 else: 829 cursor_color = Gdk.RGBA() 830 cursor_color.parse(self.config['cursor_color']) 831 self.vte.set_color_cursor(cursor_color) 832 833 def get_window_title(self): 834 """Return the window title""" 835 return self.vte.get_window_title() or str(self.command) 836 837 def on_group_button_press(self, widget, event): 838 """Handler for the group button""" 839 if event.button == 1: 840 if event.type == Gdk.EventType._2BUTTON_PRESS or \ 841 event.type == Gdk.EventType._3BUTTON_PRESS: 842 # Ignore these, or they make the interaction bad 843 return True 844 # Super key applies interaction to all terms in group 845 include_siblings=event.get_state() & Gdk.ModifierType.MOD4_MASK == Gdk.ModifierType.MOD4_MASK 846 if include_siblings: 847 targets=self.terminator.get_sibling_terms(self) 848 else: 849 targets=[self] 850 if event.get_state() & Gdk.ModifierType.CONTROL_MASK == Gdk.ModifierType.CONTROL_MASK: 851 dbg('on_group_button_press: toggle terminal to focused terminals group') 852 focused=self.get_toplevel().get_focussed_terminal() 853 if focused in targets: targets.remove(focused) 854 if self != focused: 855 if self.group == focused.group: 856 new_group = None 857 else: 858 new_group = focused.group 859 [term.set_group(None, new_group) for term in targets] 860 [term.titlebar.update(focused) for term in targets] 861 return True 862 elif event.get_state() & Gdk.ModifierType.SHIFT_MASK == Gdk.ModifierType.SHIFT_MASK: 863 dbg('on_group_button_press: rename of terminals group') 864 self.targets_for_new_group = targets 865 self.titlebar.create_group() 866 return True 867 elif event.type == Gdk.EventType.BUTTON_PRESS: 868 # Single Click gives popup 869 dbg('on_group_button_press: group menu popup') 870 window = self.get_toplevel() 871 window.preventHide = True 872 self.create_popup_group_menu(widget, event) 873 return True 874 else: 875 dbg('on_group_button_press: unknown group button interaction') 876 return False 877 878 def on_keypress(self, widget, event): 879 """Handler for keyboard events""" 880 if not event: 881 dbg('Terminal::on_keypress: Called on %s with no event' % widget) 882 return False 883 884 # FIXME: Does keybindings really want to live in Terminator()? 885 mapping = self.terminator.keybindings.lookup(event) 886 887 # Just propagate tab-swictch events if there is only one tab 888 if ( 889 mapping and ( 890 mapping.startswith('switch_to_tab') or 891 mapping in ('next_tab', 'prev_tab') 892 ) 893 ): 894 window = self.get_toplevel() 895 child = window.get_children()[0] 896 if isinstance(child, Terminal): 897 # not a Notebook instance => a single tab is used 898 # .get_n_pages() can not be used 899 return False 900 901 if mapping == "hide_window": 902 return False 903 904 if mapping and mapping not in ['close_window', 905 'full_screen']: 906 dbg('Terminal::on_keypress: lookup found: %r' % mapping) 907 # handle the case where user has re-bound copy to ctrl+<key> 908 # we only copy if there is a selection otherwise let it fall through 909 # to ^<key> 910 if (mapping == "copy" and event.get_state() & Gdk.ModifierType.CONTROL_MASK): 911 if self.vte.get_has_selection(): 912 getattr(self, "key_" + mapping)() 913 return True 914 elif not self.config['smart_copy']: 915 return True 916 else: 917 getattr(self, "key_" + mapping)() 918 return True 919 920 # FIXME: This is all clearly wrong. We should be doing this better 921 # maybe we can emit the key event and let Terminator() care? 922 groupsend = self.terminator.groupsend 923 groupsend_type = self.terminator.groupsend_type 924 window_focussed = self.vte.get_toplevel().get_property('has-toplevel-focus') 925 if groupsend != groupsend_type['off'] and window_focussed and self.vte.is_focus(): 926 if self.group and groupsend == groupsend_type['group']: 927 self.terminator.group_emit(self, self.group, 'key-press-event', 928 event) 929 if groupsend == groupsend_type['all']: 930 self.terminator.all_emit(self, 'key-press-event', event) 931 932 return False 933 934 def on_buttonpress(self, widget, event): 935 """Handler for mouse events""" 936 # Any button event should grab focus 937 widget.grab_focus() 938 939 if type(widget) == Gtk.VScrollbar and event.type == Gdk.EventType._2BUTTON_PRESS: 940 # Suppress double-click behavior 941 return True 942 943 if self.config['putty_paste_style']: 944 middle_click = [self.popup_menu, (widget, event)] 945 right_click = [self.paste_clipboard, (not self.config['putty_paste_style_source_clipboard'], )] 946 else: 947 middle_click = [self.paste_clipboard, (True, )] 948 right_click = [self.popup_menu, (widget, event)] 949 950 # Ctrl-click event here. 951 if event.button == self.MOUSEBUTTON_LEFT: 952 # Ctrl+leftclick on a URL should open it 953 if event.get_state() & Gdk.ModifierType.CONTROL_MASK == Gdk.ModifierType.CONTROL_MASK: 954 # Check new OSC-8 method first 955 url = self.vte.hyperlink_check_event(event) 956 dbg('url: %s' % url) 957 if url: 958 self.open_url(url, prepare=False) 959 else: 960 dbg('OSC-8 URL not detected dropping back to regex match') 961 url = self.vte.match_check_event(event) 962 if url[0]: 963 self.open_url(url, prepare=True) 964 elif event.button == self.MOUSEBUTTON_MIDDLE: 965 # middleclick should paste the clipboard 966 # try to pass it to vte widget first though 967 if event.get_state() & Gdk.ModifierType.CONTROL_MASK == 0: 968 if event.get_state() & Gdk.ModifierType.SHIFT_MASK == 0: 969 gtk_settings=Gtk.Settings().get_default() 970 primary_state = gtk_settings.get_property('gtk-enable-primary-paste') 971 gtk_settings.set_property('gtk-enable-primary-paste', False) 972 if not Vte.Terminal.do_button_press_event(self.vte, event): 973 middle_click[0](*middle_click[1]) 974 gtk_settings.set_property('gtk-enable-primary-paste', primary_state) 975 else: 976 middle_click[0](*middle_click[1]) 977 return True 978 return Vte.Terminal.do_button_press_event(self.vte, event) 979 elif event.button == self.MOUSEBUTTON_RIGHT: 980 # rightclick should display a context menu if Ctrl is not pressed, 981 # plus either the app is not interested in mouse events or Shift is pressed 982 if event.get_state() & Gdk.ModifierType.CONTROL_MASK == 0: 983 if event.get_state() & Gdk.ModifierType.SHIFT_MASK == 0: 984 if not Vte.Terminal.do_button_press_event(self.vte, event): 985 right_click[0](*right_click[1]) 986 else: 987 right_click[0](*right_click[1]) 988 return True 989 return False 990 991 def on_mousewheel(self, widget, event): 992 """Handler for modifier + mouse wheel scroll events""" 993 SMOOTH_SCROLL_UP = event.direction == Gdk.ScrollDirection.SMOOTH and event.delta_y <= 0. 994 SMOOTH_SCROLL_DOWN = event.direction == Gdk.ScrollDirection.SMOOTH and event.delta_y > 0. 995 if event.state & Gdk.ModifierType.CONTROL_MASK == Gdk.ModifierType.CONTROL_MASK: 996 # Zoom the terminal(s) in or out if not disabled in config 997 if self.config["disable_mousewheel_zoom"] is True: 998 return False 999 # Choice of target terminals depends on Shift and Super modifiers 1000 if event.state & Gdk.ModifierType.MOD4_MASK == Gdk.ModifierType.MOD4_MASK: 1001 targets = self.terminator.terminals 1002 elif event.state & Gdk.ModifierType.SHIFT_MASK == Gdk.ModifierType.SHIFT_MASK: 1003 targets = self.terminator.get_target_terms(self) 1004 else: 1005 targets = [self] 1006 if event.direction == Gdk.ScrollDirection.UP or SMOOTH_SCROLL_UP: 1007 for target in targets: 1008 target.zoom_in() 1009 return True 1010 elif event.direction == Gdk.ScrollDirection.DOWN or SMOOTH_SCROLL_DOWN: 1011 for target in targets: 1012 target.zoom_out() 1013 return True 1014 if event.state & Gdk.ModifierType.SHIFT_MASK == Gdk.ModifierType.SHIFT_MASK: 1015 # Shift + mouse wheel up/down 1016 if event.direction == Gdk.ScrollDirection.UP or SMOOTH_SCROLL_UP: 1017 self.scroll_by_page(-1) 1018 return True 1019 elif event.direction == Gdk.ScrollDirection.DOWN or SMOOTH_SCROLL_DOWN: 1020 self.scroll_by_page(1) 1021 return True 1022 return False 1023 1024 def popup_menu(self, widget, event=None): 1025 """Display the context menu""" 1026 window = self.get_toplevel() 1027 window.preventHide = True 1028 menu = TerminalPopupMenu(self) 1029 menu.show(widget, event) 1030 1031 def do_scrollbar_toggle(self): 1032 """Show or hide the terminal scrollbar""" 1033 self.toggle_widget_visibility(self.scrollbar) 1034 1035 def toggle_widget_visibility(self, widget): 1036 """Show or hide a widget""" 1037 if widget.get_property('visible'): 1038 widget.hide() 1039 else: 1040 widget.show() 1041 1042 def on_encoding_change(self, _widget, encoding): 1043 """Handle the encoding changing""" 1044 current = self.vte.get_encoding() 1045 if current != encoding: 1046 dbg('on_encoding_change: setting encoding to: %s' % encoding) 1047 self.custom_encoding = not (encoding == self.config['encoding']) 1048 self.vte.set_encoding(encoding) 1049 1050 def on_drag_begin(self, widget, drag_context, _data): 1051 """Handle the start of a drag event""" 1052 Gtk.drag_set_icon_pixbuf(drag_context, util.widget_pixbuf(self, 512), 0, 0) 1053 1054 def on_drag_data_get(self, _widget, _drag_context, selection_data, info, 1055 _time, data): 1056 """I have no idea what this does, drag and drop is a mystery. sorry.""" 1057 selection_data.set(Gdk.atom_intern('vte', False), info, 1058 bytes(str(data.terminator.terminals.index(self)), 1059 'utf-8')) 1060 1061 def on_drag_motion(self, widget, drag_context, x, y, _time, _data): 1062 """*shrug*""" 1063 if not drag_context.list_targets() == [Gdk.atom_intern('vte', False)] and \ 1064 (Gtk.targets_include_text(drag_context.list_targets()) or 1065 Gtk.targets_include_uri(drag_context.list_targets())): 1066 # copy text from another widget 1067 return 1068 srcwidget = Gtk.drag_get_source_widget(drag_context) 1069 if(isinstance(srcwidget, Gtk.EventBox) and 1070 srcwidget == self.titlebar) or widget == srcwidget: 1071 # on self 1072 return 1073 1074 alloc = widget.get_allocation() 1075 1076 if self.config['use_theme_colors']: 1077 color = self.vte.get_style_context().get_color(Gtk.StateType.NORMAL) # VERIFY FOR GTK3 as above 1078 else: 1079 color = Gdk.RGBA() 1080 color.parse(self.config['foreground_color']) # VERIFY FOR GTK3 1081 1082 pos = self.get_location(widget, x, y) 1083 topleft = (0, 0) 1084 topright = (alloc.width, 0) 1085 topmiddle = (alloc.width/2, 0) 1086 bottomleft = (0, alloc.height) 1087 bottomright = (alloc.width, alloc.height) 1088 bottommiddle = (alloc.width/2, alloc.height) 1089 middleleft = (0, alloc.height/2) 1090 middleright = (alloc.width, alloc.height/2) 1091 1092 coord = () 1093 if pos == "right": 1094 coord = (topright, topmiddle, bottommiddle, bottomright) 1095 elif pos == "top": 1096 coord = (topleft, topright, middleright , middleleft) 1097 elif pos == "left": 1098 coord = (topleft, topmiddle, bottommiddle, bottomleft) 1099 elif pos == "bottom": 1100 coord = (bottomleft, bottomright, middleright , middleleft) 1101 1102 # here, we define some widget internal values 1103 widget._draw_data = { 'color': color, 'coord' : coord } 1104 # redraw by forcing an event 1105 connec = widget.connect_after('draw', self.on_draw) 1106 widget.queue_draw_area(0, 0, alloc.width, alloc.height) 1107 widget.get_window().process_updates(True) 1108 # finaly reset the values 1109 widget.disconnect(connec) 1110 widget._draw_data = None 1111 1112 def background_draw(self, widget, cr): 1113 if not self.config['background_type'] == 'image' or not self.background_image: 1114 return False 1115 1116 rect = self.vte.get_allocation() 1117 xratio = float(rect.width) / float(self.background_image.get_width()) 1118 yratio = float(rect.height) / float(self.background_image.get_height()) 1119 cr.save() 1120 cr.scale(xratio,yratio) 1121 Gdk.cairo_set_source_pixbuf(cr, self.background_image, 0, 0) 1122 cr.paint() 1123 Gdk.cairo_set_source_rgba(cr,self.bgcolor) 1124 cr.paint() 1125 cr.restore() 1126 1127 def on_draw(self, widget, context): 1128 if not widget._draw_data: 1129 return False 1130 1131 color = widget._draw_data['color'] 1132 coord = widget._draw_data['coord'] 1133 1134 context.set_source_rgba(color.red, color.green, color.blue, 0.5) 1135 if len(coord) > 0: 1136 context.move_to(coord[len(coord)-1][0], coord[len(coord)-1][1]) 1137 for i in coord: 1138 context.line_to(i[0], i[1]) 1139 1140 context.fill() 1141 return False 1142 1143 def on_drag_data_received(self, widget, drag_context, x, y, selection_data, 1144 info, _time, data): 1145 """Something has been dragged into the terminal. Handle it as either a 1146 URL or another terminal.""" 1147 # FIXME this code is a mess that I don't quite understand how it works. 1148 dbg('drag data received of type: %s' % (selection_data.get_data_type())) 1149 # print(selection_data.get_urls()) 1150 if Gtk.targets_include_text(drag_context.list_targets()) or \ 1151 Gtk.targets_include_uri(drag_context.list_targets()): 1152 # copy text with no modification yet to destination 1153 txt = selection_data.get_data() 1154 # https://bugs.launchpad.net/terminator/+bug/1518705 1155 if info == self.TARGET_TYPE_MOZ: 1156 txt = txt.decode('utf-16') 1157 # KDE ends it's text/x-moz-url text with CRLF, :shrug: 1158 if not txt.endswith('\r\n'): 1159 txt = txt.split('\n')[0] 1160 else: 1161 txt = txt.decode() 1162 1163 txt_lines = txt.split( "\r\n" ) 1164 if txt_lines[-1] == '': 1165 for line in txt_lines[:-1]: 1166 if line[0:7] != 'file://': 1167 txt = txt.replace('\r\n','\n') 1168 break 1169 else: 1170 # It is a list of crlf terminated file:// URL. let's 1171 # iterate over all elements except the last one. 1172 str='' 1173 for fname in txt_lines[:-1]: 1174 fname = "'%s'" % urlunquote(fname[7:].replace("'", 1175 '\'\\\'\'')) 1176 str += fname + ' ' 1177 txt = str 1178 # Never send a CRLF to the terminal from here 1179 txt = txt.rstrip('\r\n') 1180 for term in self.terminator.get_target_terms(self): 1181 term.feed(txt.encode()) 1182 return 1183 1184 widgetsrc = data.terminator.terminals[int(selection_data.get_data())] 1185 srcvte = Gtk.drag_get_source_widget(drag_context) 1186 # check if computation requireds 1187 if (isinstance(srcvte, Gtk.EventBox) and 1188 srcvte == self.titlebar) or srcvte == widget: 1189 return 1190 1191 srchbox = widgetsrc 1192 1193 # The widget argument is actually a Vte.Terminal(). Turn that into a 1194 # terminatorlib Terminal() 1195 maker = Factory() 1196 while True: 1197 widget = widget.get_parent() 1198 if not widget: 1199 # We've run out of widgets. Something is wrong. 1200 err('Failed to find Terminal from vte') 1201 return 1202 if maker.isinstance(widget, 'Terminal'): 1203 break 1204 1205 dsthbox = widget 1206 1207 dstpaned = dsthbox.get_parent() 1208 srcpaned = srchbox.get_parent() 1209 1210 pos = self.get_location(widget, x, y) 1211 1212 srcpaned.remove(widgetsrc) 1213 dstpaned.split_axis(dsthbox, pos in ['top', 'bottom'], None, widgetsrc, pos in ['bottom', 'right']) 1214 srcpaned.hoover() 1215 widgetsrc.ensure_visible_and_focussed() 1216 1217 def get_location(self, term, x, y): 1218 """Get our location within the terminal""" 1219 pos = '' 1220 # get the diagonales function for the receiving widget 1221 term_alloc = term.get_allocation() 1222 coef1 = float(term_alloc.height)/float(term_alloc.width) 1223 coef2 = -float(term_alloc.height)/float(term_alloc.width) 1224 b1 = 0 1225 b2 = term_alloc.height 1226 #determine position in rectangle 1227 #-------- 1228 #|\ /| 1229 #| \ / | 1230 #| \/ | 1231 #| /\ | 1232 #| / \ | 1233 #|/ \| 1234 #-------- 1235 if (x*coef1 + b1 > y) and (x*coef2 + b2 < y): 1236 pos = "right" 1237 if (x*coef1 + b1 > y) and (x*coef2 + b2 > y): 1238 pos = "top" 1239 if (x*coef1 + b1 < y) and (x*coef2 + b2 > y): 1240 pos = "left" 1241 if (x*coef1 + b1 < y) and (x*coef2 + b2 < y): 1242 pos = "bottom" 1243 return pos 1244 1245 def grab_focus(self): 1246 """Steal focus for this terminal""" 1247 if self.vte and not self.vte.has_focus(): 1248 self.vte.grab_focus() 1249 1250 def ensure_visible_and_focussed(self): 1251 """Make sure that we're visible and focussed""" 1252 window = self.get_toplevel() 1253 try: 1254 topchild = window.get_children()[0] 1255 except IndexError: 1256 dbg('unable to get top child') 1257 return 1258 maker = Factory() 1259 1260 if maker.isinstance(topchild, 'Notebook'): 1261 # Find which page number this term is on 1262 tabnum = topchild.page_num_descendant(self) 1263 # If terms page number is not the current one, switch to it 1264 current_page = topchild.get_current_page() 1265 if tabnum != current_page: 1266 topchild.set_current_page(tabnum) 1267 1268 self.grab_focus() 1269 1270 def on_vte_focus(self, _widget): 1271 """Update our UI when we get focus""" 1272 self.emit('title-change', self.get_window_title()) 1273 1274 def on_vte_focus_in(self, _widget, _event): 1275 """Inform other parts of the application when focus is received""" 1276 self.vte.set_colors(self.fgcolor_active, self.bgcolor, 1277 self.palette_active) 1278 self.set_cursor_color() 1279 if not self.terminator.doing_layout: 1280 self.terminator.last_focused_term = self 1281 if self.get_toplevel().is_child_notebook(): 1282 notebook = self.get_toplevel().get_children()[0] 1283 notebook.set_last_active_term(self.uuid) 1284 notebook.clean_last_active_term() 1285 self.get_toplevel().last_active_term = None 1286 else: 1287 self.get_toplevel().last_active_term = self.uuid 1288 self.emit('focus-in') 1289 1290 def on_vte_focus_out(self, _widget, _event): 1291 """Inform other parts of the application when focus is lost""" 1292 self.vte.set_colors(self.fgcolor_inactive, self.bgcolor, 1293 self.palette_inactive) 1294 self.set_cursor_color() 1295 self.emit('focus-out') 1296 1297 def on_window_focus_out(self): 1298 """Update our UI when the window loses focus""" 1299 self.titlebar.update('window-focus-out') 1300 1301 def scrollbar_jump(self, position): 1302 """Move the scrollbar to a particular row""" 1303 self.scrollbar.set_value(position) 1304 1305 def on_search_done(self, _widget): 1306 """We've finished searching, so clean up""" 1307 self.searchbar.hide() 1308 self.scrollbar.set_value(self.vte.get_cursor_position()[1]) 1309 self.vte.grab_focus() 1310 1311 def on_edit_done(self, _widget): 1312 """A child widget is done editing a label, return focus to VTE""" 1313 self.vte.grab_focus() 1314 1315 def deferred_on_vte_size_allocate(self, widget, allocation): 1316 # widget & allocation are not used in on_vte_size_allocate, so we 1317 # can use the on_vte_size_allocate instead of duplicating the code 1318 if self.pending_on_vte_size_allocate: 1319 return 1320 self.pending_on_vte_size_allocate = True 1321 GObject.idle_add(self.do_deferred_on_vte_size_allocate, widget, allocation) 1322 1323 def do_deferred_on_vte_size_allocate(self, widget, allocation): 1324 self.pending_on_vte_size_allocate = False 1325 self.on_vte_size_allocate(widget, allocation) 1326 1327 def on_vte_size_allocate(self, widget, allocation): 1328 self.titlebar.update_terminal_size(self.vte.get_column_count(), 1329 self.vte.get_row_count()) 1330 if self.config['geometry_hinting']: 1331 window = self.get_toplevel() 1332 window.deferred_set_rough_geometry_hints() 1333 1334 def on_vte_notify_enter(self, term, event): 1335 """Handle the mouse entering this terminal""" 1336 # FIXME: This shouldn't be looking up all these values every time 1337 sloppy = False 1338 if self.config['focus'] == 'system': 1339 sloppy = self.config.get_system_focus() in ['sloppy', 'mouse'] 1340 elif self.config['focus'] in ['sloppy', 'mouse']: 1341 sloppy = True 1342 if sloppy and self.titlebar.editing() == False: 1343 term.grab_focus() 1344 return False 1345 1346 def get_zoom_data(self): 1347 """Return a dict of information for Window""" 1348 data = {'old_font': self.vte.get_font().copy(), 1349 'old_char_height': self.vte.get_char_height(), 1350 'old_char_width': self.vte.get_char_width(), 1351 'old_allocation': self.vte.get_allocation(), 1352 'old_columns': self.vte.get_column_count(), 1353 'old_rows': self.vte.get_row_count(), 1354 'old_parent': self.get_parent()} 1355 1356 return data 1357 1358 def zoom_scale(self, widget, allocation, old_data): 1359 """Scale our font correctly based on how big we are not vs before""" 1360 self.cnxids.remove_signal(self, 'size-allocate') 1361 # FIXME: Is a zoom signal actualy used anywhere? 1362 self.cnxids.remove_signal(self, 'zoom') 1363 1364 new_columns = self.vte.get_column_count() 1365 new_rows = self.vte.get_row_count() 1366 new_font = self.vte.get_font() 1367 1368 dbg('Terminal::zoom_scale: Resized from %dx%d to %dx%d' % ( 1369 old_data['old_columns'], 1370 old_data['old_rows'], 1371 new_columns, 1372 new_rows)) 1373 1374 if new_rows == old_data['old_rows'] or \ 1375 new_columns == old_data['old_columns']: 1376 dbg('Terminal::zoom_scale: One axis unchanged, not scaling') 1377 return 1378 1379 scale_factor = min ( (new_columns / old_data['old_columns'] * 0.97), 1380 (new_rows / old_data['old_rows'] * 1.05) ) 1381 1382 new_size = int(old_data['old_font'].get_size() * scale_factor) 1383 if new_size == 0: 1384 err('refusing to set a zero sized font') 1385 return 1386 new_font.set_size(new_size) 1387 dbg('setting new font: %s' % new_font) 1388 self.set_font(new_font) 1389 1390 def is_zoomed(self): 1391 """Determine if we are a zoomed terminal""" 1392 prop = None 1393 window = self.get_toplevel() 1394 1395 try: 1396 prop = window.get_property('term-zoomed') 1397 except TypeError: 1398 prop = False 1399 1400 return prop 1401 1402 def zoom(self, widget=None): 1403 """Zoom ourself to fill the window""" 1404 self.emit('zoom') 1405 1406 def maximise(self, widget=None): 1407 """Maximise ourself to fill the window""" 1408 self.emit('maximise') 1409 1410 def unzoom(self, widget=None): 1411 """Restore normal layout""" 1412 self.emit('unzoom') 1413 1414 def set_cwd(self, cwd=None): 1415 """Set our cwd""" 1416 if cwd is not None: 1417 self.cwd = cwd 1418 1419 def held_open(self, widget=None, respawn=False, debugserver=False): 1420 self.is_held_open = True 1421 self.titlebar.update() 1422 1423 def spawn_child(self, widget=None, respawn=False, debugserver=False): 1424 args = [] 1425 shell = None 1426 command = None 1427 1428 if self.terminator.doing_layout: 1429 dbg('still laying out, refusing to spawn a child') 1430 return 1431 1432 if respawn is False: 1433 self.vte.grab_focus() 1434 1435 self.is_held_open = False 1436 1437 options = self.config.options_get() 1438 if options and options.command: 1439 command = options.command 1440 self.relaunch_command = command 1441 options.command = None 1442 elif options and options.execute: 1443 command = options.execute 1444 self.relaunch_command = command 1445 options.execute = None 1446 elif self.relaunch_command: 1447 command = self.relaunch_command 1448 elif self.config['use_custom_command']: 1449 command = self.config['custom_command'] 1450 elif self.layout_command: 1451 command = self.layout_command 1452 elif debugserver is True: 1453 details = self.terminator.debug_address 1454 dbg('spawning debug session with: %s:%s' % (details[0], 1455 details[1])) 1456 command = 'telnet %s %s' % (details[0], details[1]) 1457 1458 # working directory set in layout config 1459 if self.directory: 1460 self.set_cwd(self.directory) 1461 # working directory given as argument 1462 elif options and options.working_directory and \ 1463 options.working_directory != '': 1464 self.set_cwd(options.working_directory) 1465 options.working_directory = '' 1466 1467 if type(command) is list: 1468 shell = util.path_lookup(command[0]) 1469 args = command 1470 else: 1471 shell = util.shell_lookup() 1472 1473 if self.config['login_shell']: 1474 args.insert(0, "-l") 1475 else: 1476 args.insert(0, shell) 1477 1478 if command is not None: 1479 args += ['-c', command] 1480 1481 if shell is None: 1482 self.vte.feed(_('Unable to find a shell')) 1483 return -1 1484 1485 try: 1486 os.putenv('WINDOWID', '%s' % self.vte.get_parent_window().xid) 1487 except AttributeError: 1488 pass 1489 1490 envv = ['TERM=%s' % self.config['term'], 1491 'COLORTERM=%s' % self.config['colorterm'], 'PWD=%s' % self.cwd, 1492 'TERMINATOR_UUID=%s' % self.uuid.urn] 1493 if self.terminator.dbus_name: 1494 envv.append('TERMINATOR_DBUS_NAME=%s' % self.terminator.dbus_name) 1495 if self.terminator.dbus_path: 1496 envv.append('TERMINATOR_DBUS_PATH=%s' % self.terminator.dbus_path) 1497 1498 dbg('Forking shell: "%s" with args: %s' % (shell, args)) 1499 args.insert(0, shell) 1500 result, self.pid = self.vte.spawn_sync(Vte.PtyFlags.DEFAULT, 1501 self.cwd, 1502 args, 1503 envv, 1504 GLib.SpawnFlags.FILE_AND_ARGV_ZERO, 1505 None, 1506 None, 1507 None) 1508 self.command = shell 1509 1510 self.titlebar.update() 1511 1512 if self.pid == -1: 1513 self.vte.feed(_('Unable to start shell:') + shell) 1514 return -1 1515 1516 def prepare_url(self, urlmatch): 1517 """Prepare a URL from a VTE match""" 1518 url = urlmatch[0] 1519 match = urlmatch[1] 1520 1521 if match == self.matches['addr_only'] and url[0:3] == 'ftp': 1522 url = 'ftp://' + url 1523 elif match == self.matches['addr_only']: 1524 url = 'http://' + url 1525 elif match in list(self.matches.values()): 1526 # We have a match, but it's not a hard coded one, so it's a plugin 1527 try: 1528 registry = plugin.PluginRegistry() 1529 registry.load_plugins() 1530 plugins = registry.get_plugins_by_capability('url_handler') 1531 1532 for urlplugin in plugins: 1533 if match == self.matches[urlplugin.handler_name]: 1534 newurl = urlplugin.callback(url) 1535 if newurl is not None: 1536 dbg('Terminal::prepare_url: URL prepared by \ 1537%s plugin' % urlplugin.handler_name) 1538 url = newurl 1539 break 1540 except Exception as ex: 1541 err('Exception occurred preparing URL: %s' % ex) 1542 1543 return url 1544 1545 def open_url(self, url, prepare=False): 1546 """Open a given URL, conditionally unpacking it from a VTE match""" 1547 if prepare: 1548 url = self.prepare_url(url) 1549 dbg('open_url: URL: %s (prepared: %s)' % (url, prepare)) 1550 1551 if self.config['use_custom_url_handler']: 1552 dbg("Using custom URL handler: %s" % 1553 self.config['custom_url_handler']) 1554 try: 1555 subprocess.Popen([self.config['custom_url_handler'], url]) 1556 return 1557 except: 1558 dbg('custom url handler did not work, falling back to defaults') 1559 1560 try: 1561 Gtk.show_uri(None, url, Gdk.CURRENT_TIME) 1562 return 1563 except: 1564 dbg('Gtk.show_uri did not work, falling through to xdg-open') 1565 1566 try: 1567 subprocess.Popen(["xdg-open", url]) 1568 except: 1569 dbg('xdg-open did not work, falling back to webbrowser.open') 1570 import webbrowser 1571 webbrowser.open(url) 1572 1573 def paste_clipboard(self, primary=False): 1574 """Paste one of the two clipboards""" 1575 for term in self.terminator.get_target_terms(self): 1576 if primary: 1577 term.vte.paste_primary() 1578 else: 1579 term.vte.paste_clipboard() 1580 self.vte.grab_focus() 1581 1582 def feed(self, text): 1583 """Feed the supplied text to VTE""" 1584 self.vte.feed_child(text) 1585 1586 def zoom_in(self): 1587 """Increase the font size""" 1588 self.zoom_font(True) 1589 1590 def zoom_out(self): 1591 """Decrease the font size""" 1592 self.zoom_font(False) 1593 1594 def zoom_font(self, zoom_in): 1595 """Change the font size""" 1596 pangodesc = self.vte.get_font() 1597 fontsize = pangodesc.get_size() 1598 1599 if fontsize > Pango.SCALE and not zoom_in: 1600 fontsize -= Pango.SCALE 1601 elif zoom_in: 1602 fontsize += Pango.SCALE 1603 1604 pangodesc.set_size(fontsize) 1605 self.set_font(pangodesc) 1606 self.custom_font_size = fontsize 1607 1608 def zoom_orig(self): 1609 """Restore original font size""" 1610 if self.config['use_system_font']: 1611 font = self.config.get_system_mono_font() 1612 else: 1613 font = self.config['font'] 1614 dbg("Terminal::zoom_orig: restoring font to: %s" % font) 1615 self.set_font(Pango.FontDescription(font)) 1616 self.custom_font_size = None 1617 1618 def set_font(self, fontdesc): 1619 """Set the font we want in VTE""" 1620 self.vte.set_font(fontdesc) 1621 1622 def get_cursor_position(self): 1623 """Return the co-ordinates of our cursor""" 1624 # FIXME: THIS METHOD IS DEPRECATED AND UNUSED 1625 col, row = self.vte.get_cursor_position() 1626 width = self.vte.get_char_width() 1627 height = self.vte.get_char_height() 1628 return col * width, row * height 1629 1630 def get_font_size(self): 1631 """Return the width/height of our font""" 1632 return self.vte.get_char_width(), self.vte.get_char_height() 1633 1634 def get_size(self): 1635 """Return the column/rows of the terminal""" 1636 return self.vte.get_column_count(), self.vte.get_row_count() 1637 1638 def on_bell(self, widget): 1639 """Set the urgency hint/icon/flash for our window""" 1640 if self.config['urgent_bell']: 1641 window = self.get_toplevel() 1642 if window.is_toplevel(): 1643 window.set_urgency_hint(True) 1644 if self.config['icon_bell']: 1645 self.titlebar.icon_bell() 1646 if self.config['visible_bell']: 1647 # Repurposed the code used for drag and drop overlay to provide a visual terminal flash 1648 alloc = widget.get_allocation() 1649 1650 if self.config['use_theme_colors']: 1651 color = self.vte.get_style_context().get_color(Gtk.StateType.NORMAL) # VERIFY FOR GTK3 as above 1652 else: 1653 color = Gdk.RGBA() 1654 color.parse(self.config['foreground_color']) # VERIFY FOR GTK3 1655 1656 coord = ((0, 0), (alloc.width, 0), (alloc.width, alloc.height), (0, alloc.height)) 1657 1658 # here, we define some widget internal values 1659 widget._draw_data = { 'color': color, 'coord' : coord } 1660 # redraw by forcing an event 1661 connec = widget.connect_after('draw', self.on_draw) 1662 widget.queue_draw_area(0, 0, alloc.width, alloc.height) 1663 widget.get_window().process_updates(True) 1664 # finaly reset the values 1665 widget.disconnect(connec) 1666 widget._draw_data = None 1667 1668 # Add timeout to clean up display 1669 GObject.timeout_add(100, self.on_bell_cleanup, widget, alloc) 1670 1671 def on_bell_cleanup(self, widget, alloc): 1672 """Queue a redraw to clear the visual flash overlay""" 1673 widget.queue_draw_area(0, 0, alloc.width, alloc.height) 1674 widget.get_window().process_updates(True) 1675 return False 1676 1677 def describe_layout(self, count, parent, global_layout, child_order): 1678 """Describe our layout""" 1679 layout = {'type': 'Terminal', 'parent': parent, 'order': child_order} 1680 if self.group: 1681 layout['group'] = self.group 1682 profile = self.get_profile() 1683 if layout != "default": 1684 # There's no point explicitly noting default profiles 1685 layout['profile'] = profile 1686 title = self.titlebar.get_custom_string() 1687 if title: 1688 layout['title'] = title 1689 layout['uuid'] = self.uuid 1690 name = 'terminal%d' % count 1691 count = count + 1 1692 global_layout[name] = layout 1693 return count 1694 1695 def create_layout(self, layout): 1696 """Apply our layout""" 1697 dbg('Setting layout') 1698 if 'command' in layout and layout['command'] != '': 1699 self.layout_command = layout['command'] 1700 if 'profile' in layout and layout['profile'] != '': 1701 if layout['profile'] in self.config.list_profiles(): 1702 self.set_profile(self, layout['profile']) 1703 if 'group' in layout and layout['group'] != '': 1704 # This doesn't need/use self.titlebar, but it's safer than sending 1705 # None 1706 self.really_create_group(self.titlebar, layout['group']) 1707 if 'title' in layout and layout['title'] != '': 1708 self.titlebar.set_custom_string(layout['title']) 1709 if 'directory' in layout and layout['directory'] != '': 1710 self.directory = layout['directory'] 1711 if 'uuid' in layout and layout['uuid'] != '': 1712 self.uuid = make_uuid(layout['uuid']) 1713 1714 def scroll_by_page(self, pages): 1715 """Scroll up or down in pages""" 1716 amount = pages * self.vte.get_vadjustment().get_page_increment() 1717 self.scroll_by(int(amount)) 1718 1719 def scroll_by_line(self, lines): 1720 """Scroll up or down in lines""" 1721 amount = lines * self.vte.get_vadjustment().get_step_increment() 1722 self.scroll_by(int(amount)) 1723 1724 def scroll_by(self, amount): 1725 """Scroll up or down by an amount of lines""" 1726 adjustment = self.vte.get_vadjustment() 1727 bottom = adjustment.get_upper() - adjustment.get_page_size() 1728 value = adjustment.get_value() + amount 1729 adjustment.set_value(min(value, bottom)) 1730 1731 def get_allocation(self): 1732 """Get a real allocation which includes the bloody x and y coordinates 1733 (grumble, grumble) """ 1734 alloc = super(Terminal, self).get_allocation() 1735 rv = self.translate_coordinates(self.get_toplevel(), 0, 0) 1736 if rv: 1737 alloc.x, alloc.y = rv 1738 return alloc 1739 1740 # There now begins a great list of keyboard event handlers 1741 def key_zoom_in(self): 1742 self.zoom_in() 1743 1744 def key_next_profile(self): 1745 self.switch_to_next_profile() 1746 1747 def key_previous_profile(self): 1748 self.switch_to_previous_profile() 1749 1750 def key_zoom_out(self): 1751 self.zoom_out() 1752 1753 def key_copy(self): 1754 self.vte.copy_clipboard() 1755 if self.config['clear_select_on_copy']: 1756 self.vte.unselect_all() 1757 1758 def key_paste(self): 1759 self.paste_clipboard() 1760 1761 def key_toggle_scrollbar(self): 1762 self.do_scrollbar_toggle() 1763 1764 def key_zoom_normal(self): 1765 self.zoom_orig () 1766 1767 def key_search(self): 1768 self.searchbar.start_search() 1769 1770 # bindings that should be moved to Terminator as they all just call 1771 # a function of Terminator. It would be cleaner if TerminatorTerm 1772 # has absolutely no reference to Terminator. 1773 # N (next) - P (previous) - O (horizontal) - E (vertical) - W (close) 1774 def key_zoom_in_all(self): 1775 self.terminator.zoom_in_all() 1776 1777 def key_zoom_out_all(self): 1778 self.terminator.zoom_out_all() 1779 1780 def key_zoom_normal_all(self): 1781 self.terminator.zoom_orig_all() 1782 1783 def key_cycle_next(self): 1784 self.key_go_next() 1785 1786 def key_cycle_prev(self): 1787 self.key_go_prev() 1788 1789 def key_go_next(self): 1790 self.emit('navigate', 'next') 1791 1792 def key_go_prev(self): 1793 self.emit('navigate', 'prev') 1794 1795 def key_go_up(self): 1796 self.emit('navigate', 'up') 1797 1798 def key_go_down(self): 1799 self.emit('navigate', 'down') 1800 1801 def key_go_left(self): 1802 self.emit('navigate', 'left') 1803 1804 def key_go_right(self): 1805 self.emit('navigate', 'right') 1806 1807 def key_split_horiz(self): 1808 self.emit('split-horiz', self.get_cwd()) 1809 1810 def key_split_vert(self): 1811 self.emit('split-vert', self.get_cwd()) 1812 1813 def key_rotate_cw(self): 1814 self.emit('rotate-cw') 1815 1816 def key_rotate_ccw(self): 1817 self.emit('rotate-ccw') 1818 1819 def key_close_term(self): 1820 self.close() 1821 1822 def key_resize_up(self): 1823 self.emit('resize-term', 'up') 1824 1825 def key_resize_down(self): 1826 self.emit('resize-term', 'down') 1827 1828 def key_resize_left(self): 1829 self.emit('resize-term', 'left') 1830 1831 def key_resize_right(self): 1832 self.emit('resize-term', 'right') 1833 1834 def key_move_tab_right(self): 1835 self.emit('move-tab', 'right') 1836 1837 def key_move_tab_left(self): 1838 self.emit('move-tab', 'left') 1839 1840 def key_toggle_zoom(self): 1841 if self.is_zoomed(): 1842 self.unzoom() 1843 else: 1844 self.maximise() 1845 1846 def key_scaled_zoom(self): 1847 if self.is_zoomed(): 1848 self.unzoom() 1849 else: 1850 self.zoom() 1851 1852 def key_next_tab(self): 1853 self.emit('tab-change', -1) 1854 1855 def key_prev_tab(self): 1856 self.emit('tab-change', -2) 1857 1858 def key_switch_to_tab_1(self): 1859 self.emit('tab-change', 0) 1860 1861 def key_switch_to_tab_2(self): 1862 self.emit('tab-change', 1) 1863 1864 def key_switch_to_tab_3(self): 1865 self.emit('tab-change', 2) 1866 1867 def key_switch_to_tab_4(self): 1868 self.emit('tab-change', 3) 1869 1870 def key_switch_to_tab_5(self): 1871 self.emit('tab-change', 4) 1872 1873 def key_switch_to_tab_6(self): 1874 self.emit('tab-change', 5) 1875 1876 def key_switch_to_tab_7(self): 1877 self.emit('tab-change', 6) 1878 1879 def key_switch_to_tab_8(self): 1880 self.emit('tab-change', 7) 1881 1882 def key_switch_to_tab_9(self): 1883 self.emit('tab-change', 8) 1884 1885 def key_switch_to_tab_10(self): 1886 self.emit('tab-change', 9) 1887 1888 def key_reset(self): 1889 self.vte.reset (True, False) 1890 1891 def key_reset_clear(self): 1892 self.vte.reset (True, True) 1893 1894 def key_create_group(self): 1895 self.titlebar.create_group() 1896 1897 def key_group_all(self): 1898 self.emit('group-all') 1899 1900 def key_group_all_toggle(self): 1901 self.emit('group-all-toggle') 1902 1903 def key_ungroup_all(self): 1904 self.emit('ungroup-all') 1905 1906 def key_group_tab(self): 1907 self.emit('group-tab') 1908 1909 def key_group_tab_toggle(self): 1910 self.emit('group-tab-toggle') 1911 1912 def key_ungroup_tab(self): 1913 self.emit('ungroup-tab') 1914 1915 def key_new_window(self): 1916 self.terminator.new_window(self.get_cwd(), self.get_profile()) 1917 1918 def key_new_tab(self): 1919 self.get_toplevel().tab_new(self) 1920 1921 def key_new_terminator(self): 1922 spawn_new_terminator(self.origcwd, ['-u']) 1923 1924 def key_broadcast_off(self): 1925 self.set_groupsend(None, self.terminator.groupsend_type['off']) 1926 self.terminator.focus_changed(self) 1927 1928 def key_broadcast_group(self): 1929 self.set_groupsend(None, self.terminator.groupsend_type['group']) 1930 self.terminator.focus_changed(self) 1931 1932 def key_broadcast_all(self): 1933 self.set_groupsend(None, self.terminator.groupsend_type['all']) 1934 self.terminator.focus_changed(self) 1935 1936 def key_insert_number(self): 1937 self.emit('enumerate', False) 1938 1939 def key_insert_padded(self): 1940 self.emit('enumerate', True) 1941 1942 def key_edit_window_title(self): 1943 window = self.get_toplevel() 1944 dialog = Gtk.Dialog(_('Rename Window'), window, 1945 Gtk.DialogFlags.MODAL, 1946 (Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, 1947 Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT)) 1948 dialog.set_default_response(Gtk.ResponseType.ACCEPT) 1949 dialog.set_resizable(False) 1950 dialog.set_border_width(8) 1951 1952 label = Gtk.Label(label=_('Enter a new title for the Terminator window...')) 1953 name = Gtk.Entry() 1954 name.set_activates_default(True) 1955 if window.title.text != self.vte.get_window_title(): 1956 name.set_text(self.get_toplevel().title.text) 1957 1958 dialog.vbox.pack_start(label, False, False, 6) 1959 dialog.vbox.pack_start(name, False, False, 6) 1960 1961 dialog.show_all() 1962 res = dialog.run() 1963 if res == Gtk.ResponseType.ACCEPT: 1964 if name.get_text(): 1965 window.title.force_title(None) 1966 window.title.force_title(name.get_text()) 1967 else: 1968 window.title.force_title(None) 1969 dialog.destroy() 1970 return 1971 1972 def key_edit_tab_title(self): 1973 window = self.get_toplevel() 1974 if not window.is_child_notebook(): 1975 return 1976 1977 notebook = window.get_children()[0] 1978 n_page = notebook.get_current_page() 1979 page = notebook.get_nth_page(n_page) 1980 label = notebook.get_tab_label(page) 1981 label.edit() 1982 1983 def key_edit_terminal_title(self): 1984 self.titlebar.label.edit() 1985 1986 def key_layout_launcher(self): 1987 LAYOUTLAUNCHER=LayoutLauncher() 1988 1989 def key_page_up(self): 1990 self.scroll_by_page(-1) 1991 1992 def key_page_down(self): 1993 self.scroll_by_page(1) 1994 1995 def key_page_up_half(self): 1996 self.scroll_by_page(-0.5) 1997 1998 def key_page_down_half(self): 1999 self.scroll_by_page(0.5) 2000 2001 def key_line_up(self): 2002 self.scroll_by_line(-1) 2003 2004 def key_line_down(self): 2005 self.scroll_by_line(1) 2006 2007 def key_preferences(self): 2008 PrefsEditor(self) 2009 2010 def key_help(self): 2011 manual_index_page = manual_lookup() 2012 if manual_index_page: 2013 self.open_url(manual_index_page) 2014 2015# End key events 2016 2017 2018GObject.type_register(Terminal) 2019# vim: set expandtab ts=4 sw=4: 2020