diff --git a/.gitignore b/.gitignore index b6e4761..1b02a69 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# Idea configurations +.idea/ diff --git a/README.md b/README.md index b4fddad..3e255ff 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ xontrib load xlsd ### Registering an icon -In xlsd, icons are registered using a name. The name is then used by the different rules to get an icon for an `os.DirEntry`. +In xlsd, icons are registered using a name. The name is then used by the different rules to get an icon for an `PathEntry`. You can view the built-in icons in [xlsd/icons.py](xlsd/icons.py#L99). @@ -162,7 +162,7 @@ All the built-in columns are used in this config. ### Writing your own column -A column is a function taking for only argument an `os.DirEntry` and that outputs a string. +A column is a function taking for only argument an `PathEntry` and that outputs a string. A simple filename column could be registered like this: ```python diff --git a/xlsd/__init__.py b/xlsd/__init__.py index f4d44ba..7411aa0 100644 --- a/xlsd/__init__.py +++ b/xlsd/__init__.py @@ -1,5 +1,6 @@ -import os from typing import Callable +from .path import PathEntry + COLORS = { 'symlink_target': "{CYAN}", @@ -8,10 +9,11 @@ 'size_unit': "{CYAN}", } -XlsdSortMethod = Callable[[list[os.DirEntry]], list[os.DirEntry]] + +XlsdSortMethod = Callable[[list[PathEntry]], list[PathEntry]] XLSD_SORT_METHODS: dict[str, XlsdSortMethod] = {} -#XLSD_SORT_METHODS = {} + def xlsd_register_sort_method(name: str): """ @@ -22,7 +24,8 @@ def decorator(func: XlsdSortMethod): return func return decorator -def _direntry_lowercase_name(entry: os.DirEntry) -> str: + +def _direntry_lowercase_name(entry: PathEntry) -> str: """ Return the lowercase name for a DirEntry. @@ -30,8 +33,9 @@ def _direntry_lowercase_name(entry: os.DirEntry) -> str: """ return entry.name.lower() + @xlsd_register_sort_method('directories_first') -def xlsd_sort_directories_first(entries: list[os.DirEntry]) -> list[os.DirEntry]: +def xlsd_sort_directories_first(entries: list[PathEntry]) -> list[PathEntry]: """ Sort the entries in alphabetical order, directories first. """ @@ -44,7 +48,7 @@ def xlsd_sort_directories_first(entries: list[os.DirEntry]) -> list[os.DirEntry] directories.append(entry) else: files.append(entry) - except OSError: # Probably circular symbolic link + except OSError: # Probably circular symbolic link directories.append(entry) directories.sort(key=_direntry_lowercase_name) @@ -52,16 +56,18 @@ def xlsd_sort_directories_first(entries: list[os.DirEntry]) -> list[os.DirEntry] return directories + files + @xlsd_register_sort_method('alphabetical') -def xlsd_sort_alphabetical(entries: list[os.DirEntry]) -> list[os.DirEntry]: +def xlsd_sort_alphabetical(entries: list[PathEntry]) -> list[PathEntry]: """ Sort the entries in alphabetical order. """ entries.sort(key=_direntry_lowercase_name) return entries + @xlsd_register_sort_method('as_is') -def xlsd_sort_as_is(entries: list[os.DirEntry]) -> list[os.DirEntry]: +def xlsd_sort_as_is(entries: list[PathEntry]) -> list[PathEntry]: """ Keep the entries in the same order they were returned by the OS. """ diff --git a/xlsd/icons.py b/xlsd/icons.py index b13b484..5421676 100644 --- a/xlsd/icons.py +++ b/xlsd/icons.py @@ -5,6 +5,7 @@ T = TypeVar('T') + class IconSet(Generic[T]): """ A storage for icons. diff --git a/xlsd/path.py b/xlsd/path.py new file mode 100644 index 0000000..fc540fd --- /dev/null +++ b/xlsd/path.py @@ -0,0 +1,80 @@ +import os +import stat + + +class PathEntry: + """ + PathEntry is a utility class that represents a filesystem entry at a given path. The entry can represent a file, + directory, or symbolic link. The class provides methods for checking the type of the entry and obtaining file + statistics for it. + + The class can be used to mirror the behavior of PathEntry, because PathEntry objects returned by os.scandir() + do not handle individual file paths and do not allow selective control over following symbolic links for + different operations. + """ + + def __init__(self, path: str) -> None: + """ + Initialize a new instance of the PathEntry class. + + :param path: A string containing a path to a file or directory. + :type path: str + """ + self.path = path + self.name = os.path.basename(path) + + def is_dir(self, follow_symlinks: bool = True) -> bool: + """ + Check if the path represents a directory. + + :param follow_symlinks: If True (default), symlinks are followed (i.e., if the path points to a directory + through a symlink, it will return True). If False, symlinks are not followed (i.e., + if the path points to a directory through a symlink, it will return False). + :type follow_symlinks: bool + :return: True if the path represents a directory, False otherwise. + :rtype: bool + """ + if follow_symlinks: + return os.path.isdir(self.path) + else: + return stat.S_ISDIR(self.stat(follow_symlinks=follow_symlinks).st_mode) + + def is_file(self, follow_symlinks: bool = True) -> bool: + """ + Check if the path represents a file. + + :param follow_symlinks: If True (default), symlinks are followed (i.e., if the path points to a file through a + symlink, it will return True). If False, symlinks are not followed (i.e., if the path + points to a file through a symlink, it will return False). + :type follow_symlinks: bool + :return: True if the path represents a file, False otherwise. + :rtype: bool + """ + if follow_symlinks: + return os.path.isfile(self.path) + else: + return stat.S_ISREG(self.stat(follow_symlinks=follow_symlinks).st_mode) + + def is_symlink(self) -> bool: + """ + Check if the path represents a symbolic link. + + :return: True if the path represents a symbolic link, False otherwise. + :rtype: bool + """ + return os.path.islink(self.path) + + def stat(self, follow_symlinks: bool = True) -> os.stat_result: + """ + Perform a stat system call on the given path. + + :param follow_symlinks: If True (default), symlinks are followed, similar to os.stat(). If False, symlinks are + not followed, similar to os.lstat(). + :type follow_symlinks: bool + :return: The result of the stat or lstat call on the path. + :rtype: os.stat_result + """ + if follow_symlinks: + return os.stat(self.path) + else: + return os.lstat(self.path) \ No newline at end of file diff --git a/xontrib/xlsd.xsh b/xontrib/xlsd.xsh index 51816b5..d8e7c4b 100644 --- a/xontrib/xlsd.xsh +++ b/xontrib/xlsd.xsh @@ -4,14 +4,18 @@ An improved ls for xonsh, inspired by lsd. Registers automatically as an alias for ls on load. """ -from enum import Enum, auto -import math import os -from typing import Callable, Dict, List, Optional, Tuple, Union +import math + +from enum import Enum, auto +from typing import Callable, List, Optional from xonsh.lazyasd import lazyobject from xonsh.tools import format_color, print_color, is_string_seq +from xlsd.path import PathEntry + + # Lazy imports @lazyobject def grp(): @@ -129,7 +133,7 @@ ${...}.register('XLSD_LIST_COLUMNS', validate=is_string_seq, convert=csv_to_list ${...}.register('XLSD_ICON_SOURCES', validate=is_string_seq, convert=csv_to_list, detype=list_to_csv, default=['extension', 'libmagic']) -XlsdColumn = Callable[[os.DirEntry],str] +XlsdColumn = Callable[[PathEntry],str] #TODO see with xonsh devs, imo shouldn't crash #_XLSD_COLUMNS: Dict[str, Tuple[XlsdColumn, ColumnAlignment]] = {} @@ -144,7 +148,7 @@ def xlsd_register_column(name: str, alignment: ColumnAlignment): return func return decorator -XlsdIconSource = Callable[[os.DirEntry], Optional[str]] +XlsdIconSource = Callable[[PathEntry], Optional[str]] #TODO see with xonsh devs, imo shouldn't crash #_XLSD_ICON_SOURCES: Dict[str, XlsdIconSource] = {} @@ -205,7 +209,7 @@ def _format_size(size: int) -> str: # the 'magic' lib might only be included in arch linux, it doesn't seem to work # on macos. @xlsd_register_icon_source('libmagic') -def _xlsd_icon_source_libmagic(direntry: os.DirEntry) -> Optional[str]: +def _xlsd_icon_source_libmagic(direntry: PathEntry) -> Optional[str]: """ Return the icon for a direntry using the file's mimetype. """ @@ -228,7 +232,7 @@ def _xlsd_icon_source_libmagic(direntry: os.DirEntry) -> Optional[str]: @xlsd_register_icon_source('extension') -def _xlsd_icon_source_extension(direntry: os.DirEntry) -> Optional[str]: +def _xlsd_icon_source_extension(direntry: PathEntry) -> Optional[str]: """ Return the emoji for a direntry using the file extension. """ @@ -250,7 +254,7 @@ def _xlsd_icon_source_extension(direntry: os.DirEntry) -> Optional[str]: # /Icon sources # ################# -def _icon_for_direntry(entry: os.DirEntry) -> str: +def _icon_for_direntry(entry: PathEntry) -> str: """ Return the icon for a direntry. """ @@ -266,7 +270,7 @@ def _icon_for_direntry(entry: os.DirEntry) -> str: return icons.LS_ICONS.get(name) -def _get_color_for_direntry(entry: os.DirEntry) -> str: +def _get_color_for_direntry(entry: PathEntry) -> str: """Return one or multiple xonsh colors for the entry using $LS_COLORS.""" colors = [] @@ -311,11 +315,11 @@ def _get_color_for_direntry(entry: os.DirEntry) -> str: return "".join("{"+color+"}" for color in colors) -def _format_direntry_name(entry: os.DirEntry, show_target: bool = True) -> str: +def _format_direntry_name(entry: PathEntry, show_target: bool = True) -> str: """ Return a string containing a bunch of ainsi escape codes as well as the "width" of the new name. """ - path = entry.path if not entry.is_symlink() else os.readlink(entry.path) + # path = entry.path if not entry.is_symlink() else os.readlink(entry.path) name = entry.name # Show the icon @@ -343,8 +347,20 @@ def _format_direntry_name(entry: os.DirEntry, show_target: bool = True) -> str: return "".join(colors) + name +def _scan_dir(path: str): + """ + Scan the directory with the given path and yield PathEntry instances for each entry. -def _direntry_lowercase_name(entry: os.DirEntry) -> str: + :param path: A string containing a path to a directory. + :type path: str + :yield: PathEntry instance for each entry in path. + :rtype: Iterator[PathEntry] + """ + for entry in os.scandir(path): + yield PathEntry(entry.path) + + +def _direntry_lowercase_name(entry: PathEntry) -> str: """ Return the lowercase name for a DirEntry. @@ -353,21 +369,26 @@ def _direntry_lowercase_name(entry: os.DirEntry) -> str: return entry.name.lower() -def _get_entries(path: str, show_hidden: bool) -> List[os.DirEntry]: +def _get_entries(path: str, show_hidden: bool) -> List[PathEntry]: """ - Return the list of DirEntrys for a path, sorted by name, directories first. + Return the list of PathEntry's for a path, sorted by name, directories first. """ entries = [] - try: - with os.scandir(path) as iterator: - for entry in iterator: + + path_entry = PathEntry(path) + if path_entry.is_dir(path): + try: + for entry in _scan_dir(path): # Skip entries that start with a '.' if not show_hidden and entry.name.startswith('.'): continue entries.append(entry) - except PermissionError: - pass + except PermissionError: + pass + elif path_entry.is_file(path): + # If the path is a file, create a DirEntry for it + entries.append(PathEntry(path)) sort_method = xlsd.XLSD_SORT_METHODS.get($XLSD_SORT_METHOD, lambda x: x) @@ -503,7 +524,7 @@ def _show_table(columns: List[List[str]], column_alignments: List[ColumnAlignmen ################ @xlsd_register_column('mode', ColumnAlignment.LEFT) -def _xlsd_column_mode(direntry: os.DirEntry) -> str: +def _xlsd_column_mode(direntry: PathEntry) -> str: """ Format the mode from the stat structure for a file. """ @@ -515,7 +536,7 @@ def _xlsd_column_mode(direntry: os.DirEntry) -> str: @xlsd_register_column('hardlinks', ColumnAlignment.RIGHT) -def _xlsd_column_hardlinks(direntry: os.DirEntry) -> str: +def _xlsd_column_hardlinks(direntry: PathEntry) -> str: """ Show the number of hardlinks for a file. """ @@ -523,7 +544,7 @@ def _xlsd_column_hardlinks(direntry: os.DirEntry) -> str: @xlsd_register_column('uid', ColumnAlignment.LEFT) -def _xlsd_column_uid(direntry: os.DirEntry) -> str: +def _xlsd_column_uid(direntry: PathEntry) -> str: """ Show the owner (user) of the file. """ @@ -532,7 +553,7 @@ def _xlsd_column_uid(direntry: os.DirEntry) -> str: @xlsd_register_column('gid', ColumnAlignment.LEFT) -def _xlsd_column_gid(direntry: os.DirEntry) -> str: +def _xlsd_column_gid(direntry: PathEntry) -> str: """ Show the group that owns the file. """ @@ -541,7 +562,7 @@ def _xlsd_column_gid(direntry: os.DirEntry) -> str: @xlsd_register_column('size', ColumnAlignment.RIGHT) -def _xlsd_column_size(direntry: os.DirEntry) -> str: +def _xlsd_column_size(direntry: PathEntry) -> str: """ Format the size of a file. """ @@ -549,7 +570,7 @@ def _xlsd_column_size(direntry: os.DirEntry) -> str: @xlsd_register_column('mtime', ColumnAlignment.LEFT) -def _xlsd_column_mtime(direntry: os.DirEntry) -> str: +def _xlsd_column_mtime(direntry: PathEntry) -> str: """ Format the last modification date for a direntry. """ @@ -557,7 +578,7 @@ def _xlsd_column_mtime(direntry: os.DirEntry) -> str: @xlsd_register_column('name', ColumnAlignment.LEFT) -def _xlsd_column_name(direntry: os.DirEntry) -> str: +def _xlsd_column_name(direntry: PathEntry) -> str: """ Simply format the filename of the direntry. """