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
1=== modified file 'terminatorlib/plugins/custom_commands.py'
2--- terminatorlib/plugins/custom_commands.py 2017-02-06 11:09:36 +0000
3+++ terminatorlib/plugins/custom_commands.py 2018-11-14 13:07:15 +0000
4@@ -4,11 +4,16 @@
5 """custom_commands.py - Terminator Plugin to add custom command menu entries"""
6 import sys
7 import os
8+import re
9+import threading
10
11 # Fix imports when testing this file directly
12 if __name__ == '__main__':
13+ print os.path.dirname(__file__)
14 sys.path.append( os.path.join(os.path.dirname(__file__), "../.."))
15
16+import gi
17+gi.require_version('Gtk', '3.0')
18 from gi.repository import Gtk
19 from gi.repository import GObject
20 import terminatorlib.plugin as plugin
21@@ -16,7 +21,7 @@
22 from terminatorlib.translation import _
23 from terminatorlib.util import get_config_dir, err, dbg, gerr
24
25-(CC_COL_ENABLED, CC_COL_NAME, CC_COL_COMMAND) = range(0,3)
26+(CC_COL_ENABLED, CC_COL_NAME, CC_COL_COMMAND,CC_COL_COMPLEX) = range(0,4)
27
28 # Every plugin you want Terminator to load *must* be listed in 'AVAILABLE'
29 AVAILABLE = ['CustomCommandsMenu']
30@@ -30,6 +35,7 @@
31 def __init__( self):
32 config = Config()
33 sections = config.plugin_get_config(self.__class__.__name__)
34+
35 if not isinstance(sections, dict):
36 return
37 noord_cmds = []
38@@ -41,16 +47,22 @@
39 name = s["name"]
40 command = s["command"]
41 enabled = s["enabled"] and s["enabled"] or False
42+ if not (s.has_key("complex")):
43+ cmplx = False
44+ else:
45+ cmplx = s["complex"] and s["complex"] or False
46 if s.has_key("position"):
47 self.cmd_list[int(s["position"])] = {'enabled' : enabled,
48 'name' : name,
49- 'command' : command
50+ 'command' : command,
51+ 'complex' : cmplx
52 }
53 else:
54 noord_cmds.append(
55 {'enabled' : enabled,
56 'name' : name,
57- 'command' : command
58+ 'command' : command,
59+ 'complex' : cmplx
60 }
61 )
62 for cmd in noord_cmds:
63@@ -100,7 +112,7 @@
64 else:
65 menuitem = Gtk.MenuItem(leaf_name)
66 terminals = terminal.terminator.get_target_terms(terminal)
67- menuitem.connect("activate", self._execute, {'terminals' : terminals, 'command' : command['command'] })
68+ menuitem.connect("activate", self._execute, {'terminals' : terminals, 'command' : command['command'], 'complex' : command['complex'] })
69 target_submenu.append(menuitem)
70
71 def _save_config(self):
72@@ -109,6 +121,7 @@
73 i = 0
74 for command in [ self.cmd_list[key] for key in sorted(self.cmd_list.keys()) ] :
75 enabled = command['enabled']
76+ cmplx = command['complex']
77 name = command['name']
78 command = command['command']
79
80@@ -116,18 +129,60 @@
81 item['enabled'] = enabled
82 item['name'] = name
83 item['command'] = command
84+ item['complex'] = cmplx
85 item['position'] = i
86
87 config.plugin_set(self.__class__.__name__, name, item)
88 i = i + 1
89 config.save()
90
91+ def _parse(self, command_str):
92+ ret_command_list = []
93+ command_list = command_str.split("\\n")
94+ delay=0
95+
96+ for idx, line in enumerate(command_list):
97+ if line == '':
98+ line = '\n'
99+ ret_command_list.append({'command': line, 'delay': delay})
100+ else:
101+ if line[-1] != '\n':
102+ line = line + '\n'
103+ # We've got separete command but it might have some other special characters
104+ # So first of all - check whether line contains them.
105+ obj = re.search(r"(?:\\p)", line)
106+ if obj:
107+ # Using 0x00 char to parse string.
108+ _list = list(filter(None, re.sub(r"(?:\\p)", '\0\\p\0', line).split('\0')))
109+ for part in _list:
110+ if part == '\\p':
111+ delay += 1;
112+ else:
113+ ret_command_list.append({'command': part, 'delay': delay})
114+ delay = 0;
115+ else:
116+ ret_command_list.append({'command': line, 'delay': delay})
117+ return ret_command_list
118+
119+ def deferred_execute(self, terminal, command, rest_command_list):
120+ terminal.vte.feed_child(command, len(command))
121+ if rest_command_list:
122+ threading.Timer(rest_command_list[0]['delay'], self.deferred_execute, [terminal, rest_command_list[0]['command'], rest_command_list[1:]]).start()
123+
124 def _execute(self, widget, data):
125- command = data['command']
126- if command[-1] != '\n':
127- command = command + '\n'
128- for terminal in data['terminals']:
129- terminal.vte.feed_child(command, len(command))
130+ cmplx = data['complex']
131+
132+ if cmplx:
133+ command_list = self._parse(data['command'])
134+ for terminal in data['terminals']:
135+ threading.Timer(command_list[0]['delay'], self.deferred_execute, [terminal, command_list[0]['command'], command_list[1:]]).start()
136+
137+ else:
138+ command = data['command']
139+ if command[-1] != '\n':
140+ command = command + '\n'
141+ for terminal in data['terminals']:
142+ terminal.vte.feed_child(command, len(command))
143
144 def configure(self, widget, data = None):
145 ui = {}
146@@ -150,10 +205,10 @@
147 icon = dbox.render_icon(Gtk.STOCK_DIALOG_INFO, Gtk.IconSize.BUTTON)
148 dbox.set_icon(icon)
149
150- store = Gtk.ListStore(bool, str, str)
151+ store = Gtk.ListStore(bool, str, str, bool)
152
153 for command in [ self.cmd_list[key] for key in sorted(self.cmd_list.keys()) ]:
154- store.append([command['enabled'], command['name'], command['command']])
155+ store.append([command['enabled'], command['name'], command['command'], command['complex']])
156
157 treeview = Gtk.TreeView(store)
158 #treeview.connect("cursor-changed", self.on_cursor_changed, ui)
159@@ -163,7 +218,7 @@
160 ui['treeview'] = treeview
161
162 renderer = Gtk.CellRendererToggle()
163- renderer.connect('toggled', self.on_toggled, ui)
164+ renderer.connect('toggled', self.on_toggled_enabled, ui)
165 column = Gtk.TreeViewColumn(_("Enabled"), renderer, active=CC_COL_ENABLED)
166 treeview.append_column(column)
167
168@@ -175,6 +230,11 @@
169 column = Gtk.TreeViewColumn(_("Command"), renderer, text=CC_COL_COMMAND)
170 treeview.append_column(column)
171
172+ renderer = Gtk.CellRendererToggle()
173+ renderer.connect('toggled', self.on_toggled_complex, ui)
174+ column = Gtk.TreeViewColumn(_("Complex"), renderer, active=CC_COL_COMPLEX)
175+ treeview.append_column(column)
176+
177 scroll_window = Gtk.ScrolledWindow()
178 scroll_window.set_size_request(500, 250)
179 scroll_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
180@@ -246,28 +306,43 @@
181 self.cmd_list = {}
182 i=0
183 while iter:
184- (enabled, name, command) = store.get(iter,
185+ (enabled, name, command, cmplx) = store.get(iter,
186 CC_COL_ENABLED,
187 CC_COL_NAME,
188- CC_COL_COMMAND)
189+ CC_COL_COMMAND,
190+ CC_COL_COMPLEX)
191 self.cmd_list[i] = {'enabled' : enabled,
192 'name': name,
193- 'command' : command}
194+ 'command' : command,
195+ 'complex' : cmplx}
196 iter = store.iter_next(iter)
197 i = i + 1
198
199
200- def on_toggled(self, widget, path, data):
201+ def on_toggled_enabled(self, widget, path, data):
202 treeview = data['treeview']
203 store = treeview.get_model()
204 iter = store.get_iter(path)
205- (enabled, name, command) = store.get(iter,
206+ (enabled, name, command, cmplx) = store.get(iter,
207 CC_COL_ENABLED,
208 CC_COL_NAME,
209- CC_COL_COMMAND
210+ CC_COL_COMMAND,
211+ CC_COL_COMPLEX
212 )
213 store.set_value(iter, CC_COL_ENABLED, not enabled)
214
215+ def on_toggled_complex(self, widget, path, data):
216+ treeview = data['treeview']
217+ store = treeview.get_model()
218+ iter = store.get_iter(path)
219+ (enabled, name, command, cmplx) = store.get(iter,
220+ CC_COL_ENABLED,
221+ CC_COL_NAME,
222+ CC_COL_COMMAND,
223+ CC_COL_COMPLEX
224+ )
225+ store.set_value(iter, CC_COL_COMPLEX, not cmplx)
226+
227
228 def on_selection_changed(self,selection, data=None):
229 treeview = selection.get_tree_view()
230@@ -279,7 +354,7 @@
231 data['button_edit'].set_sensitive(iter is not None)
232 data['button_delete'].set_sensitive(iter is not None)
233
234- def _create_command_dialog(self, enabled_var = False, name_var = "", command_var = ""):
235+ def _create_command_dialog(self, enabled_var = False, name_var = "", command_var = "",cmplx_var = False):
236 dialog = Gtk.Dialog(
237 _("New Command"),
238 None,
239@@ -290,7 +365,7 @@
240 )
241 )
242 dialog.set_transient_for(self.dbox)
243- table = Gtk.Table(3, 2)
244+ table = Gtk.Table(6, 2)
245
246 label = Gtk.Label(label=_("Enabled:"))
247 table.attach(label, 0, 1, 0, 1)
248@@ -309,19 +384,28 @@
249 command = Gtk.Entry()
250 command.set_text(command_var)
251 table.attach(command, 1, 2, 2, 3)
252+
253+ label = Gtk.Label(label=_("Complex:"))
254+ table.attach(label, 0, 1, 3, 4)
255+ cmplx = Gtk.CheckButton()
256+ cmplx.set_active(cmplx_var)
257+ table.attach(cmplx, 1, 2, 3, 4)
258+ label = Gtk.Label(label=_('Complex Help: \n \\n - send end of line\n \\p - add 1 second delay'))
259+ table.attach(label, 0, 2, 4, 6)
260
261 dialog.vbox.pack_start(table, True, True, 0)
262 dialog.show_all()
263- return (dialog,enabled,name,command)
264+ return (dialog,enabled,name,command,cmplx)
265
266 def on_new(self, button, data):
267- (dialog,enabled,name,command) = self._create_command_dialog()
268+ (dialog,enabled,name,command,cmplx) = self._create_command_dialog()
269 res = dialog.run()
270 item = {}
271 if res == Gtk.ResponseType.ACCEPT:
272 item['enabled'] = enabled.get_active()
273 item['name'] = name.get_text()
274 item['command'] = command.get_text()
275+ item['complex'] = cmplx.get_active()
276 if item['name'] == '' or item['command'] == '':
277 err = Gtk.MessageDialog(dialog,
278 Gtk.DialogFlags.MODAL,
279@@ -342,7 +426,7 @@
280 break
281 iter = store.iter_next(iter)
282 if not name_exist:
283- store.append((item['enabled'], item['name'], item['command']))
284+ store.append((item['enabled'], item['name'], item['command'], item['complex']))
285 else:
286 gerr(_("Name *%s* already exist") % item['name'])
287 dialog.destroy()
288@@ -421,10 +505,11 @@
289 if not iter:
290 return
291
292- (dialog,enabled,name,command) = self._create_command_dialog(
293+ (dialog,enabled,name,command,cmplx) = self._create_command_dialog(
294 enabled_var = store.get_value(iter, CC_COL_ENABLED),
295 name_var = store.get_value(iter, CC_COL_NAME),
296- command_var = store.get_value(iter, CC_COL_COMMAND)
297+ command_var = store.get_value(iter, CC_COL_COMMAND),
298+ cmplx_var = store.get_value(iter, CC_COL_COMPLEX)
299 )
300 res = dialog.run()
301 item = {}
302@@ -432,6 +517,7 @@
303 item['enabled'] = enabled.get_active()
304 item['name'] = name.get_text()
305 item['command'] = command.get_text()
306+ item['complex'] = cmplx.get_active()
307 if item['name'] == '' or item['command'] == '':
308 err = Gtk.MessageDialog(dialog,
309 Gtk.DialogFlags.MODAL,
310@@ -453,7 +539,8 @@
311 store.set(iter,
312 CC_COL_ENABLED,item['enabled'],
313 CC_COL_NAME, item['name'],
314- CC_COL_COMMAND, item['command']
315+ CC_COL_COMMAND, item['command'],
316+ CC_COL_COMPLEX, item['complex']
317 )
318 else:
319 gerr(_("Name *%s* already exist") % item['name'])
320
321=== added file 'tests/test_custom_command_parser.py'
322--- tests/test_custom_command_parser.py 1970-01-01 00:00:00 +0000
323+++ tests/test_custom_command_parser.py 2018-11-14 13:07:15 +0000
324@@ -0,0 +1,31 @@
325+#!/usr/bin/env python2
326+# Terminator by Chris Jones <cmsj@tenshu.net>
327+# GPL v2 only
328+# File created by Igor Korotin <Korotin.Igor.87@gmail.com>
329+
330+import unittest
331+import re
332+import os
333+import sys, os.path
334+sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), "..")))
335+
336+from terminatorlib.plugins.custom_commands import CustomCommandsMenu
337+
338+class TestCustomCommandParser(unittest.TestCase):
339+ def test_parse(self):
340+ menu = CustomCommandsMenu()
341+ self.assertEqual(menu._parse( "word"), [{ 'command': 'word\n' , 'delay': 0}])
342+ self.assertEqual(menu._parse( "\\nword"), [{ 'command': '\n' , 'delay': 0}, { 'command': 'word\n', 'delay': 0}])
343+ self.assertEqual(menu._parse( "\\nword\\n\\n"), [{ 'command': '\n' , 'delay': 0}, { 'command': 'word\n', 'delay': 0}, {'command': '\n', 'delay': 0}, {'command': '\n', 'delay': 0}])
344+ self.assertEqual(menu._parse( "\\pword"), [{ 'command': 'word\n' , 'delay': 1}])
345+ self.assertEqual(menu._parse("\\p\\pword"), [{ 'command': 'word\n' , 'delay': 2}])
346+ self.assertEqual(menu._parse(" \\\\pword"), [{ 'command': ' \\' , 'delay': 0}, { 'command': 'word\n', 'delay': 1}])
347+ self.assertEqual(menu._parse( " word"), [{ 'command': ' word\n' , 'delay': 0}])
348+ self.assertEqual(menu._parse( "\\p word"), [{ 'command': ' word\n' , 'delay': 1}])
349+ self.assertEqual(menu._parse( "word\\p"), [{ 'command': 'word' , 'delay': 0}, { 'command': '\n' , 'delay': 1}])
350+ self.assertEqual(menu._parse("\\pword\\pword"), [{ 'command': 'word' , 'delay': 1}, { 'command': 'word\n', 'delay': 1}])
351+ self.assertEqual(menu._parse("\\pword\\word"), [{ 'command': 'word\\word\n', 'delay': 1}])
352+ self.assertEqual(menu._parse("\\p\\nword\\n\\p"), [{ 'command': '\n' , 'delay': 1}, { 'command': 'word\n', 'delay': 0}, {'command': '\n', 'delay': 1}])
353+
354+if __name__ == '__main__':
355+ unittest.main()

Subscribers

People subscribed via source and target branches

to status/vote changes: