Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setting to set the desired file ordering #387

Closed
wants to merge 11 commits into from
18 changes: 18 additions & 0 deletions tests/unit/api/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.OrderSetting("order", "alphabetical")
o.value = "alphabetical-desc"
assert o.value == "alphabetical-desc"


def test_set_order_setting_non_str():
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", "alphabetical")
with pytest.raises(ValueError, match="must be one of"):
o.value = "invalid"
27 changes: 21 additions & 6 deletions tests/unit/utils/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="")
Expand Down Expand Up @@ -122,6 +116,27 @@ def test_images_supported(mocker):
assert not directories


@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):
"""Test if is_image for a tar.gz returns False.

Expand Down
16 changes: 16 additions & 0 deletions tests/unit/utils/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
68 changes: 65 additions & 3 deletions vimiv/api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
import abc
import contextlib
import enum
from typing import Any, Dict, ItemsView, List
import os
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"] = {}
Expand Down Expand Up @@ -315,8 +316,51 @@ def __str__(self) -> str:
return "String"


# Initialize all settings
class OrderSetting(Setting):
"""Stores an ordering setting."""

typ = str

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:
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 suggestions(self) -> List[str]:
return [str(value) for value in self.order_types]
karlch marked this conversation as resolved.
Show resolved Hide resolved

def __str__(self) -> str:
return "Order"


# Initialize all settings
monitor_fs = BoolSetting(
"monitor_filesystem",
True,
Expand All @@ -333,6 +377,24 @@ def __str__(self) -> str:
read_only = BoolSetting(
"read_only", False, desc="Disable any commands that are able to edit files on disk"
)
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 = 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),
},
)


class command: # pylint: disable=invalid-name
Expand Down
8 changes: 5 additions & 3 deletions vimiv/api/working_directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ 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)
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
Expand Down Expand Up @@ -219,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.supported(paths)
return files.order(*files.supported(paths))


handler = cast(WorkingDirectoryHandler, None)
Expand Down
12 changes: 12 additions & 0 deletions vimiv/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
33 changes: 29 additions & 4 deletions vimiv/utils/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

from vimiv.utils import imagereader

from vimiv import api


ImghdrTestFuncT = Callable[[bytes, Optional[BinaryIO]], bool]

Expand All @@ -26,13 +28,18 @@ 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()
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,
karlch marked this conversation as resolved.
Show resolved Hide resolved
)


Expand All @@ -55,6 +62,24 @@ def supported(paths: Iterable[str]) -> Tuple[List[str], List[str]]:
return images, directories


def order(images: List[str], directories: 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.
"""

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.

Expand Down