diff --git a/terminatorlib/config.py b/terminatorlib/config.py index 1d62254c..f58a0716 100644 --- a/terminatorlib/config.py +++ b/terminatorlib/config.py @@ -195,6 +195,7 @@ DEFAULTS = { 'layout_launcher' : 'l', 'next_profile' : '', 'previous_profile' : '', + 'preferences' : '', 'help' : 'F1' }, 'profiles': { diff --git a/terminatorlib/plugins/command_notify.py b/terminatorlib/plugins/command_notify.py new file mode 100644 index 00000000..83849332 --- /dev/null +++ b/terminatorlib/plugins/command_notify.py @@ -0,0 +1,67 @@ +""" +This plugin will launch a notification when a long running command finishes +and terminal is not active. + +It uses VTE's special sequence which is sent when shell prints the prompt. It +depends on https://github.com/GNOME/vte/blob/vte-0-58/src/vte.sh (which has to +be added to /etc/profile.d) and you need to ensure `__vte_prompt_command` is +executed on `PROMPT_COMMAND` in Bash or in `precmd_functions` in Zsh. + +This plugin also relies on the widely distributed vte patches to wire in a +`notification_received` signal to catch the OSC escape sequence in the prompt + +Code is adapted from https://github.com/x4lldux/terminator-long-cmd-notify +Thanks to @xll4dux on Github for the code and his permission to use it + +""" +import terminatorlib.plugin as plugin +from terminatorlib.terminator import Terminator +import gi +gi.require_version('Notify', '0.7') +from gi.repository import GObject, GLib, Notify, Vte +VERSION = '0.1.0' + +### Test for proper signal +try: + Vte.Terminal().connect('notification-received',lambda *args: None,None) + AVAILABLE = ['CommandNotify'] +except TypeError as e: + pass + +class CommandNotify(plugin.Plugin): + capabilities = ['command_watch'] + watched = set() + + def __init__(self): + self.update_watched() + Notify.init('Terminator') + return None + + def update_watched(self): + """Updates the list of watched terminals""" + new_watched = set() + for term in Terminator().terminals: + new_watched.add(term) + if not term in self.watched: + vte = term.get_vte() + term.connect('focus-out', self.update_watched_delayed, None) + vte.connect('focus-out-event', self. + update_watched_delayed, None) + notify = vte.connect( + 'notification-received', self.notification_received, term) + else: + notify = None + self.watched = new_watched + + def update_watched_delayed(self, term, event, arg1 = None): + def add_watch(self): + self.update_watched() + return False + GObject.idle_add(add_watch, self) + return True + + def notification_received(self, vte, summary, body, _terminator_term): + Notify.Notification.new(summary, body, 'terminator').show( + ) if not vte.has_focus() else None + return None + diff --git a/terminatorlib/prefseditor.py b/terminatorlib/prefseditor.py index c77ab7c9..a198fc5c 100755 --- a/terminatorlib/prefseditor.py +++ b/terminatorlib/prefseditor.py @@ -174,6 +174,7 @@ class PrefsEditor: 'layout_launcher' : _('Open layout launcher window'), 'next_profile' : _('Switch to next profile'), 'previous_profile' : _('Switch to previous profile'), + 'preferences' : _('Open the Preferences window'), 'help' : _('Open the manual') } @@ -1673,7 +1674,9 @@ class PrefsEditor: def on_cellrenderer_accel_edited(self, liststore, path, key, mods, _code): """Handle an edited keybinding""" - if mods & Gdk.ModifierType.SHIFT_MASK: + # Ignore `Gdk.KEY_Tab` so that `Shift+Tab` is displayed as `Shift+Tab` + # in `Preferences>Keybindings` and NOT `Left Tab` (see `Gdk.KEY_ISO_Left_Tab`). + if mods & Gdk.ModifierType.SHIFT_MASK and key != Gdk.KEY_Tab: key_with_shift = Gdk.Keymap.translate_keyboard_state( self.keybindings.keymap, hardware_keycode=_code, @@ -1687,6 +1690,7 @@ class PrefsEditor: # Shift key. if key_with_shift.level != 0 and keyval_lower == keyval_upper: mods = Gdk.ModifierType(mods & ~Gdk.ModifierType.SHIFT_MASK) + key = key_with_shift.keyval accel = Gtk.accelerator_name(key, mods) current_binding = liststore.get_value(liststore.get_iter(path), 0) diff --git a/terminatorlib/terminal.py b/terminatorlib/terminal.py index df3bf29c..c012475d 100644 --- a/terminatorlib/terminal.py +++ b/terminatorlib/terminal.py @@ -23,6 +23,7 @@ from .factory import Factory from .terminator import Terminator from .titlebar import Titlebar from .terminal_popup_menu import TerminalPopupMenu +from .prefseditor import PrefsEditor from .searchbar import Searchbar from .translation import _ from .signalman import Signalman @@ -1512,14 +1513,18 @@ class Terminal(Gtk.VBox): dbg('Forking shell: "%s" with args: %s' % (shell, args)) args.insert(0, shell) - result, self.pid = self.vte.spawn_sync(Vte.PtyFlags.DEFAULT, - self.cwd, - args, - envv, - GLib.SpawnFlags.FILE_AND_ARGV_ZERO | GLib.SpawnFlags.DO_NOT_REAP_CHILD, - None, - None, - None) + self.pid = self.vte.spawn_async( + Vte.PtyFlags.DEFAULT, + self.cwd, + args, + envv, + GLib.SpawnFlags.FILE_AND_ARGV_ZERO | GLib.SpawnFlags.DO_NOT_REAP_CHILD, + None, + None, + -1, + None, + None, + None) self.command = shell self.titlebar.update() @@ -2012,6 +2017,9 @@ class Terminal(Gtk.VBox): def key_line_down(self): self.scroll_by_line(1) + def key_preferences(self): + PrefsEditor(self) + def key_help(self): manual_index_page = manual_lookup() if manual_index_page: diff --git a/tests/test_prefseditor_keybindings.py b/tests/test_prefseditor_keybindings.py index 2fd2314d..4fa9db70 100644 --- a/tests/test_prefseditor_keybindings.py +++ b/tests/test_prefseditor_keybindings.py @@ -178,3 +178,76 @@ def test_duplicate_accels_not_possible_to_set(accel_params): assert default_accelerator == new_accelerator reset_config_keybindings() + + +@pytest.mark.parametrize( + "input_key_params,expected_accel", + [ + # 1) `Ctrl+Shift+1` should become `Ctrl+!` + ( + (Gdk.KEY_1, CONTROL_SHIFT_MOD, 10), + (Gdk.KEY_exclam, Gdk.ModifierType.CONTROL_MASK), + ), + # 2) `Ctrl+a` shouldn't change + ( + (Gdk.KEY_a, Gdk.ModifierType.CONTROL_MASK, 38), + (Gdk.KEY_a, Gdk.ModifierType.CONTROL_MASK), + ), + # 3) `Ctrl+Shift+a` shouldn't change + ((Gdk.KEY_a, CONTROL_SHIFT_MOD, 38), (Gdk.KEY_a, CONTROL_SHIFT_MOD),), + # 4) `Ctrl+Shift+Alt+F1` shouldn't change + ( + (Gdk.KEY_F1, CONTROL_ALT_SHIFT_MOD, 67), + (Gdk.KEY_F1, CONTROL_ALT_SHIFT_MOD), + ), + # 5) `Shift+Up` shouldn't change + ( + (Gdk.KEY_Up, Gdk.ModifierType.SHIFT_MASK, 111), + (Gdk.KEY_Up, Gdk.ModifierType.SHIFT_MASK), + ), + # 6) `Ctrl+Shift+[` should become `Ctrl+{` + ( + (Gdk.KEY_bracketleft, CONTROL_SHIFT_MOD, 34), + (Gdk.KEY_braceleft, Gdk.ModifierType.CONTROL_MASK), + ), + # 7) `Shift+Tab` shouldn't change + ( + (Gdk.KEY_Tab, Gdk.ModifierType.SHIFT_MASK, 23), + (Gdk.KEY_Tab, Gdk.ModifierType.SHIFT_MASK), + ), + ], +) +def test_keybinding_edit_produce_expected_accels( + input_key_params, expected_accel +): + """ + Tests that editing a key binding using a predefined key combination + `input_key_params` produces the expected accelerator. + """ + from terminatorlib import terminal + from terminatorlib import prefseditor + + term = terminal.Terminal() + prefs_editor = prefseditor.PrefsEditor(term=term) + + widget = prefs_editor.builder.get_object("keybindingtreeview") + liststore = widget.get_model() + + path = 0 # Edit the first listed key binding in `Preferences>Keybindings` + key, mods, hardware_keycode = input_key_params + + prefs_editor.on_cellrenderer_accel_edited( + liststore=liststore, + path=str(path), + key=key, + mods=mods, + _code=hardware_keycode, + ) + + liststore_iter = liststore.get_iter(path) + keyval_after_edit = liststore.get_value(liststore_iter, 2) + mods_after_edit = Gdk.ModifierType(liststore.get_value(liststore_iter, 3)) + + accel_after_edit = (keyval_after_edit, mods_after_edit) + + assert accel_after_edit == expected_accel