1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import os
18 import traceback
19
20 import libxyz.ui
21 import libxyz.core
22 import libxyz.const
23 import libxyz.exceptions
24
25 from libxyz.core.utils import ustring, bstring
26 from libxyz.ui import lowui
27 from libxyz.ui import align
28 from libxyz.ui import Shortcut
29 from libxyz.ui.utils import refresh
30 from libxyz.ui.utils import truncate
31 from libxyz.vfs.local import LocalVFSObject
32 from libxyz.vfs.types import *
33
34 -class Panel(lowui.WidgetWrap):
35 """
36 Panel is used to display filesystem hierarchy
37 """
38
39 resolution = (u"panel", u"widget")
40 context = u":sys:panel"
41
62
63
64
66 """
67 Compose widgets
68 """
69
70 columns = lowui.Columns([self.block1, self.block2], 0)
71 self._widget = lowui.Pile([columns, self._cmd])
72
73
74
75 @property
77 if self.block1.active:
78 return self.block1
79 else:
80 return self.block2
81
82
83
84 @property
86 if self.block1.active:
87 return self.block2
88 else:
89 return self.block1
90
91
92
94 """
95 Start working loop
96 """
97
98 _dim = self.xyz.screen.get_cols_rows()
99
100 while True:
101 if self._stop:
102 break
103
104 canv = self.xyz.top.render(_dim, True)
105 self.xyz.screen.draw_screen(_dim, canv)
106
107 _input = self.xyz.input.get()
108
109 if _input:
110 try:
111 self._cmd.keypress(_dim, _input)
112 except Exception, e:
113 xyzlog.error(_(u"Error executing bind (%s): %s") %
114 (Shortcut(_input), ustring(str(e))))
115 xyzlog.debug(ustring(traceback.format_exc(),
116 self._enc))
117
118 if self.xyz.input.resized:
119 self._resize = True
120
121 if self._resize:
122 self._resize = False
123 _dim = self.xyz.screen.get_cols_rows()
124 _bsize = libxyz.ui.Size(rows=_dim[1] - 1,
125 cols=_dim[0] / 2 - 2)
126
127 self.block1.size = _bsize
128 self.block2.size = _bsize
129 self._cmd._invalidate()
130 self.block1._invalidate()
131 self.block2._invalidate()
132
133
134
188
189
190
201
202
203
205 """
206 Reparint screen
207 """
208
209 self._resize = True
210 self.xyz.screen.clear()
211
212
213
214 - def entry_next(self):
215 """
216 Next entry
217 """
218
219 return self.active.next()
220
221
222
223 - def entry_prev(self):
224 """
225 Previous entry
226 """
227
228 return self.active.prev()
229
230
231
232 - def entry_top(self):
233 """
234 Top entry
235 """
236
237 return self.active.top()
238
239
240
241 - def entry_bottom(self):
242 """
243 Bottom entry
244 """
245
246 return self.active.bottom()
247
248
249
261
262
263
270
271
272
279
280
281
283 """
284 Get selected VFSFile instance
285 """
286
287 return self.active.get_selected()
288
289
290
292 """
293 Get selected VFSFile instance on inactive block
294 """
295
296 return self.inactive.get_selected()
297
298
299
301 """
302 Return list of tagged VFSFile instances
303 """
304
305 return self.active.get_tagged()
306
307
308
315
316
317
319 """
320 Tag every single object in current dir
321 """
322
323 return self.active.tag_all()
324
325
326
328 """
329 Untag every single object in current dir
330 """
331
332 return self.active.untag_all()
333
334
335
337 """
338 Invert currently tagged files
339 """
340
341 return self.active.tag_invert()
342
343
344
346 """
347 Tag files by combined rule
348 """
349
350 return self.active.tag_rule()
351
352
353
355 """
356 Untag files by combined rules
357 """
358
359 return self.active.untag_rule()
360
361
362
364 """
365 Swap panel blocks
366 """
367
368 self.block1, self.block2 = self.block2, self.block1
369 self._compose()
370 self.set_w(self._widget)
371
372
373
375 """
376 Reload contents
377 """
378
379 return self.active.reload()
380
381
382
384 """
385 Reload inactive panel contents
386 """
387
388 return self.inactive.reload()
389
390
391
399
400
401
403 """
404 Perfrom action on selected object
405 """
406
407 return self.active.action()
408
409
410
412 """
413 Change directory
414 """
415
416 return self.active.chdir(path)
417
418
419
426
427
428
435
436
437
444
445
446
448 """
449 Select VFS object by given name in current directory
450 """
451
452 self.active.select(name)
453
454
455
457 """
458 Get current working directory
459 """
460
461 return self.active.cwd
462
463
464
466 """
467 Get current working directory of inactive panel
468 """
469
470 return self.inactive.cwd
471
472
473
474 -class Block(lowui.FlowWidget):
475 """
476 Single panel block
477 """
478
479 - def __init__(self, xyz, size, path, enc, active=False):
480 """
481 @param xyz: XYZData instance
482 @param size: Block widget size
483 @type size: L{libxyz.ui.Size}
484 @param enc: Local encoding
485 @param active: Boolean flag, True if block is active
486
487 Required resources: cwdtitle, cwdtitleinact, panel, cursor, info
488 border, tagged
489 """
490
491 self.xyz = xyz
492 self.size = size
493 self.attr = lambda x: self.xyz.skin.attr(Panel.resolution, x)
494
495 self.term_width = lambda x: lowui.util.calc_width(x, 0, len(x))
496
497 self.active = active
498 self.selected = 0
499 self.cwd = path
500
501 self._display = []
502 self._vindex = 0
503 self._from = 0
504 self._to = 0
505 self._force_reload = False
506 self.entries = []
507 self._dir = None
508 self._len = 0
509 self._palettes = []
510 self._vfsobj = None
511 self._title = u""
512 self._tagged = []
513
514 self._cursor_attr = None
515 self._custom_info = None
516 self._keys = libxyz.ui.Keys()
517 self._cmd = self.xyz.pm.load(":sys:cmd")
518
519 self._pending = libxyz.core.Queue(20)
520 self._re_raw = r".*"
521 self._rule_raw = ""
522 self._enc = enc
523
524 self.chdir(path)
525
526 self._winfo = lowui.Text(u"")
527 self._sep = libxyz.ui.Separator()
528
529 _info = self._make_info()
530 _title_attr = self._get_title_attr()
531
532 self.frame = lowui.Frame(lowui.Filler(lowui.Text("")), footer=_info)
533 self.border = libxyz.ui.Border(self.frame, self._title,
534 _title_attr, self.attr(u"border"))
535 self.block = lowui.AttrWrap(self.border, self.attr(u"panel"))
536
537 super(Block, self).__init__()
538
539
540
541 - def rows(self, (maxcol,), focus=False):
545
546
547
550
551
552
554 _parent, _dir, _dirs, _files = vfsobj.walk()
555
556 self._dir = _dir
557
558 _entries = [_parent]
559 _entries.extend(_dirs)
560 _entries.extend(_files)
561
562 self._title = truncate(_dir.path, self.size.cols - 4, self._enc, True)
563
564 if hasattr(self, "border"):
565 self.border.set_title(self._title)
566
567 self._tagged = []
568
569 self.entries = _entries
570 self._len = len(self.entries)
571 self._palettes = self._process_skin_rulesets()
572 self._vfsobj = vfsobj
573
574 self._force_reload = True
575
576
577
580
581
582
583 - def render(self, (maxcol,), focus=False):
584 """
585 Render block
586 """
587
588 w = self.display_widget((maxcol,), focus)
589 maxrow = w.rows((maxcol,), focus)
590
591
592 maxcol_orig, maxcol = maxcol, maxcol - 2
593 maxrow_orig, maxrow = maxrow, maxrow - 4
594
595
596 while True:
597 try:
598 _act = self._pending.pop()
599 except IndexError:
600 break
601 else:
602 _act(maxcol, maxrow)
603
604 if self._custom_info is not None:
605 self._set_custom_info(self._custom_info, maxcol)
606 else:
607 self._set_info(self.entries[self.selected], maxcol)
608
609 _tlen = len(self._tagged)
610
611 if _tlen > 0:
612 _text = _(u"%s bytes (%d)") % (
613 self._make_number_readable(
614 reduce(lambda x, y: x + y,
615 [self.entries[x].size for x in self._tagged
616 if isinstance(self.entries[x].ftype, VFSTypeFile)
617 ], 0)), _tlen)
618
619 self._sep.set_text(bstring(_text, self._enc),
620 self.attr(u"tagged"))
621 else:
622 self._sep.clear_text()
623
624 self._display = self._get_visible(maxrow, maxcol, self._force_reload)
625 self._force_reload = False
626
627 _len = len(self._display)
628
629 canvases = []
630
631 for i in xrange(0, _len):
632 _text = self._display[i]
633 _own_attr = None
634 _abs_i = self._from + i
635
636 if self._cursor_attr is not None and i == self._vindex:
637 _own_attr = self._cursor_attr
638 elif self.active and i == self._vindex:
639 _own_attr = self.attr(u"cursor")
640 elif _abs_i in self._tagged:
641 _own_attr = self.attr(u"tagged")
642 elif _abs_i in self._palettes:
643 _own_attr = self._palettes[_abs_i]
644
645 if _own_attr is not None:
646 x = lowui.AttrWrap(lowui.Text(bstring(_text, self._enc)),
647 _own_attr).render((maxcol,))
648 canvases.append((x, i, False))
649 else:
650 canvases.append((lowui.Text(_text).render((maxcol,)),
651 i, False))
652
653 if _len < maxrow:
654 _pad = lowui.AttrWrap(lowui.Text(" "), self.attr(u"panel"))
655 canvases.append((_pad.render((maxcol,), focus), 0, False))
656
657 _info = self._make_info()
658 self.frame.set_footer(_info)
659
660 combined = lowui.CanvasCombine(canvases)
661 border = self.block.render((maxcol_orig, maxrow_orig), focus)
662
663 if _len > maxrow:
664 combined.trim_end(_len - maxrow)
665
666 return lowui.CanvasOverlay(combined, border, 1, 1)
667
668
669
675
676
677
679 _res = []
680
681 i = 0
682 _sep = False
683
684 for x in reversed(unicode(num)):
685 if _sep:
686 _res.append(u"_")
687 _sep = False
688
689 _res.append(x)
690
691 if i > 0 and (i + 1) % 3 == 0:
692 _sep = True
693
694 i += 1
695
696 _res.reverse()
697
698 return u"".join(_res)
699
700
701
703 """
704 Get currently visible piece of entries
705 """
706
707 _len = self._len
708 _from, _to, self._vindex = self._update_vindex(rows)
709
710 if reload or ((_from, _to) != (self._from, self._to)):
711 self._from, self._to = _from, _to
712 self._display = []
713
714 for _obj in self.entries[self._from:self._to]:
715 _text = u"%s%s "% (_obj.vtype, _obj.name)
716 _text = truncate(_text, cols, self._enc)
717 self._display.append(_text)
718
719 return self._display
720
721
722
724 """
725 Process defined fs.* rulesets
726 """
727
728 _result = {}
729
730 try:
731 _rules = self.xyz.skin[u"fs.rules"]
732 except KeyError:
733 return _result
734
735 for i in xrange(self._len):
736 for _exp, _attr in _rules.iteritems():
737 if _exp.match(self.entries[i]):
738 _result[i] = _attr.name
739 break
740
741 return _result
742
743
744
746 """
747 Return title attr
748 """
749
750 if self.active:
751 return self.attr(u"cwdtitle")
752 else:
753 return self.attr(u"cwdtitleinact")
754
755
756
758 """
759 Set info text
760 """
761
762 _part2 = vfsobj.info
763 _part1 = truncate(vfsobj.visual, cols - len(_part2) - 2, self._enc)
764
765 _text = u"%s%s%s" % (_part1,
766 u" " * (cols - (self.term_width(_part1) +
767 self.term_width(_part2)) -
768 1), _part2)
769
770 self._winfo.set_text(bstring(_text, self._enc))
771
772
773
775 """
776 Set custom info text
777 """
778
779 _text = truncate(custom_text, cols, self._enc, True)
780 self._winfo.set_text(bstring(_text, self._enc))
781
782
783
785 """
786 Calculate vindex according to selected position
787 """
788
789 pos = self.selected
790
791 _from = pos / rows * rows
792 _to = _from + rows
793 _vindex = pos - (rows * (pos / rows))
794
795 return (_from, _to, _vindex)
796
797
798
799 @refresh
807
808
809
810 @refresh
819
820
821
822 @refresh
824 """
825 Next entry
826 """
827
828 if self.selected < self._len - 1:
829 self.selected += 1
830
831
832
833 @refresh
835 """
836 Previous entry
837 """
838
839 if self.selected > 0:
840 self.selected -= 1
841
842
843
844 @refresh
846 """
847 Top entry
848 """
849
850 self.selected = 0
851
852
853
854 @refresh
856 """
857 Bottom entry
858 """
859
860 self.selected = self._len - 1
861
862
863
864 @refresh
866 """
867 One block down
868 """
869
870 def _do_next_block(cols, rows):
871 if self.selected + rows >= self._len:
872 return self.bottom()
873 else:
874 self.selected += rows
875
876
877
878
879
880
881 self._pending.push(_do_next_block)
882
883
884
885 @refresh
887 """
888 One block up
889 """
890
891 def _do_prev_block(cols, rows):
892 if self.selected - rows < 0:
893 return self.top()
894 else:
895 self.selected -= rows
896
897
898
899 self._pending.push(_do_prev_block)
900
901
902
904 """
905 Get selected VFSFile instance
906 """
907
908 return self.entries[self.selected]
909
910
911
913 """
914 Return list of tagged VFSFile instances
915 """
916
917 return [self.entries[x] for x in self._tagged]
918
919
920
922 """
923 Toggle tagged selected file
924 """
925
926 if self.selected in self._tagged:
927 self._tagged.remove(self.selected)
928 else:
929 self._tagged.append(self.selected)
930
931 self.next()
932
933
934
935 @refresh
937 """
938 Tag files by combined rule
939 """
940
941 self._tag_rule(tag=True)
942
943
944
945 @refresh
947 """
948 Untag files by combined rule
949 """
950
951 self._tag_rule(tag=False)
952
953
954
956 """
957 Tag engine
958 """
959
960 if tag:
961 _title = _(u"Tag group")
962 else:
963 _title = _(u"Untag group")
964
965 _input = libxyz.ui.InputBox(self.xyz, self.xyz.top,
966 _("Type FS Rule"),
967 title=_title, text=self._rule_raw)
968
969 _raw = _input.show()
970
971 if _raw is None:
972 return
973 else:
974 self._rule_raw = _raw
975
976 try:
977 _rule = libxyz.core.FSRule(ustring(_raw, self._enc))
978 except libxyz.exceptions.ParseError, e:
979 xyzlog.error(ustring(str(e)))
980 return
981
982 try:
983 if tag:
984 self._tagged = [i for i in xrange(self._len) if
985 _rule.match(self.entries[i])]
986 else:
987 self._tagged = [i for i in self._tagged if not
988 _rule.match(self.entries[i])]
989 except libxyz.exceptions.FSRuleError, e:
990 self._tagged = []
991
992 xyzlog.error(ustring(str(e)))
993 return
994
995
996
997 @refresh
999 """
1000 Invert currently tagged files
1001 """
1002
1003 self._tagged = [i for i in xrange(self._len)
1004 if i not in self._tagged]
1005
1006
1007
1008 @refresh
1010 """
1011 Tag every single object in current dir
1012 """
1013
1014 self._tagged = [i for i in xrange(self._len)]
1015
1016
1017
1018 @refresh
1020 """
1021 Untag every single object in current dir
1022 """
1023
1024 self._tagged = []
1025
1026
1027
1028 @refresh
1030 """
1031 Reload contents
1032 """
1033
1034 _selected = self.entries[self.selected]
1035
1036 self._setup(self._vfsobj)
1037
1038 if self.selected >= self._len:
1039 self.selected = self._len - 1
1040
1041
1042 if self.entries[self.selected].name != _selected.name:
1043 self.select(_selected.name)
1044
1045
1046
1047 @refresh
1049 """
1050 Select VFS object by given name in current directory
1051 """
1052
1053 for i in xrange(self._len):
1054 if self.entries[i].name == name:
1055 self.selected = i
1056 break
1057
1058
1059
1061 """
1062 Perform action on selected file
1063 """
1064
1065 _selected = self.entries[self.selected]
1066
1067 _action = self.xyz.am.match(_selected)
1068
1069 if _action is not None:
1070 try:
1071 _action(_selected)
1072 except Exception, e:
1073 xyzlog.error(_(u"Action error: %s") % (ustring(str(e))))
1074
1075
1076
1077 @refresh
1078 - def chdir(self, path, reload=True):
1079 """
1080 Change directory
1081 If reload is not True only execute os.chdir, without reloading
1082 directory contents
1083 """
1084
1085 if reload:
1086 _path = os.path.normpath(path)
1087 _parent = None
1088 _old_vfs = None
1089
1090 if self.entries:
1091 _parent = os.path.normpath(self.entries[0].path)
1092 _old = self._dir.name
1093 _old_vfs = self._vfsobj
1094
1095 try:
1096 _vfsobj = LocalVFSObject(path, self._enc)
1097 except libxyz.exceptions.VFSError, e:
1098 xyzlog.error(_(u"Unable to chdir to %s: %s") %
1099 (ustring(path), ustring(e)))
1100 return
1101
1102 try:
1103 self._setup(_vfsobj)
1104 except libxyz.exceptions.XYZRuntimeError, e:
1105 xyzlog.info(_(u"Unable to chdir to %s: %s") %
1106 (ustring(path), ustring(e)))
1107 return
1108
1109 self.selected = 0
1110
1111
1112 if _parent == _path:
1113 for x in xrange(self._len):
1114 if self.entries[x].name == _old:
1115 self.selected = x
1116 break
1117
1118
1119 if _old_vfs:
1120 del(_old_vfs)
1121
1122 self.cwd = path
1123 os.chdir(path)
1124
1125
1126
1127 @refresh
1129 """
1130 Search forward for matching object while user types
1131 """
1132
1133 return self._search_engine(lambda x: (xrange(x, self._len)))
1134
1135
1136
1137 @refresh
1139 """
1140 Search backward for matching object while user types
1141 """
1142
1143 return self._search_engine(lambda x: (xrange(x, 0, -1)))
1144
1145
1146
1147 @refresh
1149 """
1150 Show only tagged entries
1151 """
1152
1153 if not self._tagged:
1154 return
1155
1156 self.entries = [self.entries[x] for x in self._tagged]
1157 self._len = len(self.entries)
1158 self.selected = 0
1159 self._tagged = []
1160 self._palettes = self._process_skin_rulesets()
1161
1162 _tagged = _(u"TAGGED")
1163
1164 if not self._title.endswith(_tagged):
1165 self._title = truncate(u"%s:%s" % (self._title, _tagged),
1166 self.size.cols - 4, self._enc, True)
1167
1168 if hasattr(self, "border"):
1169 self.border.set_title(self._title)
1170
1171 self._force_reload = True
1172
1173
1174
1176 """
1177 Search for matching filenames while user types
1178 @param order: A function that returns generator for search order
1179 @param pattern: A search type pattern
1180 """
1181
1182 self._cursor_attr = self.attr(u"search")
1183
1184 if pattern is None:
1185
1186 pattern = lambda pat, obj: pat in obj
1187
1188
1189
1190 _dim = self.xyz.screen.get_cols_rows()
1191 _collected = []
1192
1193 _current_pos = self.selected
1194
1195
1196 while True:
1197 self._custom_info = u"".join(_collected)
1198
1199 self._invalidate()
1200 self.xyz.screen.draw_screen(_dim, self.xyz.top.render(_dim, True))
1201
1202 try:
1203 _raw = self.xyz.input.get()
1204
1205 if self.xyz.input.WIN_RESIZE in _raw:
1206 _dim = self.xyz.screen.get_cols_rows()
1207 continue
1208
1209 if self._keys.ESCAPE in _raw or self._keys.ENTER in _raw:
1210 break
1211 elif self._keys.BACKSPACE in _raw:
1212 if _collected:
1213 _collected.pop()
1214
1215 _tmp = _collected[:]
1216 _tmp.extend([ustring(x, self._enc) for x in _raw
1217 if len(x) == 1])
1218 _pattern = u"".join(_tmp)
1219 except Exception:
1220 break
1221
1222
1223 for i in order(_current_pos):
1224 if pattern(_pattern, self.entries[i].name):
1225 self.selected = i
1226 _collected = _tmp
1227 break
1228
1229 self._cursor_attr = None
1230 self._custom_info = None
1231