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, split_cmd
30 from libxyz.core.dsl import XYZ
31 from libxyz.exceptions import XYZRuntimeError
32
33 -class Cmd(lowui.FlowWidget):
34 """
35 Command line widget
36 """
37
38 resolution = (u"cmd",)
39
40 LEFT = u"left"
41 RIGHT = u"right"
42 END = u"end"
43 UNDER = u"under"
44
82
83
84
86 """
87 Hook for update conf event
88 """
89
90
91 if sect != "plugins" or var != self._plugin.ns.pfull:
92 return
93
94 mapping = {
95 "prompt": lambda x: self._set_prompt(x),
96 "undo_depth": lambda x: self._undo.set_size(x),
97 "history_depth": lambda x: self._history.set_size(x),
98 }
99
100 for k, v in val.iteritems():
101 if k in mapping:
102 mapping[k](v)
103
104
105
107 """
108 Save history at shutdown
109 """
110
111 f = None
112 try:
113 f = self._ud.openfile(self._history_file, "w", "data")
114 f.write("\n".join([bstring(u"".join(x)) for x in self._history]))
115 except XYZRuntimeError, e:
116 if f:
117 f.close()
118
119 xyzlog.info(_(u"Unable to open history data file: %s")
120 % ustring(str(e)))
121 else:
122 if f:
123 f.close()
124
125
126
128 """
129 Load history at startup
130 """
131
132 f = None
133
134 try:
135 f = self._ud.openfile(self._history_file, "r", "data")
136 data = f.readlines()
137
138 if len(data) > self._history.maxsize:
139 data = data[-self._history.maxsize]
140
141 self._history.clear()
142
143 for line in data:
144 self._history.push([x for x in ustring(line.rstrip())])
145 except Exception:
146 pass
147
148 if f:
149 f.close()
150
151
152
210
211
212
215
216
217
218 - def rows(self, (maxcol,), focus=False):
219 """
220 Return the number of lines that will be rendered
221 """
222
223 return 1
224
225
226
227 - def render(self, (maxcol,), focus=False):
228 """
229 Render the command line
230 """
231
232 if self.prompt is not None:
233 _canv_prompt = self.prompt.render((maxcol,))
234 _prompt_len = len(self.prompt)
235 else:
236 _canv_prompt = lowui.Text(u"").render((maxcol,))
237 _prompt_len = 0
238
239 _data = [bstring(x) for x in self._get_visible(maxcol)]
240 _text_len = abs(maxcol - _prompt_len)
241
242 _canv_text = lowui.AttrWrap(lowui.Text("".join(_data)),
243 self._text_attr).render((maxcol,))
244
245 _canvases = []
246
247 if _prompt_len > 0:
248 _canvases.append((_canv_prompt, None, False, _prompt_len))
249
250 _canvases.append((_canv_text, 0, True, _text_len))
251
252 canv = lowui.CanvasJoin(_canvases)
253 canv.cursor = self.get_cursor_coords((maxcol,))
254
255 return canv
256
257
258
260 """
261 Calculate and return currently visible piece of cmd data
262 """
263
264 maxcol -= 1
265
266 _plen = len(self.prompt)
267 _dlen = len(self._data)
268 _xindex = _plen + self._index
269
270 if self._vindex >= maxcol:
271 self._vindex = maxcol - 1
272
273 if _plen + _dlen >= maxcol:
274 _off = _xindex - maxcol
275 _to = _xindex
276
277 if _off < 0:
278 _off = 0
279 _to = maxcol - _plen + 1
280
281 _data = self._data[_off:_to]
282 else:
283 _data = self._data
284
285 return _data
286
287
288
290 """
291 Return the (x,y) coordinates of cursor within widget.
292 """
293
294 return len(self.prompt) + self._vindex, 0
295
296
297
299 self._data.insert(self._index, ustring(char))
300 self._index += 1
301 self._vindex += 1
302
303
304
306 """
307 Process pressed key
308 """
309
310 _meth = self.xyz.km.process(key)
311
312 if _meth is not None:
313 return _meth()
314 else:
315 _good = [x for x in key if len(x) == 1]
316
317 if _good:
318 try:
319 map(lambda x: self._put_object(x), _good)
320 except Exception, e:
321 xyzlog.error(_(ustring(str(e))))
322 xyzlog.debug(ustring(traceback.format_exc()))
323 else:
324 self._invalidate()
325
326
327
329 """
330 Save undo data
331 """
332
333 self._undo.push((self._index, copy.copy(self._data)))
334
335
336
338 """
339 Restore one undo level
340 """
341
342 if self._undo:
343 self._index, self._data = self._undo.pop()
344 self._vindex = self._index
345
346
347
348 - def _save_history(self):
349 """
350 Save typed command history
351 """
352
353
354 if not self._history.tail() == self._data:
355 self._history.push(copy.copy(self._data))
356
357 self._hindex = len(self._history)
358
359
360
362 """
363 Internal clear
364 """
365
366 self._data = []
367 self._index = 0
368 self._vindex = 0
369
370
371
372 - def _move_cursor(self, direction, chars=None, topred=None):
373 """
374 Generic cursor moving procedure
375 @param direction: LEFT or RIGHT
376 @param chars: Number of character to move or END to move to the end
377 in corresponding direction
378 @param topred: Predicate function which must return True if char
379 under the cursor is endpoint in move
380 """
381
382 _newindex = None
383
384
385 if callable(topred):
386 if direction == self.LEFT:
387 _range = range(self._index - 1, 0, -1)
388 else:
389 _range = range(self._index + 1, len(self._data))
390
391 for i in _range:
392 if topred(self._data[i]):
393 _newindex = i
394 break
395
396 if _newindex is None:
397
398 return self._move_cursor(direction, chars=self.END)
399
400 elif direction == self.LEFT:
401 if chars == self.END:
402 _newindex = 0
403 elif chars is not None and self._index >= chars:
404 _newindex = self._index - chars
405
406 elif direction == self.RIGHT:
407 if chars == self.END:
408 _newindex = len(self._data)
409
410 elif (self._index + chars) <= len(self._data):
411 _newindex = self._index + chars
412
413 if _newindex is not None:
414 self._index = _newindex
415 self._vindex = _newindex
416 self._invalidate()
417
418
419
420 @refresh
421 - def _delete(self, direction, chars=None, topred=None):
422 """
423 Generic delete procedure
424 @param direction: LEFT, RIGHT or UNDER
425 @param chars: Number of characters to delete
426 @param topred: Predicate function which must return True if char
427 under the cursor is endpoint in delete
428 """
429
430 _newindex = None
431 _delindex = None
432 _newdata = None
433
434 if callable(topred):
435 if direction == self.LEFT:
436 _range = range(self._index - 1, 0, -1)
437 else:
438 _range = range(self._index + 1, len(self._data))
439
440 _found = False
441
442 for i in _range:
443 if topred(self._data[i]):
444 _found = True
445 if direction == self.LEFT:
446 _newindex = i
447 _newdata = self._data[:_newindex] + \
448 self._data[self._index:]
449 else:
450 _newdata = self._data[:self._index] + self._data[i:]
451
452 self._save_undo()
453 break
454
455 if not _found:
456 return self._delete(direction, chars=self.END)
457
458 elif direction == self.UNDER:
459 if self._index >= 0 and self._index < len(self._data):
460 _delindex = self._index
461
462 elif direction == self.LEFT:
463 if chars == self.END:
464 self._save_undo()
465 _newdata = self._data[self._index:]
466 _newindex = 0
467 elif chars is not None and self._index >= chars:
468 _newindex = self._index - chars
469 _delindex = _newindex
470
471 elif direction == self.RIGHT:
472 if chars == self.END:
473 self._save_undo()
474 _newdata = self._data[:self._index]
475
476 if _newindex is not None:
477 self._index = _newindex
478 self._vindex = _newindex
479 if _newdata is not None:
480 self._data = _newdata
481 if _delindex is not None:
482 del(self._data[_delindex])
483
484
485
486
487
489 """
490 Delete single character left to the cursor
491 """
492
493 self._delete(self.LEFT, chars=1)
494
495
496
498 """
499 Delete single character under the cursor
500 """
501
502 return self._delete(self.UNDER)
503
504
505
507 """
508 Delete a word left to the cursor
509 """
510
511 return self._delete(self.LEFT, topred=lambda x: x.isspace())
512
513
514
516 """
517 Delete a word right to the cursor
518 """
519
520 return self._delete(self.RIGHT, topred=lambda x: x.isspace())
521
522
523
525 """
526 Clear the whole cmd line
527 """
528
529 self._save_undo()
530 self._clear_cmd()
531 self._invalidate()
532
533
534
536 """
537 Clear the cmd line from the cursor to the left
538 """
539
540 self._delete(self.LEFT, chars=self.END)
541
542
543
545 """
546 Clear the cmd line from the cursor to the right
547 """
548
549 return self._delete(self.RIGHT, chars=self.END)
550
551
552
554 """
555 Move cursor to the beginning of the command line
556 """
557
558 self._move_cursor(self.LEFT, chars=self.END)
559
560
561
563 """
564 Move cursor to the end of the command line
565 """
566
567 self._move_cursor(self.RIGHT, chars=self.END)
568
569
570
572 """
573 Move cursor left
574 """
575
576 self._move_cursor(self.LEFT, chars=1)
577
578
579
586
587
588
590 """
591 Move cursor one word left
592 """
593
594 self._move_cursor(self.LEFT, topred=lambda x: x.isspace())
595
596
597
599 """
600 Move cursor one word right
601 """
602
603 self._move_cursor(self.RIGHT, topred=lambda x: x.isspace())
604
605
606
608 """
609 Execute cmd contents
610 """
611
612
613 if XYZ.call(":sys:panel:vfs_driver"):
614 xyzlog.error(
615 _(u"Unable to execute commands on non-local filesystems"))
616 return
617
618 if not self._data:
619 return
620
621 self._save_history()
622 _data = self.replace_aliases(bstring(u"".join(self._data)))
623 _cmd, _rest = _split_cmd(_data)
624
625
626 if _cmd in self.xyz.conf["commands"]:
627 try:
628 if _rest is None:
629 arg = _rest
630 else:
631 arg = _rest
632
633 self.xyz.conf["commands"][_cmd](arg)
634 except Exception, e:
635 xyzlog.error(_("Error executing internal command %s: %s") %
636 (_cmd, ustring(str(e))))
637 else:
638 if not hasattr(self, "_execf"):
639 self._execf = self.xyz.pm.from_load(":core:shell", "execute")
640
641 if not hasattr(self, "_reloadf"):
642 self._reloadf = self.xyz.pm.from_load(":sys:panel",
643 "reload_all")
644
645 self._execf(_data)
646 self._reloadf()
647
648 self._clear_cmd()
649 self._invalidate()
650
651
652
654 """
655 Check if first word of the command line (which is supposed to be a
656 command to execute) is in our aliases table, if it is, replace it.
657
658 @param data: String
659 """
660
661 cmd, _ = _split_cmd(data)
662
663 try:
664 raw_alias = self.xyz.conf["aliases"][cmd]
665
666 if isinstance(raw_alias, basestring):
667 alias = raw_alias
668 elif is_func(raw_alias):
669 alias = raw_alias()
670 else:
671 xyzlog.error(_(u"Invalid alias type: %s") %
672 ustring(str(type(raw_alias))))
673 return data
674
675 return re.sub(r"^%s" % cmd, alias, data)
676 except KeyError:
677 return data
678 except Exception, e:
679 xyzlog.error(_(u"Unable to replace an alias %s: %s") %
680 (ustring(cmd), ustring(str(e))))
681 return data
682
683
684
685
687 """
688 Return True if cmd is empty, i.e. has no contents
689 """
690
691 return self._data == []
692
693
694
696 """
697 Restore one level from undo buffer
698 """
699
700 self._restore_undo()
701 self._invalidate()
702
703
704
706 """
707 Clear undo buffer
708 """
709
710 self._undo.clear()
711 self._invalidate()
712
713
714
715 - def history_prev(self):
716 """
717 Scroll through list of saved commands backward
718 """
719
720 if self._hindex > 0:
721 self._hindex -= 1
722 self._data = copy.copy(self._history[self._hindex])
723 self.cursor_end()
724
725
726
727 - def history_next(self):
728 """
729 Scroll through list of saved commands forward
730 """
731
732 if self._hindex < len(self._history) - 1:
733 self._hindex += 1
734 self._data = copy.copy(self._history[self._hindex])
735 self.cursor_end()
736
737
738
739 - def history_clear(self):
740 """
741 Clear commands history
742 """
743
744 self._history.clear()
745
746
747
748 - def show_history(self):
749 """
750 Show commands history list
751 """
752
753 def _enter_cb(num):
754 if num >= len(self._history):
755 return
756
757 self._data = copy.copy(self._history[num])
758 self.cursor_end()
759
760
761
762 _sel_attr = self.xyz.skin.attr(XYZListBox.resolution, u"selected")
763
764 _wdata = []
765
766 for i in range(len(self._history)):
767 _wdata.append(NumEntry(u"".join([ustring(x) for x in
768 self._history[i]]),
769 _sel_attr, i,
770 enter_cb=_enter_cb))
771
772 _walker = lowui.SimpleListWalker(_wdata)
773 _walker.focus = len(_walker) - 1
774
775 _dim = tuple([x - 2 for x in self.xyz.screen.get_cols_rows()])
776
777 _ek = [self._keys.ENTER]
778
779 XYZListBox(self.xyz, self.xyz.top, _walker, _(u"History"),
780 _dim).show(exit_keys=_ek)
781
782
783
785 """
786 Put currently selected VFS object name in panel to cmd line
787 """
788
789 return self._put_engine(self._panel.get_selected().name)
790
791
792
794 """
795 Put currently selected VFS object full path in panel to cmd line
796 """
797
798 return self._put_engine(self._panel.get_selected().path)
799
800
801
803 """
804 Put selected VFS object name in inactive panel to cmd line
805 """
806
807 return self._put_engine(self._panel.get_selected(False).name)
808
809
810
812 """
813 Put selected VFS object full path in inactive panel to cmd line
814 """
815
816 return self._put_engine(self._panel.get_selected(False).path)
817
818
819
821 """
822 Put current working directory of active panel to cmd line
823 """
824
825 return self._put_engine(self._panel.cwd())
826
827
828
830 """
831 Put current working directory of inactive panel to cmd line
832 """
833
834 return self._put_engine(self._panel.cwd(False))
835
836
837
838 - def put(self, obj, space=True):
839 """
840 Put arbitrary string to cmd line starting from the cursor position
841 @param space: Flag indicating whether to append space char after the obj
842 """
843
844 return self._put_engine(obj, space=space)
845
846
847
849 """
850 Get cmd contents
851 """
852
853 return bstring(u"".join(self._data))
854
855
856
858 """
859 Append arbitrary string at the end of cmd
860 """
861
862 self.cursor_end()
863 self.put(obj, space=False)
864
865
866
868 """
869 Put list content to cmd
870 """
871
872 if space:
873 extra = [u" "]
874 else:
875 extra = []
876
877 map(lambda x: self._put_object(x),
878 self.escape([x for x in ustring(obj)]) + extra)
879 self._invalidate()
880
881
882
883 - def escape(self, obj, join=False):
884 """
885 Escape filename
886 @param obj: String to escape
887 @param join: If False return list otherwise return joined string
888 """
889
890 result = []
891 toescape = [u" ", u"'", u'"', u"*", u"?", u"\\", u"&", u"#",
892 u"(", ")",
893 u"[", "]",
894 u"{", "}",
895 ]
896
897 if isinstance(obj, basestring):
898 obj = ustring(obj)
899
900 for x in obj:
901 if x in toescape:
902 result.extend([u"\\", x])
903 else:
904 result.append(x)
905
906 if join:
907 return u"".join(result)
908 else:
909 return result
910
911
912
914 """
915 Set command line prompt
916 """
917
918 self.prompt = Prompt(new, self._attr(u"prompt"))
919 self._invalidate()
920
924 """
925 Return command name and the rest of the command line
926 """
927
928 _r = split_cmd(cmdline)
929
930 if len(_r) == 1:
931 return _r[0], None
932 else:
933 return _r[0], _r[1]
934