From 0ae2aba1672e82de46e6050c414fa41b926b4be9 Mon Sep 17 00:00:00 2001 From: Vishweshwar Saran Singh Deo Date: Sat, 19 Nov 2022 20:06:24 +0530 Subject: [PATCH] 1) Mouse less/free opening / yanking feature 2) plugin_util file to assist this and other plugins with key-press Helper function 3) adding Key Binding Help Functions in above (2) to config and preferences->keybinding to have a consistent behavior --- terminatorlib/config.py | 15 +- terminatorlib/plugin_util.py | 93 +++++++++++ .../plugins/mousefree_url_handler.py | 158 ++++++++++++++++++ terminatorlib/prefseditor.py | 24 +++ 4 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 terminatorlib/plugin_util.py create mode 100644 terminatorlib/plugins/mousefree_url_handler.py diff --git a/terminatorlib/config.py b/terminatorlib/config.py index daaa1964..4c6307bd 100644 --- a/terminatorlib/config.py +++ b/terminatorlib/config.py @@ -77,6 +77,7 @@ from configobj import ConfigObj, flatten_errors from validate import Validator from .borg import Borg from .util import dbg, err, DEBUG, get_system_config_dir, get_config_dir, dict_diff, update_config_to_cell_height +from terminatorlib.plugin_util import KeyBindUtil from gi.repository import Gio @@ -718,7 +719,19 @@ class ConfigBase(Borg): for section_name in ['global_config', 'keybindings']: dbg('Processing section: %s' % section_name) section = getattr(self, section_name) - parser[section_name] = dict_diff(DEFAULTS[section_name], section) + if section_name == 'keybindings': + # for plugin KeyBindUtil assist in plugin_util + keybindutil = KeyBindUtil(); + keyb_keys = keybindutil.get_act_to_keys() + # we only need keys as a reference so to match them + # against new values + keyb_keys = dict.fromkeys(keyb_keys, "") + + default_merged_section = {**keyb_keys, **DEFAULTS[section_name]} + merged_section = {**keyb_keys, **section} + parser[section_name] = dict_diff(default_merged_section, merged_section) + else: + parser[section_name] = dict_diff(DEFAULTS[section_name], section) from .configjson import JSON_PROFILE_NAME, JSON_LAYOUT_NAME diff --git a/terminatorlib/plugin_util.py b/terminatorlib/plugin_util.py new file mode 100644 index 00000000..e4a7c5c4 --- /dev/null +++ b/terminatorlib/plugin_util.py @@ -0,0 +1,93 @@ +""" +-Basic plugin util for key-press handling, has all mapping to be used +in layout keybindings + +Vishweshwar Saran Singh Deo vssdeo@gmail.com +""" + +from gi.repository import Gtk, Gdk + +from terminatorlib.util import get_config_dir, err, dbg, gerr +from terminatorlib.keybindings import Keybindings, KeymapError + +PLUGIN_UTIL_DESC = 0 +PLUGIN_UTIL_ACT = 1 +PLUGIN_UTIL_KEYS = 2 + +#FIXME to sync this with keybinding preferences +class KeyBindUtil: + + keybindings = Keybindings() + + map_key_to_act = {} + map_act_to_keys = {} + map_act_to_desc = {} + + + #Example + # bind + # first param is desc, second is action str + # self.keyb.bindkey([PluginUrlFindNext , PluginUrlActFindNext, "j"]) + # + # get action name + # act = self.keyb.keyaction(event) + + # if act == "url_find_next": + + + #check map key_val_mask -> action + def _check_keybind_change(self, key): + act = key[PLUGIN_UTIL_ACT] + for key_val_mask in self.map_key_to_act: + old_act = self.map_key_to_act[key_val_mask] + if act == old_act: + return key_val_mask + return None + + #check in config before binding + def bindkey_check_config(self, key, config): + actstr = key[PLUGIN_UTIL_ACT] + kbsect = config.base.get_item('keybindings') + keystr = kbsect[actstr] if actstr in kbsect else "" + dbg("bindkey_check_config:action (%s) key str:(%s)" % (actstr, keystr)) + if len(keystr): + key[PLUGIN_UTIL_KEYS] = keystr + dbg("found new Action->KeyVal in config: (%s, %s)" + % (actstr, keystr)); + self.bindkey(key) + + def bindkey(self, key): + (keyval, mask) = self.keybindings._parsebinding(key[PLUGIN_UTIL_KEYS]) + mask = Gdk.ModifierType(mask) + + ret = (keyval, mask) + dbg("bindkey: (%s) (%s)" % (key[PLUGIN_UTIL_KEYS], str(ret))) + + #remove if any old key_val_mask + old_key_val_mask = self._check_keybind_change(key) + if old_key_val_mask: + dbg("found old key binding, removing: (%s)" % str(old_key_val_mask)) + del self.map_key_to_act[old_key_val_mask] + + #map key-val-mask to action, used to ease key-press management + self.map_key_to_act[ret] = key[PLUGIN_UTIL_ACT] + + + #map action to key-combo-str, used in preferences->keybinding + self.map_act_to_keys[key[PLUGIN_UTIL_ACT]] = key[PLUGIN_UTIL_KEYS] + #map action to key-combo description, in used preferences->keybinding + self.map_act_to_desc[key[PLUGIN_UTIL_ACT]] = key[PLUGIN_UTIL_DESC] + + def keyaction(self, event): + #FIXME MOD2 mask comes in the event, remove + event.state &= ~Gdk.ModifierType.MOD2_MASK + + ret = (event.keyval, event.state) + dbg("keyaction: (%s)" % str(ret)) + return self.map_key_to_act.get(ret, None) + + def get_act_to_keys(self): + return self.map_act_to_keys + + def get_act_to_desc(self): + return self.map_act_to_desc diff --git a/terminatorlib/plugins/mousefree_url_handler.py b/terminatorlib/plugins/mousefree_url_handler.py new file mode 100644 index 00000000..37c80f90 --- /dev/null +++ b/terminatorlib/plugins/mousefree_url_handler.py @@ -0,0 +1,158 @@ +""" +- With min changes to main code base, implementing mouse free url handling. +- Using shortcuts to browse URL, next / previous, clear search. Selected URL +is copied to clipboard. + +- Vishweshwar Saran Singh Deo vssdeo@gmail.com +""" + +import gi +gi.require_version('Vte', '2.91') # vte-0.38 (gnome-3.14) +from gi.repository import Vte + +from terminatorlib.terminator import Terminator + +from terminatorlib.config import Config +import terminatorlib.plugin as plugin +from terminatorlib.plugin_util import KeyBindUtil + +from terminatorlib.util import get_config_dir, err, dbg, gerr +from terminatorlib.keybindings import Keybindings, KeymapError +from terminatorlib import regex + +import re + + +AVAILABLE = ['MouseFreeURLHandler'] + +PluginUrlActFindNext = "plugin_url_find_next" +PluginUrlActFindPrev = "plugin_url_find_prev" +PluginUrlActEsc = "plugin_url_esc" +PluginUrlActLaunch = "plugin_url_launch" + + +PluginUrlFindNext = "Plugin Url Find Next" +PluginUrlFindPrev = "Plugin Url Find Prev" +PluginUrlEsc = "Plugin Url Esc" +PluginUrlLaunch = "Plugin Url Launch" + +class MouseFreeURLHandler(plugin.Plugin): + + capabilities = ['url_handler'] + handler_name = 'MouseFreeURLHandler' + nameopen = None + namecopy = None + match = None + + flag_http_on = False + keyb = KeyBindUtil() + matches = [] + matches_ptr = -1 + #basic pattern + searchtext = "https?\:\/\/[^\s]+[\/\w]" + + def __init__(self): + self.connect_signals() + + config = Config() + self.keyb.bindkey_check_config([PluginUrlFindNext , PluginUrlActFindNext, "j"], config) + self.keyb.bindkey_check_config([PluginUrlFindPrev , PluginUrlActFindPrev, "k"], config) + self.keyb.bindkey_check_config([PluginUrlEsc , PluginUrlActEsc, "Escape"], config) + self.keyb.bindkey_check_config([PluginUrlLaunch, PluginUrlActLaunch, "Return"], config) + + def connect_signals(self): + self.windows = Terminator().get_windows() + for window in self.windows: + window.connect('key-press-event', self.on_keypress) + return + + def callback(self, url): + """Actually we don't need to do anything for this to work""" + return(url) + + def extract(self): + #can we do extract more efficiently + col, row = self.vte.get_cursor_position() + (txt, attr) = self.vte.get_text_range(0,0,row, col) + self.matches = re.findall(self.searchtext, txt) + self.matches_ptr = len(self.matches)-1 + + def get_term(self): + return Terminator().last_focused_term + + def get_selected_url(self): + if len(self.matches): + dbg("selected URL (%s %s)" % (self.matches_ptr, self.matches[self.matches_ptr])) + return self.matches[self.matches_ptr] + dbg("selected URL (%s %s)" % (self.matches_ptr, "not found")) + return None + + def on_keypress(self, widget, event): + act = self.keyb.keyaction(event) + dbg("keyaction: (%s) (%s)" % (str(act), event.keyval)) + + if act == PluginUrlActFindNext: + if not self.flag_http_on: + dbg("search URL on") + self.search() + self.extract() + #so when we start search last item be selected + self.vte.search_find_previous() + self.get_selected_url() # dbg url print + self.vte.copy_clipboard() + return True + else: + self.vte.search_find_next() + + if (self.matches_ptr < len(self.matches)-1): + self.matches_ptr += 1 + else: + self.matches_ptr = 0 + + self.vte.copy_clipboard() + self.get_selected_url() # dbg url print + return True + + if act == PluginUrlActFindPrev: + if not self.flag_http_on: + self.search() + self.extract() + self.get_selected_url() # dbg url print + self.vte.search_find_previous() + self.vte.copy_clipboard() + return True + + self.vte.search_find_previous() + + if self.matches_ptr > 0: + self.matches_ptr -= 1 + elif len(self.matches): + self.matches_ptr = len(self.matches)-1 + + self.vte.copy_clipboard() + self.get_selected_url() # dbg url print + return True + + if act == PluginUrlActEsc: + self.matches = [] + self.vte = self.get_term().get_vte() + self.vte.search_set_regex(None, 0) + self.flag_http_on = False + dbg("search URL off") + + if act == PluginUrlActLaunch: + url = self.get_selected_url() + if url: + self.get_term().open_url(url, prepare=False) + + def search(self): + self.flag_http_on = True + self.vte = self.get_term().get_vte() + + self.vte.search_set_wrap_around(True) + regex_flags_pcre2 = (regex.FLAGS_PCRE2 | regex.PCRE2_CASELESS) + searchre = Vte.Regex.new_for_search(self.searchtext, + len(self.searchtext), regex_flags_pcre2) + + self.vte.search_set_regex(searchre, 0) + diff --git a/terminatorlib/prefseditor.py b/terminatorlib/prefseditor.py index 10339c30..2a394c62 100755 --- a/terminatorlib/prefseditor.py +++ b/terminatorlib/prefseditor.py @@ -18,6 +18,8 @@ from .terminator import Terminator from .plugin import PluginRegistry from .version import APP_NAME +from .plugin_util import KeyBindUtil + def get_color_string(widcol): return('#%02x%02x%02x' % (widcol.red>>8, widcol.green>>8, widcol.blue>>8)) @@ -419,6 +421,15 @@ class PrefsEditor: liststore = widget.get_model() liststore.set_sort_column_id(0, Gtk.SortType.ASCENDING) keybindings = self.config['keybindings'] + + keybindutil = KeyBindUtil() + plugin_keyb_act = keybindutil.get_act_to_keys() + plugin_keyb_desc = keybindutil.get_act_to_desc() + #merge give preference to main bindings over plugin + keybindings = {**plugin_keyb_act, **keybindings} + self.keybindingnames = {**plugin_keyb_desc, **self.keybindingnames} + #dbg("appended actions %s names %s" % (keybindings, self.keybindingnames)) + for keybinding in keybindings: keyval = 0 mask = 0 @@ -1797,6 +1808,19 @@ class PrefsEditor: binding = liststore.get_value(liststore.get_iter(path), 0) accel = Gtk.accelerator_name(key, mods) self.config['keybindings'][binding] = accel + + keybindutil = KeyBindUtil() + plugin_keyb_desc = keybindutil.get_act_to_desc() + + if binding in plugin_keyb_desc: + if binding in plugin_keyb_desc: + desc = plugin_keyb_desc[binding] + dbg("modifying plugin binding: %s, %s, %s" % (desc, binding, accel)) + keybindutil.bindkey([desc, binding, accel]) + else: + dbg("skipping: %s" % binding) + pass + self.config.save() def on_cellrenderer_accel_cleared(self, liststore, path):