692 lines
28 KiB
Python
Executable File
692 lines
28 KiB
Python
Executable File
#!/usr/bin/env python2
|
|
# Terminator by Chris Jones <cmsj@tenshu.net>
|
|
# GPL v2 only
|
|
"""terminator.py - class for the master Terminator singleton"""
|
|
|
|
import copy
|
|
import os
|
|
import gi
|
|
gi.require_version('Vte', '2.91')
|
|
from gi.repository import Gtk, Gdk, Vte, GdkX11
|
|
from gi.repository.GLib import GError
|
|
|
|
import borg
|
|
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
|
|
|
|
def eventkey2gdkevent(eventkey): # FIXME FOR GTK3: is there a simpler way of casting from specific EventKey to generic (union) GdkEvent?
|
|
gdkevent = Gdk.Event.new(eventkey.type)
|
|
gdkevent.key.window = eventkey.window
|
|
gdkevent.key.send_event = eventkey.send_event
|
|
gdkevent.key.time = eventkey.time
|
|
gdkevent.key.state = eventkey.state
|
|
gdkevent.key.keyval = eventkey.keyval
|
|
gdkevent.key.length = eventkey.length
|
|
gdkevent.key.string = eventkey.string
|
|
gdkevent.key.hardware_keycode = eventkey.hardware_keycode
|
|
gdkevent.key.group = eventkey.group
|
|
gdkevent.key.is_modifier = eventkey.is_modifier
|
|
return gdkevent
|
|
|
|
class Terminator(Borg):
|
|
"""master object for the application"""
|
|
|
|
windows = None
|
|
launcher_windows = None
|
|
windowtitle = None
|
|
terminals = None
|
|
groups = None
|
|
config = None
|
|
keybindings = None
|
|
style_providers = None
|
|
last_focused_term = None
|
|
|
|
origcwd = None
|
|
dbus_path = None
|
|
dbus_name = None
|
|
pid_cwd = None
|
|
gnome_client = None
|
|
debug_address = None
|
|
ibus_running = None
|
|
|
|
doing_layout = None
|
|
layoutname = None
|
|
last_active_window = None
|
|
prelayout_windows = None
|
|
|
|
groupsend = None
|
|
groupsend_type = {'all':0, 'group':1, 'off':2}
|
|
|
|
cur_gtk_theme_name = None
|
|
gtk_settings = None
|
|
|
|
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_default']]
|
|
if not self.keybindings:
|
|
self.keybindings = Keybindings()
|
|
self.keybindings.configure(self.config['keybindings'])
|
|
if not self.style_providers:
|
|
self.style_providers = []
|
|
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()
|
|
self.connect_signals()
|
|
|
|
def connect_signals(self):
|
|
"""Connect all the gtk signals"""
|
|
self.gtk_settings=Gtk.Settings().get_default()
|
|
self.gtk_settings.connect('notify::gtk-theme-name', self.on_gtk_theme_name_notify)
|
|
self.cur_gtk_theme_name = self.gtk_settings.get_property('gtk-theme-name')
|
|
|
|
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:
|
|
from gi.repository import Gnome
|
|
self.gnome_program = Gnome.init(APP_NAME, APP_VERSION) # VERIFY FOR GTK3
|
|
self.gnome_client = Gnome.Ui.master_client() # VERIFY FOR GTK3
|
|
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 find_window_by_uuid(self, uuid):
|
|
"""Search our terminals for one matching the supplied UUID"""
|
|
dbg('searching self.terminals for: %s' % uuid)
|
|
for window in self.windows:
|
|
dbg('checking: %s (%s)' % (window.uuid.urn, window))
|
|
if window.uuid.urn == uuid:
|
|
return window
|
|
return None
|
|
|
|
def new_window(self, cwd=None, profile=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)
|
|
if profile and self.config['always_split_with_profile']:
|
|
terminal.force_set_profile(None, profile)
|
|
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
|
|
self.last_active_window = None
|
|
self.prelayout_windows = self.windows[:]
|
|
|
|
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])
|
|
|
|
self.layoutname = layoutname
|
|
|
|
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()
|
|
'''
|
|
OK, So it turned out the fix was not yet complete. We also needed to ensure that the active window was on top and focused... Oh boy, what fun that was to figure out. The windows get a timestamp, but even if the timestamps are in the right order, and you pop them up in the right order so that the last active is the last one popped, you can't guarantee that that is the one that will end with the focus. Instead we have to pop all the windows up, then find the last active one, then we need to repeatedly flush the pending events, and focus the window. If we don't do this we get semi-random window focused. Even doing all this is not 100% reliable, but I'm at the end of my tether trying to figure out why the wrong window is occasionally focused.
|
|
|
|
I'm going to push it to the repo so some more people can try it. Hopefully someone can suggest an improvement.
|
|
|
|
'''
|
|
# Build list of new windows using prelayout list
|
|
new_win_list = []
|
|
for window in self.windows:
|
|
if window not in self.prelayout_windows:
|
|
new_win_list.append(window)
|
|
|
|
# Make sure all new windows get bumped to the top
|
|
for window in new_win_list:
|
|
window.show()
|
|
window.grab_focus()
|
|
try:
|
|
t = GdkX11.x11_get_server_time(window.get_window())
|
|
except AttributeError:
|
|
t = 0
|
|
window.get_window().focus(t)
|
|
|
|
# Awful workaround to be sure that the last focused window is actually the one focused.
|
|
# Don't ask, don't tell policy on this. Even this is not 100%
|
|
if self.last_active_window:
|
|
window = self.find_window_by_uuid(self.last_active_window.urn)
|
|
count = 0
|
|
while count < 1000 and Gtk.events_pending():
|
|
count += 1
|
|
Gtk.main_iteration_do(False)
|
|
window.show()
|
|
window.grab_focus()
|
|
try:
|
|
t = GdkX11.x11_get_server_time(window.get_window())
|
|
except AttributeError:
|
|
t = 0
|
|
window.get_window().focus(t)
|
|
|
|
self.prelayout_windows = None
|
|
|
|
def on_gtk_theme_name_notify(self, settings, prop):
|
|
"""Reconfigure if the gtk theme name changes"""
|
|
new_gtk_theme_name = settings.get_property(prop.name)
|
|
if new_gtk_theme_name != self.cur_gtk_theme_name:
|
|
self.cur_gtk_theme_name = new_gtk_theme_name
|
|
self.reconfigure()
|
|
|
|
def reconfigure(self):
|
|
"""Update configuration for the whole application"""
|
|
|
|
if self.style_providers != []:
|
|
for style_provider in self.style_providers:
|
|
Gtk.StyleContext.remove_provider_for_screen(
|
|
Gdk.Screen.get_default(),
|
|
style_provider)
|
|
self.style_providers = []
|
|
|
|
# Force the window background to be transparent for newer versions of
|
|
# GTK3. We then have to fix all the widget backgrounds because the
|
|
# widgets theming may not render it's own background.
|
|
css = """
|
|
.terminator-terminal-window {
|
|
background-color: alpha(@theme_bg_color,0); }
|
|
|
|
.terminator-terminal-window .notebook.header,
|
|
.terminator-terminal-window notebook header {
|
|
background-color: @theme_bg_color; }
|
|
|
|
.terminator-terminal-window .pane-separator {
|
|
background-color: @theme_bg_color; }
|
|
|
|
.terminator-terminal-window .terminator-terminal-searchbar {
|
|
background-color: @theme_bg_color; }
|
|
"""
|
|
|
|
# Fix several themes that put a borders, corners, or backgrounds around
|
|
# viewports, making the titlebar look bad.
|
|
css += """
|
|
.terminator-terminal-window GtkViewport,
|
|
.terminator-terminal-window viewport {
|
|
border-width: 0px;
|
|
border-radius: 0px;
|
|
background-color: transparent; }
|
|
"""
|
|
|
|
# Add per profile snippets for setting the background of the HBox
|
|
template = """
|
|
.terminator-profile-%s {
|
|
background-color: alpha(%s, %s); }
|
|
"""
|
|
profiles = self.config.base.profiles
|
|
for profile in profiles.keys():
|
|
if profiles[profile]['use_theme_colors']:
|
|
# Create a dummy window/vte and realise it so it has correct
|
|
# values to read from
|
|
tmp_win = Gtk.Window()
|
|
tmp_vte = Vte.Terminal()
|
|
tmp_win.add(tmp_vte)
|
|
tmp_win.realize()
|
|
bgcolor = tmp_vte.get_style_context().get_background_color(Gtk.StateType.NORMAL)
|
|
bgcolor = "#{0:02x}{1:02x}{2:02x}".format(int(bgcolor.red * 255),
|
|
int(bgcolor.green * 255),
|
|
int(bgcolor.blue * 255))
|
|
tmp_win.remove(tmp_vte)
|
|
del(tmp_vte)
|
|
del(tmp_win)
|
|
else:
|
|
bgcolor = Gdk.RGBA()
|
|
bgcolor = profiles[profile]['background_color']
|
|
if profiles[profile]['background_type'] == 'transparent':
|
|
bgalpha = profiles[profile]['background_darkness']
|
|
else:
|
|
bgalpha = "1"
|
|
|
|
munged_profile = "".join([c if c.isalnum() else "-" for c in profile])
|
|
css += template % (munged_profile, bgcolor, bgalpha)
|
|
|
|
style_provider = Gtk.CssProvider()
|
|
style_provider.load_from_data(css)
|
|
self.style_providers.append(style_provider)
|
|
|
|
# Attempt to load some theme specific stylistic tweaks for appearances
|
|
usr_theme_dir = os.path.expanduser('~/.local/share/themes')
|
|
(head, _tail) = os.path.split(borg.__file__)
|
|
app_theme_dir = os.path.join(head, 'themes')
|
|
|
|
theme_name = self.gtk_settings.get_property('gtk-theme-name')
|
|
|
|
theme_part_list = ['terminator.css']
|
|
if self.config['extra_styling']: # checkbox_style - needs adding to prefs
|
|
theme_part_list.append('terminator_styling.css')
|
|
for theme_part_file in theme_part_list:
|
|
for theme_dir in [usr_theme_dir, app_theme_dir]:
|
|
path_to_theme_specific_css = os.path.join(theme_dir,
|
|
theme_name,
|
|
'gtk-3.0/apps',
|
|
theme_part_file)
|
|
if os.path.isfile(path_to_theme_specific_css):
|
|
style_provider = Gtk.CssProvider()
|
|
style_provider.connect('parsing-error', self.on_css_parsing_error)
|
|
try:
|
|
style_provider.load_from_path(path_to_theme_specific_css)
|
|
except GError:
|
|
# Hmmm. Should we try to provide GTK version specific files here on failure?
|
|
gtk_version_string = '.'.join([str(Gtk.get_major_version()),
|
|
str(Gtk.get_minor_version()),
|
|
str(Gtk.get_micro_version())])
|
|
err('Error(s) loading css from %s into Gtk %s' % (path_to_theme_specific_css,
|
|
gtk_version_string))
|
|
self.style_providers.append(style_provider)
|
|
break
|
|
|
|
# Size the GtkPaned splitter handle size.
|
|
css = ""
|
|
if self.config['handle_size'] in xrange(0, 21):
|
|
css += """
|
|
.terminator-terminal-window GtkPaned,
|
|
.terminator-terminal-window paned {
|
|
-GtkPaned-handle-size: %s; }
|
|
""" % self.config['handle_size']
|
|
style_provider = Gtk.CssProvider()
|
|
style_provider.load_from_data(css)
|
|
self.style_providers.append(style_provider)
|
|
|
|
# Apply the providers, incrementing priority so they don't cancel out
|
|
# each other
|
|
for idx in xrange(0, len(self.style_providers)):
|
|
Gtk.StyleContext.add_provider_for_screen(
|
|
Gdk.Screen.get_default(),
|
|
self.style_providers[idx],
|
|
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION+idx)
|
|
|
|
# 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()
|
|
|
|
def on_css_parsing_error(self, provider, section, error, user_data=None):
|
|
"""Report CSS parsing issues"""
|
|
file_path = section.get_file().get_path()
|
|
line_no = section.get_end_line() +1
|
|
col_no = section.get_end_position() + 1
|
|
err('%s, at line %d, column %d, of file %s' % (error.message,
|
|
line_no, col_no,
|
|
file_path))
|
|
|
|
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, eventkey2gdkevent(event))
|
|
|
|
def all_emit(self, terminal, type, event):
|
|
"""Emit to all terminals"""
|
|
for term in self.terminals:
|
|
if term != terminal:
|
|
term.vte.emit(type, eventkey2gdkevent(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.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:
|