terminator/terminatorlib/plugins/run_cmd_on_match.py

523 lines
18 KiB
Python

import re
import os
import sys
import subprocess
from gi.repository import Gtk
from gi.repository import GObject
from terminatorlib.util import dbg
import terminatorlib.plugin as plugin
from terminatorlib.config import Config
from terminatorlib.translation import _
from terminatorlib.util import get_config_dir, err, dbg, gerr
(CC_COL_ENABLED, CC_COL_REGEXP, CC_COL_COMMAND) = list(range(0,3))
AVAILABLE = ['RunCmdOnMatchMenu']
# For example, open scripts names outputted by python in Vim at the given line number:
# match = r'\B(/\S+?\.py)\S{2}\sline\s(\d+)' # Python's log file matching
# cmd = "gvim --servername IDE --remote +{1} {0}"
# This class is not useful as is, it needs to be forged through MetaRCOM metaclass to be useful
# (because the API use static class properties).
class RunCmdOnMatch(plugin.URLHandler):
"""Template for a class that run a command when a regexp match something printed on the terminal screen."""
capabilities = ['url_handler']
nameopen = "Open file"
namecopy = "Copy file path"
handler_name = None
match = None
cmd = None
def callback(self, url):
assert(self.__class__.match)
assert(self.__class__.cmd)
try:
found = re.search(self.__class__.match, url)
except Exception as e:
dbg("ERROR while searching in the captured URL: {}".format(e))
return None
if not found:
dbg("ERROR pattern not found")
return None
try:
groups = found.groups()
dbg("Groups: {}".format(groups))
except Exception as e:
dbg("ERROR while accessing groups: {}".format(e))
return None
for group in groups:
if not group:
dbg("ERROR groups not captured correctly: {groups}".format(groups=groups))
return None
try:
runcmd = self.__class__.cmd.format(*groups)
except Exception as e:
err("Exception occurred while formatting the command: {} {}".format(type(e).__name__, e))
dbg("run: {cmd}".format(cmd=runcmd))
subprocess.run(runcmd.split())
# To avoid the fallback to the default URL handler, use the `terminator://` protocol tag.
# Terminator will not try to open the URL, so any string after is just for debugging.
return "terminator://{cmd}".format(cmd=runcmd)
# This metaclass is used to populate RunCmdOnMatch's static members.
class MetaRCOM(type):
"""A meta-class for creating RunCmdOnMatch plugins on the fly."""
def __new__(cls, name, regexp, cmd):
return super().__new__(cls, name, (RunCmdOnMatch,), {"match":regexp, "cmd":cmd, "handler_name":name})
# Add a contextual menu for opening a preference window to configure regexp/commands.
# FIXME this is essentially the same code than custom_commands, one need to refactor to keep a single codebase
# (and waiting for a plugin's preference window).
class RunCmdOnMatchMenu(plugin.MenuItem):
"""Add custom match/commands preference setting to the terminal menu"""
capabilities = ['terminal_menu']
cmd_list = {}
conf_file = os.path.join(get_config_dir(),"run_cmd_on_match")
def __init__( self):
config = Config()
sections = config.plugin_get_config(self.__class__.__name__)
if not isinstance(sections, dict):
return
noord_cmds = []
for part in sections:
s = sections[part]
if not ("regexp" in s and "command" in s):
dbg("Ignoring section %s" % s)
continue
regexp = s["regexp"]
command = s["command"]
enabled = s["enabled"] and s["enabled"] or False
if "position" in s:
self.cmd_list[int(s["position"])] = {'enabled' : enabled,
'regexp' : regexp,
'command' : command
}
else:
noord_cmds.append(
{'enabled' : enabled,
'regexp' : regexp,
'command' : command
}
)
for cmd in noord_cmds:
self.cmd_list[len(self.cmd_list)] = cmd
self._load_configured_handlers()
def callback(self, menuitems, menu, terminal):
"""Add our menu items to the menu"""
submenus = {}
item = Gtk.MenuItem.new_with_mnemonic(_('_Run command on matches'))
menuitems.append(item)
submenu = Gtk.Menu()
item.set_submenu(submenu)
menuitem = Gtk.MenuItem.new_with_mnemonic(_('_Preferences'))
menuitem.connect("activate", self.configure)
submenu.append(menuitem)
menuitem = Gtk.SeparatorMenuItem()
submenu.append(menuitem)
theme = Gtk.IconTheme.get_default()
def _save_config(self):
config = Config()
config.plugin_del_config(self.__class__.__name__)
i = 0
for command in [ self.cmd_list[key] for key in sorted(self.cmd_list.keys()) ] :
enabled = command['enabled']
regexp = command['regexp']
command = command['command']
item = {}
item['enabled'] = enabled
item['regexp'] = regexp
item['command'] = command
item['position'] = i
config.plugin_set(self.__class__.__name__, regexp, item)
i = i + 1
config.save()
self._load_configured_handlers()
def _load_configured_handlers(self):
"""Forge an URLhandler plugin and hide it in the available ones."""
me = sys.modules[__name__] # Current module.
config = Config()
for key,handler in [ (key,self.cmd_list[key]) for key in sorted(self.cmd_list.keys()) ] :
# Forge a hidden/managed plugin
# (names starting with an underscore will not be displayed in the preference/plugins window).
rcom_name = "_RunCmdOnMatch_{}".format(key) # key is just the index
# Make a handler class.
RCOM = MetaRCOM(rcom_name, handler["regexp"], handler["command"])
# Instantiate the class.
setattr(me, rcom_name, RCOM)
if rcom_name not in AVAILABLE:
AVAILABLE.append(rcom_name)
dbg("add {} to the list of URL handlers: '{}' -> '{}'".format(rcom_name, RCOM.match, RCOM.cmd))
if handler['enabled'] and rcom_name not in config["enabled_plugins"]:
config["enabled_plugins"].append(rcom_name)
config.save()
def _execute(self, widget, data):
command = data['command']
if command[-1] != '\n':
command = command + '\n'
for terminal in data['terminals']:
terminal.vte.feed_child(command.encode())
def configure(self, widget, data = None):
ui = {}
dbox = Gtk.Dialog(
_("Run command on match Configuration"),
None,
Gtk.DialogFlags.MODAL,
(
_("_Cancel"), Gtk.ResponseType.REJECT,
_("_OK"), Gtk.ResponseType.ACCEPT
)
)
dbox.set_transient_for(widget.get_toplevel())
icon_theme = Gtk.IconTheme.get_default()
if icon_theme.lookup_icon('terminator-run-cmd-on-match', 48, 0):
dbox.set_icon_name('terminator-run-cmd-on-match')
else:
dbg('Unable to load Terminator run-cmd-on-match icon')
icon = dbox.render_icon(Gtk.STOCK_DIALOG_INFO, Gtk.IconSize.BUTTON)
dbox.set_icon(icon)
store = Gtk.ListStore(bool, str, str)
for command in [ self.cmd_list[key] for key in sorted(self.cmd_list.keys()) ]:
store.append([command['enabled'], command['regexp'], command['command']])
treeview = Gtk.TreeView(store)
#treeview.connect("cursor-changed", self.on_cursor_changed, ui)
selection = treeview.get_selection()
selection.set_mode(Gtk.SelectionMode.SINGLE)
selection.connect("changed", self.on_selection_changed, ui)
ui['treeview'] = treeview
renderer = Gtk.CellRendererToggle()
renderer.connect('toggled', self.on_toggled, ui)
column = Gtk.TreeViewColumn(_("Enabled"), renderer, active=CC_COL_ENABLED)
treeview.append_column(column)
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(_("regexp"), renderer, text=CC_COL_REGEXP)
treeview.append_column(column)
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(_("Command"), renderer, text=CC_COL_COMMAND)
treeview.append_column(column)
scroll_window = Gtk.ScrolledWindow()
scroll_window.set_size_request(500, 250)
scroll_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scroll_window.add_with_viewport(treeview)
hbox = Gtk.HBox()
hbox.pack_start(scroll_window, True, True, 0)
dbox.vbox.pack_start(hbox, True, True, 0)
button_box = Gtk.VBox()
button = Gtk.Button(_("Top"))
button_box.pack_start(button, False, True, 0)
button.connect("clicked", self.on_goto_top, ui)
button.set_sensitive(False)
ui['button_top'] = button
button = Gtk.Button(_("Up"))
button_box.pack_start(button, False, True, 0)
button.connect("clicked", self.on_go_up, ui)
button.set_sensitive(False)
ui['button_up'] = button
button = Gtk.Button(_("Down"))
button_box.pack_start(button, False, True, 0)
button.connect("clicked", self.on_go_down, ui)
button.set_sensitive(False)
ui['button_down'] = button
button = Gtk.Button(_("Last"))
button_box.pack_start(button, False, True, 0)
button.connect("clicked", self.on_goto_last, ui)
button.set_sensitive(False)
ui['button_last'] = button
button = Gtk.Button(_("New"))
button_box.pack_start(button, False, True, 0)
button.connect("clicked", self.on_new, ui)
ui['button_new'] = button
button = Gtk.Button(_("Edit"))
button_box.pack_start(button, False, True, 0)
button.set_sensitive(False)
button.connect("clicked", self.on_edit, ui)
ui['button_edit'] = button
button = Gtk.Button(_("Delete"))
button_box.pack_start(button, False, True, 0)
button.connect("clicked", self.on_delete, ui)
button.set_sensitive(False)
ui['button_delete'] = button
hbox.pack_start(button_box, False, True, 0)
self.dbox = dbox
dbox.show_all()
res = dbox.run()
if res == Gtk.ResponseType.ACCEPT:
self.update_cmd_list(store)
self._save_config()
del(self.dbox)
dbox.destroy()
return
def update_cmd_list(self, store):
iter = store.get_iter_first()
self.cmd_list = {}
i=0
while iter:
(enabled, regexp, command) = store.get(iter,
CC_COL_ENABLED,
CC_COL_REGEXP,
CC_COL_COMMAND)
self.cmd_list[i] = {'enabled' : enabled,
'regexp': regexp,
'command' : command}
iter = store.iter_next(iter)
i = i + 1
def on_toggled(self, widget, path, data):
treeview = data['treeview']
store = treeview.get_model()
iter = store.get_iter(path)
(enabled, regexp, command) = store.get(iter,
CC_COL_ENABLED,
CC_COL_REGEXP,
CC_COL_COMMAND
)
store.set_value(iter, CC_COL_ENABLED, not enabled)
def on_selection_changed(self,selection, data=None):
treeview = selection.get_tree_view()
(model, iter) = selection.get_selected()
data['button_top'].set_sensitive(iter is not None)
data['button_up'].set_sensitive(iter is not None)
data['button_down'].set_sensitive(iter is not None)
data['button_last'].set_sensitive(iter is not None)
data['button_edit'].set_sensitive(iter is not None)
data['button_delete'].set_sensitive(iter is not None)
def _create_command_dialog(self, enabled_var = False, regexp_var = "", command_var = ""):
dialog = Gtk.Dialog(
_("New Command"),
None,
Gtk.DialogFlags.MODAL,
(
_("_Cancel"), Gtk.ResponseType.REJECT,
_("_OK"), Gtk.ResponseType.ACCEPT
)
)
dialog.set_transient_for(self.dbox)
table = Gtk.Table(3, 2)
label = Gtk.Label(label=_("Enabled:"))
table.attach(label, 0, 1, 0, 1)
enabled = Gtk.CheckButton()
enabled.set_active(enabled_var)
table.attach(enabled, 1, 2, 0, 1)
label = Gtk.Label(label=_("regexp:"))
table.attach(label, 0, 1, 1, 2)
regexp = Gtk.Entry()
regexp.set_text(regexp_var)
table.attach(regexp, 1, 2, 1, 2)
label = Gtk.Label(label=_("Command:"))
table.attach(label, 0, 1, 2, 3)
command = Gtk.Entry()
command.set_text(command_var)
table.attach(command, 1, 2, 2, 3)
dialog.vbox.pack_start(table, True, True, 0)
dialog.show_all()
return (dialog,enabled,regexp,command)
def on_new(self, button, data):
(dialog,enabled,regexp,command) = self._create_command_dialog()
res = dialog.run()
item = {}
if res == Gtk.ResponseType.ACCEPT:
item['enabled'] = enabled.get_active()
item['regexp'] = regexp.get_text()
item['command'] = command.get_text()
if item['regexp'] == '' or item['command'] == '':
err = Gtk.MessageDialog(dialog,
Gtk.DialogFlags.MODAL,
Gtk.MessageType.ERROR,
Gtk.ButtonsType.CLOSE,
_("You need to define a regexp and command")
)
err.run()
err.destroy()
else:
# we have a new command
store = data['treeview'].get_model()
iter = store.get_iter_first()
regexp_exist = False
while iter != None:
if store.get_value(iter,CC_COL_REGEXP) == item['regexp']:
regexp_exist = True
break
iter = store.iter_next(iter)
if not regexp_exist:
store.append((item['enabled'], item['regexp'], item['command']))
else:
gerr(_("regexp *%s* already exist") % item['regexp'])
dialog.destroy()
def on_goto_top(self, button, data):
treeview = data['treeview']
selection = treeview.get_selection()
(store, iter) = selection.get_selected()
if not iter:
return
firstiter = store.get_iter_first()
store.move_before(iter, firstiter)
def on_go_up(self, button, data):
treeview = data['treeview']
selection = treeview.get_selection()
(store, iter) = selection.get_selected()
if not iter:
return
tmpiter = store.get_iter_first()
if(store.get_path(tmpiter) == store.get_path(iter)):
return
while tmpiter:
next = store.iter_next(tmpiter)
if(store.get_path(next) == store.get_path(iter)):
store.swap(iter, tmpiter)
break
tmpiter = next
def on_go_down(self, button, data):
treeview = data['treeview']
selection = treeview.get_selection()
(store, iter) = selection.get_selected()
if not iter:
return
next = store.iter_next(iter)
if next:
store.swap(iter, next)
def on_goto_last(self, button, data):
treeview = data['treeview']
selection = treeview.get_selection()
(store, iter) = selection.get_selected()
if not iter:
return
lastiter = iter
tmpiter = store.get_iter_first()
while tmpiter:
lastiter = tmpiter
tmpiter = store.iter_next(tmpiter)
store.move_after(iter, lastiter)
def on_delete(self, button, data):
treeview = data['treeview']
selection = treeview.get_selection()
(store, iter) = selection.get_selected()
if iter:
store.remove(iter)
return
def on_edit(self, button, data):
treeview = data['treeview']
selection = treeview.get_selection()
(store, iter) = selection.get_selected()
if not iter:
return
(dialog,enabled,regexp,command) = self._create_command_dialog(
enabled_var = store.get_value(iter, CC_COL_ENABLED),
regexp_var = store.get_value(iter, CC_COL_REGEXP),
command_var = store.get_value(iter, CC_COL_COMMAND)
)
res = dialog.run()
item = {}
if res == Gtk.ResponseType.ACCEPT:
item['enabled'] = enabled.get_active()
item['regexp'] = regexp.get_text()
item['command'] = command.get_text()
if item['regexp'] == '' or item['command'] == '':
err = Gtk.MessageDialog(dialog,
Gtk.DialogFlags.MODAL,
Gtk.MessageType.ERROR,
Gtk.ButtonsType.CLOSE,
_("You need to define a regexp and a command")
)
err.run()
err.destroy()
else:
tmpiter = store.get_iter_first()
regexp_exist = False
while tmpiter != None:
if store.get_path(tmpiter) != store.get_path(iter) and store.get_value(tmpiter,CC_COL_REGEXP) == item['regexp']:
regexp_exist = True
break
tmpiter = store.iter_next(tmpiter)
if not regexp_exist:
store.set(iter,
CC_COL_ENABLED,item['enabled'],
CC_COL_REGEXP, item['regexp'],
CC_COL_COMMAND, item['command']
)
else:
gerr(_("regexp *%s* already exist") % item['regexp'])
dialog.destroy()