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 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
|
# coding: utf-8
"""Render a FS object as text tree views.
Color is supported on UNIX terminals.
"""
from __future__ import print_function, unicode_literals
import sys
import typing
from fs.path import abspath, join, normpath
if typing.TYPE_CHECKING:
from typing import List, Optional, Text, TextIO, Tuple
from .base import FS
from .info import Info
def render(
fs, # type: FS
path="/", # type: Text
file=None, # type: Optional[TextIO]
encoding=None, # type: Optional[Text]
max_levels=5, # type: int
with_color=None, # type: Optional[bool]
dirs_first=True, # type: bool
exclude=None, # type: Optional[List[Text]]
filter=None, # type: Optional[List[Text]]
):
# type: (...) -> Tuple[int, int]
"""Render a directory structure in to a pretty tree.
Arguments:
fs (~fs.base.FS): A filesystem instance.
path (str): The path of the directory to start rendering
from (defaults to root folder, i.e. ``'/'``).
file (io.IOBase): An open file-like object to render the
tree, or `None` for stdout.
encoding (str, optional): Unicode encoding, or `None` to
auto-detect.
max_levels (int, optional): Maximum number of levels to
display, or `None` for no maximum.
with_color (bool, optional): Enable terminal color output,
or `None` to auto-detect terminal.
dirs_first (bool): Show directories first.
exclude (list, optional): Option list of directory patterns
to exclude from the tree render.
filter (list, optional): Optional list of files patterns to
match in the tree render.
Returns:
(int, int): A tuple of ``(<directory count>, <file count>)``.
"""
file = file or sys.stdout
if encoding is None:
encoding = getattr(file, "encoding", "utf-8") or "utf-8"
is_tty = hasattr(file, "isatty") and file.isatty()
if with_color is None:
is_windows = sys.platform.startswith("win")
with_color = False if is_windows else is_tty
if encoding.lower() == "utf-8" and with_color:
char_vertline = "│"
char_newnode = "├"
char_line = "──"
char_corner = "└"
else:
char_vertline = "|"
char_newnode = "|"
char_line = "--"
char_corner = "`"
indent = " " * 4
line_indent = char_vertline + " " * 3
def write(line):
# type: (Text) -> None
"""Write a line to the output."""
print(line, file=file)
# FIXME(@althonos): define functions using `with_color` and
# avoid checking `with_color` at every function call !
def format_prefix(prefix):
# type: (Text) -> Text
"""Format the prefix lines."""
if not with_color:
return prefix
return "\x1b[32m%s\x1b[0m" % prefix
def format_dirname(dirname):
# type: (Text) -> Text
"""Format a directory name."""
if not with_color:
return dirname
return "\x1b[1;34m%s\x1b[0m" % dirname
def format_error(msg):
# type: (Text) -> Text
"""Format an error."""
if not with_color:
return msg
return "\x1b[31m%s\x1b[0m" % msg
def format_filename(fname):
# type: (Text) -> Text
"""Format a filename."""
if not with_color:
return fname
if fname.startswith("."):
fname = "\x1b[33m%s\x1b[0m" % fname
return fname
def sort_key_dirs_first(info):
# type: (Info) -> Tuple[bool, Text]
"""Get the info sort function with directories first."""
return (not info.is_dir, info.name.lower())
def sort_key(info):
# type: (Info) -> Text
"""Get the default info sort function using resource name."""
return info.name.lower()
counts = {"dirs": 0, "files": 0}
def format_directory(path, levels):
# type: (Text, List[bool]) -> None
"""Recursive directory function."""
try:
directory = sorted(
fs.filterdir(path, exclude_dirs=exclude, files=filter),
key=sort_key_dirs_first if dirs_first else sort_key, # type: ignore
)
except Exception as error:
prefix = (
"".join(indent if last else line_indent for last in levels)
+ char_corner
+ char_line
)
write(
"{} {}".format(
format_prefix(prefix), format_error("error ({})".format(error))
)
)
return
_last = len(directory) - 1
for i, info in enumerate(directory):
is_last_entry = i == _last
counts["dirs" if info.is_dir else "files"] += 1
prefix = "".join(indent if last else line_indent for last in levels)
prefix += char_corner if is_last_entry else char_newnode
if info.is_dir:
write(
"{} {}".format(
format_prefix(prefix + char_line), format_dirname(info.name)
)
)
if max_levels is None or len(levels) < max_levels:
format_directory(join(path, info.name), levels + [is_last_entry])
else:
write(
"{} {}".format(
format_prefix(prefix + char_line), format_filename(info.name)
)
)
format_directory(abspath(normpath(path)), [])
return counts["dirs"], counts["files"]
|