Merge lp:~ipv64ever/terminator/custom_commands_plugin_complex_cmd into lp:terminator/gtk3

Proposed by Korotin Igor
Status: Needs review
Proposed branch: lp:~ipv64ever/terminator/custom_commands_plugin_complex_cmd
Merge into: lp:terminator/gtk3
Diff against target: 355 lines (+144/-26)
2 files modified
terminatorlib/plugins/custom_commands.py (+113/-26)
tests/test_custom_command_parser.py (+31/-0)
To merge this branch: bzr merge lp:~ipv64ever/terminator/custom_commands_plugin_complex_cmd
Reviewer Review Type Date Requested Status
Terminator Pending
Review via email: mp+358771@code.launchpad.net

Commit message

Custom Commands Plugin: Added complex option.

This option enables parsing of special characters in command:
  \n - send new line
  \p - add 1 sec delay

This new option doesn't conflict with old Custom Command config.
By default all existing commands get this option disabled

Added unit tests of command parsing command.

Description of the change

At this moment Custom commands plugin allow to send only one string with ending '\n' character.

I suggest to add ability to create complex command. Each part of command is sent one by one.
For example command "command1\ncommand2\ncommand3" will be made as following:

'command1\n'
'command2\n'
'command3\n'

There's also an ability to add delays between commands using "\p" chars.
Example 'command1\n\pcommand2\n\p\pcommand3' will be made as following:

'command1\n'
1 sec delay
'command2\n'
2 sec delay
'command3\n'

Command parsing affects only on these two "\n" and "\p" characters in command

I use terminator in my work and i often have to login on embedded boards remotely through serial port.
Each time i type login, then send password using custom command, then sudo su and finally root password again. It's anoying especially when i have to do it a lot.

To post a comment you must log in.

Unmerged revisions

1811. By Igor Korotin <email address hidden>

Custom Commands Plugin: Added complex option.

This option enables parsing of special characters in command:
  \n - send new line
  \p - add 1 sec delay

This new option doesn't conflict with old Custom Command config.
By default all existing commands get this option disabled

Added unit tests of command parsing command.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'terminatorlib/plugins/custom_commands.py'
--- terminatorlib/plugins/custom_commands.py 2017-02-06 11:09:36 +0000
+++ terminatorlib/plugins/custom_commands.py 2018-11-14 13:07:15 +0000
@@ -4,11 +4,16 @@
4"""custom_commands.py - Terminator Plugin to add custom command menu entries"""4"""custom_commands.py - Terminator Plugin to add custom command menu entries"""
5import sys5import sys
6import os6import os
7import re
8import threading
79
8# Fix imports when testing this file directly10# Fix imports when testing this file directly
9if __name__ == '__main__':11if __name__ == '__main__':
12 print os.path.dirname(__file__)
10 sys.path.append( os.path.join(os.path.dirname(__file__), "../.."))13 sys.path.append( os.path.join(os.path.dirname(__file__), "../.."))
1114
15import gi
16gi.require_version('Gtk', '3.0')
12from gi.repository import Gtk17from gi.repository import Gtk
13from gi.repository import GObject18from gi.repository import GObject
14import terminatorlib.plugin as plugin19import terminatorlib.plugin as plugin
@@ -16,7 +21,7 @@
16from terminatorlib.translation import _21from terminatorlib.translation import _
17from terminatorlib.util import get_config_dir, err, dbg, gerr22from terminatorlib.util import get_config_dir, err, dbg, gerr
1823
19(CC_COL_ENABLED, CC_COL_NAME, CC_COL_COMMAND) = range(0,3)24(CC_COL_ENABLED, CC_COL_NAME, CC_COL_COMMAND,CC_COL_COMPLEX) = range(0,4)
2025
21# Every plugin you want Terminator to load *must* be listed in 'AVAILABLE'26# Every plugin you want Terminator to load *must* be listed in 'AVAILABLE'
22AVAILABLE = ['CustomCommandsMenu']27AVAILABLE = ['CustomCommandsMenu']
@@ -30,6 +35,7 @@
30 def __init__( self):35 def __init__( self):
31 config = Config()36 config = Config()
32 sections = config.plugin_get_config(self.__class__.__name__)37 sections = config.plugin_get_config(self.__class__.__name__)
38
33 if not isinstance(sections, dict):39 if not isinstance(sections, dict):
34 return40 return
35 noord_cmds = []41 noord_cmds = []
@@ -41,16 +47,22 @@
41 name = s["name"]47 name = s["name"]
42 command = s["command"]48 command = s["command"]
43 enabled = s["enabled"] and s["enabled"] or False49 enabled = s["enabled"] and s["enabled"] or False
50 if not (s.has_key("complex")):
51 cmplx = False
52 else:
53 cmplx = s["complex"] and s["complex"] or False
44 if s.has_key("position"):54 if s.has_key("position"):
45 self.cmd_list[int(s["position"])] = {'enabled' : enabled,55 self.cmd_list[int(s["position"])] = {'enabled' : enabled,
46 'name' : name,56 'name' : name,
47 'command' : command57 'command' : command,
58 'complex' : cmplx
48 }59 }
49 else:60 else:
50 noord_cmds.append(61 noord_cmds.append(
51 {'enabled' : enabled,62 {'enabled' : enabled,
52 'name' : name,63 'name' : name,
53 'command' : command64 'command' : command,
65 'complex' : cmplx
54 }66 }
55 )67 )
56 for cmd in noord_cmds:68 for cmd in noord_cmds:
@@ -100,7 +112,7 @@
100 else:112 else:
101 menuitem = Gtk.MenuItem(leaf_name)113 menuitem = Gtk.MenuItem(leaf_name)
102 terminals = terminal.terminator.get_target_terms(terminal)114 terminals = terminal.terminator.get_target_terms(terminal)
103 menuitem.connect("activate", self._execute, {'terminals' : terminals, 'command' : command['command'] })115 menuitem.connect("activate", self._execute, {'terminals' : terminals, 'command' : command['command'], 'complex' : command['complex'] })
104 target_submenu.append(menuitem)116 target_submenu.append(menuitem)
105 117
106 def _save_config(self):118 def _save_config(self):
@@ -109,6 +121,7 @@
109 i = 0121 i = 0
110 for command in [ self.cmd_list[key] for key in sorted(self.cmd_list.keys()) ] :122 for command in [ self.cmd_list[key] for key in sorted(self.cmd_list.keys()) ] :
111 enabled = command['enabled']123 enabled = command['enabled']
124 cmplx = command['complex']
112 name = command['name']125 name = command['name']
113 command = command['command']126 command = command['command']
114 127
@@ -116,18 +129,60 @@
116 item['enabled'] = enabled129 item['enabled'] = enabled
117 item['name'] = name130 item['name'] = name
118 item['command'] = command131 item['command'] = command
132 item['complex'] = cmplx
119 item['position'] = i133 item['position'] = i
120134
121 config.plugin_set(self.__class__.__name__, name, item)135 config.plugin_set(self.__class__.__name__, name, item)
122 i = i + 1136 i = i + 1
123 config.save()137 config.save()
124138
139 def _parse(self, command_str):
140 ret_command_list = []
141 command_list = command_str.split("\\n")
142 delay=0
143
144 for idx, line in enumerate(command_list):
145 if line == '':
146 line = '\n'
147 ret_command_list.append({'command': line, 'delay': delay})
148 else:
149 if line[-1] != '\n':
150 line = line + '\n'
151 # We've got separete command but it might have some other special characters
152 # So first of all - check whether line contains them.
153 obj = re.search(r"(?:\\p)", line)
154 if obj:
155 # Using 0x00 char to parse string.
156 _list = list(filter(None, re.sub(r"(?:\\p)", '\0\\p\0', line).split('\0')))
157 for part in _list:
158 if part == '\\p':
159 delay += 1;
160 else:
161 ret_command_list.append({'command': part, 'delay': delay})
162 delay = 0;
163 else:
164 ret_command_list.append({'command': line, 'delay': delay})
165 return ret_command_list
166
167 def deferred_execute(self, terminal, command, rest_command_list):
168 terminal.vte.feed_child(command, len(command))
169 if rest_command_list:
170 threading.Timer(rest_command_list[0]['delay'], self.deferred_execute, [terminal, rest_command_list[0]['command'], rest_command_list[1:]]).start()
171
125 def _execute(self, widget, data):172 def _execute(self, widget, data):
126 command = data['command']173 cmplx = data['complex']
127 if command[-1] != '\n':174
128 command = command + '\n'175 if cmplx:
129 for terminal in data['terminals']:176 command_list = self._parse(data['command'])
130 terminal.vte.feed_child(command, len(command))177 for terminal in data['terminals']:
178 threading.Timer(command_list[0]['delay'], self.deferred_execute, [terminal, command_list[0]['command'], command_list[1:]]).start()
179
180 else:
181 command = data['command']
182 if command[-1] != '\n':
183 command = command + '\n'
184 for terminal in data['terminals']:
185 terminal.vte.feed_child(command, len(command))
131186
132 def configure(self, widget, data = None):187 def configure(self, widget, data = None):
133 ui = {}188 ui = {}
@@ -150,10 +205,10 @@
150 icon = dbox.render_icon(Gtk.STOCK_DIALOG_INFO, Gtk.IconSize.BUTTON)205 icon = dbox.render_icon(Gtk.STOCK_DIALOG_INFO, Gtk.IconSize.BUTTON)
151 dbox.set_icon(icon)206 dbox.set_icon(icon)
152207
153 store = Gtk.ListStore(bool, str, str)208 store = Gtk.ListStore(bool, str, str, bool)
154209
155 for command in [ self.cmd_list[key] for key in sorted(self.cmd_list.keys()) ]:210 for command in [ self.cmd_list[key] for key in sorted(self.cmd_list.keys()) ]:
156 store.append([command['enabled'], command['name'], command['command']])211 store.append([command['enabled'], command['name'], command['command'], command['complex']])
157 212
158 treeview = Gtk.TreeView(store)213 treeview = Gtk.TreeView(store)
159 #treeview.connect("cursor-changed", self.on_cursor_changed, ui)214 #treeview.connect("cursor-changed", self.on_cursor_changed, ui)
@@ -163,7 +218,7 @@
163 ui['treeview'] = treeview218 ui['treeview'] = treeview
164219
165 renderer = Gtk.CellRendererToggle()220 renderer = Gtk.CellRendererToggle()
166 renderer.connect('toggled', self.on_toggled, ui)221 renderer.connect('toggled', self.on_toggled_enabled, ui)
167 column = Gtk.TreeViewColumn(_("Enabled"), renderer, active=CC_COL_ENABLED)222 column = Gtk.TreeViewColumn(_("Enabled"), renderer, active=CC_COL_ENABLED)
168 treeview.append_column(column)223 treeview.append_column(column)
169224
@@ -175,6 +230,11 @@
175 column = Gtk.TreeViewColumn(_("Command"), renderer, text=CC_COL_COMMAND)230 column = Gtk.TreeViewColumn(_("Command"), renderer, text=CC_COL_COMMAND)
176 treeview.append_column(column)231 treeview.append_column(column)
177232
233 renderer = Gtk.CellRendererToggle()
234 renderer.connect('toggled', self.on_toggled_complex, ui)
235 column = Gtk.TreeViewColumn(_("Complex"), renderer, active=CC_COL_COMPLEX)
236 treeview.append_column(column)
237
178 scroll_window = Gtk.ScrolledWindow()238 scroll_window = Gtk.ScrolledWindow()
179 scroll_window.set_size_request(500, 250)239 scroll_window.set_size_request(500, 250)
180 scroll_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)240 scroll_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
@@ -246,28 +306,43 @@
246 self.cmd_list = {}306 self.cmd_list = {}
247 i=0307 i=0
248 while iter:308 while iter:
249 (enabled, name, command) = store.get(iter,309 (enabled, name, command, cmplx) = store.get(iter,
250 CC_COL_ENABLED,310 CC_COL_ENABLED,
251 CC_COL_NAME,311 CC_COL_NAME,
252 CC_COL_COMMAND)312 CC_COL_COMMAND,
313 CC_COL_COMPLEX)
253 self.cmd_list[i] = {'enabled' : enabled,314 self.cmd_list[i] = {'enabled' : enabled,
254 'name': name,315 'name': name,
255 'command' : command}316 'command' : command,
317 'complex' : cmplx}
256 iter = store.iter_next(iter)318 iter = store.iter_next(iter)
257 i = i + 1319 i = i + 1
258 320
259321
260 def on_toggled(self, widget, path, data):322 def on_toggled_enabled(self, widget, path, data):
261 treeview = data['treeview']323 treeview = data['treeview']
262 store = treeview.get_model()324 store = treeview.get_model()
263 iter = store.get_iter(path)325 iter = store.get_iter(path)
264 (enabled, name, command) = store.get(iter,326 (enabled, name, command, cmplx) = store.get(iter,
265 CC_COL_ENABLED,327 CC_COL_ENABLED,
266 CC_COL_NAME,328 CC_COL_NAME,
267 CC_COL_COMMAND329 CC_COL_COMMAND,
330 CC_COL_COMPLEX
268 )331 )
269 store.set_value(iter, CC_COL_ENABLED, not enabled)332 store.set_value(iter, CC_COL_ENABLED, not enabled)
270333
334 def on_toggled_complex(self, widget, path, data):
335 treeview = data['treeview']
336 store = treeview.get_model()
337 iter = store.get_iter(path)
338 (enabled, name, command, cmplx) = store.get(iter,
339 CC_COL_ENABLED,
340 CC_COL_NAME,
341 CC_COL_COMMAND,
342 CC_COL_COMPLEX
343 )
344 store.set_value(iter, CC_COL_COMPLEX, not cmplx)
345
271346
272 def on_selection_changed(self,selection, data=None):347 def on_selection_changed(self,selection, data=None):
273 treeview = selection.get_tree_view()348 treeview = selection.get_tree_view()
@@ -279,7 +354,7 @@
279 data['button_edit'].set_sensitive(iter is not None)354 data['button_edit'].set_sensitive(iter is not None)
280 data['button_delete'].set_sensitive(iter is not None)355 data['button_delete'].set_sensitive(iter is not None)
281356
282 def _create_command_dialog(self, enabled_var = False, name_var = "", command_var = ""):357 def _create_command_dialog(self, enabled_var = False, name_var = "", command_var = "",cmplx_var = False):
283 dialog = Gtk.Dialog(358 dialog = Gtk.Dialog(
284 _("New Command"),359 _("New Command"),
285 None,360 None,
@@ -290,7 +365,7 @@
290 )365 )
291 )366 )
292 dialog.set_transient_for(self.dbox)367 dialog.set_transient_for(self.dbox)
293 table = Gtk.Table(3, 2)368 table = Gtk.Table(6, 2)
294369
295 label = Gtk.Label(label=_("Enabled:"))370 label = Gtk.Label(label=_("Enabled:"))
296 table.attach(label, 0, 1, 0, 1)371 table.attach(label, 0, 1, 0, 1)
@@ -309,19 +384,28 @@
309 command = Gtk.Entry()384 command = Gtk.Entry()
310 command.set_text(command_var)385 command.set_text(command_var)
311 table.attach(command, 1, 2, 2, 3)386 table.attach(command, 1, 2, 2, 3)
387
388 label = Gtk.Label(label=_("Complex:"))
389 table.attach(label, 0, 1, 3, 4)
390 cmplx = Gtk.CheckButton()
391 cmplx.set_active(cmplx_var)
392 table.attach(cmplx, 1, 2, 3, 4)
393 label = Gtk.Label(label=_('Complex Help: \n \\n - send end of line\n \\p - add 1 second delay'))
394 table.attach(label, 0, 2, 4, 6)
312395
313 dialog.vbox.pack_start(table, True, True, 0)396 dialog.vbox.pack_start(table, True, True, 0)
314 dialog.show_all()397 dialog.show_all()
315 return (dialog,enabled,name,command)398 return (dialog,enabled,name,command,cmplx)
316399
317 def on_new(self, button, data):400 def on_new(self, button, data):
318 (dialog,enabled,name,command) = self._create_command_dialog()401 (dialog,enabled,name,command,cmplx) = self._create_command_dialog()
319 res = dialog.run()402 res = dialog.run()
320 item = {}403 item = {}
321 if res == Gtk.ResponseType.ACCEPT:404 if res == Gtk.ResponseType.ACCEPT:
322 item['enabled'] = enabled.get_active()405 item['enabled'] = enabled.get_active()
323 item['name'] = name.get_text()406 item['name'] = name.get_text()
324 item['command'] = command.get_text()407 item['command'] = command.get_text()
408 item['complex'] = cmplx.get_active()
325 if item['name'] == '' or item['command'] == '':409 if item['name'] == '' or item['command'] == '':
326 err = Gtk.MessageDialog(dialog,410 err = Gtk.MessageDialog(dialog,
327 Gtk.DialogFlags.MODAL,411 Gtk.DialogFlags.MODAL,
@@ -342,7 +426,7 @@
342 break426 break
343 iter = store.iter_next(iter)427 iter = store.iter_next(iter)
344 if not name_exist:428 if not name_exist:
345 store.append((item['enabled'], item['name'], item['command']))429 store.append((item['enabled'], item['name'], item['command'], item['complex']))
346 else:430 else:
347 gerr(_("Name *%s* already exist") % item['name'])431 gerr(_("Name *%s* already exist") % item['name'])
348 dialog.destroy()432 dialog.destroy()
@@ -421,10 +505,11 @@
421 if not iter:505 if not iter:
422 return506 return
423 507
424 (dialog,enabled,name,command) = self._create_command_dialog(508 (dialog,enabled,name,command,cmplx) = self._create_command_dialog(
425 enabled_var = store.get_value(iter, CC_COL_ENABLED),509 enabled_var = store.get_value(iter, CC_COL_ENABLED),
426 name_var = store.get_value(iter, CC_COL_NAME),510 name_var = store.get_value(iter, CC_COL_NAME),
427 command_var = store.get_value(iter, CC_COL_COMMAND)511 command_var = store.get_value(iter, CC_COL_COMMAND),
512 cmplx_var = store.get_value(iter, CC_COL_COMPLEX)
428 )513 )
429 res = dialog.run()514 res = dialog.run()
430 item = {}515 item = {}
@@ -432,6 +517,7 @@
432 item['enabled'] = enabled.get_active()517 item['enabled'] = enabled.get_active()
433 item['name'] = name.get_text()518 item['name'] = name.get_text()
434 item['command'] = command.get_text()519 item['command'] = command.get_text()
520 item['complex'] = cmplx.get_active()
435 if item['name'] == '' or item['command'] == '':521 if item['name'] == '' or item['command'] == '':
436 err = Gtk.MessageDialog(dialog,522 err = Gtk.MessageDialog(dialog,
437 Gtk.DialogFlags.MODAL,523 Gtk.DialogFlags.MODAL,
@@ -453,7 +539,8 @@
453 store.set(iter,539 store.set(iter,
454 CC_COL_ENABLED,item['enabled'],540 CC_COL_ENABLED,item['enabled'],
455 CC_COL_NAME, item['name'],541 CC_COL_NAME, item['name'],
456 CC_COL_COMMAND, item['command']542 CC_COL_COMMAND, item['command'],
543 CC_COL_COMPLEX, item['complex']
457 )544 )
458 else:545 else:
459 gerr(_("Name *%s* already exist") % item['name'])546 gerr(_("Name *%s* already exist") % item['name'])
460547
=== added file 'tests/test_custom_command_parser.py'
--- tests/test_custom_command_parser.py 1970-01-01 00:00:00 +0000
+++ tests/test_custom_command_parser.py 2018-11-14 13:07:15 +0000
@@ -0,0 +1,31 @@
1#!/usr/bin/env python2
2# Terminator by Chris Jones <cmsj@tenshu.net>
3# GPL v2 only
4# File created by Igor Korotin <Korotin.Igor.87@gmail.com>
5
6import unittest
7import re
8import os
9import sys, os.path
10sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), "..")))
11
12from terminatorlib.plugins.custom_commands import CustomCommandsMenu
13
14class TestCustomCommandParser(unittest.TestCase):
15 def test_parse(self):
16 menu = CustomCommandsMenu()
17 self.assertEqual(menu._parse( "word"), [{ 'command': 'word\n' , 'delay': 0}])
18 self.assertEqual(menu._parse( "\\nword"), [{ 'command': '\n' , 'delay': 0}, { 'command': 'word\n', 'delay': 0}])
19 self.assertEqual(menu._parse( "\\nword\\n\\n"), [{ 'command': '\n' , 'delay': 0}, { 'command': 'word\n', 'delay': 0}, {'command': '\n', 'delay': 0}, {'command': '\n', 'delay': 0}])
20 self.assertEqual(menu._parse( "\\pword"), [{ 'command': 'word\n' , 'delay': 1}])
21 self.assertEqual(menu._parse("\\p\\pword"), [{ 'command': 'word\n' , 'delay': 2}])
22 self.assertEqual(menu._parse(" \\\\pword"), [{ 'command': ' \\' , 'delay': 0}, { 'command': 'word\n', 'delay': 1}])
23 self.assertEqual(menu._parse( " word"), [{ 'command': ' word\n' , 'delay': 0}])
24 self.assertEqual(menu._parse( "\\p word"), [{ 'command': ' word\n' , 'delay': 1}])
25 self.assertEqual(menu._parse( "word\\p"), [{ 'command': 'word' , 'delay': 0}, { 'command': '\n' , 'delay': 1}])
26 self.assertEqual(menu._parse("\\pword\\pword"), [{ 'command': 'word' , 'delay': 1}, { 'command': 'word\n', 'delay': 1}])
27 self.assertEqual(menu._parse("\\pword\\word"), [{ 'command': 'word\\word\n', 'delay': 1}])
28 self.assertEqual(menu._parse("\\p\\nword\\n\\p"), [{ 'command': '\n' , 'delay': 1}, { 'command': 'word\n', 'delay': 0}, {'command': '\n', 'delay': 1}])
29
30if __name__ == '__main__':
31 unittest.main()

Subscribers

People subscribed via source and target branches

to status/vote changes: