#!/usr/bin/python
#
# Urwid example lazy directory browser / tree view
#    Original version:
#      Copyright (C) 2004-2007  Ian Ward
#    Modified by Rob Lanphier to use general TreeWidget/TreeWalker classes
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: http://excess.org/urwid/

"""
Urwid example lazy directory browser / tree view

Features:
- custom selectable widgets for files and directories
- custom message widgets to identify access errors and empty directories
- custom list walker for displaying widgets in a tree fashion
- outputs a quoted list of files and directories "selected" on exit
"""


import urwid
import urwid.raw_display

import os

from treetools import *

class FileTreeWidget(TreeWidget):
    def __init__(self, parent, name, index, display):
        self.process_parent_param(parent)
        self.__super.__init__(self.parent, name, index, display)

    def calc_depth(self):
        parent, _ign = os.path.split(self.dir)
        # we're at the top if parent is same as dir
        if self.dir == parent:
            return 0
        else:
            return self.dir.count(dir_sep())

    def get_parent(self):
        return get_directory(self.dir)

    def get_dir(self):
        return self.dir

    def process_parent_param(self, parent):
        if isinstance(parent, str):
            self.dir = parent
            self.parent = self.get_parent()
        else:
            self.parent = parent
            self.dir = parent.get_dir()

class EmptyWidget(FileTreeWidget):
    """A marker for expanded directories with no contents."""

    def __init__(self, dir, name, index):
        self.__super.__init__(dir, name, index, 
            ('flag',"(empty directory)"))
    
    def selectable(self):
        return False


class ErrorWidget(FileTreeWidget):
    """A marker for errors reading directories."""

    def __init__(self, dir, name, index):
        self.__super.__init__(dir, name, index, 
            ('error',"(error/permission denied)"))
    
    def selectable(self):
        return False

class FileWidget(FileTreeWidget):
    """Widget for a simple file (or link, device node, etc)."""
    
    def __init__(self, dir, name, index):
        self.__super.__init__(dir, name, index, name)


class DirectoryWidget(ParentWidget, FileTreeWidget):
    """Widget for a directory."""
    def __init__(self, parent, name, index ):
        self.process_parent_param(parent)

        self.__super.__init__(parent, name, index )

        # check if this directory starts expanded
        self.expanded = starts_expanded( os.path.join(self.dir,name) )
                
        self.update_widget()
        
    def self_as_parent(self):
        """Return self as a parent object."""
        full_dir = os.path.join(self.dir, self.name)
        return get_directory( full_dir )


class Directory(ParentNode):
    """Store sorted directory contents and cache TreeWidget objects."""
    def __init__(self, path):
        self.path = path
        super(Directory, self).__init__(self.get_key())
        #raise RuntimeError(str(dir(self)))

    def get_key(self):
        _ign, key = os.path.split(self.path)
        return key

    def get_parent(self):
        parentname, myname = os.path.split(self.path)
        # give up if we can't go higher
        if parentname == self.path: return None
        return get_directory(parentname)
        
    def get_items(self):
        dirs = []
        files = []
        try:
            # separate dirs and files
            for a in os.listdir(self.path):
                if os.path.isdir(os.path.join(self.path,a)):
                    dirs.append( a )
                else:
                    files.append( a )
        except OSError, e:
            self.widgets[None] = ErrorWidget( self.path, None, 0 )

        # sort dirs and files
        dirs.sort(sensible_cmp)
        files.sort(sensible_cmp)
        # store where the first file starts
        self.dir_count = len(dirs)
        # collect dirs and files together again
        return dirs + files
    
    def get_constructor(self, key):
        """Provide constructor for the correct TreeWidget type """
        index = self.items.index(key)
        if key is None:
            constructor = EmptyWidget
        elif index < self.dir_count:
            constructor = DirectoryWidget
        else:
            constructor = FileWidget
        return constructor


class DirectoryWalker(TreeWalker):
    """ListWalker-compatible class for browsing directories.
    
    positions used are directory,filename tuples."""
    # In converting this code, I took a gross shortcut that I new would work, 
    # which was to convert every instance of a string-based parent in the focus
    # tuple over to an object before passing it to the superclass.  The right
    # way to fix this would be to get rid of all instances where the calling
    # code passes in a string. -- robla 2010-02-08
    def __init__(self, start_from):
        if isinstance(start_from, str):
            start_from = get_directory(start_from)
        super(self.__class__, self).__init__(start_from)

    def set_focus(self, focus):
        focus=self.process_focus(focus)
        return super(self.__class__, self).set_focus(focus)
    
    def get_next(self, start_from):
        start_from=self.process_focus(start_from)
        return super(self.__class__, self).get_next(start_from)

    def get_prev(self, start_from):
        start_from=self.process_focus(start_from)
        return super(self.__class__, self).get_prev(start_from)

    def process_focus(self, focus):
        parent, key = focus
        if isinstance(parent, str):
            parent = get_directory(parent)
        return parent, key



class DirectoryBrowser:
    palette = [
        ('body', 'black', 'light gray'),
        ('selected', 'black', 'dark green', ('bold','underline')),
        ('focus', 'light gray', 'dark blue', 'standout'),
        ('selected focus', 'yellow', 'dark cyan', 
                ('bold','standout','underline')),
        ('head', 'yellow', 'black', 'standout'),
        ('foot', 'light gray', 'black'),
        ('key', 'light cyan', 'black','underline'),
        ('title', 'white', 'black', 'bold'),
        ('dirmark', 'black', 'dark cyan', 'bold'),
        ('flag', 'dark gray', 'light gray'),
        ('error', 'dark red', 'light gray'),
        ]
    
    footer_text = [
        ('title', "Directory Browser"), "    ",
        ('key', "UP"), ",", ('key', "DOWN"), ",",
        ('key', "PAGE UP"), ",", ('key', "PAGE DOWN"),
        "  ",
        ('key', "SPACE" ), "  ",
        ('key', "+"), ",",
        ('key', "-"), "  ",
        ('key', "LEFT"), "  ",
        ('key', "HOME"), "  ", 
        ('key', "END"), "  ",
        ('key', "Q"),
        ]
    
    
    def __init__(self):
        cwd = os.getcwd()
        store_initial_cwd( cwd )
        self.listbox = urwid.ListBox( DirectoryWalker( cwd ) )
        self.listbox.offset_rows = 1
        self.header = urwid.Text( "" )
        self.footer = urwid.AttrWrap( urwid.Text( self.footer_text ),
            'foot')
        self.view = urwid.Frame( 
            urwid.AttrWrap( self.listbox, 'body' ), 
            header=urwid.AttrWrap(self.header, 'head' ), 
            footer=self.footer )

    def main(self):
        """Run the program."""
        
        self.ui = urwid.raw_display.Screen()
        self.ui.register_palette( self.palette )
        self.ui.run_wrapper( self.run )
    
        # on exit, write the selected filenames to the console
        names = [escape_filename_sh(x) for x in get_selected_names()]
        print " ".join(names)

    def run(self):
        """Handle user input and display updating."""
        
        self.ui.set_mouse_tracking()
        
        size = self.ui.get_cols_rows()
        while 1:
            focus, _ign = self.listbox.body.get_focus()
            # update display of focus directory
            self.header.set_text( focus.dir )
            canvas = self.view.render( size, focus=1 )
            self.ui.draw_screen( size, canvas )
            keys = None
            while not keys: 
                keys = self.ui.get_input()
            for k in keys:
                if urwid.is_mouse_event(k):
                    event, button, col, row = k
                    self.view.mouse_event( size, 
                        event, button, col, row,
                        focus=True )
                    continue

                if k == 'window resize':
                    size = self.ui.get_cols_rows()
                elif k in ('q','Q'):
                    return
                elif k == 'right':
                    # right can behave like +
                    k = "+"
                k = self.view.keypress( size, k )
                # k = unhandled key or None
                if k == 'left':
                    self.move_focus_to_parent( size )
                elif k == '-':
                    self.collapse_focus_parent( size )
                elif k == 'home':
                    self.focus_home( size )
                elif k == 'end':
                    self.focus_end( size )
                    
    def collapse_focus_parent(self, size):
        """Collapse parent directory."""
        
        widget, pos = self.listbox.body.get_focus()
        self.move_focus_to_parent( size )
        
        pwidget, ppos = self.listbox.body.get_focus()
        if widget.dir != pwidget.dir:
            self.view.keypress( size, "-")

    def move_focus_to_parent(self, size):
        """Move focus to parent of widget in focus."""
        
        middle,top,bottom = self.listbox.calculate_visible( size )
        
        row_offset, focus_widget, focus_pos, focus_rows, cursor = middle
        trim_top, fill_above = top

        parent,name = os.path.split(focus_widget.dir)
        
        if parent == focus_widget.dir:
            # no root dir, choose first element instead
            self.focus_home( size )
            return
        
        for widget, pos, rows in fill_above:
            row_offset -= rows
            if pos == (parent,name):
                self.listbox.change_focus(size, pos, row_offset)
                return
        
        self.listbox.change_focus( size, (parent, name) )
        return 
        
    def focus_home(    self, size ):
        """Move focus to very top."""
        
        dir = get_directory("/")
        widget = dir.get_first()
        parent, name = widget.dir, widget.name
        self.listbox.change_focus( size, (parent, name) )

    def focus_end( self, size ):
        """Move focus to far bottom."""
        
        maxrow, maxcol = size
        dir = get_directory("/")
        widget = dir.get_last()
        parent, name = widget.dir, widget.name
        self.listbox.change_focus( size, (parent, name), maxrow-1 )



def main():
    DirectoryBrowser().main()




#######
# global cache of directory information
_dir_cache = {}

def get_directory(name):
    """Return the Directory object for a given path.  Create if necessary."""
    
    if not _dir_cache.has_key(name):
        _dir_cache[name] = Directory(name)
    return _dir_cache[name]

def directory_cached(name):
    """Return whether the directory is in the cache."""
    
    return _dir_cache.has_key(name)

def get_selected_names():
    """Return a list of all filenames marked as selected."""
    
    l = []
    for d in _dir_cache.values():
        for w in d.widgets.values():
            if w.selected:
                l.append( os.path.join( w.dir, w.name ) )
    return l
            



######
# store path components of initial current working directory
_initial_cwd = []

def store_initial_cwd( name ):
    """Store the initial current working directory path components."""
    
    global _initial_cwd
    _initial_cwd = name.split( dir_sep() )

def starts_expanded( name ):
    """Return True if directory is a parent of initial cwd."""
    
    l = name.split( dir_sep() )
    if len(l) > len( _initial_cwd ):
        return False
    
    if l != _initial_cwd[:len(l)]:
        return False
    
    return True


def escape_filename_sh( name ):
    """Return a hopefully safe shell-escaped version of a filename."""

    # check whether we have unprintable characters
    for ch in name: 
        if ord(ch) < 32: 
            # found one so use the ansi-c escaping
            return escape_filename_sh_ansic( name )
            
    # all printable characters, so return a double-quoted version
    name.replace('\\','\\\\')
    name.replace('"','\\"')
    name.replace('`','\\`')
    name.replace('$','\\$')
    return '"'+name+'"'


def escape_filename_sh_ansic( name ):
    """Return an ansi-c shell-escaped version of a filename."""
    
    out =[]
    # gather the escaped characters into a list
    for ch in name:
        if ord(ch) < 32:
            out.append("\\x%02x"% ord(ch))
        elif ch == '\\':
            out.append('\\\\')
        else:
            out.append( ch )
            
    # slap them back together in an ansi-c quote  $'...'
    return "$'" + "".join(out) + "'"


def sensible_cmp( name_a, name_b ):
    """Case insensitive compare with sensible numeric ordering.
    
    "blah7" < "BLAH08" < "blah9" < "blah10" """
    
    # ai, bi are indexes into name_a, name_b
    ai = bi = 0
    
    def next_atom( name, i ):
        """Return the next 'atom' and the next index.
        
        An 'atom' is either a nonnegative integer or an uppercased 
        character used for defining sort order."""
        
        a = name[i].upper()
        i += 1
        if a.isdigit():
            while i < len(name) and name[i].isdigit():
                a += name[i]
                i += 1
            a = long(a)
        return a, i
        
    # compare one atom at a time
    while ai < len(name_a) and bi < len(name_b):
        a, ai = next_atom( name_a, ai )
        b, bi = next_atom( name_b, bi )
        if a < b: return -1
        if a > b: return 1
    
    # if all out of atoms to compare, do a regular cmp
    if ai == len(name_a) and bi == len(name_b): 
        return cmp(name_a,name_b)
    
    # the shorter one comes first
    if ai == len(name_a): return -1
    return 1


def dir_sep():
    """Return the separator used in this os."""
    return getattr(os.path,'sep','/')


if __name__=="__main__": 
    main()

