Commit | Line | Data |
---|---|---|
21c4e167 H |
1 | #!/usr/bin/python |
2 | """Yet another curses-based directory tree browser, in Python. | |
3 | ||
4 | I thought I could use something like this for filename entry, kind of | |
5 | like the old 4DOS 'select' command --- cd $(cursoutline.py). So you | |
6 | navigate and hit Enter, and it exits and spits out the file you're on. | |
7 | ||
8 | """ | |
9 | # There are several general approaches to the drawing-an-outline | |
10 | # problem. This program supports the following operations: | |
11 | # - move cursor to previous item (in preorder traversal) | |
12 | # - move cursor to next item (likewise) | |
13 | # - hide descendants | |
14 | # - reveal children | |
15 | # And because it runs over the filesystem, it must be at least somewhat lazy | |
16 | # about expanding children. | |
17 | # And it doesn't really bother to worry about someone else changing the outline | |
18 | # behind its back. | |
19 | # So the strategy is to store our current linear position in the | |
20 | # inorder traversal, and defer operations on the current node until the next | |
21 | # time we're traversing. | |
22 | ||
23 | ||
24 | import curses.wrapper, time, random, cgitb, os, sys | |
25 | cgitb.enable(format="text") | |
26 | ESC = 27 | |
27 | result = '' | |
28 | start = '.' | |
29 | ||
30 | def pad(data, width): | |
31 | # XXX this won't work with UTF-8 | |
32 | return data + ' ' * (width - len(data)) | |
33 | ||
34 | class File: | |
35 | def __init__(self, name): | |
36 | self.name = name | |
37 | def render(self, depth, width): | |
38 | return pad('%s%s %s' % (' ' * 4 * depth, self.icon(), | |
39 | os.path.basename(self.name)), width) | |
40 | def icon(self): return ' ' | |
41 | def traverse(self): yield self, 0 | |
42 | def expand(self): pass | |
43 | def collapse(self): pass | |
44 | ||
45 | class Dir(File): | |
46 | def __init__(self, name): | |
47 | File.__init__(self, name) | |
48 | try: self.kidnames = os.listdir(name) | |
49 | except: self.kidnames = None # probably permission denied | |
50 | self.kids = None | |
51 | self.expanded = False | |
52 | def children(self): | |
53 | if self.kidnames is None: return [] | |
54 | if self.kids is None: | |
55 | self.kids = [factory(os.path.join(self.name, kid)) | |
56 | for kid in self.kidnames] | |
57 | return self.kids | |
58 | def icon(self): | |
59 | if self.expanded: return '[-]' | |
60 | elif self.kidnames is None: return '[?]' | |
61 | elif self.children(): return '[+]' | |
62 | else: return '[ ]' | |
63 | def expand(self): self.expanded = True | |
64 | def collapse(self): self.expanded = False | |
65 | def traverse(self): | |
66 | yield self, 0 | |
67 | if not self.expanded: return | |
68 | for child in self.children(): | |
69 | for kid, depth in child.traverse(): | |
70 | yield kid, depth + 1 | |
71 | ||
72 | def factory(name): | |
73 | if os.path.isdir(name): return Dir(name) | |
74 | else: return File(name) | |
75 | ||
76 | def main(stdscr): | |
77 | cargo_cult_routine(stdscr) | |
78 | stdscr.nodelay(0) | |
79 | mydir = factory(start) | |
80 | mydir.expand() | |
81 | curidx = 3 | |
82 | pending_action = None | |
83 | pending_save = False | |
84 | ||
85 | while 1: | |
86 | stdscr.clear() | |
87 | curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) | |
88 | line = 0 | |
89 | offset = max(0, curidx - curses.LINES + 3) | |
90 | for data, depth in mydir.traverse(): | |
91 | if line == curidx: | |
92 | stdscr.attrset(curses.color_pair(1) | curses.A_BOLD) | |
93 | if pending_action: | |
94 | getattr(data, pending_action)() | |
95 | pending_action = None | |
96 | elif pending_save: | |
97 | global result | |
98 | result = data.name | |
99 | return | |
100 | else: | |
101 | stdscr.attrset(curses.color_pair(0)) | |
102 | if 0 <= line - offset < curses.LINES - 1: | |
103 | stdscr.addstr(line - offset, 0, | |
104 | data.render(depth, curses.COLS)) | |
105 | line += 1 | |
106 | stdscr.refresh() | |
107 | ch = stdscr.getch() | |
108 | if ch == curses.KEY_UP: curidx -= 1 | |
109 | elif ch == curses.KEY_DOWN: curidx += 1 | |
110 | elif ch == curses.KEY_PPAGE: | |
111 | curidx -= curses.LINES | |
112 | if curidx < 0: curidx = 0 | |
113 | elif ch == curses.KEY_NPAGE: | |
114 | curidx += curses.LINES | |
115 | if curidx >= line: curidx = line - 1 | |
116 | elif ch == curses.KEY_RIGHT: pending_action = 'expand' | |
117 | elif ch == curses.KEY_LEFT: pending_action = 'collapse' | |
118 | elif ch == ESC: return | |
119 | elif ch == ord('\n'): pending_save = True | |
120 | curidx %= line | |
121 | ||
122 | def cargo_cult_routine(win): | |
123 | win.clear() | |
124 | win.refresh() | |
125 | curses.nl() | |
126 | curses.noecho() | |
127 | win.timeout(0) | |
128 | ||
129 | def open_tty(): | |
130 | saved_stdin = os.dup(0) | |
131 | saved_stdout = os.dup(1) | |
132 | os.close(0) | |
133 | os.close(1) | |
134 | stdin = os.open('/dev/tty', os.O_RDONLY) | |
135 | stdout = os.open('/dev/tty', os.O_RDWR) | |
136 | return saved_stdin, saved_stdout | |
137 | ||
138 | def restore_stdio((saved_stdin, saved_stdout)): | |
139 | os.close(0) | |
140 | os.close(1) | |
141 | os.dup(saved_stdin) | |
142 | os.dup(saved_stdout) | |
143 | ||
144 | if __name__ == '__main__': | |
145 | global start | |
146 | if len(sys.argv) > 1: | |
147 | start = sys.argv[1] | |
148 | saved_fds = open_tty() | |
149 | try: curses.wrapper(main) | |
150 | finally: restore_stdio(saved_fds) | |
151 | print result | |
152 |