From 2b6c9eb99cd9f1664ba791135cdb4d9f4e5ee3dd Mon Sep 17 00:00:00 2001 From: Jean-Claude Graf Date: Wed, 5 May 2021 21:27:41 +0200 Subject: [PATCH 01/11] NEW: Setting to set the desired file ordering The option `global.image_order` specifies the order of files. Files can be ordere according to their name, size or modified time in ascending and descending order. The order can be set using the ordinary `:set image_order ` command. Some code was take directly from #76 --- vimiv/api/settings.py | 35 +++++++++++++++++++++++++++++++++- vimiv/api/working_directory.py | 1 + vimiv/utils/files.py | 13 ++++++++++--- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/vimiv/api/settings.py b/vimiv/api/settings.py index 36f4a7fcd..45a49f9df 100644 --- a/vimiv/api/settings.py +++ b/vimiv/api/settings.py @@ -13,7 +13,8 @@ import abc import contextlib import enum -from typing import Any, Dict, ItemsView, List +from typing import Any, Dict, ItemsView, List, Callable, Tuple +import os from PyQt5.QtCore import QObject, pyqtSignal @@ -315,6 +316,37 @@ def __str__(self) -> str: return "String" +class ImageOrderSetting(Setting): + """Stores a image ordering setting.""" + + typ = str + + ORDER_TYPES = { + "name": (os.path.basename, False), + "name-desc": (os.path.basename, True), + "modify": (os.path.getmtime, False), + "modify-desc": (os.path.getmtime, True), + "size": (os.path.getsize, False), + "size-desc": (os.path.getsize, True), + } + + def convert(self, value: str) -> str: + if value not in self.ORDER_TYPES: + raise ValueError(f"Option must be one of {', '.join(self.ORDER_TYPES)}") + + return value + + def get_para(self) -> Tuple[Callable[..., Any], bool]: + """Returns the ordering parameters according to the current set value.""" + try: + return self.ORDER_TYPES[self.value] + except KeyError: + raise ValueError(f"Option must be one of {', '.join(self.ORDER_TYPES)}") + + def __str__(self) -> str: + return "ImageOrder" + + # Initialize all settings monitor_fs = BoolSetting( @@ -333,6 +365,7 @@ def __str__(self) -> str: read_only = BoolSetting( "read_only", False, desc="Disable any commands that are able to edit files on disk" ) +image_order = ImageOrderSetting("image_order", "name", desc="Set ordering.",) class command: # pylint: disable=invalid-name diff --git a/vimiv/api/working_directory.py b/vimiv/api/working_directory.py index 08203d35b..b5c44b583 100644 --- a/vimiv/api/working_directory.py +++ b/vimiv/api/working_directory.py @@ -109,6 +109,7 @@ def __init__(self) -> None: self._directories: List[str] = [] settings.monitor_fs.changed.connect(self._on_monitor_fs_changed) + settings.image_order.changed.connect(self._reload_directory) # TODO Fix upstream and open PR self.directoryChanged.connect(self._reload_directory) # type: ignore self.fileChanged.connect(self._on_file_changed) # type: ignore diff --git a/vimiv/utils/files.py b/vimiv/utils/files.py index 7ccbcb375..7a5c6247e 100644 --- a/vimiv/utils/files.py +++ b/vimiv/utils/files.py @@ -15,6 +15,8 @@ from vimiv.utils import imagereader +from vimiv import api + ImghdrTestFuncT = Callable[[bytes, Optional[BinaryIO]], bool] @@ -29,10 +31,15 @@ def listdir(directory: str, show_hidden: bool = False) -> List[str]: Sorted list of files in the directory with their absolute path. """ directory = os.path.abspath(os.path.expanduser(directory)) + order_function, reverse = api.settings.image_order.get_para() return sorted( - os.path.join(directory, path) - for path in os.listdir(directory) - if show_hidden or not path.startswith(".") + [ + os.path.join(directory, path) + for path in os.listdir(directory) + if show_hidden or not path.startswith(".") + ], + key=order_function, + reverse=reverse, ) From beb48401e0f19da87f5cb7028c4178bb13e576f4 Mon Sep 17 00:00:00 2001 From: Jean-Claude Graf Date: Thu, 6 May 2021 11:00:00 +0200 Subject: [PATCH 02/11] NEW: specify order of images and directoreis seperately Add new option `directory_order` which specifies the order of the directores. `image_order` only influences the order of the images. --- vimiv/api/settings.py | 54 ++++++++++++++++++++++++++-------- vimiv/api/working_directory.py | 3 +- vimiv/utils/files.py | 22 +++++++++++++- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/vimiv/api/settings.py b/vimiv/api/settings.py index 45a49f9df..86258bd15 100644 --- a/vimiv/api/settings.py +++ b/vimiv/api/settings.py @@ -316,24 +316,16 @@ def __str__(self) -> str: return "String" -class ImageOrderSetting(Setting): - """Stores a image ordering setting.""" +class OrderSetting(Setting): + """Stores an ordering setting.""" typ = str - ORDER_TYPES = { - "name": (os.path.basename, False), - "name-desc": (os.path.basename, True), - "modify": (os.path.getmtime, False), - "modify-desc": (os.path.getmtime, True), - "size": (os.path.getsize, False), - "size-desc": (os.path.getsize, True), - } + ORDER_TYPES: Dict[str, Tuple[Callable[..., Any], bool]] = {} def convert(self, value: str) -> str: if value not in self.ORDER_TYPES: raise ValueError(f"Option must be one of {', '.join(self.ORDER_TYPES)}") - return value def get_para(self) -> Tuple[Callable[..., Any], bool]: @@ -343,10 +335,45 @@ def get_para(self) -> Tuple[Callable[..., Any], bool]: except KeyError: raise ValueError(f"Option must be one of {', '.join(self.ORDER_TYPES)}") + def suggestions(self) -> List[str]: + return [str(value) for value in self.ORDER_TYPES] + + def __str__(self) -> str: + return "Order" + + +class ImageOrderSetting(OrderSetting): + """Stores an image ordering setting.""" + + ORDER_TYPES = { + "name": (os.path.basename, False), + "name-desc": (os.path.basename, True), + "modify": (os.path.getmtime, False), + "modify-desc": (os.path.getmtime, True), + "size": (os.path.getsize, False), + "size-desc": (os.path.getsize, True), + } + def __str__(self) -> str: return "ImageOrder" +class DirectoryOrderSetting(OrderSetting): + """Stores an directory ordering setting.""" + + ORDER_TYPES = { + "name": (os.path.basename, False), + "name-desc": (os.path.basename, True), + "modify": (os.path.getmtime, False), + "modify-desc": (os.path.getmtime, True), + "size": (lambda e: len(os.listdir(e)), True), + "size-desc": (lambda e: len(os.listdir(e)), False), + } + + def __str__(self) -> str: + return "DirectoryOrder" + + # Initialize all settings monitor_fs = BoolSetting( @@ -365,7 +392,10 @@ def __str__(self) -> str: read_only = BoolSetting( "read_only", False, desc="Disable any commands that are able to edit files on disk" ) -image_order = ImageOrderSetting("image_order", "name", desc="Set ordering.",) +image_order = ImageOrderSetting("image_order", "name", desc="Set image ordering.",) +directory_order = DirectoryOrderSetting( + "directory_order", "name", desc="Set directory ordering.", +) class command: # pylint: disable=invalid-name diff --git a/vimiv/api/working_directory.py b/vimiv/api/working_directory.py index b5c44b583..60b5e5925 100644 --- a/vimiv/api/working_directory.py +++ b/vimiv/api/working_directory.py @@ -110,6 +110,7 @@ def __init__(self) -> None: settings.monitor_fs.changed.connect(self._on_monitor_fs_changed) settings.image_order.changed.connect(self._reload_directory) + settings.directory_order.changed.connect(self._reload_directory) # TODO Fix upstream and open PR self.directoryChanged.connect(self._reload_directory) # type: ignore self.fileChanged.connect(self._on_file_changed) # type: ignore @@ -225,7 +226,7 @@ def _get_content(self, directory: str) -> Tuple[List[str], List[str]]: """ show_hidden = settings.library.show_hidden.value paths = files.listdir(directory, show_hidden=show_hidden) - return files.supported(paths) + return files.order(files.supported(paths)) handler = cast(WorkingDirectoryHandler, None) diff --git a/vimiv/utils/files.py b/vimiv/utils/files.py index 7a5c6247e..70279f673 100644 --- a/vimiv/utils/files.py +++ b/vimiv/utils/files.py @@ -28,7 +28,7 @@ def listdir(directory: str, show_hidden: bool = False) -> List[str]: directory: Directory to check for files in via os.listdir(directory). show_hidden: Include hidden files in output. Returns: - Sorted list of files in the directory with their absolute path. + List of files in the directory with their absolute path. """ directory = os.path.abspath(os.path.expanduser(directory)) order_function, reverse = api.settings.image_order.get_para() @@ -62,6 +62,26 @@ def supported(paths: Iterable[str]) -> Tuple[List[str], List[str]]: return images, directories +def order(files: Tuple[List[str], List[str]]) -> Tuple[List[str], List[str]]: + """Orders images and directories according to the settings. + + Args: + files: Tuple of list of images and list of directories. + Returns: + images: Ordered list of images. + directories: Orderd list of directories. + """ + + images, directories = files + + image_order, image_reverse = api.settings.image_order.get_para() + directory_order, directory_reverse = api.settings.directory_order.get_para() + images = sorted(images, key=image_order, reverse=image_reverse) + directories = sorted(directories, key=directory_order, reverse=directory_reverse) + + return images, directories + + def get_size(path: str) -> str: """Get the size of a path in human readable format. From edd45141400beb1b09b87fcc1697c1327f9c435a Mon Sep 17 00:00:00 2001 From: Jean-Claude Graf Date: Thu, 6 May 2021 11:23:16 +0200 Subject: [PATCH 03/11] FIX: test expected that files.listdir returns files sorted --- tests/unit/utils/test_files.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/unit/utils/test_files.py b/tests/unit/utils/test_files.py index 041e9de10..55d60c742 100644 --- a/tests/unit/utils/test_files.py +++ b/tests/unit/utils/test_files.py @@ -87,12 +87,6 @@ def test_listdir_wrapper_returns_abspath(mocker): assert files.listdir("directory") == expected -def test_listdir_wrapper_sort(mocker): - mocker.patch("os.listdir", return_value=["b.txt", "a.txt"]) - mocker.patch("os.path.abspath", return_value="") - assert files.listdir("directory") == ["a.txt", "b.txt"] - - def test_listdir_wrapper_remove_hidden(mocker): mocker.patch("os.listdir", return_value=[".dotfile.txt", "a.txt"]) mocker.patch("os.path.abspath", return_value="") @@ -122,6 +116,13 @@ def test_images_supported(mocker): assert not directories +def test_order(): + assert files.order((["b.txt", "a.txt"], ["b", "a"])) == ( + ["a.txt", "b.txt"], + ["a", "b"], + ) + + def test_tar_gz_not_an_image(tmp_path): """Test if is_image for a tar.gz returns False. From c0e13a8abce536a7d40c5773f57956e851a1979c Mon Sep 17 00:00:00 2001 From: Jean-Claude Graf Date: Sun, 9 May 2021 21:19:56 +0200 Subject: [PATCH 04/11] NEW: Option for natural sorting --- vimiv/api/settings.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/vimiv/api/settings.py b/vimiv/api/settings.py index 86258bd15..8197328d3 100644 --- a/vimiv/api/settings.py +++ b/vimiv/api/settings.py @@ -13,8 +13,9 @@ import abc import contextlib import enum -from typing import Any, Dict, ItemsView, List, Callable, Tuple +from typing import Any, Dict, ItemsView, List, Callable, Tuple, Union import os +import re from PyQt5.QtCore import QObject, pyqtSignal @@ -338,6 +339,18 @@ def get_para(self) -> Tuple[Callable[..., Any], bool]: def suggestions(self) -> List[str]: return [str(value) for value in self.ORDER_TYPES] + @staticmethod + def _natural_sort(text: str) -> List[Union[str, int]]: + """Key function for natural sort. + + Credits to https://stackoverflow.com/a/5967539/5464989 + """ + + def convert(t: str) -> Union[str, int]: + return int(t) if t.isdigit() else t + + return [convert(c) for c in re.split(r"(\d+)", text)] + def __str__(self) -> str: return "Order" @@ -348,6 +361,8 @@ class ImageOrderSetting(OrderSetting): ORDER_TYPES = { "name": (os.path.basename, False), "name-desc": (os.path.basename, True), + "name-natural": (OrderSetting._natural_sort, False), + "name-natural-desc": (OrderSetting._natural_sort, True), "modify": (os.path.getmtime, False), "modify-desc": (os.path.getmtime, True), "size": (os.path.getsize, False), @@ -364,6 +379,8 @@ class DirectoryOrderSetting(OrderSetting): ORDER_TYPES = { "name": (os.path.basename, False), "name-desc": (os.path.basename, True), + "name-natural": (OrderSetting._natural_sort, False), + "name-natural-desc": (OrderSetting._natural_sort, True), "modify": (os.path.getmtime, False), "modify-desc": (os.path.getmtime, True), "size": (lambda e: len(os.listdir(e)), True), From 76bde483f32148d6b52ab34d0f7e2c8b7f786ddf Mon Sep 17 00:00:00 2001 From: Jean-Claude Graf Date: Sun, 9 May 2021 22:03:43 +0200 Subject: [PATCH 05/11] FIX: Import order --- vimiv/api/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vimiv/api/settings.py b/vimiv/api/settings.py index 8197328d3..ade84a46c 100644 --- a/vimiv/api/settings.py +++ b/vimiv/api/settings.py @@ -13,9 +13,9 @@ import abc import contextlib import enum -from typing import Any, Dict, ItemsView, List, Callable, Tuple, Union import os import re +from typing import Any, Dict, ItemsView, List, Callable, Tuple, Union from PyQt5.QtCore import QObject, pyqtSignal From ccc6af6591482b87ea7b6da58bef31d222df8a25 Mon Sep 17 00:00:00 2001 From: Jean-Claude Graf Date: Tue, 11 May 2021 21:14:44 +0200 Subject: [PATCH 06/11] NEW: Order_Setting test --- tests/unit/api/test_settings.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/unit/api/test_settings.py b/tests/unit/api/test_settings.py index ead9dc0a0..2805bcb42 100644 --- a/tests/unit/api/test_settings.py +++ b/tests/unit/api/test_settings.py @@ -151,3 +151,21 @@ def ask_question(*args, **kwargs): mocker.patch("vimiv.api.prompt.ask_question", ask_question) assert bool(prompt_setting) == answer + + +def test_set_order_setting(): + o = settings.ImageOrderSetting("imageorder", "name") + o.value = "name-desc" + assert o.value == "name-desc" + + +def test_set_order_setting_non_str(): + o = settings.OrderSetting("order", "name") + with pytest.raises(ValueError, match="must be one of"): + o.value = 1 + + +def test_set_order_setting_non_valid(): + o = settings.OrderSetting("order", "name") + with pytest.raises(ValueError, match="must be one of"): + o.value = "invalid" From c6665b03214d4b6d65066ea9cd1b08b884e38906 Mon Sep 17 00:00:00 2001 From: Jean-Claude Graf Date: Thu, 20 May 2021 16:36:03 +0200 Subject: [PATCH 07/11] IMPROVE: Rename orderings | Current | New | | :--- | :--- | | name | alphabetical | | name-desc | alphabetical-desc | | name-natural | natural | | name-natural-desc | natural-desc | | modify | recently-modified-first | | modify-desc | recently-modified-last | | size | smallest-first | | size-desc | largest-first | --- tests/unit/api/test_settings.py | 10 ++++----- vimiv/api/settings.py | 38 +++++++++++++++++---------------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/tests/unit/api/test_settings.py b/tests/unit/api/test_settings.py index 2805bcb42..868bef8cd 100644 --- a/tests/unit/api/test_settings.py +++ b/tests/unit/api/test_settings.py @@ -154,18 +154,18 @@ def ask_question(*args, **kwargs): def test_set_order_setting(): - o = settings.ImageOrderSetting("imageorder", "name") - o.value = "name-desc" - assert o.value == "name-desc" + o = settings.ImageOrderSetting("imageorder", "alphabetical") + o.value = "alphabetical-desc" + assert o.value == "alphabetical-desc" def test_set_order_setting_non_str(): - o = settings.OrderSetting("order", "name") + o = settings.OrderSetting("order", "alphabetical") with pytest.raises(ValueError, match="must be one of"): o.value = 1 def test_set_order_setting_non_valid(): - o = settings.OrderSetting("order", "name") + o = settings.OrderSetting("order", "alphabetical") with pytest.raises(ValueError, match="must be one of"): o.value = "invalid" diff --git a/vimiv/api/settings.py b/vimiv/api/settings.py index ade84a46c..066bbc412 100644 --- a/vimiv/api/settings.py +++ b/vimiv/api/settings.py @@ -359,14 +359,14 @@ class ImageOrderSetting(OrderSetting): """Stores an image ordering setting.""" ORDER_TYPES = { - "name": (os.path.basename, False), - "name-desc": (os.path.basename, True), - "name-natural": (OrderSetting._natural_sort, False), - "name-natural-desc": (OrderSetting._natural_sort, True), - "modify": (os.path.getmtime, False), - "modify-desc": (os.path.getmtime, True), - "size": (os.path.getsize, False), - "size-desc": (os.path.getsize, True), + "alphabetical": (os.path.basename, False), + "alphabetical-desc": (os.path.basename, True), + "natural": (OrderSetting._natural_sort, False), + "natural-desc": (OrderSetting._natural_sort, True), + "recently-modify-first": (os.path.getmtime, False), + "recently-modify-last": (os.path.getmtime, True), + "smallest-first": (os.path.getsize, False), + "largest-first": (os.path.getsize, True), } def __str__(self) -> str: @@ -377,14 +377,14 @@ class DirectoryOrderSetting(OrderSetting): """Stores an directory ordering setting.""" ORDER_TYPES = { - "name": (os.path.basename, False), - "name-desc": (os.path.basename, True), - "name-natural": (OrderSetting._natural_sort, False), - "name-natural-desc": (OrderSetting._natural_sort, True), - "modify": (os.path.getmtime, False), - "modify-desc": (os.path.getmtime, True), - "size": (lambda e: len(os.listdir(e)), True), - "size-desc": (lambda e: len(os.listdir(e)), False), + "alphabetical": (os.path.basename, False), + "alphabetical-desc": (os.path.basename, True), + "natural": (OrderSetting._natural_sort, False), + "natural-desc": (OrderSetting._natural_sort, True), + "recently-modify-first": (os.path.getmtime, False), + "recently-modify-last": (os.path.getmtime, True), + "smallest-first": (lambda e: len(os.listdir(e)), True), + "largest-first": (lambda e: len(os.listdir(e)), False), } def __str__(self) -> str: @@ -409,9 +409,11 @@ def __str__(self) -> str: read_only = BoolSetting( "read_only", False, desc="Disable any commands that are able to edit files on disk" ) -image_order = ImageOrderSetting("image_order", "name", desc="Set image ordering.",) +image_order = ImageOrderSetting( + "image_order", "alphabetical", desc="Set image ordering.", +) directory_order = DirectoryOrderSetting( - "directory_order", "name", desc="Set directory ordering.", + "directory_order", "alphabetical", desc="Set directory ordering.", ) From b7ccd8284c0d0fccfe26798bf78461ccb0e21299 Mon Sep 17 00:00:00 2001 From: Jean-Claude Graf Date: Sun, 1 Aug 2021 16:22:04 +0200 Subject: [PATCH 08/11] IMPROVE: Merge ImageOrderSetting and DirectoryOrderSetting Three classes for these order seemed a bit overkill, especially since most orderings are shared. I did not figure out the correct syntax to reference the `_natural_sort` method from the class variable `ORDER_TYPES`. --- tests/unit/api/test_settings.py | 2 +- vimiv/api/settings.py | 100 +++++++++++++------------------- vimiv/utils/__init__.py | 12 ++++ 3 files changed, 53 insertions(+), 61 deletions(-) diff --git a/tests/unit/api/test_settings.py b/tests/unit/api/test_settings.py index 868bef8cd..3f488c7ba 100644 --- a/tests/unit/api/test_settings.py +++ b/tests/unit/api/test_settings.py @@ -154,7 +154,7 @@ def ask_question(*args, **kwargs): def test_set_order_setting(): - o = settings.ImageOrderSetting("imageorder", "alphabetical") + o = settings.OrderSetting("order", "alphabetical") o.value = "alphabetical-desc" assert o.value == "alphabetical-desc" diff --git a/vimiv/api/settings.py b/vimiv/api/settings.py index 066bbc412..31149b411 100644 --- a/vimiv/api/settings.py +++ b/vimiv/api/settings.py @@ -14,13 +14,12 @@ import contextlib import enum import os -import re -from typing import Any, Dict, ItemsView, List, Callable, Tuple, Union +from typing import Any, Dict, ItemsView, List, Callable, Tuple from PyQt5.QtCore import QObject, pyqtSignal from vimiv.api import prompt -from vimiv.utils import clamp, AbstractQObjectMeta, log, customtypes +from vimiv.utils import clamp, AbstractQObjectMeta, log, customtypes, natural_sort _storage: Dict[str, "Setting"] = {} @@ -322,77 +321,46 @@ class OrderSetting(Setting): typ = str - ORDER_TYPES: Dict[str, Tuple[Callable[..., Any], bool]] = {} + ORDER_TYPES: Dict[str, Tuple[Callable[..., Any], bool]] = { + "alphabetical": (os.path.basename, False), + "alphabetical-desc": (os.path.basename, True), + "natural": (natural_sort, False), + "natural-desc": (natural_sort, True), + "recently-modify-first": (os.path.getmtime, False), + "recently-modify-last": (os.path.getmtime, True), + } + + def __init__( + self, + *args: Any, + additional_order_types: Dict[str, Tuple[Callable[..., Any], bool]] = None, + **kwargs: Any, + ): + super().__init__(*args, **kwargs) + self.order_types = dict(self.ORDER_TYPES) + if additional_order_types: + self.order_types.update(additional_order_types) def convert(self, value: str) -> str: - if value not in self.ORDER_TYPES: + if value not in self.order_types: raise ValueError(f"Option must be one of {', '.join(self.ORDER_TYPES)}") return value def get_para(self) -> Tuple[Callable[..., Any], bool]: """Returns the ordering parameters according to the current set value.""" try: - return self.ORDER_TYPES[self.value] + return self.order_types[self.value] except KeyError: raise ValueError(f"Option must be one of {', '.join(self.ORDER_TYPES)}") def suggestions(self) -> List[str]: - return [str(value) for value in self.ORDER_TYPES] - - @staticmethod - def _natural_sort(text: str) -> List[Union[str, int]]: - """Key function for natural sort. - - Credits to https://stackoverflow.com/a/5967539/5464989 - """ - - def convert(t: str) -> Union[str, int]: - return int(t) if t.isdigit() else t - - return [convert(c) for c in re.split(r"(\d+)", text)] + return [str(value) for value in self.order_types] def __str__(self) -> str: return "Order" -class ImageOrderSetting(OrderSetting): - """Stores an image ordering setting.""" - - ORDER_TYPES = { - "alphabetical": (os.path.basename, False), - "alphabetical-desc": (os.path.basename, True), - "natural": (OrderSetting._natural_sort, False), - "natural-desc": (OrderSetting._natural_sort, True), - "recently-modify-first": (os.path.getmtime, False), - "recently-modify-last": (os.path.getmtime, True), - "smallest-first": (os.path.getsize, False), - "largest-first": (os.path.getsize, True), - } - - def __str__(self) -> str: - return "ImageOrder" - - -class DirectoryOrderSetting(OrderSetting): - """Stores an directory ordering setting.""" - - ORDER_TYPES = { - "alphabetical": (os.path.basename, False), - "alphabetical-desc": (os.path.basename, True), - "natural": (OrderSetting._natural_sort, False), - "natural-desc": (OrderSetting._natural_sort, True), - "recently-modify-first": (os.path.getmtime, False), - "recently-modify-last": (os.path.getmtime, True), - "smallest-first": (lambda e: len(os.listdir(e)), True), - "largest-first": (lambda e: len(os.listdir(e)), False), - } - - def __str__(self) -> str: - return "DirectoryOrder" - - # Initialize all settings - monitor_fs = BoolSetting( "monitor_filesystem", True, @@ -409,11 +377,23 @@ def __str__(self) -> str: read_only = BoolSetting( "read_only", False, desc="Disable any commands that are able to edit files on disk" ) -image_order = ImageOrderSetting( - "image_order", "alphabetical", desc="Set image ordering.", +image_order = OrderSetting( + "image_order", + "alphabetical", + desc="Set image ordering.", + additional_order_types={ + "smallest-first": (os.path.getsize, False), + "largest-first": (os.path.getsize, True), + }, ) -directory_order = DirectoryOrderSetting( - "directory_order", "alphabetical", desc="Set directory ordering.", +directory_order = OrderSetting( + "directory_order", + "alphabetical", + desc="Set directory ordering.", + additional_order_types={ + "smallest-first": (lambda e: len(os.listdir(e)), True), + "largest-first": (lambda e: len(os.listdir(e)), False), + }, ) diff --git a/vimiv/utils/__init__.py b/vimiv/utils/__init__.py index 02cf77c39..92d9e8a5c 100644 --- a/vimiv/utils/__init__.py +++ b/vimiv/utils/__init__.py @@ -526,3 +526,15 @@ def stop_all(cls): """Stop all running throttles.""" for throttle in cls.throttles: throttle.stop() + + +def natural_sort(text: str) -> typing.List[typing.Union[str, int]]: + """Key function for natural sort. + + Credits to https://stackoverflow.com/a/5967539/5464989 + """ + + def convert(t: str) -> typing.Union[str, int]: + return int(t) if t.isdigit() else t + + return [convert(c) for c in re.split(r"(\d+)", text)] From 7cb38a9cb8a2e10176e99067d2a3924edd197cac Mon Sep 17 00:00:00 2001 From: Jean-Claude Graf Date: Sun, 1 Aug 2021 20:38:48 +0200 Subject: [PATCH 09/11] IMPROVE: Unpack function arguments before calling `files.order` --- tests/unit/utils/test_files.py | 2 +- vimiv/api/working_directory.py | 6 +++--- vimiv/utils/files.py | 4 +--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/unit/utils/test_files.py b/tests/unit/utils/test_files.py index 55d60c742..c70e08679 100644 --- a/tests/unit/utils/test_files.py +++ b/tests/unit/utils/test_files.py @@ -117,7 +117,7 @@ def test_images_supported(mocker): def test_order(): - assert files.order((["b.txt", "a.txt"], ["b", "a"])) == ( + assert files.order(["b.txt", "a.txt"], ["b", "a"]) == ( ["a.txt", "b.txt"], ["a", "b"], ) diff --git a/vimiv/api/working_directory.py b/vimiv/api/working_directory.py index 60b5e5925..ace734caa 100644 --- a/vimiv/api/working_directory.py +++ b/vimiv/api/working_directory.py @@ -221,12 +221,12 @@ def _get_content(self, directory: str) -> Tuple[List[str], List[str]]: """Get supported content of directory. Returns: - images: List of images inside the directory. - directories: List of directories inside the directory. + images: Ordered list of images inside the directory. + directories: Ordered list of directories inside the directory. """ show_hidden = settings.library.show_hidden.value paths = files.listdir(directory, show_hidden=show_hidden) - return files.order(files.supported(paths)) + return files.order(*files.supported(paths)) handler = cast(WorkingDirectoryHandler, None) diff --git a/vimiv/utils/files.py b/vimiv/utils/files.py index 70279f673..832e9cfd6 100644 --- a/vimiv/utils/files.py +++ b/vimiv/utils/files.py @@ -62,7 +62,7 @@ def supported(paths: Iterable[str]) -> Tuple[List[str], List[str]]: return images, directories -def order(files: Tuple[List[str], List[str]]) -> Tuple[List[str], List[str]]: +def order(images: List[str], directories: List[str]) -> Tuple[List[str], List[str]]: """Orders images and directories according to the settings. Args: @@ -72,8 +72,6 @@ def order(files: Tuple[List[str], List[str]]) -> Tuple[List[str], List[str]]: directories: Orderd list of directories. """ - images, directories = files - image_order, image_reverse = api.settings.image_order.get_para() directory_order, directory_reverse = api.settings.directory_order.get_para() images = sorted(images, key=image_order, reverse=image_reverse) From d66abbbc084e043558f467c2c43d45fc5a30cde5 Mon Sep 17 00:00:00 2001 From: Jean-Claude Graf Date: Sun, 1 Aug 2021 21:34:10 +0200 Subject: [PATCH 10/11] NEW: unit test natural sort --- tests/unit/utils/test_utils.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 3a08ae85f..7a42874fb 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -332,3 +332,19 @@ def test_is_hex_true(text): @pytest.mark.parametrize("text", ("00x234", "0xGC7", "not-hex", "A-67", "a:9e")) def test_is_hex_false(text): assert not utils.is_hex(text) + + +@pytest.mark.parametrize( + "input_list, sorted_list", + ( + (["a100a", "a10a", "a1a"], ["a1a", "a10a", "a100a"]), + ( + ["a10a1a", "a1a10a", "a10a10a", "a1a1a"], + ["a1a1a", "a1a10a", "a10a1a", "a10a10a"], + ), + (["100", "50", "10", "20"], ["10", "20", "50", "100"]), + (["aa", "a", "aaa"], ["a", "aa", "aaa"]), + ), +) +def test_natural_sort(input_list, sorted_list): + assert sorted(input_list, key=utils.natural_sort) == sorted_list From 21e472a22686e168d0446ac629f6aba0bfe31475 Mon Sep 17 00:00:00 2001 From: Jean-Claude Graf Date: Sun, 1 Aug 2021 21:59:26 +0200 Subject: [PATCH 11/11] IMPROVE: order test --- tests/unit/utils/test_files.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/tests/unit/utils/test_files.py b/tests/unit/utils/test_files.py index c70e08679..a3007b50d 100644 --- a/tests/unit/utils/test_files.py +++ b/tests/unit/utils/test_files.py @@ -116,11 +116,25 @@ def test_images_supported(mocker): assert not directories -def test_order(): - assert files.order(["b.txt", "a.txt"], ["b", "a"]) == ( - ["a.txt", "b.txt"], - ["a", "b"], - ) +@pytest.mark.parametrize( + "input_img, input_dir, sorted_img, sorted_dir", + ( + ( + ["a.j", "c.j", "b.j"], + ["a", "c", "b"], + ["a.j", "b.j", "c.j"], + ["a", "b", "c"], + ), + ( + ["a2.j", "a3.j", "a1.j"], + ["a2", "a3", "a1"], + ["a1.j", "a2.j", "a3.j"], + ["a1", "a2", "a3"], + ), + ), +) +def test_order(input_img, input_dir, sorted_img, sorted_dir): + assert files.order(input_img, input_dir) == (sorted_img, sorted_dir) def test_tar_gz_not_an_image(tmp_path):