163 lines
6.8 KiB
Python
163 lines
6.8 KiB
Python
|
# -*- 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
|