File: filebrowser.py

package info (click to toggle)
python-asciimatics 1.15.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,488 kB
  • sloc: python: 15,713; sh: 8; makefile: 2
file content (157 lines) | stat: -rw-r--r-- 6,733 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
"""This module defines a file browser selection"""
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().__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().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 = f"|-- {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 = f"|-+ {my_file} -> {real_path}"
                else:
                    name = f"|-+ {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 = f"|-- {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 = f"|-- {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