#!/usr/bin/python # Terminator by Chris Jones # GPL v2 only """terminator.py - class for the master Terminator singleton""" import copy import os import gtk from borg import Borg from config import Config from keybindings import Keybindings from util import dbg, err, enumerate_descendants from factory import Factory from cwd import get_pid_cwd from version import APP_NAME, APP_VERSION class Terminator(Borg): """master object for the application""" windows = None launcher_windows = None windowtitle = None terminals = None groups = None config = None keybindings = None origcwd = None dbus_path = None dbus_name = None pid_cwd = None gnome_client = None debug_address = None doing_layout = None last_active_window = None groupsend = None groupsend_type = {'all':0, 'group':1, 'off':2} def __init__(self): """Class initialiser""" Borg.__init__(self, self.__class__.__name__) self.prepare_attributes() def prepare_attributes(self): """Initialise anything that isn't already""" if not self.windows: self.windows = [] if not self.launcher_windows: self.launcher_windows = [] if not self.terminals: self.terminals = [] if not self.groups: self.groups = [] if not self.config: self.config = Config() if self.groupsend == None: self.groupsend = self.groupsend_type[self.config['broadcast_group']] if not self.keybindings: self.keybindings = Keybindings() self.keybindings.configure(self.config['keybindings']) if not self.doing_layout: self.doing_layout = False if not self.pid_cwd: self.pid_cwd = get_pid_cwd() if self.gnome_client is None: self.attempt_gnome_client() def set_origcwd(self, cwd): """Store the original cwd our process inherits""" if cwd == '/': cwd = os.path.expanduser('~') os.chdir(cwd) self.origcwd = cwd def set_dbus_data(self, dbus_service): """Store the DBus bus details, if they are available""" if dbus_service: self.dbus_name = dbus_service.bus_name.get_name() self.dbus_path = dbus_service.bus_path def attempt_gnome_client(self): """Attempt to find a GNOME Session to register with""" try: import gnome import gnome.ui self.gnome_program = gnome.init(APP_NAME, APP_VERSION) self.gnome_client = gnome.ui.master_client() self.gnome_client.connect_to_session_manager() self.gnome_client.connect('save-yourself', self.save_yourself) self.gnome_client.connect('die', self.die) dbg('GNOME session support enabled and registered') except (ImportError, AttributeError): self.gnome_client = False dbg('GNOME session support not available') def save_yourself(self, *args): """Save as much state as possible for the session manager""" dbg('preparing session manager state') # FIXME: Implement this def die(self, *args): """Die at the hands of the session manager""" dbg('session manager asked us to die') # FIXME: Implement this def get_windows(self): """Return a list of windows""" return self.windows def register_window(self, window): """Register a new window widget""" if window not in self.windows: dbg('Terminator::register_window: registering %s:%s' % (id(window), type(window))) self.windows.append(window) def deregister_window(self, window): """de-register a window widget""" dbg('Terminator::deregister_window: de-registering %s:%s' % (id(window), type(window))) if window in self.windows: self.windows.remove(window) else: err('%s is not in registered window list' % window) if len(self.windows) == 0: # We have no windows left, we should exit dbg('no windows remain, quitting') gtk.main_quit() def register_launcher_window(self, window): """Register a new launcher window widget""" if window not in self.launcher_windows: dbg('Terminator::register_launcher_window: registering %s:%s' % (id(window), type(window))) self.launcher_windows.append(window) def deregister_launcher_window(self, window): """de-register a launcher window widget""" dbg('Terminator::deregister_launcher_window: de-registering %s:%s' % (id(window), type(window))) if window in self.launcher_windows: self.launcher_windows.remove(window) else: err('%s is not in registered window list' % window) if len(self.launcher_windows) == 0 and len(self.windows) == 0: # We have no windows left, we should exit dbg('no windows remain, quitting') gtk.main_quit() def register_terminal(self, terminal): """Register a new terminal widget""" if terminal not in self.terminals: dbg('Terminator::register_terminal: registering %s:%s' % (id(terminal), type(terminal))) self.terminals.append(terminal) def deregister_terminal(self, terminal): """De-register a terminal widget""" dbg('Terminator::deregister_terminal: de-registering %s:%s' % (id(terminal), type(terminal))) self.terminals.remove(terminal) if len(self.terminals) == 0: dbg('no terminals remain, destroying all windows') for window in self.windows: window.destroy() else: dbg('Terminator::deregister_terminal: %d terminals remain' % len(self.terminals)) def find_terminal_by_uuid(self, uuid): """Search our terminals for one matching the supplied UUID""" dbg('searching self.terminals for: %s' % uuid) for terminal in self.terminals: dbg('checking: %s (%s)' % (terminal.uuid.urn, terminal)) if terminal.uuid.urn == uuid: return terminal return None def new_window(self, cwd=None): """Create a window with a Terminal in it""" maker = Factory() window = maker.make('Window') terminal = maker.make('Terminal') if cwd: terminal.set_cwd(cwd) window.add(terminal) window.show(True) terminal.spawn_child() return(window, terminal) def create_layout(self, layoutname): """Create all the parts necessary to satisfy the specified layout""" layout = None objects = {} self.doing_layout = True layout = copy.deepcopy(self.config.layout_get_config(layoutname)) if not layout: # User specified a non-existent layout. default to one Terminal err('layout %s not defined' % layout) self.new_window() return # Wind the flat objects into a hierarchy hierarchy = {} count = 0 # Loop over the layout until we have consumed it, or hit 1000 loops. # This is a stupid artificial limit, but it's safe. while len(layout) > 0 and count < 1000: count = count + 1 if count == 1000: err('hit maximum loop boundary. THIS IS VERY LIKELY A BUG') for obj in layout.keys(): if layout[obj]['type'].lower() == 'window': hierarchy[obj] = {} hierarchy[obj]['type'] = 'Window' hierarchy[obj]['children'] = {} # Copy any additional keys for objkey in layout[obj].keys(): if layout[obj][objkey] != '' and not hierarchy[obj].has_key(objkey): hierarchy[obj][objkey] = layout[obj][objkey] objects[obj] = hierarchy[obj] del(layout[obj]) else: # Now examine children to see if their parents exist yet if not layout[obj].has_key('parent'): err('Invalid object: %s' % obj) del(layout[obj]) continue if objects.has_key(layout[obj]['parent']): # Our parent has been created, add ourselves childobj = {} childobj['type'] = layout[obj]['type'] childobj['children'] = {} # Copy over any additional object keys for objkey in layout[obj].keys(): if not childobj.has_key(objkey): childobj[objkey] = layout[obj][objkey] objects[layout[obj]['parent']]['children'][obj] = childobj objects[obj] = childobj del(layout[obj]) layout = hierarchy for windef in layout: if layout[windef]['type'] != 'Window': err('invalid layout format. %s' % layout) raise(ValueError) dbg('Creating a window') window, terminal = self.new_window() if layout[windef].has_key('position'): parts = layout[windef]['position'].split(':') if len(parts) == 2: window.move(int(parts[0]), int(parts[1])) if layout[windef].has_key('size'): parts = layout[windef]['size'] winx = int(parts[0]) winy = int(parts[1]) if winx > 1 and winy > 1: window.resize(winx, winy) if layout[windef].has_key('title'): window.title.force_title(layout[windef]['title']) if layout[windef].has_key('maximised'): if layout[windef]['maximised'] == 'True': window.ismaximised = True else: window.ismaximised = False window.set_maximised(window.ismaximised) if layout[windef].has_key('fullscreen'): if layout[windef]['fullscreen'] == 'True': window.isfullscreen = True else: window.isfullscreen = False window.set_fullscreen(window.isfullscreen) window.create_layout(layout[windef]) def layout_done(self): """Layout operations have finished, record that fact""" self.doing_layout = False maker = Factory() window_last_active_term_mapping = {} for window in self.windows: if window.is_child_notebook(): source = window.get_toplevel().get_children()[0] else: source = window window_last_active_term_mapping[window] = copy.copy(source.last_active_term) for terminal in self.terminals: if not terminal.pid: terminal.spawn_child() for window in self.windows: if window.is_child_notebook(): # For windows with a notebook notebook = window.get_toplevel().get_children()[0] # Cycle through pages by number for page in xrange(0, notebook.get_n_pages()): # Try and get the entry in the previously saved mapping mapping = window_last_active_term_mapping[window] page_last_active_term = mapping.get(notebook.get_nth_page(page), None) if page_last_active_term is None: # Couldn't find entry, so we find the first child of type Terminal children = notebook.get_nth_page(page).get_children() for page_last_active_term in children: if maker.isinstance(page_last_active_term, 'Terminal'): page_last_active_term = page_last_active_term.uuid break else: err('Should never reach here!') page_last_active_term = None if page_last_active_term is None: # Bail on this tab as we're having no luck here, continue with the next continue # Set the notebook entry, then ensure Terminal is visible and focussed urn = page_last_active_term.urn notebook.last_active_term[notebook.get_nth_page(page)] = page_last_active_term if urn: term = self.find_terminal_by_uuid(urn) if term: term.ensure_visible_and_focussed() else: # For windows without a notebook ensure Terminal is visible and focussed if window_last_active_term_mapping[window]: term = self.find_terminal_by_uuid(window_last_active_term_mapping[window].urn) term.ensure_visible_and_focussed() for window in self.windows: if window.uuid == self.last_active_window: window.show() def reconfigure(self): """Update configuration for the whole application""" if self.config['handle_size'] in xrange(0, 6): gtk.rc_parse_string(""" style "terminator-paned-style" { GtkPaned::handle_size = %s } class "GtkPaned" style "terminator-paned-style" """ % self.config['handle_size']) gtk.rc_reset_styles(gtk.settings_get_default()) # Cause all the terminals to reconfigure for terminal in self.terminals: terminal.reconfigure() # Reparse our keybindings self.keybindings.configure(self.config['keybindings']) # Update tab position if appropriate maker = Factory() for window in self.windows: child = window.get_child() if maker.isinstance(child, 'Notebook'): child.configure() # Set broadcasting group self.groupsend = self.groupsend_type[self.config['broadcast_group']] def create_group(self, name): """Create a new group""" if name not in self.groups: dbg('Terminator::create_group: registering group %s' % name) self.groups.append(name) def closegroupedterms(self, group): """Close all terminals in a group""" for terminal in self.terminals[:]: if terminal.group == group: terminal.close() def group_hoover(self): """Clean out unused groups""" if self.config['autoclean_groups']: inuse = [] todestroy = [] for terminal in self.terminals: if terminal.group: if not terminal.group in inuse: inuse.append(terminal.group) for group in self.groups: if not group in inuse: todestroy.append(group) dbg('Terminator::group_hoover: %d groups, hoovering %d' % (len(self.groups), len(todestroy))) for group in todestroy: self.groups.remove(group) def group_emit(self, terminal, group, type, event): """Emit to each terminal in a group""" dbg('Terminator::group_emit: emitting a keystroke for group %s' % group) for term in self.terminals: if term != terminal and term.group == group: term.vte.emit(type, event) def all_emit(self, terminal, type, event): """Emit to all terminals""" for term in self.terminals: if term != terminal: term.vte.emit(type, event) def do_enumerate(self, widget, pad): """Insert the number of each terminal in a group, into that terminal""" if pad: numstr = '%0'+str(len(str(len(self.terminals))))+'d' else: numstr = '%d' terminals = [] for window in self.windows: containers, win_terminals = enumerate_descendants(window) terminals.extend(win_terminals) for term in self.get_target_terms(widget): idx = terminals.index(term) term.feed(numstr % (idx + 1)) def get_sibling_terms(self, widget): termset = [] for term in self.terminals: if term.group == widget.group: termset.append(term) return(termset) def get_target_terms(self, widget): """Get the terminals we should currently be broadcasting to""" if self.groupsend == self.groupsend_type['all']: return(self.terminals) elif self.groupsend == self.groupsend_type['group']: if widget.group != None: return(self.get_sibling_terms(widget)) return([widget]) def get_focussed_terminal(self): """iterate over all the terminals to find which, if any, has focus""" for terminal in self.terminals: if terminal.flags()>k.HAS_FOCUS: return(terminal) return(None) def focus_changed(self, widget): """We just moved focus to a new terminal""" for terminal in self.terminals: terminal.titlebar.update(widget) return def focus_left(self, widget): self.last_focused_term=widget def describe_layout(self): """Describe our current layout""" layout = {} count = 0 for window in self.windows: parent = '' count = window.describe_layout(count, parent, layout, 0) return(layout) # vim: set expandtab ts=4 sw=4: