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 % unicode(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(unicode(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 _data = self.replace_aliases(bstring(u"".join(self._data)))
622 _cmd, _rest = _split_cmd(_data)
623
624 if _cmd:
625 self._save_history()
626
627
628 if _cmd in self.xyz.conf["commands"]:
629 try:
630 if _rest is None:
631 arg = _rest
632 else:
633 arg = _rest
634
635 self.xyz.conf["commands"][_cmd](arg)
636 except Exception, e:
637 xyzlog.error(_("Error executing internal command %s: %s") %
638 (_cmd, unicode(e)))
639 elif _cmd:
640 if not hasattr(self, "_execf"):
641 self._execf = self.xyz.pm.from_load(":core:shell", "execute")
642
643 if not hasattr(self, "_reloadf"):
644 self._reloadf = self.xyz.pm.from_load(":sys:panel",
645 "reload_all")
646
647 self._execf(_data)
648 self._reloadf()
649
650 self._clear_cmd()
651 self._invalidate()
652
653
654
656 """
657 Check if first word of the command line (which is supposed to be a
658 command to execute) is in our aliases table, if it is, replace it.
659
660 @param data: String
661 """
662
663 cmd, _ = _split_cmd(data)
664
665 try:
666 raw_alias = self.xyz.conf["aliases"][cmd]
667
668 if isinstance(raw_alias, basestring):
669 alias = raw_alias
670 elif is_func(raw_alias):
671 alias = raw_alias()
672 else:
673 xyzlog.error(_(u"Invalid alias type: %s") %
674 ustring(str(type(raw_alias))))
675 return data
676
677 return re.sub(r"^%s" % cmd, alias, data)
678 except KeyError:
679 return data
680 except Exception, e:
681 xyzlog.error(_(u"Unable to replace an alias %s: %s") %
682 (ustring(cmd), unicode(e)))
683 return data
684
685
686
688 """
689 Return True if cmd is empty, i.e. has no contents
690 """
691
692 return self._data == []
693
694
695
697 """
698 Restore one level from undo buffer
699 """
700
701 self._restore_undo()
702 self._invalidate()
703
704
705
707 """
708 Clear undo buffer
709 """
710
711 self._undo.clear()
712 self._invalidate()
713
714
715
716 - def history_prev(self):
717 """
718 Scroll through list of saved commands backward
719 """
720
721 if self._hindex > 0:
722 self._hindex -= 1
723 self._data = copy.copy(self._history[self._hindex])
724 self.cursor_end()
725
726
727
728 - def history_next(self):
729 """
730 Scroll through list of saved commands forward
731 """
732
733 if self._hindex < len(self._history) - 1:
734 self._hindex += 1
735 self._data = copy.copy(self._history[self._hindex])
736 self.cursor_end()
737
738
739
740 - def history_clear(self):
741 """
742 Clear commands history
743 """
744
745 self._history.clear()
746
747
748
749 - def show_history(self):
750 """
751 Show commands history list
752 """
753
754 def _enter_cb(num):
755 if num >= len(self._history):
756 return
757
758 self._data = copy.copy(self._history[num])
759 self.cursor_end()
760
761
762
763 _sel_attr = self.xyz.skin.attr(XYZListBox.resolution, u"selected")
764
765 _wdata = []
766
767 for i in range(len(self._history)):
768 _wdata.append(NumEntry(u"".join([ustring(x) for x in
769 self._history[i]]),
770 _sel_attr, i,
771 enter_cb=_enter_cb))
772
773 _walker = lowui.SimpleListWalker(_wdata)
774 _walker.focus = len(_walker) - 1
775
776 _dim = tuple([x - 2 for x in self.xyz.screen.get_cols_rows()])
777
778 _ek = [self._keys.ENTER]
779
780 XYZListBox(self.xyz, self.xyz.top, _walker, _(u"History"),
781 _dim).show(exit_keys=_ek)
782
783
784
786 """
787 Put currently selected VFS object name in panel to cmd line
788 """
789
790 return self._put_engine(self._panel.get_selected().name)
791
792
793
795 """
796 Put currently selected VFS object full path in panel to cmd line
797 """
798
799 return self._put_engine(self._panel.get_selected().full_path)
800
801
802
804 """
805 Put selected VFS object name in inactive panel to cmd line
806 """
807
808 return self._put_engine(self._panel.get_selected(False).name)
809
810
811
813 """
814 Put selected VFS object full path in inactive panel to cmd line
815 """
816
817 return self._put_engine(self._panel.get_selected(False).full_path)
818
819
820
822 """
823 Put current working directory of active panel to cmd line
824 """
825
826 return self._put_engine(self._panel.cwd())
827
828
829
831 """
832 Put current working directory of inactive panel to cmd line
833 """
834
835 return self._put_engine(self._panel.cwd(False))
836
837
838
839 - def put(self, obj, space=True):
840 """
841 Put arbitrary string to cmd line starting from the cursor position
842 @param space: Flag indicating whether to append space char after the obj
843 """
844
845 return self._put_engine(obj, space=space)
846
847
848
850 """
851 Get cmd contents
852 """
853
854 return bstring(u"".join(self._data))
855
856
857
859 """
860 Append arbitrary string at the end of cmd
861 """
862
863 self.cursor_end()
864 self.put(obj, space=False)
865
866
867
869 """
870 Put list content to cmd
871 """
872
873 if space:
874 extra = [u" "]
875 else:
876 extra = []
877
878 map(lambda x: self._put_object(x),
879 self.escape([x for x in ustring(obj)]) + extra)
880 self._invalidate()
881
882
883
884 - def escape(self, obj, join=False):
885 """
886 Escape filename
887 @param obj: String to escape
888 @param join: If False return list otherwise return joined string
889 """
890
891 result = []
892 toescape = [u" ", u"'", u'"', u"*", u"?", u"\\", u"&",
893 u"(", ")",
894 u"[", "]",
895 u"{", "}",
896 ]
897
898 if isinstance(obj, basestring):
899 obj = ustring(obj)
900
901 for x in obj:
902 if x in toescape:
903 result.extend([u"\\", x])
904 else:
905 result.append(x)
906
907 if join:
908 return u"".join(result)
909 else:
910 return result
911
912
913
915 """
916 Set command line prompt
917 """
918
919 self.prompt = Prompt(new, self._attr(u"prompt"))
920 self._invalidate()
921
925 """
926 Return command name and the rest of the command line
927 """
928
929 _r = split_cmd(cmdline)
930 _len = len(_r)
931
932 if _len == 0:
933 return "", None
934 elif _len == 1:
935 return _r[0], None
936 else:
937 return _r[0], _r[1]
938