| 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 | |