1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import copy
18 import traceback
19 import re
20
21 import libxyz.core
22
23 from libxyz.ui import lowui
24 from libxyz.ui import Prompt
25 from libxyz.ui import XYZListBox
26 from libxyz.ui import NumEntry
27 from libxyz.ui import Keys
28 from libxyz.ui.utils import refresh
29 from libxyz.core.utils import ustring, bstring, is_func
30
31 -class Cmd(lowui.FlowWidget):
32 """
33 Command line widget
34 """
35
36 resolution = (u"cmd",)
37
38 LEFT = u"left"
39 RIGHT = u"right"
40 END = u"end"
41 UNDER = u"under"
42
44 """
45 @param xyz: XYZData instance
46
47 Resources used: text, prompt
48 """
49
50 super(Cmd, self).__init__()
51
52 self.xyz = xyz
53 self._attr = lambda x: xyz.skin.attr(self.resolution, x)
54
55 self._keys = Keys()
56
57 self._text_attr = self._attr(u"text")
58 self._data = []
59
60 self._index = 0
61
62 self._vindex = 0
63 self._hindex = 0
64
65 self.context = None
66 self._panel = self.xyz.pm.load(":sys:panel")
67
68 self._plugin = self._init_plugin()
69
70 _conf = self._plugin.conf
71 self.prompt = Prompt(_conf[u"prompt"], self._attr(u"prompt"))
72 self._undo = libxyz.core.Queue(_conf[u"undo_depth"])
73 self._history = libxyz.core.Queue(_conf[u"history_depth"])
74
75
76
131
132
133
136
137
138
139 - def rows(self, (maxcol,), focus=False):
140 """
141 Return the number of lines that will be rendered
142 """
143
144 return 1
145
146
147
148 - def render(self, (maxcol,), focus=False):
149 """
150 Render the command line
151 """
152
153 if self.prompt is not None:
154 _canv_prompt = self.prompt.render((maxcol,))
155 else:
156 _canv_prompt = lowui.Text(u"").render((maxcol,))
157
158 _data = [bstring(x) for x in self._get_visible(maxcol)]
159 _text_len = abs(maxcol - self.prompt.length())
160
161 _canv_text = lowui.AttrWrap(lowui.Text("".join(_data)),
162 self._text_attr).render((maxcol,))
163
164 _canvases = []
165
166 if self.prompt.length() > 0:
167 _canvases.append((_canv_prompt, None, False, self.prompt.length()))
168
169 _canvases.append((_canv_text, 0, True, _text_len))
170
171 canv = lowui.CanvasJoin(_canvases)
172 canv.cursor = self.get_cursor_coords((maxcol,))
173
174 return canv
175
176
177
179 """
180 Calculate and return currently visible piece of cmd data
181 """
182
183 maxcol -= 1
184
185 _plen = self.prompt.length()
186 _dlen = len(self._data)
187 _xindex = _plen + self._index
188
189 if self._vindex >= maxcol:
190 self._vindex = maxcol - 1
191
192 if _plen + _dlen >= maxcol:
193 _off = _xindex - maxcol
194 _to = _xindex
195
196 if _off < 0:
197 _off = 0
198 _to = maxcol - _plen + 1
199
200 _data = self._data[_off:_to]
201 else:
202 _data = self._data
203
204 return _data
205
206
207
209 """
210 Return the (x,y) coordinates of cursor within widget.
211 """
212
213 return self.prompt.length() + self._vindex, 0
214
215
216
218 self._data.insert(self._index, char)
219 self._index += 1
220 self._vindex += 1
221
222
223
225 """
226 Process pressed key
227 """
228
229 _meth = self.xyz.km.process(key)
230
231 if _meth is not None:
232 return _meth()
233 else:
234 _good = [x for x in key if len(x) == 1]
235
236 if _good:
237 try:
238 map(lambda x: self._put_object(x), _good)
239 except Exception, e:
240 xyzlog.error(_(ustring(str(e))))
241 xyzlog.debug(ustring(traceback.format_exc()))
242 else:
243 self._invalidate()
244
245
246
248 """
249 Save undo data
250 """
251
252 self._undo.push((self._index, copy.copy(self._data)))
253
254
255
257 """
258 Restore one undo level
259 """
260
261 if self._undo:
262 self._index, self._data = self._undo.pop()
263 self._vindex = self._index
264
265
266
267 - def _save_history(self):
268 """
269 Save typed command history
270 """
271
272
273 if not self._history.tail() == self._data:
274 self._history.push(copy.copy(self._data))
275
276 self._hindex = len(self._history)
277
278
279
281 """
282 Internal clear
283 """
284
285 self._data = []
286 self._index = 0
287 self._vindex = 0
288
289
290
291 - def _move_cursor(self, direction, chars=None, topred=None):
292 """
293 Generic cursor moving procedure
294 @param direction: LEFT or RIGHT
295 @param chars: Number of character to move or END to move to the end
296 in corresponding direction
297 @param topred: Predicate function which must return True if char
298 under the cursor is endpoint in move
299 """
300
301 _newindex = None
302
303
304 if callable(topred):
305 if direction == self.LEFT:
306 _range = range(self._index - 1, 0, -1)
307 else:
308 _range = range(self._index + 1, len(self._data))
309
310 for i in _range:
311 if topred(self._data[i]):
312 _newindex = i
313 break
314
315 if _newindex is None:
316
317 return self._move_cursor(direction, chars=self.END)
318
319 elif direction == self.LEFT:
320 if chars == self.END:
321 _newindex = 0
322 elif chars is not None and self._index >= chars:
323 _newindex = self._index - chars
324
325 elif direction == self.RIGHT:
326 if chars == self.END:
327 _newindex = len(self._data)
328
329 elif (self._index + chars) <= len(self._data):
330 _newindex = self._index + chars
331
332 if _newindex is not None:
333 self._index = _newindex
334 self._vindex = _newindex
335 self._invalidate()
336
337
338
339 @refresh
340 - def _delete(self, direction, chars=None, topred=None):
341 """
342 Generic delete procedure
343 @param direction: LEFT, RIGHT or UNDER
344 @param chars: Number of characters to delete
345 @param topred: Predicate function which must return True if char
346 under the cursor is endpoint in delete
347 """
348
349 _newindex = None
350 _delindex = None
351 _newdata = None
352
353 if callable(topred):
354 if direction == self.LEFT:
355 _range = range(self._index - 1, 0, -1)
356 else:
357 _range = range(self._index + 1, len(self._data))
358
359 _found = False
360
361 for i in _range:
362 if topred(self._data[i]):
363 _found = True
364 if direction == self.LEFT:
365 _newindex = i
366 _newdata = self._data[:_newindex] + \
367 self._data[self._index:]
368 else:
369 _newdata = self._data[:self._index] + self._data[i:]
370
371 self._save_undo()
372 break
373
374 if not _found:
375 return self._delete(direction, chars=self.END)
376
377 elif direction == self.UNDER:
378 if self._index >= 0 and self._index < len(self._data):
379 _delindex = self._index
380
381 elif direction == self.LEFT:
382 if chars == self.END:
383 self._save_undo()
384 _newdata = self._data[self._index:]
385 _newindex = 0
386 elif chars is not None and self._index >= chars:
387 _newindex = self._index - chars
388 _delindex = _newindex
389
390 elif direction == self.RIGHT:
391 if chars == self.END:
392 self._save_undo()
393 _newdata = self._data[:self._index]
394
395 if _newindex is not None:
396 self._index = _newindex
397 self._vindex = _newindex
398 if _newdata is not None:
399 self._data = _newdata
400 if _delindex is not None:
401 del(self._data[_delindex])
402
403
404
405
406
408 """
409 Delete single character left to the cursor
410 """
411
412 self._delete(self.LEFT, chars=1)
413
414
415
417 """
418 Delete single character under the cursor
419 """
420
421 return self._delete(self.UNDER)
422
423
424
426 """
427 Delete a word left to the cursor
428 """
429
430 return self._delete(self.LEFT, topred=lambda x: x.isspace())
431
432
433
435 """
436 Delete a word right to the cursor
437 """
438
439 return self._delete(self.RIGHT, topred=lambda x: x.isspace())
440
441
442
444 """
445 Clear the whole cmd line
446 """
447
448 self._save_undo()
449 self._clear_cmd()
450 self._invalidate()
451
452
453
455 """
456 Clear the cmd line from the cursor to the left
457 """
458
459 self._delete(self.LEFT, chars=self.END)
460
461
462
464 """
465 Clear the cmd line from the cursor to the right
466 """
467
468 return self._delete(self.RIGHT, chars=self.END)
469
470
471
473 """
474 Move cursor to the beginning of the command line
475 """
476
477 self._move_cursor(self.LEFT, chars=self.END)
478
479
480
482 """
483 Move cursor to the end of the command line
484 """
485
486 self._move_cursor(self.RIGHT, chars=self.END)
487
488
489
491 """
492 Move cursor left
493 """
494
495 self._move_cursor(self.LEFT, chars=1)
496
497
498
505
506
507
509 """
510 Move cursor one word left
511 """
512
513 self._move_cursor(self.LEFT, topred=lambda x: x.isspace())
514
515
516
518 """
519 Move cursor one word right
520 """
521
522 self._move_cursor(self.RIGHT, topred=lambda x: x.isspace())
523
524
525
527 """
528 Execute cmd contents
529 """
530
531 if not self._data:
532 return
533
534 self._save_history()
535
536 _data = self.replace_aliases("".join(self._data))
537
538 _cmd, _rest = split_cmd(_data)
539
540
541 if _cmd in self.xyz.conf["commands"]:
542 try:
543 self.xyz.conf["commands"][_cmd](
544 _rest if _rest is None else bstring(_rest))
545 except Exception, e:
546 xyzlog.error(_("Error executing internal command %s: %s") %
547 (_cmd, ustring(str(e))))
548 else:
549 if not hasattr(self, "_execf"):
550 self._execf = self.xyz.pm.from_load(":core:shell", "execute")
551
552 if not hasattr(self, "_reloadf"):
553 self._reloadf =self.xyz.pm.from_load(":sys:panel",
554 "reload_all")
555
556 self._execf(_data)
557 self._reloadf()
558
559 self._clear_cmd()
560 self._invalidate()
561
562
563
565 """
566 Check if first word of the command line (which is supposed to be a
567 command to execute) is in our aliases table, if it is, replace it.
568
569 @param data: String
570 """
571
572 cmd, _ = split_cmd(data)
573
574 try:
575 raw_alias = self.xyz.conf["aliases"][cmd]
576
577 if isinstance(raw_alias, basestring):
578 alias = raw_alias
579 elif is_func(raw_alias):
580 alias = raw_alias()
581 else:
582 xyzlog.error(_(u"Invalid alias type: %s") %
583 ustring(str(type(raw_alias))))
584 return data
585
586
587 return re.sub(r"^%s" % cmd, alias, data)
588 except KeyError:
589 return data
590 except Exception, e:
591 xyzlog.error(_(u"Unable to replace an alias %s: %s") %
592 (ustring(cmd), ustring(str(e))))
593 return data
594
595
596
597
599 """
600 Return True if cmd is empty, i.e. has no contents
601 """
602
603 return self._data == []
604
605
606
608 """
609 Restore one level from undo buffer
610 """
611
612 self._restore_undo()
613 self._invalidate()
614
615
616
618 """
619 Clear undo buffer
620 """
621
622 self._undo.clear()
623 self._invalidate()
624
625
626
627 - def history_prev(self):
628 """
629 Scroll through list of saved commands backward
630 """
631
632 if self._hindex > 0:
633 self._hindex -= 1
634 self._data = copy.copy(self._history[self._hindex])
635 self.cursor_end()
636
637
638
639 - def history_next(self):
640 """
641 Scroll through list of saved commands forward
642 """
643
644 if self._hindex < len(self._history) - 1:
645 self._hindex += 1
646 self._data = copy.copy(self._history[self._hindex])
647 self.cursor_end()
648
649
650
651 - def history_clear(self):
652 """
653 Clear commands history
654 """
655
656 self._history.clear()
657
658
659
660 - def show_history(self):
661 """
662 Show commands history list
663 """
664
665 def _enter_cb(num):
666 if num >= len(self._history):
667 return
668
669 self._data = copy.copy(self._history[num])
670 self.cursor_end()
671
672
673
674 _sel_attr = self.xyz.skin.attr(XYZListBox.resolution, u"selected")
675
676 _wdata = []
677
678 for i in range(len(self._history)):
679 _wdata.append(NumEntry(u"".join([ustring(x) for x in
680 self._history[i]]),
681 _sel_attr, i,
682 enter_cb=_enter_cb))
683
684 _walker = lowui.SimpleListWalker(_wdata)
685 _walker.focus = len(_walker) - 1
686
687 _dim = tuple([x - 2 for x in self.xyz.screen.get_cols_rows()])
688
689 _ek = [self._keys.ENTER]
690
691 XYZListBox(self.xyz, self.xyz.top, _walker, _(u"History"),
692 _dim).show(exit_keys=_ek)
693
694
695
697 """
698 Put currently selected VFS object name in panel to cmd line
699 """
700
701 return self._put_engine(self._panel.get_selected().name)
702
703
704
706 """
707 Put currently selected VFS object full path in panel to cmd line
708 """
709
710 return self._put_engine(self._panel.get_selected().path)
711
712
713
720
721
722
729
730
731
733 """
734 Put current working directory of active panel to cmd line
735 """
736
737 return self._put_engine(self._panel.cwd())
738
739
740
742 """
743 Put current working directory of inactive panel to cmd line
744 """
745
746 return self._put_engine(self._panel.cwd_inactive())
747
748
749
751 """
752 Put list content to cmd
753 """
754
755 map(lambda x: self._put_object(x),
756 self.escape([bstring(x) for x in ustring(obj)]) + [" "])
757 self._invalidate()
758
759
760
761 - def escape(self, obj, join=False):
762 """
763 Escape filename
764 @param obj: String to escape
765 @param join: If False return list otherwise return joined string
766 """
767
768 result = []
769 toescape = [" ", "'", '"', "*", "?", "\\", "&",
770 "(", ")",
771 "[", "]",
772 "{", "}",
773 ]
774
775 for x in obj:
776 if x in toescape:
777 result.extend(["\\", x])
778 else:
779 result.append(x)
780
781 return "".join(result) if join else result
782
786 """
787 Return command name and the rest of the command line
788 """
789
790 _r = cmdline.split(" ", 1)
791
792 if len(_r) == 1:
793 return _r[0], None
794 else:
795 return _r[0], _r[1]
796