# -*- coding: utf-8 -*- """This module defines a file browser selection""" from __future__ import division from __future__ import absolute_import from __future__ import print_function from __future__ import unicode_literals from re import compile as re_compile import os import unicodedata from collections import namedtuple from asciimatics.utilities import readable_timestamp, readable_mem from asciimatics.widgets.multicolumnlistbox import MultiColumnListBox class FileBrowser(MultiColumnListBox): """ A FileBrowser is a widget for finding a file on the local disk. """ def __init__(self, height, root, name=None, on_select=None, on_change=None, file_filter=None): r""" :param height: The desired height for this widget. :param root: The starting root directory to display in the widget. :param name: The name of this widget. :param on_select: Optional function that gets called when user selects a file (by pressing enter or double-clicking). :param on_change: Optional function that gets called on any movement of the selection. :param file_filter: Optional RegEx string that can be passed in to filter the files to be displayed. Most people will want to use a filter to finx files with a particular extension. In this case, you must use a regex that matches to the end of the line - e.g. use ".*\.txt$" to find files ending with ".txt". This ensures that you don't accidentally pick up files containing the filter. """ super(FileBrowser, self).__init__( height, [0, ">8", ">14"], [], titles=["Filename", "Size", "Last modified"], name=name, on_select=self._on_selection, on_change=on_change) # Remember the on_select handler for external notification. This allows us to wrap the # normal on_select notification with a function that will open new sub-directories as # needed. self._external_notification = on_select self._root = root self._in_update = False self._initialized = False self._file_filter = None if file_filter is None else re_compile(file_filter) def update(self, frame_no): # Defer initial population until we first display the widget in order to avoid race # conditions in the Frame that may be using this widget. if not self._initialized: self._populate_list(self._root) self._initialized = True super(FileBrowser, self).update(frame_no) def _on_selection(self): """ Internal function to handle directory traversal or bubble notifications up to user of the Widget as needed. """ if self.value and os.path.isdir(self.value): self._populate_list(self.value) elif self._external_notification: self._external_notification() def clone(self, new_widget): # Copy the data into the new widget. Notes: # 1) I don't really want to expose these methods, so am living with the protected access. # 2) I need to populate the list and then assign the values to ensure that we get the # right selection on re-sizing. # pylint: disable=protected-access new_widget._populate_list(self._root) new_widget._start_line = self._start_line new_widget._root = self._root new_widget.value = self.value def _populate_list(self, value): """ Populate the current multi-column list with the contents of the selected directory. :param value: The new value to use. """ # Nothing to do if the value is rubbish. if value is None: return # Stop any recursion - no more returns from here to end of fn please! if self._in_update: return self._in_update = True # We need to update the tree view. self._root = os.path.abspath(value if os.path.isdir(value) else os.path.dirname(value)) # The absolute expansion of "/" or "\" is the root of the disk, so is a cross-platform # way of spotting when to insert ".." or not. tree_view = [] if len(self._root) > len(os.path.abspath(os.sep)): tree_view.append((["|-+ .."], os.path.abspath(os.path.join(self._root, "..")))) tree_dirs = [] tree_files = [] try: files = os.listdir(self._root) except OSError: # Can fail on Windows due to access permissions files = [] for my_file in files: full_path = os.path.join(self._root, my_file) try: details = os.lstat(full_path) except OSError: # Can happen on Windows due to access permissions details = namedtuple("stat_type", "st_size st_mtime") details.st_size = 0 details.st_mtime = 0 name = "|-- {}".format(my_file) tree = tree_files if os.path.isdir(full_path): tree = tree_dirs if os.path.islink(full_path): # Show links separately for directories real_path = os.path.realpath(full_path) name = "|-+ {} -> {}".format(my_file, real_path) else: name = "|-+ {}".format(my_file) elif self._file_filter and not self._file_filter.match(my_file): # Skip files that don't match the filter (if present) continue elif os.path.islink(full_path): # Check if link target exists and if it does, show statistics of the # linked file, otherwise just display the link try: real_path = os.path.realpath(full_path) except OSError: # Can fail on Linux prof file system. real_path = None if real_path and os.path.exists(real_path): details = os.stat(real_path) name = "|-- {} -> {}".format(my_file, real_path) else: # Both broken directory and file links fall to this case. # Actually using the files will cause a FileNotFound exception name = "|-- {} -> {}".format(my_file, real_path) # Normalize names for MacOS and then add to the list. tree.append(([unicodedata.normalize("NFC", name), readable_mem(details.st_size), readable_timestamp(details.st_mtime)], full_path)) tree_view.extend(sorted(tree_dirs)) tree_view.extend(sorted(tree_files)) self.options = tree_view self._titles[0] = self._root # We're out of the function - unset recursion flag. self._in_update = False