├── .bumpversion.cfg
├── .codecov.yml
├── .coveragerc
├── .github
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.rst
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ ├── docs.yml
│ └── pythonpublish.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .pydocstylerc
├── .pylintrc
├── AUTHORS
├── LICENSE
├── MANIFEST.in
├── README.md
├── c-extension
├── brightness_contrast.h
├── definitions.h
├── helper_func.h
├── hue_saturation_lightness.h
├── manipulate.c
└── math_func_eval.h
├── docs
├── Makefile
├── _static
│ ├── custom.css
│ ├── favicon.ico
│ ├── scrots
│ │ ├── command_dark.png
│ │ ├── command_light.png
│ │ ├── image_dark.png
│ │ ├── image_light.png
│ │ ├── library_dark.png
│ │ ├── library_light.png
│ │ ├── manipulate_dark.png
│ │ ├── manipulate_light.png
│ │ ├── thumbnail_dark.png
│ │ └── thumbnail_light.png
│ └── vimiv
│ │ ├── vimiv.png
│ │ ├── vimiv.svg
│ │ ├── vimiv_banner_800.png
│ │ └── vimiv_banner_darkmode_800.png
├── changelog.rst
├── conf.py
├── description.rst
├── documentation
│ ├── cl_options
│ │ └── index.rst
│ ├── commands.rst
│ ├── configuration
│ │ ├── index.rst
│ │ ├── keybindings.rst
│ │ ├── plugins.rst
│ │ ├── settings.rst
│ │ ├── statusbar.rst
│ │ └── style.rst
│ ├── contributing.rst
│ ├── contributing_bugs.rst
│ ├── datafile_warning.rst
│ ├── dependency_info.rst
│ ├── getting_help.rst
│ ├── getting_started.rst
│ ├── hacking.rst
│ ├── index.rst
│ ├── install.rst
│ ├── metadata.rst
│ ├── migrating.rst
│ └── updating_icon_cache.rst
├── index.rst
├── manpage
│ └── vimiv.1.rst
├── screenshots.rst
└── theme.conf
├── fastentrypoints.py
├── icons
├── vimiv.svg
├── vimiv_128x128.png
├── vimiv_16x16.png
├── vimiv_256x256.png
├── vimiv_32x32.png
├── vimiv_512x512.png
└── vimiv_64x64.png
├── misc
├── Makefile
├── org.karlch.vimiv.qt.metainfo.xml
├── requirements
│ ├── requirements_cov.txt
│ ├── requirements_docs.txt
│ ├── requirements_lint.txt
│ ├── requirements_mypy.txt
│ ├── requirements_packaging.txt
│ ├── requirements_piexif.txt
│ ├── requirements_pyexiv2.txt
│ ├── requirements_pyqt5.txt
│ ├── requirements_pyqt6.txt
│ ├── requirements_pyside6.txt
│ ├── requirements_setup.txt
│ ├── requirements_tests.txt
│ └── requirements_tox.txt
├── vimiv.1
└── vimiv.desktop
├── mypy.ini
├── pytest.ini
├── scripts
├── gen_manpage.sh
├── lint_tests.py
├── maybe_build_cextension.py
├── pylint_checkers
│ ├── __init__.py
│ ├── check_count.py
│ ├── check_docstring.py
│ └── check_header.py
├── rstutils.py
├── src2rst.py
├── uninstall_pythonpkg.sh
└── vimiv_history.py
├── setup.py
├── tests
├── conftest.py
├── end2end
│ ├── conftest.py
│ ├── features
│ │ ├── api
│ │ │ ├── conftest.py
│ │ │ ├── keybindings.feature
│ │ │ ├── mark.feature
│ │ │ ├── modeswitch.feature
│ │ │ ├── print.feature
│ │ │ ├── prompt.feature
│ │ │ ├── rename.feature
│ │ │ ├── test_keybindings_bdd.py
│ │ │ ├── test_mark_bdd.py
│ │ │ ├── test_modeswitch_bdd.py
│ │ │ ├── test_print_bdd.py
│ │ │ ├── test_prompt_bdd.py
│ │ │ ├── test_rename_bdd.py
│ │ │ ├── test_working_directory_bdd.py
│ │ │ └── working_directory.feature
│ │ ├── command
│ │ │ ├── aliases.feature
│ │ │ ├── chaining.feature
│ │ │ ├── conftest.py
│ │ │ ├── expand_wildcards.feature
│ │ │ ├── external.feature
│ │ │ ├── fail_run_command.feature
│ │ │ ├── misccommands.feature
│ │ │ ├── repeat.feature
│ │ │ ├── search.feature
│ │ │ ├── test_aliases_bdd.py
│ │ │ ├── test_chaining_bdd.py
│ │ │ ├── test_expand_wildcards_bdd.py
│ │ │ ├── test_external_bdd.py
│ │ │ ├── test_fail_run_command_bdd.py
│ │ │ ├── test_misccommands_bdd.py
│ │ │ ├── test_repeat_bdd.py
│ │ │ └── test_search_bdd.py
│ │ ├── commandline
│ │ │ ├── commandline.feature
│ │ │ └── test_commandline_bdd.py
│ │ ├── completion
│ │ │ ├── completion.feature
│ │ │ └── test_completion_bdd.py
│ │ ├── config
│ │ │ ├── configcommands.feature
│ │ │ └── test_configcommands_bdd.py
│ │ ├── conftest.py
│ │ ├── edit
│ │ │ ├── conftest.py
│ │ │ ├── crop.feature
│ │ │ ├── manipulate.feature
│ │ │ ├── manipulate_segfault.feature
│ │ │ ├── straighten.feature
│ │ │ ├── test_crop_bdd.py
│ │ │ ├── test_manipulate_bdd.py
│ │ │ ├── test_straighten_bdd.py
│ │ │ ├── test_transform_bdd.py
│ │ │ └── transform.feature
│ │ ├── image
│ │ │ ├── conftest.py
│ │ │ ├── gif.feature
│ │ │ ├── imagedelete.feature
│ │ │ ├── imagefit.feature
│ │ │ ├── imagenavigate.feature
│ │ │ ├── imageopen.feature
│ │ │ ├── imageorder.feature
│ │ │ ├── imagescroll.feature
│ │ │ ├── imagetitle.feature
│ │ │ ├── imagezoom.feature
│ │ │ ├── metadata.feature
│ │ │ ├── multidirectory.feature
│ │ │ ├── slideshow.feature
│ │ │ ├── svg.feature
│ │ │ ├── test_gif_bdd.py
│ │ │ ├── test_imagedelete_bdd.py
│ │ │ ├── test_imagefit_bdd.py
│ │ │ ├── test_imagenavigate_bdd.py
│ │ │ ├── test_imageopen_bdd.py
│ │ │ ├── test_imageorder_bdd.py
│ │ │ ├── test_imagescroll_bdd.py
│ │ │ ├── test_imagetitle_bdd.py
│ │ │ ├── test_imagezoom_bdd.py
│ │ │ ├── test_metadata_bdd.py
│ │ │ ├── test_multidirectory_bdd.py
│ │ │ ├── test_slideshow_bdd.py
│ │ │ ├── test_svg_bdd.py
│ │ │ ├── test_write_bdd.py
│ │ │ └── write.feature
│ │ ├── library
│ │ │ ├── library.feature
│ │ │ ├── libraryresize.feature
│ │ │ ├── libraryscroll.feature
│ │ │ ├── test_library_bdd.py
│ │ │ ├── test_libraryresize_bdd.py
│ │ │ └── test_libraryscroll_bdd.py
│ │ ├── misc
│ │ │ ├── clipboard.feature
│ │ │ ├── conftest.py
│ │ │ ├── fullscreen.feature
│ │ │ ├── keybindings_popup.feature
│ │ │ ├── keyhint.feature
│ │ │ ├── migration_popup.feature
│ │ │ ├── no_optional.feature
│ │ │ ├── startup.feature
│ │ │ ├── symlink.feature
│ │ │ ├── teardown.feature
│ │ │ ├── test_clipboard_bdd.py
│ │ │ ├── test_fullscreen_bdd.py
│ │ │ ├── test_keybindings_popup_bdd.py
│ │ │ ├── test_keyhint_bdd.py
│ │ │ ├── test_migration_popup_bdd.py
│ │ │ ├── test_no_optional_bdd.py
│ │ │ ├── test_startup_bdd.py
│ │ │ ├── test_symlink_bdd.py
│ │ │ ├── test_teardown_bdd.py
│ │ │ ├── test_version_bdd.py
│ │ │ └── version.feature
│ │ ├── plugins
│ │ │ ├── plugins.feature
│ │ │ └── test_plugins_bdd.py
│ │ ├── statusbar
│ │ │ ├── message.feature
│ │ │ ├── status.feature
│ │ │ ├── test_message_bdd.py
│ │ │ └── test_status_bdd.py
│ │ └── thumbnail
│ │ │ ├── .zcompdump
│ │ │ ├── conftest.py
│ │ │ ├── test_thumbnailgoto_bdd.py
│ │ │ ├── test_thumbnailmark_bdd.py
│ │ │ ├── test_thumbnailscroll_bdd.py
│ │ │ ├── test_thumbnailzoom_bdd.py
│ │ │ ├── thumbnailgoto.feature
│ │ │ ├── thumbnailmark.feature
│ │ │ ├── thumbnailscroll.feature
│ │ │ └── thumbnailzoom.feature
│ └── mockdecorators.py
├── integration
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_app.py
│ ├── test_edit.py
│ ├── test_metadata.py
│ ├── test_read_bindings.py
│ └── test_read_settings.py
└── unit
│ ├── __init__.py
│ ├── api
│ ├── test_keybindings.py
│ ├── test_mark.py
│ ├── test_objreg.py
│ ├── test_prompt.py
│ ├── test_settings.py
│ └── test_status.py
│ ├── commands
│ ├── test_aliases.py
│ ├── test_argtypes.py
│ ├── test_commands.py
│ ├── test_history.py
│ ├── test_history_deque.py
│ ├── test_runners.py
│ ├── test_search.py
│ └── test_wildcards.py
│ ├── config
│ ├── test_config.py
│ ├── test_external_configparser.py
│ └── test_styles.py
│ ├── conftest.py
│ ├── gui
│ ├── test_eventhandler.py
│ ├── test_statusbar.py
│ └── test_thumbnail.py
│ ├── imutils
│ └── test_imtransform.py
│ ├── plugins
│ ├── mock_plugin.py
│ ├── mock_plugin_syntax_error.py
│ └── test_plugins.py
│ ├── test_checkversion.py
│ ├── test_parser.py
│ ├── test_version.py
│ └── utils
│ ├── _module_for_lazy.py
│ ├── test_crash_handler.py
│ ├── test_debug.py
│ ├── test_files.py
│ ├── test_imageheader.py
│ ├── test_lazy.py
│ ├── test_log.py
│ ├── test_migration.py
│ ├── test_thumbnail_manager.py
│ ├── test_trash_manager.py
│ ├── test_trie.py
│ ├── test_utils.py
│ └── test_xdg.py
├── tox.ini
└── vimiv
├── __init__.py
├── __main__.py
├── api
├── __init__.py
├── _mark.py
├── _modules.py
├── commands.py
├── completion.py
├── keybindings.py
├── modes.py
├── objreg.py
├── prompt.py
├── settings.py
├── signals.py
├── status.py
└── working_directory.py
├── app.py
├── checkversion.py
├── commands
├── __init__.py
├── aliases.py
├── argtypes.py
├── delete_command.py
├── external.py
├── help_command.py
├── history.py
├── misccommands.py
├── runners.py
├── search.py
└── wildcards.py
├── completion
├── __init__.py
├── completer.py
└── completionmodels.py
├── config
├── __init__.py
├── _style_options.py
├── configcommands.py
├── configfile.py
├── external_configparser.py
├── keyfile.py
└── styles.py
├── gui
├── __init__.py
├── commandline.py
├── commandwidget.py
├── completionwidget.py
├── crop_widget.py
├── eventhandler.py
├── image.py
├── keybindings_popup.py
├── keyhintwidget.py
├── library.py
├── mainwindow.py
├── manipulate.py
├── message.py
├── metadatawidget.py
├── prompt.py
├── resize.py
├── statusbar.py
├── straightenwidget.py
├── synchronize.py
├── thumbnail.py
├── transformwidget.py
└── version_popup.py
├── imutils
├── __init__.py
├── _file_handler.py
├── current_pixmap.py
├── edit_handler.py
├── filelist.py
├── immanipulate.py
├── imtransform.py
├── metadata.py
└── slideshow.py
├── parser.py
├── plugins
├── __init__.py
├── demo.py
├── imageformats.py
├── metadata.py
├── metadata_piexif.py
├── metadata_pyexiv2.py
└── print.py
├── qt
├── __init__.py
├── core.py
├── gui.py
├── printsupport.py
├── svg.py
└── widgets.py
├── startup.py
├── utils
├── __init__.py
├── crash_handler.py
├── customtypes.py
├── debug.py
├── files.py
├── imageheader.py
├── imagereader.py
├── lazy.py
├── log.py
├── migration.py
├── thumbnail_manager.py
├── trash_manager.py
├── trie.py
└── xdg.py
├── version.py
└── widgets.py
/.bumpversion.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 0.9.0
3 | commit = True
4 | message = Release v{new_version}
5 | tag = True
6 | tag_name = v{new_version}
7 |
8 | [bumpversion:file:vimiv/__init__.py]
9 | serialize = ({major}, {minor}, {patch})
10 |
11 | [bumpversion:file:misc/org.karlch.vimiv.qt.metainfo.xml]
12 | search =
13 | replace =
14 |
15 |
16 | [bumpversion:file:docs/changelog.rst]
17 | search = (unreleased)
18 | replace = ({now:%Y-%m-%d})
19 |
--------------------------------------------------------------------------------
/.codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | project:
4 | default:
5 | informational: true
6 | patch:
7 | default:
8 | informational: true
9 | changes: off
10 |
11 | comment: off
12 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | omit = *__main__.py
3 |
4 | [report]
5 | exclude_lines =
6 | pragma: no cover
7 | coverage: ignore
8 | raise NotImplementedError
9 | if __name__ == "__main__"
10 | @utils.asyncfunc
11 | @asyncfunc
12 | def __repr__
13 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 |
4 | - package-ecosystem: pip
5 | directory: "/misc/requirements"
6 | schedule:
7 | interval: weekly
8 | day: saturday
9 | time: "04:00"
10 | pull-request-branch-name:
11 | separator: "-"
12 | open-pull-requests-limit: 10
13 |
14 | - package-ecosystem: "github-actions"
15 | directory: "/"
16 | schedule:
17 | interval: weekly
18 | day: saturday
19 | time: "04:00"
20 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Rebuild gh-pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | docs:
10 |
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4.1.5
14 | - uses: actions/cache@v4
15 | with:
16 | path: |
17 | .tox
18 | ~/.cache/pip
19 | key: "ghpages_${{ hashFiles('misc/requirements/requirements*.txt') }}_${{ hashFiles('scripts/src2rst.py') }}"
20 | - name: Set up python
21 | uses: actions/setup-python@v5.1.0
22 | with:
23 | python-version: '3.11'
24 | - name: Install dependencies
25 | run: |
26 | sudo apt-get install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 libxcb-shape0 libegl1 libxcb-cursor0
27 | python -m pip install --upgrade pip
28 | pip install -r misc/requirements/requirements_tox.txt
29 | - name: Clone gh-pages branch
30 | run:
31 | git clone https://github.com/karlch/vimiv-qt.git gh-pages --branch gh-pages --single-branch
32 | - name: Rebuild documentation with tox
33 | run: |
34 | tox -e docs -- gh-pages
35 | - name: Commit documentation changes
36 | run: |
37 | cd gh-pages
38 | git config --local user.email "karlch@users.noreply.github.com"
39 | git config --local user.name "Christian Karl"
40 | git add .
41 | git commit -a -m "Automatic update of gh-pages website" -m "Caused by $(git --git-dir ../.git rev-parse HEAD)." || true
42 | - name: Push changes
43 | uses: ad-m/github-push-action@master
44 | with:
45 | branch: gh-pages
46 | directory: gh-pages
47 | github_token: ${{ secrets.GITHUB_TOKEN }}
48 |
--------------------------------------------------------------------------------
/.github/workflows/pythonpublish.yml:
--------------------------------------------------------------------------------
1 | name: Upload Python Package
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4.1.5
13 | - name: Set up Python
14 | uses: actions/setup-python@v5.1.0
15 | with:
16 | python-version: '3.x'
17 | - name: Install dependencies
18 | run: |
19 | python -m pip install --upgrade pip
20 | pip install setuptools wheel twine
21 | - name: Build and publish
22 | env:
23 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
24 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
25 | run: |
26 | python setup.py sdist
27 | twine upload dist/*
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/__pycache__/
2 | *.egg-info
3 | *.cache
4 | *.coverage
5 | *.hypothesis
6 | *.tox
7 | coverage/*
8 | build*
9 | dist*
10 | install_log.txt
11 | **/*.so
12 | docs/_build
13 | TODO
14 | .pytest_cache
15 | Makefile
16 | .venv
17 | **/.doctrees
18 | **/.mypy_cache
19 | **/.dmypy.json
20 | **/*.rstsrc
21 | tests/data
22 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v4.1.0
4 | hooks:
5 | - id: check-yaml
6 | - id: end-of-file-fixer
7 | - id: trailing-whitespace
8 | - repo: https://github.com/psf/black
9 | rev: 24.3.0
10 | hooks:
11 | - id: black
12 | args: [--force-exclude, .*syntax_error.*.py]
13 |
--------------------------------------------------------------------------------
/.pydocstylerc:
--------------------------------------------------------------------------------
1 | [pydocstyle]
2 | # Disabled:
3 | # D100-105: Check for docstrings is handled by pylint.
4 | # D107: Missing docstring in __init__, not always needed IMHO
5 | # D202: No blank lines allowed after function docstring, false positives with decorators
6 | # D402: First line should not be function's "signature", false positives
7 | # D413: Multi-line docstring summary should start at the second line
8 | # pep257: D203,D212,D213,D214,D215,D404,D405,D406,D407,D408,D409,D410,D411
9 | ignore = D100,D101,D102,D103,D105,D105,D107,D202,D203,D212,D213,D214,D215,D401,D402,D404,D405,D406,D407,D408,D409,D410,D411,D413
10 |
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | # vim: ft=dosini fileencoding=utf-8:
2 | [MASTER]
3 | extension-pkg-whitelist=PyQt5,
4 | vimiv.imutils._c_manipulate
5 | # Add custom checkers
6 | init-hook='import sys; sys.path.append("./scripts/pylint_checkers/")'
7 | load-plugins=check_count,
8 | check_docstring,
9 | check_header,
10 | pylint.extensions.check_elif,
11 | pylint.extensions.overlapping_exceptions,
12 | pylint.extensions.for_any_all,
13 |
14 | [MESSAGES CONTROL]
15 | enable=all
16 | disable=fixme,
17 | cyclic-import,
18 | global-statement,
19 | locally-disabled,
20 | too-many-ancestors,
21 | too-few-public-methods,
22 | blacklisted-name,
23 | file-ignored,
24 | wrong-import-position,
25 | ungrouped-imports,
26 | suppressed-message,
27 | arguments-differ,
28 | duplicate-code,
29 | import-outside-toplevel,
30 | raise-missing-from,
31 | arguments-renamed,
32 |
33 | [BASIC]
34 | function-rgx=[a-z_][a-z0-9_]{2,50}$
35 | const-rgx=[A-Za-z_][A-Za-z0-9_]{0,30}$
36 | class-const-rgx=[A-Za-z_][A-Za-z0-9_]{0,30}$
37 | method-rgx=[a-z_][A-Za-z0-9_]{2,50}$
38 | attr-rgx=[a-z_][a-z0-9_]{0,30}$
39 | argument-rgx=[a-z_][a-z0-9_]{0,30}$
40 | variable-rgx=[a-z_][a-z0-9_]{0,30}$
41 | docstring-min-length=3
42 | no-docstring-rgx=(^__|^main$|^test_|decorator|inside|^_on)
43 |
44 | [FORMAT]
45 | max-line-length=88
46 | max-module-lines=1000
47 | ignore-long-lines=(
9 | Wolfgang Popp (woefe)
10 | Ankur Sinha (sanjayankur31)
11 | Jean-Claude Graf (jcjgraf)
12 | Mateus Etto (Yutsuten)
13 | buzzingwires (buzzingwires)
14 |
15 | Please send an email to or open an issue / pull request
16 | if you feel like your name is missing or in case of any other problems.
17 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | graft c-extension
2 |
3 | graft icons
4 |
5 | include AUTHORS
6 | include LICENSE
7 |
8 | prune docs
9 | include docs/description.rst
10 |
11 | prune tests
12 |
13 | prune scripts
14 |
15 | include misc/Makefile
16 | include misc/vimiv.1
17 | include misc/vimiv.desktop
18 | include misc/org.karlch.vimiv.qt.metainfo.xml
19 | graft misc/requirements
20 |
21 | include fastentrypoints.py
22 |
--------------------------------------------------------------------------------
/c-extension/brightness_contrast.h:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * C extension for vimiv
3 | * Functions to enhance brightness and contrast of an image.
4 | *******************************************************************************/
5 |
6 | #include "definitions.h"
7 | #include "helper_func.h"
8 | #include "math_func_eval.h"
9 |
10 | /**
11 | * Enhance brightness using the GIMP algorithm.
12 | *
13 | * @param value Current R/G/B value of the pixel.
14 | * @param factor Factor to enhance brightness by.
15 | */
16 | static inline float enhance_brightness(float value, float factor)
17 | {
18 | if (factor < 0)
19 | return value * (1 + factor);
20 | return value + (1 - value) * factor;
21 | }
22 |
23 | /**
24 | * Enhance contrast using the GIMP algorithm:
25 | *
26 | * value = (value - 0.5) * (tan ((factor + 1) * PI/4) ) + 0.5
27 | *
28 | * @param value Current R/G/B value of the pixel.
29 | * @param factor Factor to enhance contrast by.
30 | */
31 | static inline float enhance_contrast(float value, float factor)
32 | {
33 | U_CHAR tan_pos = (U_CHAR) (factor * 127 + 127);
34 | return (value - 0.5) * (TAN[tan_pos]) + 0.5;
35 | }
36 |
37 | /**
38 | * Enhance brightness and contrast of an image.
39 | *
40 | * @param data Image pixel data to update.
41 | * @param size Total size of the data.
42 | * @param brightness Factor to enhance brightness by.
43 | * @param contrast Factor to enhance contrast by.
44 | */
45 | static void enhance_bc_c(U_CHAR* data, const int size, float brightness, float contrast)
46 | {
47 | float value;
48 |
49 | for (int pixel = 0; pixel < size; pixel++) {
50 | /* Skip alpha channel */
51 | if (pixel % 4 != ALPHA_CHANNEL) {
52 | value = ((float) data[pixel]) / 255.;
53 | value = enhance_brightness(value, brightness);
54 | value = enhance_contrast(value, contrast);
55 | value = pixel_value(value);
56 | data[pixel] = value;
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/c-extension/definitions.h:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * C extension for vimiv
3 | * definitions usable for more modules.
4 | *******************************************************************************/
5 | #ifndef definitions_h__
6 | #define definitions_h__
7 |
8 | /*****************************************
9 | * Alpha channel depends on endianness *
10 | *****************************************/
11 | #if G_BYTE_ORDER == G_LITTLE_ENDIAN /* BGRA */
12 |
13 | #define ALPHA_CHANNEL 3
14 | #define R_CHANNEL 2
15 | #define G_CHANNEL 1
16 | #define B_CHANNEL 0
17 |
18 | #elif G_BYTE_ORDER == G_BIG_ENDIAN /* ARGB */
19 |
20 | #define ALPHA_CHANNEL 0
21 | #define R_CHANNEL 1
22 | #define G_CHANNEL 2
23 | #define B_CHANNEL 3
24 |
25 | #else /* PDP endianness: RABG */
26 |
27 | #define ALPHA_CHANNEL 1
28 | #define R_CHANNEL 0
29 | #define G_CHANNEL 2
30 | #define B_CHANNEL 3
31 |
32 | #endif
33 |
34 | /*************
35 | * Typedefs *
36 | *************/
37 | typedef unsigned short U_SHORT;
38 | typedef unsigned char U_CHAR;
39 |
40 | #endif // ifndef definitions_h__
41 |
--------------------------------------------------------------------------------
/c-extension/helper_func.h:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * C extension for vimiv
3 | * Small inline helper functions
4 | *******************************************************************************/
5 | #ifndef helper_func_h__
6 | #define helper_func_h__
7 |
8 | #include "definitions.h"
9 |
10 | /**
11 | * Return the minimum of two numbers.
12 | */
13 | inline float min2(float a, float b) {
14 | return a < b ? a : b;
15 | }
16 |
17 | /**
18 | * Return the maximum of two numbers.
19 | */
20 | inline float max2(float a, float b) {
21 | return a > b ? a : b;
22 | }
23 |
24 | /**
25 | * Return the minimum of three numbers.
26 | */
27 | inline float min3(float a, float b, float c) {
28 | if (a <= b && a <= c)
29 | return a;
30 | else if (b <= c)
31 | return b;
32 | return c;
33 | }
34 |
35 | /**
36 | * Return the maximum of three numbers.
37 | */
38 | inline float max3(float a, float b, float c) {
39 | if (a >= b && a >= c)
40 | return a;
41 | else if (b >= c)
42 | return b;
43 | return c;
44 | }
45 |
46 | /**
47 | * Ensure a number stays within lower and upper.
48 | */
49 | inline float clamp(float value, float lower, float upper) {
50 | if (value < lower)
51 | return lower;
52 | if (value > upper)
53 | return upper;
54 | return value;
55 | }
56 |
57 | /**
58 | * Return the remainder of a floating point division.
59 | */
60 | inline float remainder_fl(float dividend, float divisor) {
61 | int intdiv = dividend / divisor;
62 | return dividend - intdiv * divisor;
63 | }
64 |
65 | /**
66 | * Return a valid pixel value (0..255) from a floating point value (0..1).
67 | */
68 | static inline U_CHAR pixel_value(float value) {
69 | return (U_CHAR) clamp(value * 255, 0, 255);
70 | }
71 |
72 | #endif // ifndef helper_func_h__
73 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SPHINXPROJ = vimiv
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/_static/custom.css:
--------------------------------------------------------------------------------
1 | html[data-theme="light"] {
2 | --pst-color-primary: #3F8DB4;
3 | --pst-color-info: #3F8DB4;
4 |
5 | --pst-color-secondary: #7AACBC;
6 | --pst-color-link-hover: #7AACBC;
7 |
8 | --pst-color-warning: #c79537;
9 | --pst-color-success: #4aa56f;
10 | --pst-color-attention: #f44336;
11 | --pst-color-danger: #f44336;
12 |
13 | --pst-color-background: rgb(244, 243, 245);
14 | --pst-color-on-background: rgb(244, 243, 245);
15 | --pst-color-surface: rgb(234, 233, 235);
16 | --pst-color-on-surface: rgb(214, 213, 215);
17 |
18 | --pst-color-text-base: #1F1D21;
19 | }
20 |
21 | html[data-theme="dark"] {
22 | --pst-color-secondary: #9FE2F6;
23 | --pst-color-link-hover: #9FE2F6;
24 |
25 | --pst-color-primary: #89C3D4;
26 | --pst-color-info: #89C3D4;
27 |
28 | --pst-color-warning: #c79537;
29 | --pst-color-success: #4aa56f;
30 | --pst-color-attention: #f44336;
31 | --pst-color-danger: #f44336;
32 |
33 | --pst-color-background: #1F1D21;
34 | --pst-color-on-background: #2b292d;
35 | --pst-color-surface: #2e2c30;
36 | --pst-color-on-surface: #444246;
37 |
38 | --pst-color-text-base: #F4F3F6;
39 | --pst-color-text-muted: rgb(208, 204, 212)
40 | }
41 |
42 | .bordered-image-light img {
43 | border: 3px solid #3F8DB4;
44 | margin: 4px;
45 | margin-top: 0px;
46 | margin-bottom: 16px;
47 | }
48 |
49 | .bordered-image-dark img {
50 | border: 3px solid #7AACBC;
51 | margin: 4px;
52 | margin-top: 0px;
53 | margin-bottom: 16px;
54 | }
55 |
--------------------------------------------------------------------------------
/docs/_static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/docs/_static/favicon.ico
--------------------------------------------------------------------------------
/docs/_static/scrots/command_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/docs/_static/scrots/command_dark.png
--------------------------------------------------------------------------------
/docs/_static/scrots/command_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/docs/_static/scrots/command_light.png
--------------------------------------------------------------------------------
/docs/_static/scrots/image_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/docs/_static/scrots/image_dark.png
--------------------------------------------------------------------------------
/docs/_static/scrots/image_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/docs/_static/scrots/image_light.png
--------------------------------------------------------------------------------
/docs/_static/scrots/library_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/docs/_static/scrots/library_dark.png
--------------------------------------------------------------------------------
/docs/_static/scrots/library_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/docs/_static/scrots/library_light.png
--------------------------------------------------------------------------------
/docs/_static/scrots/manipulate_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/docs/_static/scrots/manipulate_dark.png
--------------------------------------------------------------------------------
/docs/_static/scrots/manipulate_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/docs/_static/scrots/manipulate_light.png
--------------------------------------------------------------------------------
/docs/_static/scrots/thumbnail_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/docs/_static/scrots/thumbnail_dark.png
--------------------------------------------------------------------------------
/docs/_static/scrots/thumbnail_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/docs/_static/scrots/thumbnail_light.png
--------------------------------------------------------------------------------
/docs/_static/vimiv/vimiv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/docs/_static/vimiv/vimiv.png
--------------------------------------------------------------------------------
/docs/_static/vimiv/vimiv_banner_800.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/docs/_static/vimiv/vimiv_banner_800.png
--------------------------------------------------------------------------------
/docs/_static/vimiv/vimiv_banner_darkmode_800.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/docs/_static/vimiv/vimiv_banner_darkmode_800.png
--------------------------------------------------------------------------------
/docs/description.rst:
--------------------------------------------------------------------------------
1 | Vimiv is an image viewer with vim-like keybindings. It is written in python3
2 | using the Qt5 toolkit and is free software, licensed under the GPL. Some of the
3 | features are:
4 |
5 | * Simple library browser
6 | * Thumbnail mode
7 | * Basic image editing
8 | * Command line with tab completion
9 | * Complete customization with style sheets
10 |
--------------------------------------------------------------------------------
/docs/documentation/cl_options/index.rst:
--------------------------------------------------------------------------------
1 | Command Line Arguments
2 | ======================
3 |
4 | When starting vimiv form the command line you have the ability to pass a number of
5 | different argument to vimiv.
6 |
7 | Examples
8 | --------
9 |
10 | In the following we present a few use cases of command line arguments.
11 |
12 | * Start in library view with the thumbnail grid displayed::
13 |
14 | vimiv * --command "enter thumbnail" --command "enter library"
15 |
16 | * Start in read-only mode. This prevents accidental modification (renaming, moving, editing etc.) of any images::
17 |
18 | vimiv --set read_only true
19 |
20 | * Change WM_CLASS_INSTANCE to identify a vimiv instance::
21 |
22 | vimiv --qt-args "--name myVimivInstance"
23 |
24 | * Print the last selected image to STDOUT when quitting::
25 |
26 | vimiv --output "%"
27 |
28 | * Use vimiv as *Rofi for Images* to make a selection from candidate images::
29 |
30 | mySel=$(echo $myCand | vimiv --input --output "%m" --command "enter thumbnail")
31 |
32 | * Print debug logs of your amazing plugin you are writing and of the `api._mark` module which does not behave as you are expecting::
33 |
34 | vimiv --debug myAmazingPlugin api._mark
35 |
36 | * Grab image from the web and open it::
37 |
38 | curl https://i.imgur.com/somefile.png | vimiv -
39 |
40 | Command Line Arguments
41 | ----------------------
42 |
43 | The general calling structure is:
44 |
45 | .. include:: synopsis.rstsrc
46 |
47 | The following is an exhaustive list of all available arguments:
48 |
49 | .. include:: options.rstsrc
50 |
--------------------------------------------------------------------------------
/docs/documentation/commands.rst:
--------------------------------------------------------------------------------
1 | .. _commands:
2 |
3 | ########
4 | Commands
5 | ########
6 |
7 | In vimiv all keybindings are mapped to commands.
8 |
9 | Most of the commands can also be run from the command line but some are hidden
10 | as they are not useful to run by hand. A typical example is the ``:command``
11 | command which enters the command line.
12 |
13 | Every command is limited to its mode. This allows commands with equal names to
14 | do different things depending on the mode they are run in. For example the
15 | ``:zoom`` command rescales the image in ``image`` mode but changes the size of
16 | thumbnails in ``thumbnail`` mode. This also prevents running commands that are
17 | useless in the current mode. Commands available in the special ``global`` mode
18 | can be run in ``image``, ``library`` and ``thumbnail`` mode.
19 |
20 | Below is a complete list of all commands in every mode.
21 |
22 | .. include:: commands_desc.rstsrc
23 |
--------------------------------------------------------------------------------
/docs/documentation/configuration/index.rst:
--------------------------------------------------------------------------------
1 | Configuration
2 | =============
3 |
4 | The following documents are available:
5 |
6 | .. toctree::
7 | :maxdepth: 2
8 |
9 | settings
10 | keybindings
11 | statusbar
12 | style
13 | plugins
14 |
--------------------------------------------------------------------------------
/docs/documentation/configuration/statusbar.rst:
--------------------------------------------------------------------------------
1 | .. _statusbar:
2 |
3 | #########
4 | Statusbar
5 | #########
6 |
7 | In vimiv a statusbar at the bottom is used to display useful information.
8 |
9 | The content of the statusbar is completely configurable and mode dependent. The
10 | bar is grouped into three parts: left, center and right. Each part has a
11 | fallback content and can have a mode-dependent content which overrides the
12 | default. The content is defined in the ``STATUSBAR`` section of the
13 | ``vimiv.conf`` configuration file located in ``$XDG_CONFIG_HOME/vimiv/`` where
14 | ``$XDG_CONFIG_HOME`` is usually ``~/.config/`` if you have not configured it.
15 |
16 | The three default options are defined via:
17 |
18 | * ``left = the content to display on the left``
19 | * ``center = the content to display in the center``
20 | * ``right = the content to display on the right``
21 |
22 | And can be extended by adding options in the form of e.g.
23 |
24 | * ``left_image = content to display on the left in image mode``
25 | * ``center_thumbnail = content to display in the center in thumbnail mode``
26 |
27 | Text you enter is displayed *as is*. Formatting using a subset of the html
28 | styles is possible. As this is not very useful, vimiv provides a set of so
29 | called ``modules`` for the statusbar. A module is indicated by surrounding its
30 | name in curly braces, e.g. ``{pwd}``. This gets replaced by the output of the
31 | module. A list of available modules can be found
32 | :ref:`in the table below `.
33 |
34 | You can find information on the supported html subset
35 | `in the Qt documentation `_. Some
36 | useful examples:
37 |
38 | * ``bold text``
39 | * ``italic text``
40 | * ``underlined text``
41 | * ``red text``
42 | * ``fancy html-cyan text``
43 | * ``red background``
44 |
45 | .. _status_modules:
46 |
47 | .. include:: status_modules.rstsrc
48 |
--------------------------------------------------------------------------------
/docs/documentation/configuration/style.rst:
--------------------------------------------------------------------------------
1 | .. _styles:
2 |
3 | Style
4 | =====
5 | There are two default styles: a light and a dark one. They are both based upon
6 | `base16 tomorrow `_,
7 | the dark one using tomorrow night. To switch between them, set the ``style``
8 | setting in the ``vimiv.conf`` file to ``default`` or ``default-dark``
9 | respectively.
10 |
11 | The style files of the default styles are written to the ``styles`` directory
12 | on startup if they do not exist. The styles directory is located in
13 | ``$XDG_CONFIG_HOME/vimiv/`` where ``$XDG_CONFIG_HOME`` is usually
14 | ``~/.config/`` if you have not updated it.
15 |
16 | A bunch of base16 styles to pick from are available in the
17 | `base16-vimiv repository `_.
18 |
19 | Creating your own style is easy:
20 |
21 | #. Create a new file in the ``styles`` directory. The file must start with the
22 | ``[STYLE]`` header.
23 | #. Define the colors ``base00`` to ``base0f``. This is required as these colors are
24 | referenced by the individual options.
25 | #. Change the ``style`` setting in the ``vimiv.conf`` file to the name of your newly
26 | created file.
27 | #. Optional: override any other option such as the global ``font`` or individual
28 | settings like ``thumbnail.padding``.
29 |
30 | .. hint:: Refer to the created default style for all available options
31 |
32 | .. hint:: Defined style options can be referenced via ``new_option = {other_option}``,
33 | for example to use a different base color for mark-related things you can use
34 | ``mark.color = {base0a}``.
35 |
36 | .. hint:: As for settings, you can refer to
37 | :ref:`external resources `.
38 |
39 | .. hint:: The python configparser module is not case sensitive. Therefore it is
40 | a good idea to keep all your style options in lower case.
41 |
--------------------------------------------------------------------------------
/docs/documentation/contributing.rst:
--------------------------------------------------------------------------------
1 | .. _contributing:
2 |
3 | .. include:: ../../.github/CONTRIBUTING.rst
4 |
--------------------------------------------------------------------------------
/docs/documentation/contributing_bugs.rst:
--------------------------------------------------------------------------------
1 | Contributing / Reporting Bugs
2 | -----------------------------
3 |
4 | You want to contribute to vimiv? Great! Feel free to take a look at the
5 | :ref:`contributing` for more details and a few tips on how to get
6 | started.
7 |
8 | The best way to report bugs is to open an
9 | `issue on github `_. If you do
10 | not have a github account, feel free to
11 | `contact me directly `_. If possible, please reproduce
12 | the bug running ``vimiv --log-level debug`` and include the log file located in
13 | ``$XDG_DATA_HOME/vimiv/vimiv.log`` where ``$XDG_DATA_HOME`` is usually
14 | ``~/.local/share/`` if you have not configured it.
15 |
--------------------------------------------------------------------------------
/docs/documentation/datafile_warning.rst:
--------------------------------------------------------------------------------
1 | .. warning::
2 |
3 | This does not install data files such as the icons or the ``vimiv.desktop``
4 | file globally. Thus, e.g., file managers may not find the vimiv program as
5 | expected. To get an idea on how to install these, you can take a look at
6 | the Makefile located in `misc/Makefile` and read the section on
7 | :ref:`system-wide installation `.
8 |
--------------------------------------------------------------------------------
/docs/documentation/dependency_info.rst:
--------------------------------------------------------------------------------
1 | .. note::
2 |
3 | You need to have the :ref:`dependencies ` installed
4 | system-wide for this to work.
5 |
--------------------------------------------------------------------------------
/docs/documentation/getting_help.rst:
--------------------------------------------------------------------------------
1 | Getting Help
2 | ------------
3 |
4 | You can contact me under `my email address `_ or
5 | open an `issue on github `_ if you
6 | think the question is of general interest.
7 |
--------------------------------------------------------------------------------
/docs/documentation/index.rst:
--------------------------------------------------------------------------------
1 | Docs
2 | ====
3 |
4 | The following documents are available:
5 |
6 | .. toctree::
7 | :maxdepth: 2
8 |
9 | install
10 | getting_started
11 | commands
12 | configuration/index
13 | cl_options/index
14 | metadata
15 | contributing
16 | migrating
17 |
18 | .. include:: getting_help.rst
19 |
20 | .. include:: contributing_bugs.rst
21 |
--------------------------------------------------------------------------------
/docs/documentation/updating_icon_cache.rst:
--------------------------------------------------------------------------------
1 | .. note::
2 |
3 | You may need to run ``gtk-update-icon-cache /usr/share/icons/hicolor`` (replace the
4 | path if installing the icons somewhere else) after the install to get the vimiv icon
5 | in your application launcher. It has been reported that the command was needed for
6 | `wofi `_.
7 |
--------------------------------------------------------------------------------
/docs/manpage/vimiv.1.rst:
--------------------------------------------------------------------------------
1 | :orphan:
2 |
3 | **************
4 | Vimiv man page
5 | **************
6 |
7 | Synopsis
8 | """"""""
9 |
10 | .. include:: synopsis.rstsrc
11 |
12 | Description
13 | """""""""""
14 |
15 | .. include:: /description.rst
16 |
17 | A much more complete documentation can be found under
18 | https://karlch.github.io/vimiv-qt/.
19 |
20 | Options
21 | """""""
22 |
23 | .. include:: options.rstsrc
24 |
25 | Website
26 | """""""
27 | https://karlch.github.io/vimiv-qt/
28 |
--------------------------------------------------------------------------------
/docs/theme.conf:
--------------------------------------------------------------------------------
1 | [theme]
2 | inherit = alabaster
3 | stylesheet = style.css
4 | pygments_style = pygments.css
5 |
--------------------------------------------------------------------------------
/icons/vimiv_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/icons/vimiv_128x128.png
--------------------------------------------------------------------------------
/icons/vimiv_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/icons/vimiv_16x16.png
--------------------------------------------------------------------------------
/icons/vimiv_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/icons/vimiv_256x256.png
--------------------------------------------------------------------------------
/icons/vimiv_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/icons/vimiv_32x32.png
--------------------------------------------------------------------------------
/icons/vimiv_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/icons/vimiv_512x512.png
--------------------------------------------------------------------------------
/icons/vimiv_64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/icons/vimiv_64x64.png
--------------------------------------------------------------------------------
/misc/Makefile:
--------------------------------------------------------------------------------
1 | PREFIX := /usr
2 | DATADIR := $(DESTDIR)/$(PREFIX)/share
3 | MANDIR := $(DATADIR)/man
4 | APPDIR := $(DATADIR)/applications
5 | LICENSEDIR := $(DATADIR)/licenses
6 | METAINFODIR := $(DATADIR)/metainfo
7 |
8 | ICONSIZES := 16 32 64 128 256 512
9 |
10 | default:
11 | @printf "There is nothing to do.\n"
12 | @printf "Run 'sudo make install' to install vimiv.\n"
13 | @printf "Run 'make options' for a list of all options.\n"
14 |
15 | options: help
16 | @printf "\nOptions:\n"
17 | @printf "DESTDIR = $(DESTDIR)/\n"
18 | @printf "PREFIX = $(PREFIX)\n"
19 | @printf "DATADIR = $(DATADIR)\n"
20 | @printf "MANDIR = $(MANDIR)\n"
21 | @printf "LICENSEDIR = $(LICENSEDIR)\n"
22 | @printf "METAINFODIR = $(METAINFODIR)\n"
23 |
24 | help:
25 | @printf "make help: Print help.\n"
26 | @printf "make options: Print help and list all options.\n"
27 | @printf "make install: Install vimiv.\n"
28 | @printf "make uninstall: Uninstall vimiv.\n"
29 | @printf "make clean: Remove build directories.\n"
30 |
31 | install:
32 | python3 setup.py install --root=$(DESTDIR)/ --prefix=$(PREFIX) --record=install_log.txt
33 | install -Dm644 misc/vimiv.desktop $(APPDIR)/vimiv.desktop
34 | install -Dm644 misc/org.karlch.vimiv.qt.metainfo.xml $(METAINFODIR)/org.karlch.vimiv.qt.metainfo.xml
35 | install -Dm644 LICENSE $(LICENSEDIR)/vimiv/LICENSE
36 | install -Dm644 misc/vimiv.1 $(MANDIR)/man1/vimiv.1
37 | gzip -n -9 -f $(MANDIR)/man1/vimiv.1
38 | $(foreach i,$(ICONSIZES),install -Dm644 icons/vimiv_${i}x${i}.png $(DATADIR)/icons/hicolor/${i}x${i}/apps/vimiv.png;)
39 | install -Dm644 icons/vimiv.svg $(DATADIR)/icons/hicolor/scalable/apps/vimiv.svg
40 |
41 | uninstall:
42 | scripts/uninstall_pythonpkg.sh
43 | rm $(APPDIR)/vimiv.desktop
44 | rm $(METAINFODIR)/org.karlch.vimiv.qt.metainfo.xml
45 | rm -r $(LICENSEDIR)/vimiv
46 | rm $(MANDIR)/man1/vimiv.1.gz
47 | $(foreach i,$(ICONSIZES),rm $(DATADIR)/icons/hicolor/${i}x${i}/apps/vimiv.png;)
48 | rm $(DATADIR)/icons/hicolor/scalable/apps/vimiv.svg
49 |
50 | clean:
51 | rm -rf build vimiv.egg-info/
52 |
--------------------------------------------------------------------------------
/misc/requirements/requirements_cov.txt:
--------------------------------------------------------------------------------
1 | coverage==7.5.1
2 | pytest-cov==5.0.0
3 |
--------------------------------------------------------------------------------
/misc/requirements/requirements_docs.txt:
--------------------------------------------------------------------------------
1 | sphinx==7.3.7
2 | pydata-sphinx-theme==0.15.2
3 | sphinxcontrib-images==0.9.4
4 |
--------------------------------------------------------------------------------
/misc/requirements/requirements_lint.txt:
--------------------------------------------------------------------------------
1 | pylint==3.1.0
2 | black==24.4.2
3 | pydocstyle==6.3.0
4 |
--------------------------------------------------------------------------------
/misc/requirements/requirements_mypy.txt:
--------------------------------------------------------------------------------
1 | mypy==1.10.0
2 | PyQt5-stubs==5.15.6.0
3 |
--------------------------------------------------------------------------------
/misc/requirements/requirements_packaging.txt:
--------------------------------------------------------------------------------
1 | pyroma==4.2
2 | check-manifest==0.49
3 |
--------------------------------------------------------------------------------
/misc/requirements/requirements_piexif.txt:
--------------------------------------------------------------------------------
1 | piexif==1.1.3
2 |
--------------------------------------------------------------------------------
/misc/requirements/requirements_pyexiv2.txt:
--------------------------------------------------------------------------------
1 | py3exiv2==0.11.0
2 |
--------------------------------------------------------------------------------
/misc/requirements/requirements_pyqt5.txt:
--------------------------------------------------------------------------------
1 | PyQt5==5.15.10
2 |
--------------------------------------------------------------------------------
/misc/requirements/requirements_pyqt6.txt:
--------------------------------------------------------------------------------
1 | PyQt6==6.7.0
2 |
--------------------------------------------------------------------------------
/misc/requirements/requirements_pyside6.txt:
--------------------------------------------------------------------------------
1 | PySide6==6.7.0
2 |
--------------------------------------------------------------------------------
/misc/requirements/requirements_setup.txt:
--------------------------------------------------------------------------------
1 | setuptools>=40.8.0
2 |
--------------------------------------------------------------------------------
/misc/requirements/requirements_tests.txt:
--------------------------------------------------------------------------------
1 | flaky==3.8.1
2 | pytest==8.2.0
3 | pytest-mock==3.14.0
4 | pytest-qt==4.4.0
5 | pytest-xvfb==3.0.0
6 | pytest-bdd==7.1.2
7 |
--------------------------------------------------------------------------------
/misc/requirements/requirements_tox.txt:
--------------------------------------------------------------------------------
1 | tox==4.15.0
2 |
--------------------------------------------------------------------------------
/misc/vimiv.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Type=Application
3 | Name=vimiv
4 | GenericName=Image Viewer
5 | Comment=An image viewer with vim like keybindings
6 | Icon=vimiv
7 | Terminal=false
8 | Exec=vimiv %F
9 | Categories=Graphics;
10 | MimeType=image/bmp;image/gif;image/icns;image/jp2;image/jpeg;image/jpeg2000;image/jpx;image/png;image/svg;image/tiff;image/webp;image/x-portable-anymap;image/x-portable-bitmap;image/x-portable-graymap;image/x-portable-greymap;image/x-portable-pixmap;image/x-tga;image/x-xbitmap;image/x-xpixmap;
11 |
--------------------------------------------------------------------------------
/mypy.ini:
--------------------------------------------------------------------------------
1 | [mypy]
2 | python_version = 3.8
3 | warn_redundant_casts = True
4 | warn_unused_ignores = True
5 | show_error_codes = True
6 | pretty = True
7 | implicit_optional = True
8 |
9 | [mypy-piexif]
10 | ignore_missing_imports = True
11 |
12 | [mypy-PyQt5.QtSvg]
13 | ignore_missing_imports = True
14 |
15 | [mypy-PyQt5.sip]
16 | ignore_missing_imports = True
17 |
18 | [mypy-vimiv.app]
19 | disallow_untyped_defs = True
20 | disallow_incomplete_defs = True
21 |
22 | [mypy-vimiv.parser]
23 | disallow_untyped_defs = True
24 | disallow_incomplete_defs = True
25 |
26 | [mypy-vimiv.startup]
27 | disallow_untyped_defs = True
28 | disallow_incomplete_defs = True
29 |
30 | [mypy-vimiv.version]
31 | disallow_untyped_defs = True
32 | disallow_incomplete_defs = True
33 |
34 | [mypy-vimiv.api.*]
35 | disallow_untyped_defs = True
36 | disallow_incomplete_defs = True
37 |
38 | [mypy-vimiv.plugins.*]
39 | disallow_untyped_defs = True
40 | disallow_incomplete_defs = True
41 |
42 | [mypy-vimiv.utils.*]
43 | disallow_untyped_defs = True
44 | disallow_incomplete_defs = True
45 |
46 | [mypy-vimiv.utils]
47 | disallow_untyped_defs = False
48 | disallow_incomplete_defs = False
49 |
50 | # TODO re-think qt type checking
51 | [mypy-vimiv.qt.*]
52 | ignore_errors = True
53 |
54 | [mypy-vimiv.qt]
55 | ignore_errors = True
56 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | testpaths = tests
3 | addopts = --no-flaky-report --strict-markers
4 | faulthandler_timeout = 30
5 | markers =
6 | current: Mark tests during development
7 | imageformats: Require retrieving images from the web to test additional formats
8 | metadata: Require metadata support
9 | piexif: Require piexif
10 | pyexiv2: Require pyexiv2
11 | nometadata: Requires metadata support NOT to be available
12 | ci: Run test only on ci
13 | ci_skip: Skip test on ci
14 |
--------------------------------------------------------------------------------
/scripts/gen_manpage.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Simple script to create the man page using sphinx
4 |
5 | # First re-create the source code documentation
6 | scripts/src2rst.py
7 |
8 | # Then call sphinx to rebuild the man page
9 | sphinx-build -b man docs misc/
10 |
--------------------------------------------------------------------------------
/scripts/lint_tests.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
3 |
4 | """Script to run pylint over the test-suite.
5 |
6 | Required due to https://github.com/PyCQA/pylint/issues/352.
7 | """
8 |
9 | import argparse
10 | import os
11 | import sys
12 | import subprocess
13 | from typing import List
14 |
15 |
16 | def get_parser() -> argparse.ArgumentParser:
17 | parser = argparse.ArgumentParser()
18 | parser.add_argument("directory")
19 | return parser
20 |
21 |
22 | def get_all_python_files(directory: str) -> List[str]:
23 | """Retrieve all files in a directory by walking it."""
24 | infiles = [
25 | os.path.join(dirpath, filename)
26 | for dirpath, _, filenames in os.walk(directory)
27 | for filename in filenames
28 | if os.path.splitext(filename)[-1] == ".py"
29 | ]
30 | return infiles
31 |
32 |
33 | def run_pylint(infiles: List[str]):
34 | """Run pylint over all files in infiles."""
35 | disabled = (
36 | "redefined-outer-name", # Fixture passed as argument
37 | "unused-argument", # Fixture used for setup / teardown only
38 | "missing-docstring", # Not required in tests
39 | "command-docstring", # Not required in tests
40 | "protected-access", # Acceptable in tests
41 | "compare-to-empty-string", # Stricter check than False in tests
42 | "import-error", # Errors on pytest related modules
43 | )
44 | ignored = ("mock_plugin_syntax_error.py",)
45 | command = (
46 | "pylint",
47 | f"--disable={','.join(disabled)}",
48 | f"--ignore={','.join(ignored)}",
49 | *infiles,
50 | )
51 | print("Running pylint over tests")
52 | return subprocess.run(command, check=False).returncode
53 |
54 |
55 | def main():
56 | parser = get_parser()
57 | args = parser.parse_args()
58 | infiles = get_all_python_files(args.directory)
59 | sys.exit(run_pylint(infiles))
60 |
61 |
62 | if __name__ == "__main__":
63 | main()
64 |
--------------------------------------------------------------------------------
/scripts/maybe_build_cextension.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Ensure the c-extension has been build inplace for testing.
4 |
5 | Locally, when running tox without compiling the c-extension, an ImportError for
6 | _c_manipulate may be raised. This is fixed by this script.
7 | """
8 |
9 | import glob
10 | import os
11 | import subprocess
12 | import sys
13 |
14 |
15 | if __name__ == "__main__":
16 | filedir = os.path.dirname(os.path.realpath(__file__))
17 | rootdir = os.path.dirname(filedir)
18 | extension_built = glob.glob(os.path.join(rootdir, "vimiv", "imutils", "_c_*"))
19 |
20 | if not extension_built:
21 | print("Building c-extension...")
22 | setup_py = os.path.join(rootdir, "setup.py")
23 | cmd = sys.executable, setup_py, "build_ext", "--inplace"
24 | subprocess.run(cmd, check=True)
25 |
--------------------------------------------------------------------------------
/scripts/pylint_checkers/__init__.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Custom checkers for pylint."""
4 |
--------------------------------------------------------------------------------
/scripts/pylint_checkers/check_header.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Checker to ensure each python file includes a modeline and no copyright notice."""
4 |
5 | from pylint.checkers import BaseChecker
6 |
7 |
8 | class FileHeaderChecker(BaseChecker):
9 | """Checker to ensure each python file includes a modeline and copyright notice."""
10 |
11 | name = "file-header"
12 | name_modeline_missing = "modeline-missing"
13 | name_copyright_included = "copyright-included"
14 |
15 | msgs = {
16 | "E9501": (
17 | "Vim modeline is missing",
18 | name_modeline_missing,
19 | "All files should include a valid vim modeline.",
20 | ),
21 | "E9502": (
22 | "Copyright included",
23 | name_copyright_included,
24 | "There should be no copyright at the top of the file.",
25 | ),
26 | }
27 | options = ()
28 |
29 | priority = -1
30 |
31 | MODELINE = "# vim: ft=python fileencoding=utf-8 sw=4 et sts=4"
32 |
33 | def process_module(self, node):
34 | """Read the module content as string and check for the necessary content."""
35 |
36 | with node.stream() as stream:
37 | content = stream.read().decode("utf-8")
38 |
39 | lines = content.split("\n")
40 |
41 | if self.MODELINE not in lines:
42 | self.add_message(self.name_modeline_missing, line=1)
43 |
44 | if any(line.lower().startswith("# copyright") for line in lines):
45 | self.add_message(self.name_copyright_included, line=1)
46 |
47 |
48 | def register(linter):
49 | """Register the defined checkers automatically."""
50 | linter.register_checker(FileHeaderChecker(linter))
51 |
--------------------------------------------------------------------------------
/scripts/uninstall_pythonpkg.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Remove the installed python package from /usr/lib/python3.x/site-packages
4 |
5 | if [[ -f install_log.txt ]]; then
6 | rm $(awk '{print "'/'"$0}' install_log.txt)
7 | else
8 | printf "python-setuptools does not provide an uninstall option.\n"
9 | printf "To completely remove vimiv you will have to remove all related"
10 | printf " files from /usr/lib/python3.x/site-packages/.\n"
11 | printf "A list of files should have been generated during make install"
12 | printf " in install_log.txt but seems to have been removed.\n"
13 | fi
14 |
--------------------------------------------------------------------------------
/scripts/vimiv_history.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
3 |
4 | """Script to print the vimiv history of a given mode.
5 |
6 | Reads the history from vimiv's json history file and prints the elements
7 | line-by-line. The default mode is image. To change the mode, pass it using its
8 | name.
9 |
10 | In case you use a custom data directory or are not using linux, please pass the
11 | filename as argument.
12 | """
13 |
14 | import argparse
15 | import json
16 | import os
17 | from typing import List
18 |
19 |
20 | def main():
21 | parser = get_parser()
22 | args = parser.parse_args()
23 | history = read_history(mode=args.mode, filename=args.filename)
24 | print(*history, sep="\n")
25 |
26 |
27 | def get_parser() -> argparse.ArgumentParser:
28 | parser = argparse.ArgumentParser(
29 | description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
30 | )
31 | parser.add_argument(
32 | "mode",
33 | choices=("image", "thumbnail", "library", "manipulate"),
34 | default="image",
35 | nargs="?",
36 | help="The mode for which history is printed",
37 | )
38 | parser.add_argument(
39 | "-f",
40 | "--filename",
41 | default=os.path.expanduser("~/.local/share/vimiv/history.json"),
42 | help="Path to the history file to read",
43 | )
44 | return parser
45 |
46 |
47 | def read_history(*, mode: str, filename: str) -> List[str]:
48 | with open(filename, "r", encoding="utf-8") as f:
49 | content = json.load(f)
50 | return content[mode]
51 |
52 |
53 | if __name__ == "__main__":
54 | main()
55 |
--------------------------------------------------------------------------------
/tests/end2end/features/api/conftest.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import os
4 |
5 | import pytest_bdd as bdd
6 |
7 | from vimiv import api
8 |
9 |
10 | @bdd.then(bdd.parsers.parse("there should be {n_marked:d} marked images"))
11 | def check_number_marked(n_marked):
12 | assert len(api.mark.paths) == n_marked
13 |
14 |
15 | @bdd.then(bdd.parsers.parse("{path} should be marked"))
16 | def check_image_marked(path):
17 | assert path in [os.path.basename(p) for p in api.mark.paths]
18 |
19 |
20 | @bdd.then(bdd.parsers.parse("{path} should not be marked"))
21 | def check_image_not_marked(path):
22 | assert path not in [os.path.basename(p) for p in api.mark.paths]
23 |
--------------------------------------------------------------------------------
/tests/end2end/features/api/keybindings.feature:
--------------------------------------------------------------------------------
1 | Feature: Bind and unbind keybindings.
2 |
3 | Scenario: Bind in library mode
4 | Given I open any directory
5 | When I run bind ZX test
6 | Then the keybinding ZX should exist for mode library
7 |
8 | Scenario: Bind in global mode
9 | Given I open any directory
10 | When I run bind ZX test --mode=global
11 | Then the keybinding ZX should exist for mode image
12 | And the keybinding ZX should exist for mode library
13 | And the keybinding ZX should exist for mode thumbnail
14 |
15 | Scenario: Bind and unbind command
16 | Given I open any directory
17 | When I run bind ZX test
18 | And I run unbind ZX
19 | Then the keybinding ZX should not exist for mode library
20 |
--------------------------------------------------------------------------------
/tests/end2end/features/api/print.feature:
--------------------------------------------------------------------------------
1 | Feature: Print to STDOUT
2 |
3 | Scenario: Print trivial list
4 | Given I start vimiv
5 | And I capture output
6 | When I run print-stdout "test"
7 | Then stdout should contain 'test'
8 |
9 | Scenario: Print longer list
10 | Given I start vimiv
11 | And I capture output
12 | When I run print-stdout "test1" "test2"
13 | Then stdout should contain 'test1'
14 | And stdout should contain 'test2'
15 |
16 | Scenario: Print list using sep
17 | Given I start vimiv
18 | And I capture output
19 | When I run print-stdout "test1" "test2" --sep ','
20 | Then stdout should contain 'test1,test2'
21 |
22 | Scenario: Print list using end
23 | Given I start vimiv
24 | And I capture output
25 | When I run print-stdout "test1" "test2" --end 'xxx\n'
26 | Then stdout should contain 'test2xxx'
27 |
28 | Scenario: Print marked images
29 | Given I open 2 images
30 | And I capture output
31 | When I run mark image_01.jpg image_02.jpg
32 | And I run print-stdout %m
33 | Then stdout should contain 'image_01.jpg'
34 | And stdout should contain 'image_02.jpg'
35 |
36 | Scenario: Print marked images using alias
37 | Given I open 2 images
38 | And I capture output
39 | When I run mark image_01.jpg image_02.jpg
40 | And I run mark-print
41 | Then stdout should contain 'image_01.jpg'
42 | And stdout should contain 'image_02.jpg'
43 |
--------------------------------------------------------------------------------
/tests/end2end/features/api/prompt.feature:
--------------------------------------------------------------------------------
1 | Feature: Prompt the user for a question
2 |
3 | Scenario Outline: Ask a question and answer it
4 | Given I open any directory
5 | And I ask a question and answer with
6 | Then I expect as answer
7 |
8 | Examples:
9 | | key | answer |
10 | | y | true |
11 | | n | false |
12 | | | false |
13 | | | none |
14 |
--------------------------------------------------------------------------------
/tests/end2end/features/api/rename.feature:
--------------------------------------------------------------------------------
1 | Feature: Rename paths
2 |
3 | Scenario: Rename images
4 | Given I open 2 images
5 | When I run rename * new_name
6 | Then the file new_name_001.jpg should exist
7 | Then the file new_name_002.jpg should exist
8 |
9 | Scenario: Keep mark state when renaming images
10 | Given I open 2 images
11 | When I run mark %
12 | When I run rename * new_name
13 | Then the file new_name_001.jpg should exist
14 | And new_name_001.jpg should be marked
15 |
16 | Scenario: Do not rename directories
17 | Given I open a directory with 2 paths
18 | When I run rename * new_directory
19 | Then no crash should happen
20 | And the directory new_directory_001 should not exist
21 |
--------------------------------------------------------------------------------
/tests/end2end/features/api/test_keybindings_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest
4 | import pytest_bdd as bdd
5 |
6 | from vimiv import api
7 |
8 |
9 | bdd.scenarios("keybindings.feature")
10 |
11 |
12 | @pytest.fixture(autouse=True)
13 | def cleanup_keybindings(cleanup_helper):
14 | """Fixture to delete any keybindings that were created during testing."""
15 | yield from cleanup_helper(api.keybindings._registry)
16 |
17 |
18 | @bdd.then(bdd.parsers.parse("the keybinding {binding} should exist for mode {mode}"))
19 | def keybinding_exists(binding, mode):
20 | mode = api.modes.get_by_name(mode)
21 | assert binding in api.keybindings._registry[mode]
22 |
23 |
24 | @bdd.then(
25 | bdd.parsers.parse("the keybinding {binding} should not exist for mode {mode}")
26 | )
27 | def keybinding_not_exists(binding, mode):
28 | mode = api.modes.get_by_name(mode)
29 | assert binding not in api.keybindings._registry[mode]
30 |
--------------------------------------------------------------------------------
/tests/end2end/features/api/test_mark_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import os
4 |
5 | import pytest_bdd as bdd
6 |
7 | from vimiv.api._mark import Tag
8 |
9 |
10 | bdd.scenarios("mark.feature")
11 |
12 |
13 | @bdd.when("I remove the delete permissions")
14 | def remove_delete_permission(mocker):
15 | mocker.patch("os.remove", side_effect=PermissionError)
16 | mocker.patch("shutil.rmtree", side_effect=PermissionError)
17 |
18 |
19 | @bdd.when(bdd.parsers.parse("I create the tag '{name}' with permissions '{mode:o}'"))
20 | def create_tag_with_permission(name, mode):
21 | with Tag(name, read_only=False):
22 | pass
23 | path = Tag.path(name)
24 | os.chmod(path, 0o000)
25 |
26 |
27 | @bdd.when(bdd.parsers.parse("I insert an empty line into the tag file {name}"))
28 | def insert_empty_lines_into_tag_file(name):
29 | assert os.path.isfile(Tag.path(name)), f"Tag file {name} does not exist"
30 | with open(Tag.path(name), "a", encoding="utf-8") as f:
31 | f.write("\n")
32 |
33 |
34 | @bdd.then(bdd.parsers.parse("the tag file {name} should exist with {n_paths:d} paths"))
35 | def check_tag_file(name, n_paths):
36 | assert os.path.isfile(Tag.path(name)), f"Tag file {name} does not exist"
37 | with Tag(name, "r") as tag:
38 | paths = tag.read()
39 | assert len(paths) == n_paths
40 |
41 |
42 | @bdd.then(bdd.parsers.parse("the tag file {name} should not contain any empty lines"))
43 | def check_tag_file_no_empty_line(name):
44 | assert os.path.isfile(Tag.path(name)), f"Tag file {name} does not exist"
45 | with open(Tag.path(name), "r", encoding="utf-8") as f:
46 | assert all(line.strip() for line in f)
47 |
48 |
49 | @bdd.then(bdd.parsers.parse("the tag file {name} should not exist"))
50 | def check_tag_file_not_exists(name):
51 | assert not os.path.exists(Tag.path(name))
52 |
--------------------------------------------------------------------------------
/tests/end2end/features/api/test_modeswitch_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 | from vimiv import api
6 |
7 |
8 | bdd.scenarios("modeswitch.feature")
9 |
10 |
11 | @bdd.when(bdd.parsers.parse("I toggle {mode} mode"))
12 | def toggle_mode(mode):
13 | api.modes.get_by_name(mode).toggle()
14 |
--------------------------------------------------------------------------------
/tests/end2end/features/api/test_print_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("print.feature")
7 |
--------------------------------------------------------------------------------
/tests/end2end/features/api/test_prompt_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 | from vimiv import api
6 |
7 |
8 | bdd.scenarios("prompt.feature")
9 |
10 |
11 | @bdd.given(
12 | bdd.parsers.parse("I ask a question and answer with {key}"),
13 | target_fixture="prompt_response",
14 | )
15 | def answer_question(answer_prompt, key):
16 | answer_prompt(key)
17 | return api.prompt.ask_question(title="end2end-question", body="Hello there?")
18 |
19 |
20 | @bdd.then(bdd.parsers.parse("I expect {answer} as answer"))
21 | def check_prompt(prompt_response, answer):
22 | answer = answer.lower()
23 | if answer in ("true", "yes", "1"):
24 | assert prompt_response
25 | elif answer in ("false", "no", "0"):
26 | assert not prompt_response
27 | elif answer == "none":
28 | assert prompt_response is None
29 | else:
30 | raise ValueError(f"Unexpected prompt answer '{answer}'")
31 |
--------------------------------------------------------------------------------
/tests/end2end/features/api/test_rename_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("rename.feature")
7 |
--------------------------------------------------------------------------------
/tests/end2end/features/api/test_working_directory_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 | from vimiv import api
6 |
7 |
8 | bdd.scenarios("working_directory.feature")
9 |
10 |
11 | @bdd.then(bdd.parsers.parse("there should be {n_directories:d} monitored directory"))
12 | @bdd.then(bdd.parsers.parse("there should be {n_directories:d} monitored directories"))
13 | def check_monitored_directories(n_directories):
14 | assert len(api.working_directory.handler.directories()) == n_directories
15 |
16 |
17 | @bdd.then(bdd.parsers.parse("there should be {n_files:d} monitored file"))
18 | @bdd.then(bdd.parsers.parse("there should be {n_files:d} monitored files"))
19 | def check_monitored_files(n_files):
20 | assert len(api.working_directory.handler.files()) == n_files
21 |
--------------------------------------------------------------------------------
/tests/end2end/features/api/working_directory.feature:
--------------------------------------------------------------------------------
1 | Feature: React to working directory changes
2 |
3 | Scenario: Monitor the current directory
4 | Given I open any directory
5 | Then there should be 1 monitored directory
6 |
7 | Scenario: Unmonitor the current directory
8 | Given I open any directory
9 | When I run set monitor_filesystem false
10 | Then there should be 0 monitored directories
11 |
12 | Scenario: Monitor the current image
13 | Given I open any image
14 | Then there should be 1 monitored file
15 |
16 | Scenario: Unmonitor the current image
17 | Given I open any image
18 | When I run set monitor_filesystem false
19 | Then there should be 0 monitored files
20 |
--------------------------------------------------------------------------------
/tests/end2end/features/command/aliases.feature:
--------------------------------------------------------------------------------
1 | Feature: Create and run aliases.
2 |
3 | Scenario: Create and run an alias.
4 | Given I start vimiv
5 | When I run alias mycount count
6 | And I run mycount
7 | Then the count should be 1
8 |
9 | Scenario: Create an alias and run it with arguments.
10 | Given I start vimiv
11 | When I run alias mycount count
12 | And I run mycount --number=5
13 | Then the count should be 5
14 |
15 | Scenario: Create and run an alias with arguments.
16 | Given I start vimiv
17 | When I run alias count-three 'count --number=3'
18 | And I run count-three
19 | Then the count should be 3
20 |
21 | Scenario: Do not overwrite existing commands with alias.
22 | Given I start vimiv
23 | When I run alias quit scroll
24 | Then the alias quit should not exist
25 | And the message
26 | 'alias: Not overriding default command quit'
27 | should be displayed
28 |
29 | Scenario: Alias to an external command
30 | Given I start vimiv
31 | When I run alias listdir !ls
32 | And I run bind zzz listdir
33 | And I press 'zzz'
34 | Then no message should be displayed
35 |
36 | Scenario: Alias including wildcards
37 | Given I open a directory with 1 paths
38 | When I run alias copythis '!cp -r \% other_directory'
39 | And I run copythis
40 | Then the directory other_directory should exist
41 |
--------------------------------------------------------------------------------
/tests/end2end/features/command/chaining.feature:
--------------------------------------------------------------------------------
1 | Feature: Chain commands together.
2 |
3 | Background:
4 | Given I start vimiv
5 |
6 | Scenario: Chain two commands together.
7 | When I run count && count
8 | Then the count should be 2
9 |
10 | Scenario: Chain three commands together.
11 | When I run count && count && count
12 | Then the count should be 3
13 |
14 | Scenario: Chain commands with count together.
15 | When I run 2count && 3count
16 | Then the count should be 5
17 |
18 | Scenario: Fail first of two chained commands
19 | When I run something wrong && count
20 | Then the count should be 0
21 | And the message
22 | 'something: unknown command for mode library'
23 | should be displayed
24 |
25 | Scenario: Fail second of two chained commands
26 | When I run count && something wrong
27 | Then the count should be 1
28 | And the message
29 | 'something: unknown command for mode library'
30 | should be displayed
31 |
32 | Scenario: Fail second of three chained commands
33 | When I run count && something wrong && count
34 | Then the count should be 1
35 | And the message
36 | 'something: unknown command for mode library'
37 | should be displayed
38 |
39 | Scenario: Run an alias of chained commands
40 | When I run alias double-count count \&\& count
41 | And I run double-count
42 | # Run twice as running once also works if the alias is only aliased to the first
43 | # command and the second command is executed right after the alias command
44 | And I run double-count
45 | Then the count should be 4
46 |
47 | Scenario: Run a chain of aliases where each alias consists of a chain of commands
48 | When I run alias double-count count \&\& count
49 | When I run alias triple-count count \&\& count \&\&count
50 | And I run double-count && double-count && triple-count
51 | Then the count should be 7
52 |
--------------------------------------------------------------------------------
/tests/end2end/features/command/conftest.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest
4 |
5 | from vimiv.commands import aliases
6 |
7 |
8 | @pytest.fixture(autouse=True)
9 | def cleanup_aliases(cleanup_helper):
10 | """Fixture to delete any aliases that were created in this feature."""
11 | yield from cleanup_helper(aliases._aliases)
12 |
--------------------------------------------------------------------------------
/tests/end2end/features/command/expand_wildcards.feature:
--------------------------------------------------------------------------------
1 | Feature: Expand wildcards when running commands
2 |
3 | Scenario: Expand % to the current path in the library
4 | Given I open a directory with 1 paths
5 | When I run !cp -r % %_bak
6 | Then the directory child_01_bak should exist
7 |
8 | Scenario: Expand % to current image in image mode
9 | Given I open any image
10 | When I run !cp % %.bak
11 | Then the file image.jpg.bak should exist
12 |
13 | Scenario: Do not expand % when escaped
14 | Given I open a directory with 1 paths
15 | When I run !cp -r \% %_bak
16 | Then the directory child_01_bak should not exist
17 | And a message should be displayed
18 |
19 | Scenario: Expand * to the list of paths in the library
20 | Given I open a directory with 2 paths
21 | When I run !rmdir *
22 | Then the directory child_01 should not exist
23 | And the directory child_02 should not exist
24 |
25 | Scenario: Expand * to the list of images in image mode
26 | Given I open 2 images
27 | When I run !rm *
28 | Then the file image_01.jpg should not exist
29 | And the file image_02.jpg should not exist
30 |
31 | Scenario: Do not expand * when escaped
32 | Given I open a directory with 2 paths
33 | When I run !rmdir \*
34 | Then the directory child_01 should exist
35 | And the directory child_02 should exist
36 | And a message should be displayed
37 |
38 | Scenario: Expand tilde to home directory
39 | Given I open a directory with 1 paths
40 | When I run !cp -r % ~/mypath.jpg
41 | Then the home directory should contain mypath.jpg
42 |
43 | Scenario: Do not expand tilde when escaped
44 | Given I open a directory with 1 paths
45 | When I run !cp -r % \~
46 | Then the directory ~ should exist
47 |
48 | Scenario: Expand %f to the list of paths in the library
49 | Given I open a directory with 2 paths
50 | When I run !rmdir *
51 | Then the directory child_01 should not exist
52 | And the directory child_02 should not exist
53 |
--------------------------------------------------------------------------------
/tests/end2end/features/command/external.feature:
--------------------------------------------------------------------------------
1 | Feature: Running external commands.
2 |
3 | Background:
4 | Given I start vimiv
5 |
6 | Scenario: Run an external command.
7 | When I run !touch file
8 | Then the file file should exist
9 | And no message should be displayed
10 |
11 | Scenario: Fail an external command.
12 | When I run !not-a-shell-command
13 | Then the message
14 | 'Error running 'not-a-shell-command': command not found or not executable'
15 | should be displayed
16 |
17 | @flaky
18 | Scenario: Pipe directory to vimiv.
19 | When I create the directory 'new_directory'
20 | And I run !ls |
21 | Then the working directory should be new_directory
22 |
23 | Scenario: Fail piping to vimiv.
24 | When I run !ls |
25 | Then the message
26 | 'ls: No paths from pipe'
27 | should be displayed
28 |
29 | Scenario: Use spawn with sub-shell
30 | When I run spawn echo anything > test.txt
31 | Then the file test.txt should exist
32 |
--------------------------------------------------------------------------------
/tests/end2end/features/command/fail_run_command.feature:
--------------------------------------------------------------------------------
1 | Feature: Do not crash and display error message on failed commands.
2 |
3 | Background:
4 | Given I start vimiv
5 |
6 | Scenario: Crash when running unknown command.
7 | When I run not-a-command
8 | Then no crash should happen
9 | And the message
10 | 'not-a-command: unknown command for mode library'
11 | should be displayed
12 |
13 | Scenario: Crash when running command with unknown arguments.
14 | When I run quit --now
15 | Then no crash should happen
16 | And the message
17 | 'quit: Unrecognized arguments: --now'
18 | should be displayed
19 |
20 | Scenario: Crash when running command with missing positional argument.
21 | When I run scroll
22 | Then no crash should happen
23 | And the message
24 | 'scroll: The following arguments are required: direction'
25 | should be displayed
26 |
27 | Scenario: Crash on plain number command
28 | When I run 3
29 | Then no crash should happen
30 |
31 | Scenario: Crash on unclosed quote
32 | When I run '
33 | Then no crash should happen
34 |
35 | Scenario: Crash on empty command
36 | When I run
37 | Then no crash should happen
38 |
--------------------------------------------------------------------------------
/tests/end2end/features/command/misccommands.feature:
--------------------------------------------------------------------------------
1 | Feature: Run miscellaneous commands.
2 |
3 | Background:
4 | Given I start vimiv
5 |
6 | Scenario: Log an info message
7 | When I run log info my message
8 | Then the message
9 | 'my message'
10 | should be displayed
11 |
12 | Scenario: Log a warning message
13 | When I run log warning oh oh
14 | Then the message
15 | 'oh oh'
16 | should be displayed
17 |
18 | Scenario: Log an error message
19 | When I run log error this is bad
20 | Then the message
21 | 'this is bad'
22 | should be displayed
23 |
24 | Scenario: Fail logging a message
25 | When I run log basement spiders
26 | Then the message
27 | 'log: Unknown log level 'basement''
28 | should be displayed
29 |
30 | Scenario: Sleep for some time
31 | Given I start a timer
32 | When I run sleep 0.01
33 | Then at least 0.01 seconds should have elapsed
34 |
--------------------------------------------------------------------------------
/tests/end2end/features/command/repeat.feature:
--------------------------------------------------------------------------------
1 | Feature: Repeat the last command.
2 |
3 | Background:
4 | Given I start vimiv
5 |
6 | Scenario: Repeat command
7 | When I run count
8 | And I run repeat-command
9 | Then the count should be 2
10 |
11 | Scenario: Repeat command with given count.
12 | When I run count
13 | And I run 2repeat-command
14 | Then the count should be 3
15 |
16 | Scenario: Repeat command with stored count.
17 | When I run 2count
18 | And I run repeat-command
19 | Then the count should be 4
20 |
21 | Scenario: Display error message when running repeat-command before running anything.
22 | When I run repeat-command
23 | Then no crash should happen
24 | And the message
25 | 'repeat-command: No command to repeat'
26 | should be displayed
27 |
28 | Scenario: Display error message when running repeat-command in other mode.
29 | When I run count
30 | And I enter image mode
31 | When I run repeat-command
32 | Then no crash should happen
33 | And the message
34 | 'repeat-command: No command to repeat'
35 | should be displayed
36 |
--------------------------------------------------------------------------------
/tests/end2end/features/command/test_aliases_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 | from vimiv import api
6 | from vimiv.commands import aliases
7 |
8 |
9 | bdd.scenarios("aliases.feature")
10 |
11 |
12 | @bdd.then(bdd.parsers.parse("the alias {name} should not exist"))
13 | def check_alias_non_existent(name):
14 | assert name not in aliases.get(api.modes.GLOBAL)
15 |
--------------------------------------------------------------------------------
/tests/end2end/features/command/test_chaining_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("chaining.feature")
7 |
--------------------------------------------------------------------------------
/tests/end2end/features/command/test_expand_wildcards_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("expand_wildcards.feature")
7 |
--------------------------------------------------------------------------------
/tests/end2end/features/command/test_external_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("external.feature")
7 |
--------------------------------------------------------------------------------
/tests/end2end/features/command/test_fail_run_command_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 | from vimiv import api
6 | from vimiv.commands import runners
7 |
8 |
9 | bdd.scenarios("fail_run_command.feature")
10 |
11 |
12 | @bdd.when("I run")
13 | def run_empty_command():
14 | runners.run("", mode=api.modes.current())
15 |
--------------------------------------------------------------------------------
/tests/end2end/features/command/test_misccommands_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import time
4 |
5 | import pytest_bdd as bdd
6 |
7 |
8 | bdd.scenarios("misccommands.feature")
9 |
10 |
11 | @bdd.given("I start a timer", target_fixture="starttime")
12 | def starttime():
13 | return time.time()
14 |
15 |
16 | @bdd.then(bdd.parsers.parse("at least {duration:f} seconds should have elapsed"))
17 | def check_time_elapsed(starttime, duration):
18 | elapsed = time.time() - starttime
19 | assert elapsed >= duration
20 | starttime = None
21 |
--------------------------------------------------------------------------------
/tests/end2end/features/command/test_repeat_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("repeat.feature")
7 |
--------------------------------------------------------------------------------
/tests/end2end/features/command/test_search_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest
4 | import pytest_bdd as bdd
5 |
6 | from vimiv import api
7 | from vimiv.commands import search
8 |
9 |
10 | bdd.scenarios("search.feature")
11 |
12 |
13 | class SearchResults:
14 | """Helper class to store search results."""
15 |
16 | def __init__(self):
17 | self.results = []
18 | search.search.new_search.connect(self._on_search)
19 | search.search.cleared.connect(self._on_search_cleared)
20 |
21 | def _on_search(self, _index, results, _mode, _incsearch):
22 | self.results = results
23 |
24 | def _on_search_cleared(self):
25 | self.results.clear()
26 |
27 | def __len__(self):
28 | return len(self.results)
29 |
30 |
31 | @pytest.fixture(autouse=True)
32 | def search_results():
33 | """Fixture to retrieve a clean helper class to store search results."""
34 | return SearchResults()
35 |
36 |
37 | @bdd.when(bdd.parsers.parse("I search for {text}"))
38 | def run_search(text):
39 | search.search(text, api.modes.current())
40 |
41 |
42 | @bdd.when(bdd.parsers.parse("I search in reverse for {text}"))
43 | def run_search_reverse(text):
44 | search.search(text, api.modes.current(), reverse=True)
45 |
46 |
47 | @bdd.then(bdd.parsers.parse("There should be {n:d} search matches"))
48 | def check_search_matches(search_results, n):
49 | assert len(search_results) == n
50 |
--------------------------------------------------------------------------------
/tests/end2end/features/config/configcommands.feature:
--------------------------------------------------------------------------------
1 | Feature: Miscellaneous commands related to the config
2 |
3 | Scenario: Toggle boolean setting
4 | Given I start vimiv
5 | When I run set completion.fuzzy!
6 | Then the boolean setting 'completion.fuzzy' should be 'true'
7 |
8 | Scenario: Toggle boolean setting twice
9 | Given I start vimiv
10 | When I run set completion.fuzzy!
11 | And I run set completion.fuzzy!
12 | Then the boolean setting 'completion.fuzzy' should be 'false'
13 |
14 | Scenario: Reset setting to default value
15 | Given I start vimiv
16 | When I run set completion.fuzzy false
17 | And I run set completion.fuzzy
18 | Then the boolean setting 'completion.fuzzy' should be 'false'
19 |
20 | Scenario: Do not crash on unknown setting
21 | Given I start vimiv
22 | When I run set anything.unknown false
23 | Then no crash should happen
24 | And the message
25 | 'set: unknown setting 'anything.unknown''
26 | should be displayed
27 |
28 | Scenario: Do not crash on unsupported setting operation
29 | Given I start vimiv
30 | When I run set completion.fuzzy +10
31 | Then no crash should happen
32 | And the message
33 | 'set: 'completion.fuzzy' does not support adding'
34 | should be displayed
35 |
36 | Scenario: Do not crash on wrong setting value
37 | Given I start vimiv
38 | When I run set library.width forty
39 | Then no crash should happen
40 | And the message
41 | 'set: Cannot convert 'forty' to Float'
42 | should be displayed
43 |
--------------------------------------------------------------------------------
/tests/end2end/features/config/test_configcommands_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("configcommands.feature")
7 |
--------------------------------------------------------------------------------
/tests/end2end/features/edit/conftest.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest
4 | import pytest_bdd as bdd
5 |
6 | from vimiv.parser import geometry
7 | from vimiv.imutils import _ImageFileHandler
8 |
9 |
10 | @pytest.fixture()
11 | def file_handler():
12 | """Fixture to retrieve the current instance of the edit handler."""
13 | return _ImageFileHandler.instance
14 |
15 |
16 | @pytest.fixture()
17 | def edit_handler(file_handler):
18 | """Fixture to retrieve the current instance of the edit handler."""
19 | return file_handler._edit_handler
20 |
21 |
22 | @bdd.then(bdd.parsers.parse("the image size should be {size}"))
23 | def ensure_size(size, image):
24 | expected = geometry(size)
25 | image_rect = image.sceneRect()
26 | assert expected.width() == pytest.approx(image_rect.width(), abs=1)
27 | assert expected.height() == pytest.approx(image_rect.height(), abs=1)
28 |
29 |
30 | @bdd.then(bdd.parsers.parse("the image size should not be {size}"))
31 | def ensure_size_not(size, image):
32 | expected = geometry(size)
33 | image_rect = image.sceneRect()
34 | width_neq = expected.width() != image_rect.width()
35 | height_neq = expected.height() != image_rect.height()
36 | assert width_neq or height_neq
37 |
38 |
39 | @bdd.then("the image should not be edited")
40 | def ensure_not_edited(edit_handler):
41 | assert not edit_handler.changed
42 |
--------------------------------------------------------------------------------
/tests/end2end/features/edit/crop.feature:
--------------------------------------------------------------------------------
1 | Feature: Crop an image.
2 |
3 | Background:
4 | Given I open any image of size 300x200
5 |
6 | Scenario: Enter crop widget
7 | When I run crop
8 | Then there should be 1 crop widget
9 | And the center status should include crop:
10 |
11 | Scenario: Enter crop widget with fixed aspectratio
12 | When I run crop --aspectratio=1:1
13 | Then there should be 1 crop widget
14 | And the center status should include crop:
15 |
16 | Scenario: Leave crop widget without changes
17 | When I run crop
18 | And I press '' in the crop widget
19 | Then there should be 0 crop widgets
20 | And the image size should be 300x200
21 |
22 | Scenario: Leave crop widget accepting changes
23 | When I run crop
24 | And I press '' in the crop widget
25 | Then there should be 0 crop widgets
26 | And the image size should be 150x100
27 |
28 | Scenario Outline: Drag crop widget with the mouse
29 | When I run crop
30 | And I drag the crop widget by +
31 | Then the crop rectangle should be
32 |
33 | Examples:
34 | | dx | dy | geometry |
35 | | 0 | 0 | 150x100+75+50 |
36 | # small dx dy
37 | | 30 | -20 | 150x100+105+30 |
38 | # dx only as far as the image allows
39 | | 125 | 0 | 150x100+150+50 |
40 | # dy only as far as the image allows
41 | | 10 | -100 | 150x100+85+0 |
42 | # Ignored as dx/dy are outside of the image
43 | | 1000 | 1000 | 150x100+75+50 |
44 |
45 | Scenario: Crop does not automatically consider a gif edited
46 | Given I open an image and an animated gif
47 | When I run next
48 | Then the image should not be edited
49 |
--------------------------------------------------------------------------------
/tests/end2end/features/edit/manipulate_segfault.feature:
--------------------------------------------------------------------------------
1 | Feature: Segfault when manipulating an image.
2 |
3 | Scenario: Segfault when applying manipulations to images larger than screen-size
4 | Given I open any image of size 1200x900
5 | When I enter manipulate mode
6 | And I apply any manipulation
7 | And I run accept
8 | Then no crash should happen
9 |
--------------------------------------------------------------------------------
/tests/end2end/features/edit/straighten.feature:
--------------------------------------------------------------------------------
1 | Feature: Straighten an image.
2 |
3 | Background:
4 | Given I open any image of size 300x200
5 |
6 | Scenario: Enter straighten widget
7 | When I run straighten
8 | Then there should be 1 straighten widget
9 | And the center status should include angle: +0.0°
10 |
11 | Scenario: Leave straighten widget discarding changes
12 | When I run straighten
13 | And I straighten by 1 degree
14 | And I press '' in the straighten widget
15 | Then there should be 0 straighten widgets
16 | And the image size should be 300x200
17 |
18 | Scenario: Straighten repeatedly
19 | When I run straighten
20 | And I straighten by 1 degree
21 | And I straighten by 1 degree
22 | Then the straighten angle should be 2.0
23 |
24 | Scenario: Leave straighten widget accepting changes
25 | When I run straighten
26 | And I straighten by 1 degree
27 | And I press '' in the straighten widget
28 | Then there should be 0 straighten widgets
29 | And the image size should not be 300x200
30 |
31 | Scenario: Straighten image using keybindings
32 | When I run straighten
33 | And I press 'l' in the straighten widget
34 | Then the straighten angle should be 0.2
35 |
36 | Scenario: Straighten already transformed image
37 | When I run rotate
38 | And I run straighten
39 | And I straighten by 1 degree
40 | And I straighten by -1 degree
41 | Then the image size should be 200x300
42 |
--------------------------------------------------------------------------------
/tests/end2end/features/edit/test_manipulate_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest
4 | import pytest_bdd as bdd
5 |
6 | import vimiv.imutils.immanipulate
7 |
8 | bdd.scenarios("manipulate.feature", "manipulate_segfault.feature")
9 |
10 |
11 | @pytest.fixture()
12 | def manipulator():
13 | return vimiv.imutils.immanipulate.Manipulator.instance
14 |
15 |
16 | @pytest.fixture()
17 | def manipulation(manipulator):
18 | return manipulator._current_manipulation
19 |
20 |
21 | @bdd.when("I apply any manipulation")
22 | def apply_any_manipulation(manipulator, qtbot):
23 | with qtbot.waitSignal(manipulator.updated) as _:
24 | manipulator.goto(10)
25 |
26 |
27 | @bdd.then(bdd.parsers.parse("The current value should be {value:d}"))
28 | def check_current_manipulation_value(manipulation, value):
29 | assert manipulation.value == value # Actual value
30 |
31 |
32 | @bdd.then(bdd.parsers.parse("The current manipulation should be {name}"))
33 | def check_current_manipulation_name(manipulation, name):
34 | assert manipulation.name == name
35 |
36 |
37 | @bdd.then(bdd.parsers.parse("There should be {n_changes:d} stored changes"))
38 | def check_stored_changes(manipulator, n_changes):
39 | assert len(manipulator._changes) == n_changes
40 |
--------------------------------------------------------------------------------
/tests/end2end/features/edit/test_straighten_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest
4 | import pytest_bdd as bdd
5 |
6 | import vimiv.gui.straightenwidget
7 |
8 |
9 | bdd.scenarios("straighten.feature")
10 |
11 |
12 | def find_straighten_widgets(image):
13 | return image.findChildren(vimiv.gui.straightenwidget.StraightenWidget)
14 |
15 |
16 | @pytest.fixture()
17 | def straighten(image):
18 | """Fixture to retrieve the current instance of the straighten widget."""
19 | widgets = find_straighten_widgets(image)
20 | assert len(widgets) == 1, "Wrong number of straighten wigets found"
21 | return widgets[0]
22 |
23 |
24 | @bdd.when(bdd.parsers.parse("I straighten by {angle:g} degrees"))
25 | @bdd.when(bdd.parsers.parse("I straighten by {angle:g} degree"))
26 | def straighten_by(qtbot, straighten, angle):
27 | def check():
28 | assert (straighten.transform.angle) % 90 == pytest.approx(expected_angle % 90)
29 |
30 | expected_angle = straighten.angle + angle
31 |
32 | straighten.rotate(angle=angle)
33 | qtbot.waitUntil(check)
34 |
35 |
36 | @bdd.when(bdd.parsers.parse("I press '{keys}' in the straighten widget"))
37 | def press_key_straighten(keypress, straighten, keys):
38 | keypress(straighten, keys)
39 |
40 |
41 | @bdd.then(bdd.parsers.parse("there should be {number:d} straighten widgets"))
42 | @bdd.then(bdd.parsers.parse("there should be {number:d} straighten widget"))
43 | def check_number_of_straighten_widgets(qtbot, image, number):
44 | def check():
45 | assert len(find_straighten_widgets(image)) == number
46 |
47 | qtbot.waitUntil(check)
48 |
49 |
50 | @bdd.then(bdd.parsers.parse("the straighten angle should be {angle:.f}"))
51 | def check_straighten_angle(qtbot, straighten, angle):
52 | def check():
53 | assert straighten.angle == pytest.approx(angle)
54 |
55 | qtbot.waitUntil(check)
56 |
--------------------------------------------------------------------------------
/tests/end2end/features/edit/test_transform_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("transform.feature")
7 |
8 |
9 | @bdd.then(bdd.parsers.parse("the orientation should be {orientation}"))
10 | def ensure_orientation(image, orientation):
11 | orientation = orientation.lower()
12 | scenerect = image.scene().sceneRect()
13 | if orientation == "landscape":
14 | assert scenerect.width() > scenerect.height()
15 | elif orientation == "portrait":
16 | assert scenerect.height() > scenerect.width()
17 | else:
18 | raise ValueError(f"Unkown orientation {orientation}")
19 |
--------------------------------------------------------------------------------
/tests/end2end/features/edit/transform.feature:
--------------------------------------------------------------------------------
1 | Feature: Transform an image.
2 |
3 | Scenario: Crash when running transform without image
4 | Given I start vimiv
5 | When I enter image mode
6 | And I run rotate
7 | Then no crash should happen
8 |
9 | Scenario: Rotate landscape image
10 | Given I open any image of size 300x200
11 | When I run rotate
12 | Then the orientation should be portrait
13 |
14 | Scenario: Rotate landscape image twice
15 | Given I open any image of size 300x200
16 | When I run 2rotate
17 | Then the orientation should be landscape
18 |
19 | Scenario: Rotate portrait image three times counter-clockwise
20 | Given I open any image of size 200x300
21 | When I run 3rotate --counter-clockwise
22 | Then the orientation should be landscape
23 |
24 | Scenario: Rescale image
25 | Given I open any image of size 300x200
26 | When I run rescale 2
27 | Then the image size should be 600x400
28 |
29 | Scenario: Rescale image changing the aspect ratio
30 | Given I open any image of size 300x200
31 | When I run rescale 2 1
32 | Then the image size should be 600x200
33 |
34 | Scenario: Resize image
35 | Given I open any image of size 300x200
36 | When I run resize 150
37 | Then the image size should be 150x100
38 |
39 | Scenario: Resize image changing the aspect ratio
40 | Given I open any image of size 300x200
41 | When I run resize 150 200
42 | Then the image size should be 150x200
43 |
44 | Scenario: Undo transformations
45 | Given I open any image of size 300x200
46 | When I run resize 150
47 | And I run undo-transformations
48 | Then the image size should be 300x200
49 |
50 | Scenario: Do not allow transforming when read_only is active
51 | Given I open any image of size 300x200
52 | When I run set read_only true
53 | And I run rotate
54 | Then the message
55 | 'rotate: Disabled due to read-only being active'
56 | should be displayed
57 | And the orientation should be landscape
58 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/conftest.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import os
4 |
5 | import pytest
6 | import pytest_bdd as bdd
7 |
8 | from vimiv import imutils, utils
9 |
10 | try:
11 | import piexif
12 | except ImportError:
13 | piexif = None
14 |
15 |
16 | @pytest.fixture()
17 | def exif_content():
18 | assert piexif is not None, "piexif required create exif information."
19 | return {
20 | "0th": {
21 | piexif.ImageIFD.Make: b"vimiv-testsuite",
22 | piexif.ImageIFD.Model: b"image-viewer",
23 | piexif.ImageIFD.Copyright: b"vimiv-AUTHORS-2020",
24 | },
25 | "Exif": {piexif.ExifIFD.ExposureTime: (1, 200)},
26 | "GPS": {piexif.GPSIFD.GPSAltitude: (1234, 1)},
27 | }
28 |
29 |
30 | @pytest.fixture()
31 | def handler():
32 | return imutils._ImageFileHandler.instance
33 |
34 |
35 | @bdd.when("I add exif information")
36 | def add_exif_information_bdd(add_exif_information, handler, exif_content):
37 | assert piexif is not None, "piexif required to add exif information"
38 | # Wait for thumbnail creation so we don't interfere with the current reading by
39 | # adding more bytes
40 | utils.Pool.wait(5000)
41 | add_exif_information(handler._path, exif_content)
42 |
43 |
44 | @bdd.then(bdd.parsers.parse("the image number {number:d} should be {basename}"))
45 | def check_image_name_at_position(number, basename):
46 | image = imutils.pathlist()[number - 1]
47 | assert os.path.basename(image) == basename
48 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/gif.feature:
--------------------------------------------------------------------------------
1 | @imageformats
2 | Feature: Open and play animated gifs
3 |
4 | Background:
5 | Given I open an animated gif
6 |
7 | Scenario: Autoplay animated gif
8 | Then the animation should be playing
9 |
10 | Scenario: Pause animated gif
11 | When I run play-or-pause
12 | Then the animation should be paused
13 |
14 | Scenario: Do not rotate animated gif
15 | When I run rotate
16 | Then the message
17 | 'rotate: File format does not support transform'
18 | should be displayed
19 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/imagefit.feature:
--------------------------------------------------------------------------------
1 | Feature: Fitting the image displayed.
2 |
3 | Scenario: Fit landscape image to width.
4 | Given I open any image of size 2000x500
5 | When I run scale --level=fit
6 | Then the pixmap width should fit
7 |
8 | Scenario: Fit portrait image to height.
9 | Given I open any image of size 500x2000
10 | When I run scale --level=fit
11 | Then the pixmap height should fit
12 |
13 | Scenario: Fit landscape image to height.
14 | Given I open any image of size 2000x500
15 | When I run scale --level=fit-height
16 | Then the pixmap height should fit
17 | And the pixmap width should not fit
18 |
19 | Scenario: Fit portrait image to width.
20 | Given I open any image of size 500x2000
21 | When I run scale --level=fit-width
22 | Then the pixmap width should fit
23 | And the pixmap height should not fit
24 |
25 | Scenario: Fit image to float scale.
26 | Given I open any image of size 200x200
27 | When I run scale --level=2
28 | Then the pixmap width should be 400
29 | And the pixmap height should be 400
30 |
31 | Scenario: Do not overzoom small image.
32 | Given I open any image of size 200x200
33 | When I run scale --level=overzoom
34 | Then the pixmap width should be 200
35 | And the pixmap height should be 200
36 |
37 | Scenario: Scale down large image on overzoom.
38 | Given I open any image of size 2000x500
39 | When I run scale --level=overzoom
40 | Then the pixmap width should fit
41 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/imagenavigate.feature:
--------------------------------------------------------------------------------
1 | Feature: Navigating through the displayed images
2 |
3 | Scenario: Move to next image
4 | Given I open 5 images
5 | When I run next
6 | Then the image should have the index 2
7 |
8 | Scenario: Move to next image and back
9 | Given I open 5 images
10 | When I run next
11 | And I run prev
12 | Then the image should have the index 1
13 |
14 | Scenario: Move to last image by wrapping :prev
15 | Given I open 5 images
16 | When I run prev
17 | Then the image should have the index 5
18 |
19 | Scenario: Wrap back and forth
20 | Given I open 5 images
21 | When I run prev
22 | And I run next
23 | Then the image should have the index 1
24 |
25 | Scenario: Move to last image with goto
26 | Given I open 5 images
27 | When I run goto -1
28 | Then the image should have the index 5
29 |
30 | Scenario: Move to last image and back to first with goto
31 | Given I open 5 images
32 | When I run goto -1
33 | And I run goto 1
34 | Then the image should have the index 1
35 |
36 | Scenario: Move to specific image using goto with count
37 | Given I open 5 images
38 | When I run 3goto
39 | Then the image should have the index 3
40 |
41 | Scenario: Crash on goto without images
42 | Given I start vimiv
43 | When I enter image mode
44 | When I run goto 3
45 | Then no crash should happen
46 | And the message
47 | 'goto: No image in list'
48 | should be displayed
49 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/imageopen.feature:
--------------------------------------------------------------------------------
1 | Feature: Open different images and image formats
2 |
3 | Scenario: Error on invalid image formats
4 | Given I start vimiv
5 | When I open broken images
6 | Then no crash should happen
7 |
8 | # This does the magic to open all images in the directory
9 | # Therefore only the index, not the total number of images changes
10 | Scenario: Open single image using the open command
11 | Given I open 5 images
12 | When I run open image_03.jpg
13 | Then the filelist should contain 5 images
14 | And the image should have the index 3
15 |
16 | Scenario: Open multiple images using the open command
17 | Given I open 5 images
18 | When I run open image_01.jpg image_02.jpg
19 | Then the filelist should contain 2 images
20 |
21 | Scenario: Open image using unix-style asterisk pattern expansion
22 | Given I open 11 images
23 | When I run open image_1*.jpg
24 | Then the filelist should contain 2 images
25 |
26 | Scenario: Open image using unix-style question mark pattern expansion
27 | Given I open 11 images
28 | When I run open image_1?.jpg
29 | Then the filelist should contain 2 images
30 |
31 | Scenario: Open image using unix-style group pattern expansion
32 | Given I open 12 images
33 | When I run open image_1[01].jpg
34 | Then the filelist should contain 2 images
35 |
36 | Scenario: Open path that does not exist
37 | Given I open any image
38 | When I run open not/a/path
39 | Then no crash should happen
40 | And the message
41 | 'open: No paths matching 'not/a/path''
42 | should be displayed
43 |
44 | Scenario: Open invalid path
45 | Given I open any image
46 | When I create the file 'not_an_image'
47 | And I run open not_an_image
48 | Then no crash should happen
49 | And the message
50 | 'open: No valid paths'
51 | should be displayed
52 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/imageorder.feature:
--------------------------------------------------------------------------------
1 | Feature: Ordering the image filelist.
2 |
3 | Scenario: Re-order current filelist
4 | Given I open 12 images without leading zeros in their name
5 | When I run set sort.image_order natural
6 | Then the image should have the index 1
7 | And the image number 1 should be image_1.jpg
8 | And the image number 2 should be image_2.jpg
9 | And the image number 11 should be image_11.jpg
10 |
11 | Scenario: Set none sorting after another sort
12 | Given I open 12 images without leading zeros in their name
13 | When I run set sort.image_order natural
14 | Then the image should have the index 1
15 | And the image number 1 should be image_1.jpg
16 | And the image number 2 should be image_2.jpg
17 | And the image number 11 should be image_11.jpg
18 | When I run set sort.image_order none
19 | Then the image should have the index 1
20 | And the image number 1 should be image_1.jpg
21 | And the image number 2 should be image_2.jpg
22 | And the image number 11 should be image_11.jpg
23 |
24 | Scenario: Reverse none sorting
25 | Given I open 5 images
26 | When I run set sort.image_order none
27 | Then the image should have the index 1
28 | When I run set sort.reverse
29 | Then the image should have the index 1
30 |
31 | Scenario: Reverse current filelist
32 | Given I open 5 images
33 | When I run set sort.reverse!
34 | # We revert the sorting, but keep the selection
35 | Then the image should have the index 5
36 |
37 | Scenario: Shuffle current filelist
38 | Given I open 15 images
39 | When I run set sort.shuffle!
40 | # We revert the sorting, but keep the selection
41 | Then the filelist should not be ordered
42 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/imagescroll.feature:
--------------------------------------------------------------------------------
1 | Feature: Scroll the current image
2 |
3 | Background:
4 | Given I open any image
5 |
6 | Scenario: Scroll to left edge
7 | When I run 10zoom in
8 | And I run scroll-edge left
9 | Then the image left-edge should be 0
10 |
11 | Scenario: Scroll to right edge
12 | When I run 10zoom in
13 | And I run scroll-edge right
14 | Then the image right-edge should be 300
15 |
16 | Scenario: Keep scroll position when size changes
17 | When I run 10zoom in
18 | And I run scroll-edge left
19 | And I resize the image
20 | Then the image left-edge should be 0
21 |
22 | Scenario Outline: Scroll image
23 | When I run 10zoom in
24 | And I run scroll
25 | Then the image should not be
26 |
27 | Examples:
28 | | direction | position | value |
29 | | right | left-edge | 0 |
30 | | down | top-edge | 0 |
31 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/imagetitle.feature:
--------------------------------------------------------------------------------
1 | Feature: Show current image name in window title
2 |
3 | Scenario: Show name of opened image in title
4 | Given I open any image
5 | Then the image name should be in the window title
6 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/imagezoom.feature:
--------------------------------------------------------------------------------
1 | Feature: Zooming the image displayed.
2 |
3 | Scenario: Zooming in.
4 | Given I open any image of size 200x200
5 | When I run zoom in
6 | Then the zoom level should be 1.25
7 |
8 | Scenario: Zooming out.
9 | Given I open any image of size 200x200
10 | When I run zoom out
11 | Then the zoom level should be 0.8
12 |
13 | Scenario: Zooming in and out.
14 | Given I open any image of size 200x200
15 | When I run zoom in
16 | And I run zoom out
17 | Then the zoom level should be 1.0
18 |
19 | Scenario: Keep zoom level when reloading image.
20 | Given I open any image of size 200x200
21 | When I run zoom in
22 | And I run reload
23 | Then the zoom level should not be 1.0
24 |
25 | Scenario: Keep zoom level with --keep-zoom flag.
26 | Given I open any image of size 200x200
27 | When I run zoom in
28 | And I run next --keep-zoom
29 | Then the zoom level should be 1.25
30 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/multidirectory.feature:
--------------------------------------------------------------------------------
1 | Feature: Support for images from multiple directories in image mode
2 |
3 | # Reasonably complicated directory structure:
4 | # .
5 | # ├── dir1
6 | # │ ├── image_dir1_1.png
7 | # │ ├── image_dir1_2.png
8 | # │ └── image_dir1_3.png
9 | # ├── dir2
10 | # │ ├── also_image_dir2_1.png
11 | # │ └── also_image_dir2_2.png
12 | # └── dir3
13 | # ├── more_image_dir3_1.png
14 | # ├── more_image_dir3_2.png
15 | # ├── more_image_dir3_3.png
16 | # └── more_image_dir3_4.png
17 |
18 | Background:
19 | Given I open images from multiple directories
20 |
21 | Scenario: Open all images in single filelist
22 | Then the filelist should contain 9 images
23 | And the image should have the index 1
24 | # We sort all images according to the basename
25 | And the image number 1 should be also_image_dir2_1.png
26 |
27 | Scenario: Reverse sorting of images from multiple directories
28 | When I run set sort.reverse!
29 | Then the image should have the index 9
30 | And the image number 1 should be more_image_dir3_4.png
31 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/slideshow.feature:
--------------------------------------------------------------------------------
1 | Feature: Play a slideshow.
2 |
3 | Scenario: Start playing slideshow
4 | Given I open any image
5 | When I run slideshow
6 | Then the center status should include slideshow
7 | And the slideshow should be playing
8 |
9 | Scenario: Start and stop slideshow
10 | Given I open any image
11 | When I run slideshow
12 | And I run slideshow
13 | Then the slideshow should not be playing
14 |
15 | Scenario: Set slideshow delay via setting
16 | Given I open any image
17 | When I run set slideshow.delay 4
18 | Then the slideshow delay should be 4.0
19 |
20 | Scenario: Set slideshow delay via count
21 | Given I open any image
22 | When I run 5slideshow
23 | Then the slideshow delay should be 5.0
24 |
25 | Scenario: Slideshow updates the displayed image
26 | Given I open 5 images
27 | And I forcefully set the slideshow delay to 10ms
28 | When I run slideshow
29 | And I let the slideshow run 2 times
30 | Then the image should have the index 3
31 | And the left status should include 03
32 |
33 | Scenario: Start slideshow upon startup
34 | Given I open 5 images with --command slideshow
35 | Then the slideshow should be playing
36 |
37 | Scenario: Leave slideshow when image is unfocused
38 | Given I open any image
39 | When I run slideshow
40 | And I unfocus the image
41 | Then the slideshow should not be playing
42 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/svg.feature:
--------------------------------------------------------------------------------
1 | @imageformats
2 | Feature: Open vector graphics
3 |
4 | Background:
5 | Given I open a vector graphic
6 |
7 | Scenario: Zoom vector graphic
8 | When I run zoom in
9 | Then no crash should happen
10 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/test_gif_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest
4 |
5 | import pytest_bdd as bdd
6 |
7 |
8 | bdd.scenarios("gif.feature")
9 |
10 |
11 | @pytest.fixture()
12 | def movie(image):
13 | widget = image.items()[0].widget()
14 | return widget.movie()
15 |
16 |
17 | @bdd.then("the animation should be playing")
18 | def check_animation_playing(movie):
19 | assert movie.state() == movie.MovieState.Running
20 |
21 |
22 | @bdd.then("the animation should be paused")
23 | def check_animation_paused(movie):
24 | assert movie.state() == movie.MovieState.Paused
25 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/test_imagedelete_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import os
4 |
5 | import pytest_bdd as bdd
6 |
7 | from vimiv.imutils import filelist
8 |
9 |
10 | bdd.scenarios("imagedelete.feature")
11 |
12 |
13 | @bdd.when("I remove move permissions")
14 | def remove_move_permissions(mocker):
15 | mocker.patch("shutil.move", side_effect=PermissionError)
16 |
17 |
18 | @bdd.then(bdd.parsers.parse("{basename} should not be in the filelist"))
19 | def check_image_not_in_filelist(basename):
20 | abspath = os.path.abspath(basename)
21 | assert abspath not in filelist._paths
22 |
23 |
24 | @bdd.then("the image widget should be empty")
25 | def check_image_widget_empty(image):
26 | assert not image.items()
27 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/test_imagefit_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("imagefit.feature")
7 |
8 |
9 | def almost_equal(size, expected):
10 | """Check if size is almost equal to the expected value.
11 |
12 | When scaling images the float value scale times the integer original size
13 | can lead to the size being smaller than the expected value by one.
14 | """
15 | assert size + 1 >= expected
16 | assert size <= expected
17 |
18 |
19 | @bdd.then(bdd.parsers.parse("the pixmap width should be {width}"))
20 | def check_pixmap_width(image, width):
21 | almost_equal(image.scene().sceneRect().width() * image.zoom_level, int(width))
22 |
23 |
24 | @bdd.then(bdd.parsers.parse("the pixmap height should be {height}"))
25 | def check_pixmap_height(image, height):
26 | almost_equal(image.scene().sceneRect().height() * image.zoom_level, int(height))
27 |
28 |
29 | @bdd.then(bdd.parsers.parse("the pixmap width should fit"))
30 | def check_pixmap_width_fit(image):
31 | almost_equal(
32 | image.viewport().width(), image.scene().sceneRect().width() * image.zoom_level
33 | )
34 |
35 |
36 | @bdd.then(bdd.parsers.parse("the pixmap height should fit"))
37 | def check_pixmap_height_fit(image):
38 | almost_equal(
39 | image.viewport().height(), image.scene().sceneRect().height() * image.zoom_level
40 | )
41 |
42 |
43 | @bdd.then(bdd.parsers.parse("the pixmap width should not fit"))
44 | def check_pixmap_width_no_fit(image):
45 | assert (
46 | image.viewport().width() != image.scene().sceneRect().width() * image.zoom_level
47 | )
48 |
49 |
50 | @bdd.then(bdd.parsers.parse("the pixmap height should not fit"))
51 | def check_pixmap_height_no_fit(image):
52 | assert (
53 | image.viewport().height()
54 | != image.scene().sceneRect().height() * image.zoom_level
55 | )
56 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/test_imagenavigate_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("imagenavigate.feature")
7 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/test_imageopen_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 | from vimiv import api
6 | from vimiv.utils import imageheader
7 |
8 |
9 | bdd.scenarios("imageopen.feature")
10 |
11 |
12 | @bdd.when("I open broken images")
13 | def open_broken_images(tmp_path):
14 | _open_file(tmp_path, b"\x89PNG\x0D\x0A\x1A\x0A") # PNG
15 | _open_file(tmp_path, b"\xFF\xD8\xFF\xDB") # JPG
16 | _open_file(tmp_path, b"GIF89a") # GIF
17 | _open_file(tmp_path, b"II\x2A\x00") # TIFF
18 | _open_file(tmp_path, b"BM") # BMP
19 |
20 |
21 | def _open_file(directory, data):
22 | """Open a file containing the bytes from data."""
23 | path = directory / "broken"
24 | path.write_bytes(data)
25 | filename = str(path)
26 | assert imageheader.detect(filename) is not None, "Invalid magic bytes in test setup"
27 | api.open_paths([filename])
28 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/test_imageorder_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 | from vimiv import api, imutils
6 |
7 |
8 | bdd.scenarios("imageorder.feature")
9 |
10 |
11 | @bdd.then("the filelist should not be ordered")
12 | def check_filelist_not_ordered():
13 | paths = imutils.pathlist()
14 | ordered_paths = api.settings.sort.image_order.sort(paths)
15 | assert paths != ordered_paths
16 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/test_imagescroll_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 | from vimiv.qt.gui import QResizeEvent
6 |
7 |
8 | bdd.scenarios("imagescroll.feature")
9 |
10 |
11 | @bdd.when("I resize the image")
12 | def resize_image(image):
13 | image.resizeEvent(QResizeEvent(image.size(), image.size()))
14 |
15 |
16 | @bdd.then(bdd.parsers.parse("the image {position} should be {value:d}"))
17 | def check_image_coordinate(image, position, value):
18 | rect = image.visible_rect
19 | assert position_to_value(position, rect) == value
20 |
21 |
22 | @bdd.then(bdd.parsers.parse("the image {position} should not be {value:d}"))
23 | def check_image_not_coordinate(image, position, value):
24 | rect = image.visible_rect
25 | assert position_to_value(position, rect) != value
26 |
27 |
28 | def position_to_value(position, rect):
29 | position = position.lower()
30 | if position == "left-edge":
31 | return rect.x()
32 | if position == "right-edge":
33 | return rect.x() + rect.width()
34 | if position == "top-edge":
35 | return rect.y()
36 | raise ValueError(f"Unknown position '{position}'")
37 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/test_imagetitle_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 | from vimiv.imutils import filelist
6 |
7 |
8 | bdd.scenarios("imagetitle.feature")
9 |
10 |
11 | @bdd.then("the image name should be in the window title")
12 | def image_name_in_title(mainwindow):
13 | assert filelist.basename() in mainwindow.windowTitle()
14 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/test_imagezoom_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest
4 | import pytest_bdd as bdd
5 |
6 |
7 | bdd.scenarios("imagezoom.feature")
8 |
9 |
10 | @bdd.then(bdd.parsers.parse("the zoom level should be {level:f}"))
11 | def check_zoom_level(image, level):
12 | assert level == pytest.approx(image.zoom_level, 0.01)
13 |
14 |
15 | @bdd.then(bdd.parsers.parse("the zoom level should not be {level:f}"))
16 | def check_zoom_level_not(image, level):
17 | assert level != pytest.approx(image.zoom_level, 0.01)
18 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/test_metadata_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest
4 | import pytest_bdd as bdd
5 |
6 | from vimiv.imutils import metadata
7 |
8 |
9 | bdd.scenarios("metadata.feature")
10 |
11 |
12 | @pytest.fixture
13 | def metadatawidget():
14 | if metadata.has_metadata_support():
15 | from vimiv.gui.metadatawidget import MetadataWidget
16 |
17 | return MetadataWidget.instance
18 | raise ValueError("No metadata support for metadata tests")
19 |
20 |
21 | @bdd.then("the metadata widget should be visible")
22 | def check_metadata_widget_visible(metadatawidget):
23 | assert metadatawidget.isVisible()
24 |
25 |
26 | @bdd.then("the metadata widget should not be visible")
27 | def check_metadata_widget_not_visible(metadatawidget):
28 | assert not metadatawidget.isVisible()
29 |
30 |
31 | @bdd.then(bdd.parsers.parse("the metadata text should contain '{text}'"))
32 | def check_text_in_metadata(metadatawidget, text):
33 | assert text in metadatawidget.text()
34 |
35 |
36 | @bdd.then(bdd.parsers.parse("the metadata text should not contain '{text}'"))
37 | def check_text_not_in_metadata(metadatawidget, text):
38 | assert text not in metadatawidget.text()
39 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/test_multidirectory_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("multidirectory.feature")
7 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/test_slideshow_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest
4 | import pytest_bdd as bdd
5 |
6 | from vimiv import api
7 | from vimiv.imutils import slideshow
8 |
9 |
10 | bdd.scenarios("slideshow.feature")
11 |
12 |
13 | @pytest.fixture
14 | def sshow():
15 | instance = slideshow._timer
16 | yield instance
17 | instance.stop()
18 |
19 |
20 | @bdd.given(bdd.parsers.parse("I forcefully set the slideshow delay to {delay:d}ms"))
21 | def set_slideshow_delay(sshow, delay):
22 | """Set the slideshow delay to a small value to increase test speed."""
23 | sshow.setInterval(delay)
24 |
25 |
26 | @bdd.then("the slideshow should be playing")
27 | def check_slideshow_playing(sshow):
28 | assert sshow.isActive()
29 |
30 |
31 | @bdd.then("the slideshow should not be playing")
32 | def check_slideshow_not_playing(sshow):
33 | assert not sshow.isActive()
34 |
35 |
36 | @bdd.then(bdd.parsers.parse("the slideshow delay should be {delay:f}"))
37 | def check_slideshow_delay(sshow, delay):
38 | # Check setting
39 | assert api.settings.slideshow.delay.value == delay
40 | # Check actual value
41 | assert sshow.interval() == delay * 1000
42 |
43 |
44 | @bdd.when(bdd.parsers.parse("I let the slideshow run {repeat:d} times"))
45 | def wait_slideshow_signal(qtbot, sshow, repeat):
46 | for _ in range(repeat):
47 | # Wait for slideshow delay and give it a small buffer
48 | with qtbot.waitSignal(sshow.timeout, timeout=int(sshow.interval() * 1.2)):
49 | pass
50 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/test_svg_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("svg.feature")
7 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/test_write_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 | try:
6 | import piexif
7 | except ImportError:
8 | piexif = None
9 |
10 |
11 | bdd.scenarios("write.feature")
12 |
13 |
14 | @bdd.when(bdd.parsers.parse("I write the image to {name}"))
15 | def write_image(handler, name):
16 | handler.write_pixmap(
17 | handler._edit_handler.pixmap,
18 | path=name,
19 | original_path=handler._path,
20 | parallel=False,
21 | )
22 |
23 |
24 | @bdd.then(bdd.parsers.parse("the image {name} should contain exif information"))
25 | def check_exif_information(exif_content, name):
26 | exif_dict = piexif.load(name)
27 | for ifd, ifd_dict in exif_content.items():
28 | for key, value in ifd_dict.items():
29 | assert exif_dict[ifd][key] == value
30 |
--------------------------------------------------------------------------------
/tests/end2end/features/image/write.feature:
--------------------------------------------------------------------------------
1 | Feature: Write an image to disk
2 |
3 | Scenario Outline: Write image to new path
4 | Given I open any image
5 | When I write the image to
6 | Then the file should exist
7 |
8 | Examples:
9 | | name |
10 | | new_path.jpg |
11 | | new_path.png |
12 | | new_path.tiff |
13 |
14 | @metadata
15 | Scenario: Write image preserving exif information
16 | Given I open any image
17 | When I add exif information
18 | And I write the image to new_path.jpg
19 | Then the image new_path.jpg should contain exif information
20 |
21 | Scenario: Prompt for writing edited image
22 | Given I open any image
23 | When I run rotate
24 | And I plan to answer the prompt with n
25 | And I run next
26 | Then no crash should happen
27 |
28 | Scenario: Crash when writing image with tilde in path
29 | Given I open any image
30 | When I write the image to ~/test.jpg
31 | Then no crash should happen
32 | And the home directory should contain test.jpg
33 |
--------------------------------------------------------------------------------
/tests/end2end/features/library/library.feature:
--------------------------------------------------------------------------------
1 | Feature: Miscellaneous features connected to the library
2 |
3 | Background:
4 | Given I open a directory with 2 paths
5 |
6 | Scenario: Hide hidden files
7 | When I create the directory '.hidden'
8 | And I reload the library
9 | Then the library should contain 2 paths
10 |
11 | Scenario: Show hidden files
12 | When I create the directory '.hidden'
13 | And I reload the library
14 | And I run set library.show_hidden true
15 | Then the library should contain 3 paths
16 | # .hidden is placed in front of the current selection
17 | And the library row should be 2
18 |
19 | Scenario: Invert directory order in the library
20 | When I run set sort.reverse!
21 | # We change the order to
22 | # - child_02
23 | # - child_01
24 | # but keep the selection on child_01 due to the stored position
25 | Then the library row should be 2
26 |
--------------------------------------------------------------------------------
/tests/end2end/features/library/libraryresize.feature:
--------------------------------------------------------------------------------
1 | Feature: Resize the library.
2 |
3 | Background:
4 | Given I open any directory
5 |
6 | Scenario: Resize the library when the main window changes width.
7 | When I resize the window to 400x600
8 | Then the library width should be 0.3
9 |
10 | Scenario: Increase the library size.
11 | When I run set library.width +0.1
12 | Then the library width should be 0.4
13 |
14 | Scenario: Decrease the library size.
15 | When I run set library.width -0.1
16 | Then the library width should be 0.2
17 |
18 | Scenario: Decrease to the minimum size.
19 | When I run set library.width 0
20 | Then the library width should be 0.05
21 |
22 | Scenario: Increase to maximum size.
23 | When I run set library.width 1
24 | Then the library width should be 0.95
25 |
26 | Scenario: Increase and reset to default size.
27 | When I run set library.width 1
28 | And I run set library.width
29 | Then the library width should be 0.3
30 |
--------------------------------------------------------------------------------
/tests/end2end/features/library/test_library_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("library.feature")
7 |
8 |
9 | @bdd.when("I reload the library")
10 | def reload_library(library):
11 | library._open_directory(".", reload_current=True)
12 |
13 |
14 | @bdd.then(bdd.parsers.parse("the library should contain {n_paths:d} paths"))
15 | def check_library_paths(library, n_paths):
16 | assert len(library.pathlist()) == n_paths
17 |
--------------------------------------------------------------------------------
/tests/end2end/features/library/test_libraryresize_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest
4 | import pytest_bdd as bdd
5 |
6 | from vimiv import api
7 |
8 |
9 | bdd.scenarios("libraryresize.feature")
10 |
11 |
12 | @bdd.then(bdd.parsers.parse("the library width should be {fraction:f}"))
13 | def check_library_size(library, mainwindow, fraction, qtbot):
14 | # Check if setting was updated
15 | assert api.settings.library.width.value == pytest.approx(fraction)
16 | # Check if width fits fraction of main window
17 | real_fraction = library.width() / mainwindow.width()
18 | assert fraction == real_fraction
19 |
--------------------------------------------------------------------------------
/tests/end2end/features/library/test_libraryscroll_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import math
4 |
5 | import pytest_bdd as bdd
6 |
7 | from vimiv.utils import quotedjoin
8 |
9 |
10 | bdd.scenarios("libraryscroll.feature")
11 |
12 |
13 | @bdd.then(bdd.parsers.parse("the library should be {num} page {direction}"))
14 | @bdd.then(bdd.parsers.parse("the library should be {num} pages {direction}"))
15 | def check_library_page(library, num, direction):
16 | nums = {"one": 1, "half a": 0.5, "two": 2}
17 | assert num in nums, f"Invalid num '{num}'. Must be one of {quotedjoin(nums)}"
18 | multiplier = nums[num]
19 | pagesize = _get_pagesize(library)
20 | scrollstep = pagesize - 1
21 | expected_row = math.ceil(scrollstep * multiplier)
22 | row = library.row()
23 | assert row == expected_row
24 |
25 |
26 | def _get_pagesize(library):
27 | """Simple implementation to get the library pagesize.
28 |
29 | Only works if there is no empty space in the viewport.
30 | """
31 | view_height = library.viewport().height()
32 | row_height = library.visualRect(library.model().index(1, 0)).height()
33 | n_rows = math.ceil(view_height / row_height)
34 | return n_rows
35 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/clipboard.feature:
--------------------------------------------------------------------------------
1 | Feature: Interaction with the system clipboard.
2 |
3 | Scenario: Copy basename from library path to clipboard.
4 | Given I open a directory with 1 paths
5 | When I run copy-name
6 | Then the clipboard should contain 'child_01'
7 |
8 | Scenario: Copy basename from library path to primary.
9 | Given I open a directory with 1 paths
10 | When I run copy-name --primary
11 | Then the primary selection should contain 'child_01'
12 |
13 | Scenario: Copy abspath from library path to clipboard.
14 | Given I open a directory with 1 paths
15 | When I run copy-name --abspath
16 | # /tmp from the directory in which tests are run
17 | Then the absolute path of child_01 should be saved in the clipboard
18 |
19 | Scenario: Copy basename from image path to clipboard.
20 | Given I open any image
21 | When I run copy-name
22 | Then the clipboard should contain 'image.jpg'
23 |
24 | Scenario: Copy and paste basename from library
25 | Given I open a directory with 1 paths
26 | When I run copy-name
27 | And I run paste-name
28 | Then the working directory should be child_01
29 |
30 | Scenario: Copy image to clipboard.
31 | Given I open any image
32 | When I run copy-image
33 | Then the clipboard should contain any image
34 |
35 | Scenario: Copy image to primary.
36 | Given I open any image
37 | When I run copy-image --primary
38 | Then the primary selection should contain any image
39 |
40 | Scenario: Copy image to clipboard and scale width.
41 | Given I open any image
42 | When I run copy-image --width=100
43 | Then the clipboard should contain an image with width 100
44 |
45 | Scenario: Copy image to clipboard and scale height.
46 | Given I open any image
47 | When I run copy-image --height=100
48 | Then the clipboard should contain an image with height 100
49 |
50 | Scenario: Copy image to clipboard and scale size.
51 | Given I open any image
52 | When I run copy-image --size=100
53 | Then the clipboard should contain an image with size 100
54 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/conftest.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest
4 | import pytest_bdd as bdd
5 |
6 | from vimiv.qt.gui import QGuiApplication, QClipboard
7 |
8 |
9 | @pytest.fixture()
10 | def clipboard():
11 | return QGuiApplication.clipboard()
12 |
13 |
14 | @bdd.then(bdd.parsers.parse("The clipboard should contain '{text}'"))
15 | def check_clipboard(clipboard, text):
16 | assert text in clipboard.text(mode=QClipboard.Mode.Clipboard)
17 |
18 |
19 | @bdd.then(bdd.parsers.parse("The primary selection should contain '{text}'"))
20 | def check_primary(clipboard, text):
21 | assert text in clipboard.text(mode=QClipboard.Mode.Selection)
22 |
23 |
24 | @bdd.then(bdd.parsers.parse("The clipboard should contain any image"))
25 | def check_clipboard_image(clipboard, image):
26 | assert not clipboard.pixmap(mode=QClipboard.Mode.Clipboard).toImage().isNull()
27 |
28 |
29 | @bdd.then(bdd.parsers.parse("The primary selection should contain any image"))
30 | def check_primary_image(clipboard, image):
31 | assert not clipboard.pixmap(mode=QClipboard.Mode.Selection).toImage().isNull()
32 |
33 |
34 | @bdd.then(bdd.parsers.parse("The clipboard should contain an image with width {width}"))
35 | def check_clipboard_image_width(clipboard, width):
36 | assert clipboard.pixmap(mode=QClipboard.Mode.Clipboard).size().width() == int(width)
37 |
38 |
39 | @bdd.then(
40 | bdd.parsers.parse("The clipboard should contain an image with height {height}")
41 | )
42 | def check_clipboard_image_height(clipboard, height):
43 | assert clipboard.pixmap(mode=QClipboard.Mode.Clipboard).size().height() == int(
44 | height
45 | )
46 |
47 |
48 | @bdd.then(bdd.parsers.parse("The clipboard should contain an image with size {size}"))
49 | def check_clipboard_image_size(clipboard, size):
50 | assert max(
51 | clipboard.pixmap(mode=QClipboard.Mode.Clipboard).size().height(),
52 | clipboard.pixmap(mode=QClipboard.Mode.Clipboard).size().width(),
53 | ) == int(size)
54 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/fullscreen.feature:
--------------------------------------------------------------------------------
1 | Feature: Toggle fullscreen mode.
2 |
3 | Background:
4 | Given I start vimiv
5 |
6 | Scenario: Enter fullscreen mode.
7 | When I run fullscreen
8 | Then the window should be fullscreen
9 |
10 | Scenario: Enter and leave fullscreen mode.
11 | When I run fullscreen
12 | And I run fullscreen
13 | Then the window should not be fullscreen
14 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/keybindings_popup.feature:
--------------------------------------------------------------------------------
1 | Feature: Keybindings command with pop up window with the keybindings for current mode
2 |
3 | Scenario: Display the keybindings pop up for library mode.
4 | Given I start vimiv
5 | When I run keybindings
6 | Then the keybindings pop up should contain 'set library.width'
7 | And the pop up 'vimiv - keybindings' should be displayed
8 |
9 | Scenario: Display the keybindings pop up for image mode.
10 | Given I open 2 images
11 | When I run keybindings
12 | Then the keybindings pop up should contain 'flip'
13 | And the pop up 'vimiv - keybindings' should be displayed
14 |
15 | Scenario: Display the keybindings pop up for thumbnail mode.
16 | Given I open 2 images
17 | When I enter thumbnail mode
18 | And I run keybindings
19 | Then the keybindings pop up should contain 'zoom in'
20 | And the pop up 'vimiv - keybindings' should be displayed
21 |
22 | Scenario: Display the keybindings pop up for manipulate mode.
23 | Given I open 2 images
24 | When I enter manipulate mode
25 | And I run keybindings
26 | Then the keybindings pop up should contain 'accept'
27 | And the pop up 'vimiv - keybindings' should be displayed
28 |
29 | Scenario: Search the keybindins pop up
30 | Given I start vimiv
31 | When I run keybindings
32 | And I press 'comm' in the pop up
33 | Then 'comm' should be highlighted in 'command'
34 | Then the keybindings pop up should describe 'command'
35 |
36 | Scenario: Do not describe single command matches
37 | Given I start vimiv
38 | When I run keybindings
39 | And I press 'a' in the pop up
40 | Then the keybindings pop up description should be empty
41 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/keyhint.feature:
--------------------------------------------------------------------------------
1 | Feature: The keyhint overlay widget
2 |
3 | Background:
4 | Given I start vimiv
5 |
6 | Scenario: Display widget on partial keybindings
7 | When I press 'g'
8 | And I wait for the keyhint widget
9 | Then the keyhint widget should be visible
10 |
11 | Scenario: Keyhint widget contains partial matches
12 | When I press 'g'
13 | Then the keyhint widget should contain goto 1
14 | And the keyhint widget should contain enter image
15 |
16 | Scenario: Keyhint widget cleared and hidden after timeout
17 | When I press 'g'
18 | And I wait for the keyhint widget
19 | And I wait for the keyhint widget timeout
20 | Then the keyhint widget should not be visible
21 |
22 | Scenario: Keyhint should be above statusbar
23 | When I press 'g'
24 | And I wait for the keyhint widget
25 | Then the keyhint widget should be above the statusbar
26 |
27 | Scenario: Keyhint at bottom with statusbar hidden
28 | When I run set statusbar.show false
29 | And I press 'g'
30 | And I wait for the keyhint widget
31 | Then the keyhint widget should be at the bottom
32 |
33 | Scenario: Do not show widget in command mode
34 | When I run command
35 | And I press '<'
36 | Then the keyhint widget should not appear
37 |
38 | Scenario: Keyhint widget cleared and hidden after escape
39 | When I press 'g'
40 | And I wait for the keyhint widget
41 | And I press ''
42 | Then the keyhint widget should not be visible
43 |
44 | Scenario: Keyhint widget displays partial matches of special keys
45 | When I run bind g log info space
46 | And I run bind g log info tab
47 | And I press 'g'
48 | Then the keyhint widget should contain <space>
49 | And the keyhint widget should contain <tab>
50 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/migration_popup.feature:
--------------------------------------------------------------------------------
1 | Feature: Welcome-to-qt command with pop up window
2 |
3 | Scenario: Show welcome pop up
4 | Given I start vimiv
5 | When I run welcome-to-qt
6 | Then the pop up 'vimiv - welcome to qt' should be displayed
7 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/no_optional.feature:
--------------------------------------------------------------------------------
1 | Feature: Ensure the application works correctly without optional dependencies
2 |
3 | @nometadata
4 | Scenario: No metadata command
5 | Given I open any image
6 | When I run metadata
7 | Then the message
8 | 'metadata: unknown command for mode image'
9 | should be displayed
10 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/startup.feature:
--------------------------------------------------------------------------------
1 | Feature: Startup vimiv with various flags
2 |
3 | Scenario: Set an option using the commandline
4 | Given I start vimiv with -s completion.fuzzy true
5 | Then the boolean setting 'completion.fuzzy' should be 'true'
6 |
7 | Scenario: Set multiple options using the commandline
8 | Given I start vimiv with -s completion.fuzzy true -s sort.shuffle true
9 | Then the boolean setting 'completion.fuzzy' should be 'true'
10 | And the boolean setting 'sort.shuffle' should be 'true'
11 |
12 | Scenario: Set an unknown option using the commandline
13 | Given I start vimiv with -s not.a.setting true
14 | Then no crash should happen
15 |
16 | Scenario: Set a wrong option value using the commandline
17 | Given I start vimiv with -s completion.fuzzy 42
18 | Then no crash should happen
19 |
20 | Scenario: Print version information
21 | Given I capture output
22 | And I run vimiv --version
23 | Then the version information should be displayed
24 |
25 | Scenario Outline: Set log level
26 | Given I start vimiv with --log-level
27 | Then the log level should be
28 |
29 | Examples:
30 | | level |
31 | | debug |
32 | | info |
33 | | warning |
34 | | error |
35 | | critical |
36 |
37 | Scenario: Open hidden image upon startup
38 | Given I open the image '.hidden.jpg'
39 | Then the filelist should contain 1 images
40 |
41 | Scenario: Pipe paths to vimiv
42 | Given I start vimiv passing 3 images via stdin
43 | Then the filelist should contain 3 images
44 |
45 | Scenario: Start with an invalid file
46 | Given I open a text file
47 | Then no crash should happen
48 | And the mode should be library
49 | And the filelist should contain 0 images
50 |
51 | Scenario: Read binary image from stdin
52 | Given I start vimiv passing a binary image via stdin
53 | Then the mode should be image
54 | And the filelist should contain 1 images
55 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/symlink.feature:
--------------------------------------------------------------------------------
1 | Feature: Handling symbolic links
2 |
3 | Scenario: Open real path when opening symlink
4 | Given I open the symlink test directory
5 | When I run open lnstem
6 | Then the working directory should be stem
7 |
8 | Scenario: Crash when searching in symlinked directory
9 | Given I open the symlink test directory
10 | When I run open lnstem
11 | And I run search
12 | And I press 'k'
13 | Then no crash should happen
14 |
15 | Scenario: Do not load path pointed to by symlink into filelist
16 | Given I open the symlink image test directory
17 | When I run open-selected --close
18 | Then the filelist should contain 1 images
19 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/teardown.feature:
--------------------------------------------------------------------------------
1 | Feature: Various features related to teardown
2 |
3 | Scenario: Print text to stdout upon quit
4 | Given I start vimiv with -o vimiv
5 | And I capture output
6 | When I quit the application
7 | Then stdout should contain 'vimiv'
8 |
9 | Scenario: Print current path to stdout upon quit
10 | Given I open 2 images with -o %
11 | And I capture output
12 | When I quit the application
13 | Then stdout should contain 'image_01.jpg'
14 |
15 | Scenario: Print marked images to stdout upon quit
16 | Given I open 2 images with -o %m
17 | And I capture output
18 | When I run mark *
19 | And I quit the application
20 | Then stdout should contain 'image_01.jpg'
21 | And stdout should contain 'image_02.jpg'
22 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/test_clipboard_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import os
4 |
5 | import pytest_bdd as bdd
6 |
7 | from vimiv.qt.gui import QClipboard
8 |
9 |
10 | bdd.scenarios("clipboard.feature")
11 |
12 |
13 | @bdd.then(
14 | bdd.parsers.parse(
15 | # Need a slightly different wording so it cannot overlap with the previous
16 | # versions
17 | "the absolute path of {text} should be saved in the clipboard"
18 | )
19 | )
20 | def check_clipboard_abspath(clipboard, text):
21 | text = os.path.abspath(text)
22 | assert clipboard.text(mode=QClipboard.Mode.Clipboard) == text
23 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/test_fullscreen_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("fullscreen.feature")
7 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/test_keybindings_popup_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest
4 | import pytest_bdd as bdd
5 |
6 | import vimiv.gui.keybindings_popup
7 | from vimiv import api, utils
8 |
9 | bdd.scenarios("keybindings_popup.feature")
10 |
11 |
12 | @pytest.fixture()
13 | def keybindings_popup(mainwindow):
14 | """Fixture to retrieve the current keybindings pop-up."""
15 | for widget in mainwindow.children():
16 | if isinstance(widget, vimiv.gui.keybindings_popup.KeybindingsPopUp):
17 | return widget
18 | raise AssertionError("No keybindings pop-up open")
19 |
20 |
21 | @bdd.when(bdd.parsers.parse("I press '{keys}' in the pop up"))
22 | def press_keys_popup(keypress, keybindings_popup, keys):
23 | keypress(keybindings_popup._search, keys)
24 |
25 |
26 | @bdd.then(bdd.parsers.parse("the keybindings pop up should contain '{text}'"))
27 | def check_keybindings_popup_text(keybindings_popup, text):
28 | assert text in keybindings_popup.text
29 |
30 |
31 | @bdd.then(bdd.parsers.parse("the keybindings pop up should describe '{command}'"))
32 | def check_keybindings_popup_description(keybindings_popup, command):
33 | description = api.commands.get(command, api.modes.current()).description
34 | assert description in keybindings_popup.description
35 |
36 |
37 | @bdd.then("the keybindings pop up description should be empty")
38 | def check_keybindings_popup_description_empty(keybindings_popup):
39 | assert not keybindings_popup.description
40 |
41 |
42 | @bdd.then(bdd.parsers.parse("'{search}' should be highlighted in '{command}'"))
43 | def check_keybindings_popup_highlighting(keybindings_popup, search, command):
44 | highlight = keybindings_popup.highlighted_search_str(search)
45 | # Ensure the search_str is the actual highlighted string
46 | assert utils.strip_html(highlight) == search
47 | # Ensure the highlighted command is in the pop up text
48 | assert command.replace(search, highlight) in keybindings_popup.text
49 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/test_migration_popup_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("migration_popup.feature")
7 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/test_no_optional_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 | bdd.scenarios("no_optional.feature")
6 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/test_startup_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import logging
4 |
5 | import pytest_bdd as bdd
6 |
7 | import vimiv
8 | from vimiv.utils import log
9 |
10 |
11 | bdd.scenarios("startup.feature")
12 |
13 |
14 | @bdd.then("the version information should be displayed")
15 | def check_version_information(output):
16 | assert vimiv.__name__ in output.out
17 | assert vimiv.__version__ in output.out
18 |
19 |
20 | @bdd.then(bdd.parsers.parse("the log level should be {level}"))
21 | def check_log_level(level):
22 | loglevel = getattr(logging, level.upper())
23 | assert log._app_logger.level == loglevel
24 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/test_symlink_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 | from vimiv.qt.gui import QPixmap
6 |
7 | from vimiv import startup
8 |
9 |
10 | bdd.scenarios("symlink.feature")
11 |
12 |
13 | @bdd.given("I open the symlink test directory")
14 | def open_symlink_test_directory(tmp_path):
15 | """Create a test directory with symlink(s) and open the base directory in vimiv.
16 |
17 | Structure:
18 | ├── lnstem -> stem
19 | └── stem
20 | └── leaf
21 | """
22 | base_directory = tmp_path / "directory"
23 | stem_directory = base_directory / "stem"
24 | leaf_directory = stem_directory / "leaf"
25 | leaf_directory.mkdir(parents=True)
26 | stem_symlink = base_directory / "lnstem"
27 | stem_symlink.symlink_to(stem_directory)
28 | argv = [str(base_directory)]
29 | args = startup.setup_pre_app(argv)
30 | startup.setup_post_app(args)
31 |
32 |
33 | @bdd.given("I open the symlink image test directory")
34 | def open_symlink_image_test_directory(tmp_path):
35 | """Create a test directory with symlink(s) and open the child directory in vimiv.
36 |
37 | Structure:
38 | ├── image.jpg
39 | └── child
40 | └── ln.jpg -> ../image.jpg
41 | """
42 | base_directory = tmp_path / "directory"
43 | base_image_path = base_directory / "image.jpg"
44 | child_directory = base_directory / "child"
45 | child_image_path = child_directory / "ln.jpg"
46 | child_directory.mkdir(parents=True)
47 | QPixmap(300, 300).save(str(base_image_path))
48 | child_image_path.symlink_to(base_image_path)
49 | argv = [str(child_directory)]
50 | args = startup.setup_pre_app(argv)
51 | startup.setup_post_app(args)
52 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/test_teardown_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("teardown.feature")
7 |
8 |
9 | @bdd.when("I quit the application")
10 | def quit_application(qapp):
11 | qapp.aboutToQuit.emit()
12 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/test_version_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("version.feature")
7 |
--------------------------------------------------------------------------------
/tests/end2end/features/misc/version.feature:
--------------------------------------------------------------------------------
1 | Feature: Version command with pop up window.
2 |
3 | Scenario: Display the version pop up.
4 | Given I start vimiv
5 | When I run version
6 | Then the pop up 'vimiv - version' should be displayed
7 |
8 | Scenario: Copy version information to clipboard.
9 | Given I start vimiv
10 | When I run version --copy
11 | Then the clipboard should contain 'vimiv'
12 |
--------------------------------------------------------------------------------
/tests/end2end/features/plugins/plugins.feature:
--------------------------------------------------------------------------------
1 | Feature: Plugin system with default plugins
2 |
3 | Scenario: Fail print command without image
4 | Given I open any directory
5 | When I enter image mode
6 | And I run print
7 | Then the message
8 | 'print: No widget to print'
9 | should be displayed
10 |
11 | @flaky
12 | Scenario: Show print dialog
13 | Given I open any image
14 | When I run print
15 | Then the pop up 'Print' should be displayed
16 |
17 | @flaky
18 | Scenario: Show print preview dialog
19 | Given I open any image
20 | When I run print --preview
21 | Then the pop up 'Print Preview' should be displayed
22 |
23 | Scenario: Load cr2 support
24 | Given I start vimiv
25 | When I load the imageformats plugin with cr2
26 | Then the cr2 format should be supported
27 |
28 | Scenario: Load demo plugin
29 | Given I start vimiv
30 | And I capture output
31 | When I load the demo plugin with hello demo
32 | And I run hello-world
33 | Then stdout should contain 'Initializing demo plugin with 'hello demo''
34 | And stdout should contain 'Hello world'
35 |
--------------------------------------------------------------------------------
/tests/end2end/features/plugins/test_plugins_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 | from vimiv import plugins
6 | from vimiv.utils import imageheader
7 |
8 |
9 | bdd.scenarios("plugins.feature")
10 |
11 |
12 | @bdd.when(bdd.parsers.parse("I load the {name} plugin with {info}"))
13 | def load_plugin(name, info):
14 | plugins._load_plugin(name, info, plugins._app_plugin_directory)
15 |
16 |
17 | @bdd.then(bdd.parsers.parse("The {name} format should be supported"))
18 | def check_format_supported(name):
19 | assert name in [
20 | filetype for filetype, _ in imageheader._registry
21 | ], f"Image format {name} is not supported"
22 |
--------------------------------------------------------------------------------
/tests/end2end/features/statusbar/message.feature:
--------------------------------------------------------------------------------
1 | Feature: Push messages to the statusbar.
2 |
3 | Background:
4 | Given I open any directory
5 |
6 | Scenario: Display warning message.
7 | When I log the warning 'this is a warning'
8 | Then the message
9 | 'this is a warning'
10 | should be displayed
11 |
12 | Scenario: Hide message widget when clearing status
13 | When I log the warning 'this is a warning'
14 | And I clear the status
15 | Then no message should be displayed
16 |
17 | Scenario: Clear message after key press
18 | When I log the warning 'this is a warning'
19 | And I press '0'
20 | Then no message should be displayed
21 |
--------------------------------------------------------------------------------
/tests/end2end/features/statusbar/status.feature:
--------------------------------------------------------------------------------
1 | Feature: Display status information in the statusbar
2 |
3 | Scenario: Display image information
4 | Given I open 3 images
5 | Then the left status should include 1/3
6 | And the left status should include image_01.jpg
7 | And the right status should include IMAGE
8 |
9 | Scenario: Display library information
10 | Given I open any directory
11 | Then the left status should include directory
12 | And the right status should include LIBRARY
13 |
14 | Scenario: Display correct mode after switch
15 | Given I open any directory
16 | When I run enter image
17 | Then the right status should include IMAGE
18 |
19 | Scenario: Show filesize in statusbar
20 | Given I start vimiv
21 | When I run set statusbar.left {filesize}
22 | # No current path selected
23 | Then the left status should include N/A
24 |
25 | Scenario: Do not crash when showing filesize in command moe
26 | Given I start vimiv
27 | When I run set statusbar.left {filesize}
28 | And I run command
29 | Then no crash should happen
30 |
31 | Scenario: Correctly escape html for keybindings
32 | Given I start vimiv
33 | When I run bind << scroll down
34 | And I press '<'
35 | Then the right status should include <
36 |
37 | Scenario: Show read-only mode in statusbar
38 | Given I start vimiv
39 | When I run set read_only true
40 | Then the left status should include [RO]
41 |
42 | Scenario: Show cursor position in statusbar
43 | Given I open any image
44 | When I run set statusbar.right_image {cursor-position}
45 | Then the image should have mouse tracking
46 |
--------------------------------------------------------------------------------
/tests/end2end/features/statusbar/test_message_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 | from vimiv import api
6 | from vimiv.utils import log
7 |
8 |
9 | bdd.scenarios("message.feature")
10 |
11 |
12 | @bdd.when(bdd.parsers.parse("I log the warning '{message}'"))
13 | def log_warning(message, qtbot):
14 | log.warning(message)
15 |
16 |
17 | @bdd.when("I clear the status")
18 | def clear_status():
19 | api.status.clear("clear in bdd step")
20 |
--------------------------------------------------------------------------------
/tests/end2end/features/statusbar/test_status_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("status.feature")
7 |
8 |
9 | @bdd.then("the image should have mouse tracking")
10 | def check_image_tracks_mouse(image):
11 | assert image.hasMouseTracking()
12 |
--------------------------------------------------------------------------------
/tests/end2end/features/thumbnail/.zcompdump:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlch/vimiv-qt/cbce18b2d36da890608ad57388482cc003d3ce03/tests/end2end/features/thumbnail/.zcompdump
--------------------------------------------------------------------------------
/tests/end2end/features/thumbnail/conftest.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | @bdd.then(bdd.parsers.parse("there should be {number:d} thumbnails"))
7 | def check_thumbnail_amount(thumbnail, number):
8 | assert thumbnail.model().rowCount() == number
9 |
--------------------------------------------------------------------------------
/tests/end2end/features/thumbnail/test_thumbnailgoto_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("thumbnailgoto.feature")
7 |
--------------------------------------------------------------------------------
/tests/end2end/features/thumbnail/test_thumbnailmark_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("thumbnailmark.feature")
7 |
8 |
9 | @bdd.then(bdd.parsers.parse("the thumbnail number {number:d} should be marked"))
10 | def check_thumbnail_marked(thumbnail, number):
11 | item = thumbnail.item(number - 1)
12 | assert item is not None and item.marked
13 |
--------------------------------------------------------------------------------
/tests/end2end/features/thumbnail/test_thumbnailscroll_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 |
6 | bdd.scenarios("thumbnailscroll.feature")
7 |
--------------------------------------------------------------------------------
/tests/end2end/features/thumbnail/test_thumbnailzoom_bdd.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | import pytest_bdd as bdd
4 |
5 | from vimiv import api
6 |
7 |
8 | bdd.scenarios("thumbnailzoom.feature")
9 |
10 |
11 | @bdd.then(bdd.parsers.parse("the thumbnail size should be {size:d}"))
12 | def check_thumbnail_size(thumbnail, size):
13 | # Check setting
14 | assert api.settings.thumbnail.size.value == size
15 | # Check actual value
16 | assert thumbnail.iconSize().width() == size
17 |
--------------------------------------------------------------------------------
/tests/end2end/features/thumbnail/thumbnailgoto.feature:
--------------------------------------------------------------------------------
1 | Feature: Select a specific thumbnail using the :goto command.
2 |
3 | Scenario: Select last thumbnail
4 | Given I open 10 images
5 | And I enter thumbnail mode
6 | When I run goto -1
7 | Then the thumbnail number 10 should be selected
8 |
9 | Scenario: Select last and then first thumbnail
10 | Given I open 10 images
11 | And I enter thumbnail mode
12 | When I run goto -1
13 | And I run goto 1
14 | Then the thumbnail number 1 should be selected
15 |
16 | Scenario: Select specific thumbnail using count
17 | Given I open 10 images
18 | And I enter thumbnail mode
19 | When I run 3goto
20 | Then the thumbnail number 3 should be selected
21 |
22 | Scenario: Crash on goto with empty thumbnail list
23 | Given I start vimiv
24 | When I enter thumbnail mode
25 | And I run goto 4
26 | Then no crash should happen
27 | And the message
28 | 'goto: No thumbnail in list'
29 | should be displayed
30 |
--------------------------------------------------------------------------------
/tests/end2end/features/thumbnail/thumbnailmark.feature:
--------------------------------------------------------------------------------
1 | Feature: Mark thumbnails
2 |
3 | Background:
4 | Given I open 5 images
5 | And I enter thumbnail mode
6 |
7 | Scenario: Mark a thumbnail
8 | When I run mark %
9 | Then the thumbnail number 1 should be marked
10 |
11 | Scenario: Keep correct highlighting when restoring thumbnails
12 | When I run mark image_03.jpg
13 | And I run mark image_05.jpg
14 | And I run delete image_04.jpg
15 | And I wait for the working directory handler
16 | And I run undelete image_04.jpg
17 | And I wait for the working directory handler
18 | Then the thumbnail number 3 should be marked
19 | And the thumbnail number 5 should be marked
20 |
--------------------------------------------------------------------------------
/tests/end2end/features/thumbnail/thumbnailzoom.feature:
--------------------------------------------------------------------------------
1 | Feature: Zoom thumbnails.
2 |
3 | Background:
4 | Given I open any image
5 | And I enter thumbnail mode
6 |
7 | Scenario: Increase thumbnail size.
8 | When I run zoom in
9 | Then the thumbnail size should be 256
10 |
11 | Scenario: Decrease thumbnail size.
12 | When I run zoom out
13 | Then the thumbnail size should be 64
14 |
15 | Scenario: Try to increase thumbnail size below limit.
16 | # 256
17 | When I run zoom in
18 | # 512
19 | And I run zoom in
20 | # 512
21 | And I run zoom in
22 | Then the thumbnail size should be 512
23 |
24 | Scenario: Try to decrease thumbnail size below limit.
25 | # 64
26 | When I run zoom out
27 | # 64
28 | And I run zoom out
29 | Then the thumbnail size should be 64
30 |
--------------------------------------------------------------------------------
/tests/end2end/mockdecorators.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Module to patch vimiv decorators before import."""
4 |
5 | import contextlib
6 | import unittest.mock
7 |
8 | _known_classes = set()
9 |
10 |
11 | def mockregister(component_init):
12 | """Patch api.objreg.register to additionally store the known classes."""
13 |
14 | def inside(component, *args, **kwargs) -> None:
15 | component.__class__.instance = component
16 | _known_classes.add(component.__class__)
17 | component_init(component, *args, **kwargs)
18 |
19 | return inside
20 |
21 |
22 | def mockregister_cleanup():
23 | for cls in _known_classes:
24 | # Instances stored as a global variable
25 | if cls.__qualname__ not in ("Mark", "ExternalRunner"):
26 | cls.instance = None
27 |
28 |
29 | @contextlib.contextmanager
30 | def apply():
31 | """Contextmanager to mock relevant vimiv decorators and restore state."""
32 | with unittest.mock.patch("vimiv.api.objreg.register", mockregister):
33 | yield
34 |
--------------------------------------------------------------------------------
/tests/integration/__init__.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Integration tests."""
4 |
--------------------------------------------------------------------------------
/tests/integration/conftest.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Common fixtures for integration testing."""
4 |
5 | import pytest
6 |
7 |
8 | @pytest.fixture()
9 | def custom_configparser():
10 | """Fixture to create a custom configparser adding to defaults."""
11 |
12 | def create_custom_parser(default_parser, **sections):
13 | parser = default_parser()
14 | for section, content in sections.items():
15 | for key, value in content.items():
16 | parser[section][key] = value
17 | return parser
18 |
19 | return create_custom_parser
20 |
21 |
22 | @pytest.fixture()
23 | def custom_configfile(tmp_path, custom_configparser):
24 | """Fixture to create a custom config file from a configparser."""
25 |
26 | def create_custom_configfile(basename, read, default_parser, **sections):
27 | parser = custom_configparser(default_parser, **sections)
28 | path = tmp_path / basename
29 | with open(path, "w", encoding="utf-8") as f:
30 | parser.write(f)
31 | read(str(path))
32 |
33 | return create_custom_configfile
34 |
--------------------------------------------------------------------------------
/tests/integration/test_app.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Integration tests for vimiv.app.
4 |
5 | These are not unit tests as the app makes use of multiple modules."""
6 |
7 | import time
8 |
9 | import pytest
10 |
11 | import vimiv.app
12 | from vimiv import qt
13 | from vimiv.utils import asyncrun
14 |
15 |
16 | @pytest.mark.ci_skip
17 | def test_load_icon(qtbot):
18 | icon = vimiv.app.Application.get_icon()
19 | assert not icon.isNull()
20 |
21 |
22 | @pytest.mark.xfail(qt.USE_PYQT5, reason="flaky under PyQt 5.15.10")
23 | def test_wait_for_running_processes(mocker):
24 | """Ensure any running threads are completed before the app exits."""
25 |
26 | def process():
27 | time.sleep(0.001)
28 | callback()
29 |
30 | callback = mocker.Mock()
31 | asyncrun(process)
32 | vimiv.app.Application.preexit(0)
33 | callback.assert_called_once()
34 |
--------------------------------------------------------------------------------
/tests/integration/test_edit.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Integration tests related to editing images."""
4 |
5 | import pytest
6 |
7 | from vimiv.qt.gui import QPixmap, QColor
8 |
9 | from vimiv.config import styles
10 | from vimiv.imutils import edit_handler
11 |
12 |
13 | WIDTH = 300
14 | HEIGHT = 200
15 | COLOR = QColor("#888888")
16 |
17 |
18 | @pytest.fixture()
19 | def edit(mocker, qtbot):
20 | mocker.patch("vimiv.api.signals")
21 | mocker.patch("vimiv.api.modes.Mode.close")
22 | mocker.patch.object(styles, "_style", styles.create_default())
23 | handler = edit_handler.EditHandler()
24 | handler._init_manipulate()
25 | handler.pixmap = QPixmap(WIDTH, HEIGHT)
26 | handler.pixmap.fill(COLOR)
27 | yield handler
28 |
29 |
30 | @pytest.fixture()
31 | def transform(edit):
32 | yield edit.transform
33 |
34 |
35 | @pytest.fixture()
36 | def manipulate(edit):
37 | edit.manipulate._enter()
38 | return edit.manipulate
39 |
40 |
41 | def current_color(pixmap):
42 | image = pixmap.toImage()
43 | return QColor(image.pixel(0, 0))
44 |
45 |
46 | def test_transform_applied(edit, transform):
47 | transform.rotate_command()
48 | assert edit.pixmap.width() == HEIGHT
49 | assert edit.pixmap.height() == WIDTH
50 |
51 |
52 | def test_manipulate_applied(edit, manipulate):
53 | manipulate.increase(10)
54 | manipulate.accept()
55 | assert current_color(edit.pixmap) != COLOR
56 |
57 |
58 | def test_manipulate_and_transform_iteratively(edit, transform, manipulate):
59 | transform.rotate_command()
60 | manipulate.increase(10)
61 | manipulate.accept()
62 | assert edit.pixmap.width() == HEIGHT
63 | assert edit.pixmap.height() == WIDTH
64 | assert current_color(edit.pixmap) != COLOR
65 | transform.rotate_command()
66 | assert edit.pixmap.height() == HEIGHT
67 | assert edit.pixmap.width() == WIDTH
68 |
--------------------------------------------------------------------------------
/tests/integration/test_metadata.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for vimiv.imutils.metadata."""
4 |
5 | import pytest
6 |
7 | from vimiv.imutils import metadata
8 | from vimiv.plugins import metadata_piexif, metadata_pyexiv2
9 |
10 |
11 | @pytest.fixture(autouse=True)
12 | def reset_to_default(cleanup_helper):
13 | """Fixture to ensure everything is reset to default after testing."""
14 | registry = list(metadata._registry)
15 | yield
16 | metadata._registry = registry
17 |
18 |
19 | @pytest.fixture
20 | def nometadata():
21 | metadata._registry = []
22 |
23 |
24 | @pytest.fixture
25 | def piexif():
26 | metadata._registry = []
27 | metadata_piexif.init()
28 |
29 |
30 | @pytest.fixture
31 | def pyexiv2():
32 | metadata._registry = []
33 | metadata_pyexiv2.init()
34 |
35 |
36 | @pytest.mark.parametrize(
37 | "methodname, args",
38 | (
39 | ("copy_metadata", ("dest.jpg",)),
40 | ("get_date_time", ()),
41 | ("get_metadata", ([],)),
42 | ("get_keys", ()),
43 | ),
44 | )
45 | def test_handler_raises(nometadata, methodname, args):
46 | assert not metadata.has_metadata_support()
47 |
48 | handler = metadata.MetadataHandler("path")
49 | method = getattr(handler, methodname)
50 | with pytest.raises(metadata.MetadataError):
51 | method(*args)
52 |
53 |
54 | def test_piexif_initializes(piexif):
55 | assert metadata_piexif.MetadataPiexif in metadata._registry
56 | assert metadata.has_metadata_support()
57 |
58 |
59 | def test_pyexiv2_initializes(pyexiv2):
60 | assert metadata_pyexiv2.MetadataPyexiv2 in metadata._registry
61 | assert metadata.has_metadata_support()
62 |
--------------------------------------------------------------------------------
/tests/unit/__init__.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Unit tests."""
4 |
--------------------------------------------------------------------------------
/tests/unit/api/test_objreg.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for vimiv.api.objreg."""
4 |
5 | import pytest
6 |
7 | from vimiv.api import objreg
8 |
9 |
10 | class Multiplier:
11 | """Test class to be registered in the objreg with a simple test method."""
12 |
13 | instance = None
14 |
15 | @objreg.register
16 | def __init__(self, factor):
17 | self._factor = factor
18 |
19 | def multiply_by(self, number):
20 | return number * self._factor
21 |
22 |
23 | @pytest.fixture()
24 | def multiply_by_two():
25 | """Fixture to create a multiplier instance that multiplies by two."""
26 | multiplier = Multiplier(2)
27 | yield multiplier
28 | Multiplier.instance = None
29 |
30 |
31 | def test_call_with_instance(multiply_by_two):
32 | number = 42
33 | result = objreg._call_with_instance(Multiplier.multiply_by, number)
34 | assert result == 2 * number
35 |
36 |
37 | def test_call_without_instance():
38 | def func(argument):
39 | return argument * 2
40 |
41 | argument = 42
42 |
43 | assert func(argument) == objreg._call_with_instance(func, argument)
44 |
--------------------------------------------------------------------------------
/tests/unit/api/test_prompt.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for vimiv.api.prompt."""
4 |
5 | import pytest
6 |
7 | from vimiv.qt.core import QObject
8 |
9 | from vimiv.api import prompt
10 |
11 |
12 | class QuestionAnswerer(QObject):
13 | """Stub class answering an asked question with a defined value."""
14 |
15 | def __init__(self, *, title: str, body: str, answer=None):
16 | super().__init__()
17 | self.title = title
18 | self.body = body
19 | self.answered = False
20 | self.answer = answer
21 | prompt.question_asked.connect(self.answer_question)
22 |
23 | def answer_question(self, question: prompt.Question):
24 | assert question.title == self.title
25 | assert question.body == self.body
26 | question.answer = self.answer
27 | self.answered = True
28 |
29 |
30 | @pytest.mark.parametrize("answer", (None, 42, "answer"))
31 | def test_ask_question(answer):
32 | title = "Question"
33 | body = "Does this test work?"
34 | answerer = QuestionAnswerer(title=title, body=body, answer=answer)
35 | received_answer = prompt.ask_question(title=title, body=body)
36 | assert answerer.answered
37 | assert received_answer == answer
38 |
--------------------------------------------------------------------------------
/tests/unit/api/test_status.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for vimiv.api.status."""
4 |
5 | import pytest
6 |
7 | from vimiv.api import status
8 |
9 |
10 | @pytest.fixture()
11 | def dummy_module():
12 | """Fixture to create and clean-up a valid status module."""
13 | name = "{dummy}"
14 | content = "dummy"
15 |
16 | @status.module(name)
17 | def dummy_method():
18 | return content
19 |
20 | yield name, dummy_method()
21 |
22 | del status._modules[name]
23 |
24 |
25 | def test_evaluate_status_module(dummy_module):
26 | name, content = dummy_module
27 | assert status.evaluate(f"Dummy: {name}") == f"Dummy: {content}"
28 |
29 |
30 | def test_fail_add_status_module():
31 | with pytest.raises(ValueError):
32 |
33 | @status.module("wrong")
34 | def wrong():
35 | """Status module with an invalid name."""
36 | return "wrong"
37 |
38 |
39 | def test_evaluate_unknown_module():
40 | name = "{unknown-module}"
41 | assert status.evaluate(f"Dummy: {name}") == "Dummy: "
42 |
--------------------------------------------------------------------------------
/tests/unit/commands/test_aliases.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for vimiv.commands.aliases."""
4 |
5 | from typing import NamedTuple
6 |
7 | import pytest
8 |
9 | from vimiv import api
10 | from vimiv.commands import aliases
11 |
12 |
13 | class AliasDefinition(NamedTuple):
14 | name: str = "test"
15 | command: str = "quit"
16 | mode: api.modes.Mode = api.modes.GLOBAL
17 |
18 |
19 | @pytest.fixture(params=[api.modes.GLOBAL])
20 | def alias(request):
21 | """Pytest fixture to create and delete an alias.
22 |
23 | Default mode for alias creation is GLOBAL. This can be changed via indirect fixture
24 | parametrization.
25 | """
26 | mode = request.param
27 | definition = AliasDefinition(mode=mode)
28 | aliases.alias(definition.name, [definition.command], mode=definition.mode.name)
29 | yield definition
30 | del aliases._aliases[definition.mode][definition.name]
31 |
32 |
33 | def test_add_global_alias(alias):
34 | """Ensure alias added for global mode is available in all global modes."""
35 | for mode in (*api.modes.GLOBALS, api.modes.GLOBAL):
36 | assert aliases.get(mode)[alias.name] == alias.command
37 |
38 |
39 | @pytest.mark.parametrize("alias", api.modes.GLOBALS, indirect=True)
40 | def test_add_local_alias(alias):
41 | """Ensure alias added for a single mode is only available in this mode."""
42 | assert aliases.get(alias.mode)[alias.name] == alias.command
43 | other = set(api.modes.ALL) - {alias.mode}
44 | for mode in other:
45 | with pytest.raises(KeyError, match=alias.name):
46 | aliases.get(mode)[alias.name] # pylint: disable=expression-not-assigned
47 |
48 |
49 | def test_fail_add_alias_no_list():
50 | with pytest.raises(AssertionError, match="defined as list"):
51 | aliases.alias("test", "any")
52 |
53 |
54 | def test_fail_mode_not_as_str():
55 | with pytest.raises(AssertionError, match="Mode must be passed"):
56 | aliases.alias("test", ["any"], mode=api.modes.GLOBAL)
57 |
--------------------------------------------------------------------------------
/tests/unit/commands/test_commands.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for vimiv.commands."""
4 |
5 | import pytest
6 |
7 | from vimiv import commands
8 |
9 |
10 | @pytest.mark.parametrize(
11 | "number, count, max_count, expected",
12 | [
13 | (1, None, 5, 0),
14 | (1, 2, 5, 1),
15 | (10, None, 5, 4),
16 | (-1, None, 5, 4),
17 | (0, None, 5, 0),
18 | ],
19 | )
20 | def test_number_for_command(number, count, max_count, expected):
21 | assert commands.number_for_command(number, count, max_count=max_count) == expected
22 |
23 |
24 | def test_fail_number_for_command():
25 | with pytest.raises(ValueError):
26 | commands.number_for_command(None, None, max_count=42)
27 |
--------------------------------------------------------------------------------
/tests/unit/commands/test_history.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for commands.history.History."""
4 |
5 | import json
6 | import os
7 |
8 | import pytest
9 |
10 | from vimiv import api
11 | from vimiv.commands.history import History
12 |
13 |
14 | MODES = (*api.modes.GLOBALS, api.modes.MANIPULATE)
15 | MAX_ITEMS = 20
16 | MODE_HISTORY = {
17 | mode.name: [f":{mode.name}-{i:01d}" for i in range(MAX_ITEMS)] for mode in MODES
18 | }
19 | LEGACY_HISTORY = [f":command-{i:01d}" for i in range(MAX_ITEMS)]
20 |
21 |
22 | @pytest.fixture()
23 | def mode_based_history_file(tmp_path, mocker):
24 | """Fixture to create mode-based history file to initialize History."""
25 | path = tmp_path / "history.json"
26 |
27 | with open(path, "w", encoding="utf-8") as f:
28 | json.dump(MODE_HISTORY, f)
29 |
30 | mocker.patch.object(History, "filename", return_value=str(path))
31 |
32 |
33 | @pytest.fixture()
34 | def legacy_history_file(tmp_path, mocker):
35 | """Fixture to create legacy file to initialize History."""
36 | path = tmp_path / "history"
37 | path.write_text("\n".join(LEGACY_HISTORY) + "\n")
38 | mocker.patch.object(History, "filename", return_value=str(path) + ".json")
39 |
40 |
41 | @pytest.fixture()
42 | def history():
43 | """Fixture to create a clean history object to test."""
44 | yield History(":", MAX_ITEMS)
45 |
46 |
47 | def test_read_history(mode_based_history_file, history):
48 | for mode, history_deque in history.items():
49 | assert list(history_deque) == MODE_HISTORY[mode.name]
50 |
51 |
52 | def test_write_history(mode_based_history_file, history):
53 | for mode, history_deque in history.items():
54 | history_deque.extend(MODE_HISTORY[mode.name])
55 | history.write()
56 |
57 | with open(history.filename(), "r", encoding="utf-8") as f:
58 | written_history = json.load(f)
59 |
60 | assert written_history == MODE_HISTORY
61 |
62 |
63 | def test_migrate_history(legacy_history_file, history):
64 | # Correctly read into new mode - deque - structure
65 | assert [mode.name for mode in history] == list(MODE_HISTORY.keys())
66 | for history_deque in history.values():
67 | assert list(history_deque) == LEGACY_HISTORY
68 | # Backup created
69 | assert os.path.isfile(history.filename().replace(".json", ".bak"))
70 |
--------------------------------------------------------------------------------
/tests/unit/commands/test_runners.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for vimiv.commands.runners."""
4 |
5 | import pytest
6 |
7 | from vimiv.commands import runners
8 |
9 |
10 | @pytest.mark.parametrize("text", [" ", "\n", " \n", "\t\t", "\n \t"])
11 | def test_text_non_whitespace_with_whitespace(text):
12 | """Ensure the decorated function is not called with plain whitespace."""
13 |
14 | @runners.text_non_whitespace
15 | def function(text):
16 | raise AssertionError("The function should not be called")
17 |
18 | function(text)
19 |
20 |
21 | @pytest.mark.parametrize("text", [" txt", "\ntxt", " \ntxt", "\ttxt\t", "\n txt\t"])
22 | def test_text_non_whitespace_with_non_whitespace(text):
23 | """Ensure the decorated function is called with stripped text."""
24 |
25 | @runners.text_non_whitespace
26 | def function(stripped_text):
27 | """Function to ensure any surrounding whitespace is removed."""
28 | assert stripped_text == text.strip()
29 |
30 | function(text)
31 |
--------------------------------------------------------------------------------
/tests/unit/commands/test_search.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for vimiv.commands.search"""
4 |
5 | from vimiv.commands import search
6 |
7 |
8 | def test_clear_search():
9 | search.search._text = "Something"
10 | search.search.clear()
11 | assert search.search._text == ""
12 |
13 |
14 | def test_sort_for_search():
15 | testlist = [1, 2, 3]
16 | updated_list = search._sort_for_search(testlist, 1, False)
17 | assert updated_list == [2, 3, 1]
18 |
19 |
20 | def test_sort_for_search_reverse():
21 | testlist = [1, 2, 3]
22 | updated_list = search._sort_for_search(testlist, 1, True)
23 | assert updated_list == [2, 1, 3]
24 |
--------------------------------------------------------------------------------
/tests/unit/commands/test_wildcards.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for vimiv.commands.wildcards."""
4 |
5 | import shlex
6 | import string
7 |
8 | import pytest
9 |
10 | from vimiv.commands import wildcards
11 |
12 |
13 | @pytest.mark.parametrize("wildcard", ("%", "%m", "%wildcard", "%f"))
14 | @pytest.mark.parametrize("escaped", (True, False))
15 | @pytest.mark.parametrize(
16 | "text", ("{wildcard} start", "in the {wildcard} middle", "end {wildcard}")
17 | )
18 | def test_expand_wildcard(wildcard, escaped, text):
19 | text = text.format(wildcard=rf"\{wildcard}" if escaped else wildcard)
20 | paths = "this", "is", "text"
21 | result = wildcards.expand(text, wildcard, lambda: paths)
22 |
23 | if escaped:
24 | expected = text.replace("\\", "")
25 | else:
26 | expected = text.replace(wildcard, " ".join(paths))
27 |
28 | assert result == expected
29 |
30 |
31 | @pytest.mark.parametrize("char", string.ascii_letters)
32 | @pytest.mark.parametrize("wildcard", ("%",))
33 | def test_expand_with_backslash(wildcard, char):
34 | paths = (rf"\{char}",)
35 | expected = " ".join(shlex.quote(path) for path in paths)
36 |
37 | result = wildcards.expand(wildcard, wildcard, lambda: paths)
38 | assert result == expected
39 |
40 |
41 | def test_recursive_wildcards():
42 | """Ensure unescaping of wildcards does not lead to them being matched later."""
43 | text = r"This has an escaped wildcard \%m"
44 | expected = "This has an escaped wildcard %m"
45 | intermediate = wildcards.expand(text, "%m", lambda: "anything")
46 | result = wildcards.expand(intermediate, "%", lambda: "anything")
47 | assert result == expected
48 |
49 |
50 | @pytest.mark.parametrize("path", (r"\.jpg", "spaced path.jpg", r"\%.jpg"))
51 | def test_escape_path(path: str):
52 | expected = "'" + path.replace("\\", "\\\\").replace("%", "\\%") + "'"
53 | assert wildcards.escape_path(path) == expected
54 |
--------------------------------------------------------------------------------
/tests/unit/config/test_config.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Unit tests for vimiv.config."""
4 |
5 | import configparser
6 |
7 | import pytest
8 |
9 | from vimiv import config
10 | from vimiv.utils import customtypes
11 |
12 |
13 | @pytest.mark.parametrize(
14 | "content, message",
15 | [
16 | ("GIBBERISH\n", "missing section header"),
17 | ("[SECTION]\nvalue", "key missing value"),
18 | ("[]\n", "empty section name"),
19 | ("[SECTION\n", "only opening section bracket"),
20 | ("SECTION]\n", "only closing section bracket"),
21 | ("[SECTION]\n[SECTION]\n", "duplicate section"),
22 | ("[SECTION]\na=0\na=1\n", "duplicate key"),
23 | ],
24 | )
25 | def test_sysexit_on_broken_config(mocker, tmp_path, content, message):
26 | """Ensure SystemExit is correctly raised for various broken config files.
27 |
28 | Args:
29 | content: Content written to the configuration file which is invalid.
30 | message: Message printed to help debugging.
31 | """
32 | print("Ensuring system exit with", message)
33 | mock_logger = mocker.Mock()
34 | parser = configparser.ConfigParser()
35 | path = tmp_path / "configfile"
36 | path.write_text(content)
37 | with pytest.raises(SystemExit, match=str(customtypes.Exit.err_config)):
38 | config.read_log_exception(parser, mock_logger, str(path))
39 | mock_logger.critical.assert_called_once()
40 |
--------------------------------------------------------------------------------
/tests/unit/conftest.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Fixtures for pytest unit tests."""
4 |
5 | import pytest
6 |
7 |
8 | @pytest.fixture()
9 | def tmpfile(tmp_path):
10 | path = tmp_path / "anything"
11 | path.touch()
12 | yield str(path)
13 |
--------------------------------------------------------------------------------
/tests/unit/gui/test_statusbar.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for vimiv.gui.statusbar."""
4 |
5 | import pytest
6 |
7 | from vimiv.gui import statusbar
8 |
9 |
10 | @pytest.mark.parametrize(
11 | "text, expected",
12 | [
13 | (" ", " "),
14 | ("this is text", "this is text"),
15 | ("one two", "one two"),
16 | ("one two three four five", "one two three four five"),
17 | ],
18 | )
19 | def test_escape_subsequent_space_for_html(text, expected):
20 | assert statusbar.StatusBar._escape_subsequent_space_for_html(text) == expected
21 |
--------------------------------------------------------------------------------
/tests/unit/gui/test_thumbnail.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for vimiv.gui.thumbnail."""
4 |
5 | import pytest
6 |
7 | from vimiv.qt.core import QSize
8 | from vimiv.qt.gui import QIcon
9 |
10 | from vimiv.gui.thumbnail import ThumbnailItem
11 |
12 |
13 | @pytest.fixture()
14 | def item(mocker):
15 | """Fixture to retrieve a vanilla ThumbnailItem."""
16 | ThumbnailItem._default_icon = None
17 | mocker.patch.object(ThumbnailItem, "create_default_icon", return_value=QIcon())
18 | yield ThumbnailItem
19 |
20 |
21 | def test_create_default_pixmap_once(item):
22 | """Ensure the default thumbnail icon is only created once."""
23 | size_hint = QSize(128, 128)
24 | for index in range(5):
25 | item(None, index, size_hint=size_hint)
26 | item.create_default_icon.assert_called_once()
27 |
--------------------------------------------------------------------------------
/tests/unit/imutils/test_imtransform.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for vimiv.imutils.imtransform."""
4 |
5 | import functools
6 |
7 | import pytest
8 |
9 | from vimiv.qt.gui import QPixmap
10 |
11 | from vimiv.imutils import current_pixmap, imtransform
12 |
13 |
14 | ACTIONS = (
15 | imtransform.Transform.rotate_command,
16 | imtransform.Transform.flip,
17 | functools.partial(imtransform.Transform.resize, width=400, height=400),
18 | functools.partial(imtransform.Transform.rescale, dx=2, dy=2),
19 | )
20 |
21 |
22 | @pytest.fixture(scope="function")
23 | def transform(qtbot, mocker):
24 | """Fixture to retrieve a clean Transform instance."""
25 | pixmap = QPixmap(300, 300)
26 | current_pm = current_pixmap.CurrentPixmap()
27 | transform = imtransform.Transform(current_pm)
28 | current_pm.pixmap = transform.original = pixmap
29 | return transform
30 |
31 |
32 | @pytest.fixture(params=ACTIONS)
33 | def action(transform, request):
34 | action = request.param
35 | return functools.partial(action, self=transform)
36 |
37 |
38 | def test_change_and_reset(action, transform):
39 | """Ensure every action leads to a change and is appropriately reset."""
40 | action()
41 | assert transform.changed
42 | transform.reset()
43 | assert not transform.changed
44 |
45 |
46 | @pytest.mark.parametrize("angle", range(0, 360, 15))
47 | def test_rotate_angle(transform, angle):
48 | transform.rotate(angle)
49 | assert transform.angle == pytest.approx(angle)
50 |
--------------------------------------------------------------------------------
/tests/unit/plugins/mock_plugin.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Mock plugin to test loading a plugin."""
4 |
5 |
6 | def init(_info: str, *args, **kwargs):
7 | pass
8 |
9 |
10 | def cleanup(*args, **kwargs):
11 | pass
12 |
--------------------------------------------------------------------------------
/tests/unit/plugins/mock_plugin_syntax_error.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Mock plugin to test loading a plugin with a syntax error."""
4 |
5 | for
6 |
--------------------------------------------------------------------------------
/tests/unit/plugins/test_plugins.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for vimiv.plugins."""
4 |
5 | import os
6 |
7 | import pytest
8 |
9 | from vimiv import plugins
10 |
11 |
12 | @pytest.fixture
13 | def mock_plugin(mocker):
14 | name = "mock_plugin"
15 | info = "useful"
16 | mocker.patch("mock_plugin.init")
17 | mocker.patch("mock_plugin.cleanup")
18 | plugins._load_plugin(name, info, os.path.abspath("."))
19 | plugins.cleanup()
20 | yield plugins._loaded_plugins[name], info
21 | del plugins._loaded_plugins[name]
22 |
23 |
24 | def test_init_and_cleanup_plugin(mock_plugin):
25 | plugin, info = mock_plugin
26 | plugin.init.assert_called_once_with(info)
27 | plugin.cleanup.assert_called_once()
28 |
29 |
30 | def test_do_not_fail_on_non_existing_plugin():
31 | plugins._load_plugin("does_not_exist", "any info", os.path.abspath("."))
32 |
33 |
34 | def test_do_not_fail_on_plugin_with_syntax_error():
35 | name = "mock_plugin_syntax_error"
36 | info = "useful"
37 | plugins._load_plugin(name, info, os.path.abspath("."))
38 |
--------------------------------------------------------------------------------
/tests/unit/test_checkversion.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Unit tests for vimiv.checkversion."""
4 |
5 | import sys
6 |
7 | import pytest
8 |
9 | from vimiv import checkversion
10 | import vimiv.qt.core
11 |
12 |
13 | @pytest.mark.parametrize("version_info", ((1,), (2, 6), (3, 5, 900)))
14 | def test_check_python_version(capsys, monkeypatch, version_info):
15 | """Tests to ensure exit and error message on too low python version."""
16 | monkeypatch.setattr(sys, "version_info", version_info)
17 |
18 | with pytest.raises(SystemExit, match=str(checkversion.ERR_CODE)):
19 | checkversion.check_python_version()
20 |
21 | expected = build_message(
22 | "python", checkversion.PYTHON_REQUIRED_VERSION, version_info
23 | )
24 | captured = capsys.readouterr()
25 | assert captured.err == expected
26 |
27 |
28 | @pytest.mark.parametrize("version_info", ((1,), (4, 6), (5, 8, 900)))
29 | def test_check_pyqt_version(capsys, monkeypatch, version_info):
30 | """Tests to ensure exit and error message on too low PyQt version."""
31 | monkeypatch.setattr(
32 | vimiv.qt.core, "PYQT_VERSION_STR", ".".join(str(i) for i in version_info)
33 | )
34 |
35 | with pytest.raises(SystemExit, match=str(checkversion.ERR_CODE)):
36 | checkversion.check_pyqt_version()
37 |
38 | expected = build_message("PyQt", checkversion.PYQT_REQUIRED_VERSION, version_info)
39 | captured = capsys.readouterr()
40 | assert captured.err == expected
41 |
42 |
43 | def build_message(software, required, version):
44 | """Helper to create the expected error message on too low software version."""
45 | # pylint: disable=consider-using-f-string
46 | return "At least %s %s is required to run vimiv. Using %s.\n" % (
47 | software,
48 | ".".join(str(i) for i in required),
49 | ".".join(str(i) for i in version),
50 | )
51 |
--------------------------------------------------------------------------------
/tests/unit/test_version.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for vimiv.version."""
4 |
5 | import pytest
6 |
7 | from vimiv import version
8 |
9 |
10 | @pytest.fixture
11 | def no_svg_support(monkeypatch):
12 | monkeypatch.setattr(version, "QtSvg", None)
13 |
14 |
15 | def test_svg_support_info():
16 | assert "svg support: true" in version.info().lower()
17 |
18 |
19 | def test_no_svg_support_info(no_svg_support):
20 | assert "svg support: false" in version.info().lower()
21 |
--------------------------------------------------------------------------------
/tests/unit/utils/_module_for_lazy.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Dummy module used for testing the lazy import mechanism."""
4 |
5 | print(__name__) # Side effect we can check for
6 |
7 | RETURN_VALUE = 42
8 |
9 |
10 | def function_of_interest():
11 | return RETURN_VALUE
12 |
--------------------------------------------------------------------------------
/tests/unit/utils/test_debug.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for vimiv.utils.debug"""
4 |
5 | import re
6 |
7 | from vimiv.utils import debug
8 |
9 |
10 | def test_profiler(capsys):
11 | with debug.profile(5):
12 | pass
13 | assert "function calls" in capsys.readouterr().out
14 |
15 |
16 | def test_timed(mocker, capsys):
17 | mocker.patch("vimiv.utils.log.info")
18 | expected = 42
19 |
20 | @debug.timed
21 | def func():
22 | return expected
23 |
24 | result = func()
25 |
26 | assert result == expected # Ensure the result is preserved
27 | captured = capsys.readouterr()
28 | assert func.__name__ in captured.out # Ensure a message was printed
29 | # Ensure the message contains the elapsed time
30 | time_match = re.search(r"\d+.\d+", captured.out)
31 | assert time_match is not None, "No time logged"
32 |
--------------------------------------------------------------------------------
/tests/unit/utils/test_lazy.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for vimiv.utils.lazy."""
4 |
5 | import functools
6 | import sys
7 |
8 | import pytest
9 |
10 | from vimiv.utils import lazy
11 |
12 | MODULE_NAME = "_module_for_lazy"
13 |
14 |
15 | @pytest.fixture(autouse=True)
16 | def clear_imported_module():
17 | """Fixture to clean the module lru_cache and the imported module."""
18 | yield
19 | lazy.Module.factory.cache_clear()
20 | if MODULE_NAME in sys.modules:
21 | del sys.modules[MODULE_NAME]
22 |
23 |
24 | @pytest.fixture()
25 | def module():
26 | """Fixture to return a lazy imported module."""
27 | yield lazy.import_module(MODULE_NAME)
28 |
29 |
30 | def test_lazy_import_is_lazy(module, capsys):
31 | """Ensure the module is not executed upon import."""
32 | captured = capsys.readouterr()
33 | assert MODULE_NAME not in captured.out
34 |
35 |
36 | def test_lazy_imported_module(module, capsys):
37 | """Ensure the module works as expected when used."""
38 | assert module.function_of_interest() == module.RETURN_VALUE
39 | captured = capsys.readouterr()
40 | assert MODULE_NAME in captured.out
41 |
42 |
43 | def test_lazy_module_single_instance(module):
44 | """Ensure we only create a single instance per module name."""
45 | module_second_import = lazy.import_module(MODULE_NAME)
46 | assert module_second_import is module
47 |
48 |
49 | @pytest.mark.parametrize("name", ("not_a_valid_module", "not_a_valid_module.submodule"))
50 | @pytest.mark.parametrize("optional", (True, False))
51 | def test_lazy_import_nonexisting_module(name, optional):
52 | importfunc = functools.partial(lazy.import_module, name, optional=optional)
53 | if optional:
54 | assert importfunc() is None
55 | else:
56 | with pytest.raises(ModuleNotFoundError, match=name):
57 | importfunc()
58 |
--------------------------------------------------------------------------------
/tests/unit/utils/test_xdg.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Tests for vimiv.utils.xdg."""
4 |
5 | import os
6 |
7 | import pytest
8 |
9 | from vimiv.utils import xdg
10 |
11 |
12 | @pytest.fixture
13 | def unset_xdg_env(monkeypatch):
14 | """Unset the XDG_*_HOME environment variables."""
15 | monkeypatch.delenv("XDG_CACHE_HOME", raising=False)
16 | monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
17 | monkeypatch.delenv("XDG_DATA_HOME", raising=False)
18 |
19 |
20 | @pytest.fixture
21 | def mock_xdg(tmp_path, monkeypatch):
22 | """Set XDG_* directories to a temporary directory."""
23 | dirname = str(tmp_path / "directory")
24 | monkeypatch.setenv("XDG_CACHE_HOME", dirname)
25 | monkeypatch.setenv("XDG_CONFIG_HOME", dirname)
26 | monkeypatch.setenv("XDG_DATA_HOME", dirname)
27 | yield dirname
28 |
29 |
30 | def test_xdg_defaults(unset_xdg_env):
31 | expected = {
32 | xdg.user_cache_dir: "~/.cache",
33 | xdg.user_config_dir: "~/.config",
34 | xdg.user_data_dir: "~/.local/share",
35 | }
36 | for func, expected_retval in expected.items():
37 | assert func() == os.path.expanduser(expected_retval)
38 |
39 |
40 | def test_xdg_from_env(mock_xdg):
41 | for func in [xdg.user_cache_dir, xdg.user_config_dir, xdg.user_data_dir]:
42 | assert func() == mock_xdg
43 |
44 |
45 | @pytest.mark.parametrize("paths", [tuple(), ("path",), ("directory", "path")])
46 | def test_vimiv_xdg_dirs(mock_xdg, paths):
47 | for func in [xdg.vimiv_cache_dir, xdg.vimiv_config_dir, xdg.vimiv_data_dir]:
48 | assert func(*paths) == os.path.join(mock_xdg, "vimiv", *paths)
49 |
--------------------------------------------------------------------------------
/vimiv/__init__.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """An image viewer with vim-like keybindings based on PyQt5."""
4 |
5 | import vimiv.checkversion
6 |
7 | __license__ = "GPL3"
8 | __version_info__ = (0, 9, 0)
9 | __version__ = ".".join(str(num) for num in __version_info__)
10 | __author__ = "Christian Karl"
11 | __maintainer__ = __author__
12 | __email__ = "karlch@protonmail.com"
13 | __description__ = "An image viewer with vim-like keybindings."
14 | __url__ = "https://karlch.github.io/vimiv-qt/"
15 |
--------------------------------------------------------------------------------
/vimiv/__main__.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Entry point for vimiv. Run the vimiv process."""
4 |
5 | import sys
6 |
7 | import vimiv.startup
8 |
9 |
10 | if __name__ == "__main__":
11 | sys.exit(vimiv.startup.main())
12 |
--------------------------------------------------------------------------------
/vimiv/api/prompt.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Prompt the user for a question."""
4 |
5 | import typing
6 |
7 | from vimiv.qt.core import QObject, Signal
8 |
9 | from vimiv.utils import log
10 |
11 |
12 | _logger = log.module_logger(__name__)
13 |
14 |
15 | class Question:
16 | """Storage class for a question to the user.
17 |
18 | Attributes:
19 | title: Title of the question.
20 | body: Sentence body representing the actual question.
21 | answer: Answer given by the user if any.
22 | """
23 |
24 | def __init__(self, *, title: str, body: str):
25 | self.title = title
26 | self.body = body
27 | self.answer = None
28 |
29 | def __repr__(self) -> str:
30 | return f"{self.__class__.__qualname__}(title={self.title}, body={self.body})"
31 |
32 |
33 | class _Bridge(QObject):
34 | """Message bridge using the signal-slot infrastructure.
35 |
36 | Signals:
37 | question_asked: Emitted when a question to the user was asked.
38 | arg1: The Question instance storing all information on the question.
39 | """
40 |
41 | question_asked = Signal(Question)
42 |
43 |
44 | def ask_question(*, title: str, body: str) -> typing.Any:
45 | """Prompt the user to answer a question.
46 |
47 | This emits the question_asked signal which leads to a gui prompt being displayed.
48 | The UI is blocked until the question was answered or aborted.
49 |
50 | Args:
51 | title: Title of the question.
52 | body: Sentence body representing the actual question.
53 | Returns:
54 | answer: Answer given by the user if any.
55 | """
56 | question = Question(title=title, body=body)
57 | _logger.debug("Asking question '%s'", question.title)
58 | question_asked.emit(question) # Enters a gui prompt widget
59 | _logger.debug("Answered '%s' with '%s'", question.title, question.answer)
60 | return question.answer
61 |
62 |
63 | # Expose only signals from the bridge
64 | _bridge = _Bridge()
65 | question_asked = _bridge.question_asked
66 |
--------------------------------------------------------------------------------
/vimiv/checkversion.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Check version and availability of required software upon import.
4 |
5 | This module is imported first in the top-level __init__.py to ensure we are running with
6 | the correct python and PyQt versions. In case this fails, error messages are written to
7 | stderr and we exit with returncode ERR_CODE.
8 |
9 | Module Attributes:
10 | PYQT_VERSION: The currently installed PyQt version as tuple if any.
11 | PYQT_REQUIRED_VERSION: The minimum PyQt version required.
12 | PYTHON_REQUIRED_VERSION: The minimum python version required.
13 | ERR_CODE: Returncode used with sys.exit.
14 | """
15 |
16 | import sys
17 |
18 |
19 | PYTHON_REQUIRED_VERSION = (3, 8)
20 | PYQT_REQUIRED_VERSION = (5, 13, 2)
21 | ERR_CODE = 2
22 |
23 |
24 | def check_python_version():
25 | """Ensure the python version is new enough."""
26 | if sys.version_info < PYTHON_REQUIRED_VERSION:
27 | _exit_version("python", PYTHON_REQUIRED_VERSION, sys.version_info[:3])
28 |
29 |
30 | def check_pyqt_version():
31 | """Ensure the PyQt version is new enough."""
32 | try:
33 | from vimiv.qt.core import PYQT_VERSION_STR
34 | except ImportError as e: # TODO add back test for this
35 | _exit(
36 | f"Error importing a valid Qt python wrapper:\n{e}\n\n"
37 | "Please install / configure a valid wrapper to run vimiv.\n"
38 | )
39 |
40 | pyqt_version = tuple(map(int, PYQT_VERSION_STR.split(".")[:3]))
41 | if pyqt_version < PYQT_REQUIRED_VERSION:
42 | _exit_version("PyQt", PYQT_REQUIRED_VERSION, pyqt_version)
43 |
44 |
45 | def join_version_tuple(version):
46 | return ".".join(map(str, version))
47 |
48 |
49 | def _exit_version(software, required, installed):
50 | """Call exit for out-of-date software."""
51 | # This module needs to work for python < 3.6
52 | # pylint: disable=consider-using-f-string
53 | _exit(
54 | "At least %s %s is required to run vimiv. Using %s.\n"
55 | % (software, join_version_tuple(required), join_version_tuple(installed))
56 | )
57 |
58 |
59 | def _exit(message):
60 | """Write message to stderr and exit with returncode 1."""
61 | sys.stderr.write(message)
62 | sys.stderr.flush()
63 | sys.exit(ERR_CODE)
64 |
65 |
66 | check_python_version()
67 | check_pyqt_version()
68 |
--------------------------------------------------------------------------------
/vimiv/commands/__init__.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Functions to store and run commands."""
4 |
5 | from typing import cast
6 |
7 |
8 | def number_for_command(
9 | number: int = None,
10 | count: int = None,
11 | *,
12 | max_count: int,
13 | number_name: str = "index",
14 | elem_name: str = "element",
15 | ) -> int:
16 | """Return correct number for command given number, optional count and a maximum.
17 |
18 | Count is preferred over the number if it is given. The command expects numbers
19 | indexed from one but returns a number indexed from zero. If the number exceeds the
20 | maximum, the modulo operator is used to reduce it accordingly.
21 | """
22 | if number is None and count is None:
23 | raise ValueError(f"Either {number_name} or count is required")
24 | if max_count <= 0:
25 | raise ValueError(f"No {elem_name} in list")
26 | if count is not None:
27 | number = count
28 | number = cast(int, number) # Ensured by the two tests above
29 | if number > 0:
30 | number -= 1
31 | return number % max_count
32 |
--------------------------------------------------------------------------------
/vimiv/commands/aliases.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Class and functions to add and get aliases.
4 |
5 | Module Attribute:
6 | _aliases: Dictionary storing aliases initialized with defaults.
7 | """
8 |
9 | from typing import Dict, List
10 |
11 | from vimiv import api
12 |
13 | Aliases = Dict[str, str]
14 |
15 |
16 | _aliases: Dict[api.modes.Mode, Aliases] = {mode: {} for mode in api.modes.ALL}
17 | # Add default aliases
18 | _aliases[api.modes.GLOBAL].update({"q": "quit", "mark-print": "print-stdout %m"})
19 | _aliases[api.modes.IMAGE].update(w="write", wq="write && quit")
20 |
21 |
22 | def get(mode: api.modes.Mode) -> Aliases:
23 | """Return all aliases for the mode 'mode'."""
24 | if mode in api.modes.GLOBALS:
25 | return {**_aliases[api.modes.GLOBAL], **_aliases[mode]}
26 | return _aliases[mode]
27 |
28 |
29 | @api.commands.register()
30 | def alias(name: str, command: List[str], mode: str = "global"):
31 | """Add an alias for a command.
32 |
33 | **syntax:** ``:alias name command [--mode=MODE]``
34 |
35 | The command can be a vimiv command like ``quit`` or an external shell
36 | command like ``!gimp``.
37 |
38 | positional arguments:
39 | * ``name``: Name of the newly defined alias.
40 | * ``command``: Name of the command to alias.
41 |
42 | optional arguments:
43 | * ``--mode``: Mode in which the alias is valid. Default: ``global``.
44 | """
45 | assert isinstance(command, list), "Aliases defined as list via nargs='*'"
46 | assert isinstance(mode, str), "Mode must be passed to alias command as string"
47 | commandstr = " ".join(command)
48 | modeobj = api.modes.get_by_name(mode)
49 | if api.commands.exists(name, modeobj):
50 | raise api.commands.CommandError(f"Not overriding default command {name}")
51 | _aliases[modeobj][name] = commandstr
52 |
--------------------------------------------------------------------------------
/vimiv/commands/misccommands.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Miscellaneous commands that don't really fit anywhere."""
4 |
5 | import logging
6 | import time
7 | from typing import List
8 |
9 | from vimiv import api, utils
10 |
11 |
12 | _logger = utils.log.module_logger(__name__)
13 |
14 |
15 | @api.commands.register()
16 | def log(level: str, message: List[str]):
17 | """Log a message with the corresponding log level.
18 |
19 | **syntax:** ``:log level message``
20 |
21 | positional arguments:
22 | * ``level``: Log level of the message (debug, info, warning, error, critical).
23 | * ``message``: Message to log.
24 | """
25 | try:
26 | log_level = getattr(logging, level.upper())
27 | except AttributeError:
28 | raise api.commands.CommandError(f"Unknown log level '{level}'")
29 | utils.log.log(log_level, " ".join(message))
30 |
31 |
32 | @api.commands.register()
33 | def sleep(duration: float):
34 | """Sleep for a given number of seconds.
35 |
36 | **syntax:** ``:sleep duration``
37 |
38 | positional arguments:
39 | * ``duration``: The number of seconds to sleep.
40 | """
41 | _logger.debug("Sleeping for %.2f seconds, good-night :)", duration)
42 | time.sleep(duration)
43 | _logger.debug("Woke up nice and refreshed!")
44 |
--------------------------------------------------------------------------------
/vimiv/completion/__init__.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Modules for command line completion."""
4 |
--------------------------------------------------------------------------------
/vimiv/gui/__init__.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """GUI widgets using Qt5."""
4 |
--------------------------------------------------------------------------------
/vimiv/gui/synchronize.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Helper module to synchronize selection of library and thumbnail mode.
4 |
5 | Module Attributes:
6 | signals: Signals used as synchronization method.
7 | """
8 |
9 |
10 | from vimiv.qt.core import Signal, QObject
11 |
12 |
13 | class _Signals(QObject):
14 | new_library_path_selected = Signal(str)
15 | new_thumbnail_path_selected = Signal(str)
16 |
17 |
18 | signals = _Signals()
19 |
--------------------------------------------------------------------------------
/vimiv/gui/version_popup.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Pop-up window to display version information."""
4 |
5 | from vimiv.qt.widgets import QLabel, QVBoxLayout, QPushButton
6 | from vimiv.qt.gui import QGuiApplication
7 |
8 | import vimiv.version
9 | from vimiv.widgets import PopUp
10 |
11 |
12 | class VersionPopUp(PopUp):
13 | """Pop up that displays version information on initialization.
14 |
15 | Class Attributes:
16 | TITLE: Window title used for the pop up.
17 | """
18 |
19 | TITLE = f"{vimiv.__name__} - version"
20 |
21 | def __init__(self, parent=None):
22 | super().__init__(self.TITLE, parent=parent)
23 | self._init_content()
24 | self.show()
25 |
26 | def _init_content(self):
27 | """Initialize all widgets of the pop-up window."""
28 | layout = QVBoxLayout()
29 | layout.addWidget(QLabel(vimiv.version.detailed_info()))
30 | layout.addWidget(
31 | QLabel("Website: {url}".format(url=vimiv.__url__))
32 | )
33 | button = QPushButton("&Copy version info to clipboard")
34 | button.clicked.connect(self.copy_to_clipboard)
35 | button.setFlat(True)
36 | layout.addWidget(button)
37 | self.setLayout(layout)
38 |
39 | @staticmethod
40 | def copy_to_clipboard() -> None:
41 | """Copy version information to clipboard."""
42 | QGuiApplication.clipboard().setText(vimiv.version.info())
43 |
--------------------------------------------------------------------------------
/vimiv/imutils/__init__.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """`Utilities to load, edit, navigate and save images`.
4 |
5 | The Image Loading Process
6 | ,,,,,,,,,,,,,,,,,,,,,,,,,
7 |
8 | The image loading process is started by emitting the ``load_images`` signal. It is for
9 | example emitted by the library when a new image path was selected, in thumbnail mode
10 | upon selection or by the ``:open`` command. There are a few different cases that are
11 | taken care of:
12 |
13 | * Loading a single path that is already in the filelist
14 | In this case the filelist navigates to the appropriate index and the image is
15 | opened.
16 | * Loading a single path that is not in the filelist
17 | The filelist is populated with all images in the same directory as this path and
18 | the path is opened.
19 | * Loading multiple paths
20 | The filelist is populated with these paths and the first file in the list is
21 | opened.
22 |
23 | To open an image the ``new_image_opened`` signal is emitted with the absolute path to
24 | this image. This signal is accepted by the file handler in
25 | ``vimiv.imutils._file_handler`` which then loads the actual image from disk using
26 | ``QImageReader``. Once the format of the image has been determined, and a displayable Qt
27 | widget has been created, the file handler emits one of:
28 |
29 | * ``pixmap_loaded`` for standard images
30 | * ``movie_loaded`` for animated Gifs
31 | * ``svg_loaded`` for vector graphics
32 |
33 | The image widget in ``vimiv.gui.image`` connects to these signals and displays
34 | the appropriate Qt widget.
35 | """
36 |
37 | from vimiv.imutils import metadata
38 | from vimiv.imutils.edit_handler import EditHandler
39 | from vimiv.imutils.filelist import current, pathlist
40 | from vimiv.imutils.filelist import SignalHandler as _FilelistSignalHandler
41 | from vimiv.imutils._file_handler import ImageFileHandler as _ImageFileHandler
42 |
43 |
44 | def init():
45 | """Initialize the classes needed for imutils."""
46 | _FilelistSignalHandler()
47 | _ImageFileHandler()
48 |
--------------------------------------------------------------------------------
/vimiv/imutils/current_pixmap.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Storage class for the current pixmap."""
4 |
5 | from vimiv.qt.gui import QPixmap
6 |
7 |
8 | class CurrentPixmap:
9 | """Storage class for the current pixmap shared between various edit-related classes.
10 |
11 | We do not use a simple QPixmap as we would have to update various attributes of the
12 | classes that wish to access the pixmap simultaneously. Like this they can all share
13 | this class and access the pixmap through it.
14 |
15 | Attributes:
16 | pixmap: The current, possibly edited, pixmap.
17 | """
18 |
19 | def __init__(self):
20 | self.pixmap = QPixmap()
21 |
22 | @property
23 | def editable(self) -> bool:
24 | """True if the currently opened image is transformable/manipulatable."""
25 | return not self.pixmap.isNull()
26 |
--------------------------------------------------------------------------------
/vimiv/imutils/slideshow.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Module to play a slideshow."""
4 |
5 | from typing import Optional
6 |
7 | from vimiv.qt.core import QTimer
8 |
9 | from vimiv import api
10 |
11 |
12 | class Timer(QTimer):
13 | """Slideshow timer with interval according to the current setting."""
14 |
15 | def __init__(self):
16 | super().__init__()
17 | api.settings.slideshow.delay.changed.connect(self._set_delay)
18 | self._set_delay(api.settings.slideshow.delay.value)
19 |
20 | def _set_delay(self, value: float):
21 | self.setInterval(int(value * 1000))
22 |
23 |
24 | # Create timer and expose a few methods to the public
25 | _timer = Timer()
26 | event = _timer.timeout
27 | stop = _timer.stop
28 | start = _timer.start
29 |
30 |
31 | @api.keybindings.register("ss", "slideshow", mode=api.modes.IMAGE)
32 | @api.commands.register(mode=api.modes.IMAGE, name="slideshow")
33 | def toggle(count: Optional[int] = None):
34 | """Toggle slideshow.
35 |
36 | **count:** Set slideshow delay to count instead.
37 | """
38 | if count is not None:
39 | api.settings.slideshow.delay.value = count
40 | _timer.setInterval(1000 * count)
41 | elif _timer.isActive():
42 | _timer.stop()
43 | else:
44 | _timer.start()
45 |
46 |
47 | @api.status.module("{slideshow-delay}")
48 | def delay() -> str:
49 | """Slideshow delay in seconds if the slideshow is running."""
50 | if _timer.isActive():
51 | interval_seconds = _timer.interval() / 1000
52 | return f"{interval_seconds:.1f}"
53 | return ""
54 |
55 |
56 | @api.status.module("{slideshow-indicator}")
57 | def running_indicator() -> str:
58 | """Indicator if slideshow is running."""
59 | return api.settings.slideshow.indicator.value if _timer.isActive() else ""
60 |
--------------------------------------------------------------------------------
/vimiv/plugins/demo.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Simple hello world greeting as example plugin.
4 |
5 | The plugin defines a new command :hello-world which simply prints a message to the
6 | terminal. The init and cleanup functions also print messages to the terminal without
7 | further usage for demonstration purposes.
8 | """
9 |
10 | from typing import Any
11 |
12 | from vimiv import api
13 |
14 |
15 | @api.commands.register()
16 | def hello_world() -> None:
17 | """Simple dummy function printing 'Hello world'."""
18 | print("Hello world")
19 |
20 |
21 | def init(info: str, *_args: Any, **_kwargs: Any) -> None:
22 | print(f"Initializing demo plugin with '{info}'")
23 |
24 |
25 | def cleanup(*_args: Any, **_kwargs: Any) -> None:
26 | print("Cleaning up demo plugin")
27 |
--------------------------------------------------------------------------------
/vimiv/plugins/metadata.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Metadata plugin wrapping the available backends to only load one."""
4 |
5 | from typing import Any
6 |
7 | from vimiv.plugins import metadata_piexif, metadata_pyexiv2
8 | from vimiv.utils import log
9 | from vimiv.imutils import metadata
10 |
11 | _logger = log.module_logger(__name__)
12 |
13 |
14 | def init(info: str, *_args: Any, **_kwargs: Any) -> None:
15 | """Initialize metadata plugin depending on available backend.
16 |
17 | If any other backend has already been registered, do not register any new one.
18 | """
19 | if metadata.has_metadata_support():
20 | _logger.debug(
21 | "Not loading a default metadata backend, as one has been loaded manually"
22 | )
23 | elif info.lower() == "none":
24 | _logger.debug("Not auto-loading metadata support as per user-request")
25 | elif metadata_pyexiv2.pyexiv2 is not None:
26 | _logger.debug("Auto-loading pyexiv2 metadata plugin")
27 | metadata_pyexiv2.init()
28 | elif metadata_piexif.piexif is not None:
29 | _logger.debug("Auto-loading piexif metadata plugin")
30 | metadata_piexif.init()
31 | else:
32 | _logger.warning(
33 | "Please install either py3exiv2 or piexif for metadata support.
\n"
34 | "For more information see
\n"
35 | "https://karlch.github.io/vimiv-qt/documentation/metadata.html",
36 | )
37 |
--------------------------------------------------------------------------------
/vimiv/qt/core.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | # pylint: disable=missing-module-docstring,wildcard-import,unused-wildcard-import
4 |
5 | from vimiv import qt
6 |
7 |
8 | if qt.USE_PYQT5:
9 | from PyQt5.QtCore import *
10 | elif qt.USE_PYQT6:
11 | from PyQt6.QtCore import *
12 | elif qt.USE_PYSIDE6:
13 | # TODO remove useless-suppression once we add PySide6 back to pylint toxenv
14 | # pylint: disable=no-name-in-module,undefined-variable,useless-suppression
15 | from PySide6.QtCore import *
16 | from PySide6.QtCore import __version__ as PYQT_VERSION_STR
17 |
18 | BoundSignal = SignalInstance
19 | QT_VERSION_STR = qVersion()
20 |
21 | if qt.USE_PYQT: # Signal aliases
22 | # pylint: disable=used-before-assignment
23 | BoundSignal = pyqtBoundSignal
24 | Signal = pyqtSignal
25 | Slot = pyqtSlot
26 |
27 |
28 | class Align:
29 | """Namespace for easier access to the Qt alignment flags."""
30 |
31 | # pylint: disable=used-before-assignment
32 | Center = Qt.AlignmentFlag.AlignCenter
33 | Left = Qt.AlignmentFlag.AlignLeft
34 | Right = Qt.AlignmentFlag.AlignRight
35 | Top = Qt.AlignmentFlag.AlignTop
36 | Bottom = Qt.AlignmentFlag.AlignBottom
37 |
--------------------------------------------------------------------------------
/vimiv/qt/gui.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | # pylint: disable=missing-module-docstring,wildcard-import,unused-wildcard-import
4 |
5 | from vimiv import qt
6 |
7 |
8 | if qt.USE_PYQT5:
9 | from PyQt5.QtGui import *
10 | elif qt.USE_PYQT6:
11 | from PyQt6.QtGui import *
12 | elif qt.USE_PYSIDE6:
13 | from PySide6.QtGui import *
14 |
--------------------------------------------------------------------------------
/vimiv/qt/printsupport.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | # pylint: disable=missing-module-docstring,wildcard-import,unused-wildcard-import
4 |
5 | from vimiv import qt
6 | from vimiv.qt import gui
7 |
8 |
9 | if qt.USE_PYQT5:
10 | from PyQt5.QtPrintSupport import *
11 |
12 | Orientation = QPrinter.Orientation
13 | elif qt.USE_PYQT6:
14 | from PyQt6.QtPrintSupport import *
15 |
16 | Orientation = gui.QPageLayout.Orientation
17 | elif qt.USE_PYSIDE6:
18 | from PySide6.QtPrintSupport import *
19 |
20 | Orientation = gui.QPageLayout.Orientation
21 |
--------------------------------------------------------------------------------
/vimiv/qt/svg.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | # pylint: disable=missing-module-docstring
4 |
5 | from vimiv import qt
6 | from vimiv.utils import lazy
7 |
8 | # Lazy import the module that implements QSvgWidget
9 | if qt.USE_PYQT5:
10 | QtSvg = lazy.import_module("PyQt5.QtSvg", optional=True)
11 | elif qt.USE_PYQT6:
12 | QtSvg = lazy.import_module("PyQt6.QtSvgWidgets", optional=True)
13 | elif qt.USE_PYSIDE6:
14 | QtSvg = lazy.import_module("PySide6.QtSvgWidgets", optional=True)
15 |
--------------------------------------------------------------------------------
/vimiv/qt/widgets.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | # pylint: disable=missing-module-docstring,wildcard-import,unused-wildcard-import
4 |
5 | from vimiv import qt
6 |
7 |
8 | if qt.USE_PYQT5:
9 | from PyQt5.QtWidgets import *
10 | if qt.USE_PYQT6:
11 | from PyQt6.QtWidgets import *
12 | elif qt.USE_PYSIDE6:
13 | from PySide6.QtWidgets import *
14 |
--------------------------------------------------------------------------------
/vimiv/utils/customtypes.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Custom data types."""
4 |
5 | import enum
6 | import typing
7 |
8 | from vimiv import checkversion
9 |
10 |
11 | AnyT = typing.TypeVar("AnyT")
12 | FuncT = typing.TypeVar("FuncT", bound=typing.Callable[..., typing.Any])
13 | FuncNoneT = typing.TypeVar("FuncNoneT", bound=typing.Callable[..., None])
14 | NumberT = typing.TypeVar("NumberT", int, float)
15 |
16 | Number = typing.Union[int, float]
17 | NumberStr = typing.Union[Number, str]
18 | IntStr = typing.Union[int, str]
19 |
20 |
21 | @enum.unique
22 | class Exit(enum.IntEnum):
23 | """Enum class for the different integer exit codes."""
24 |
25 | success = 0
26 | err_exception = 1 # Uncaught exception
27 | err_version = checkversion.ERR_CODE # Unsupported dependency version
28 | err_config = 3 # Critical error when parsing configuration files
29 | err_suicide = 42 # Forceful quit
30 | signal = 128 # Exit by signal + signum
31 |
--------------------------------------------------------------------------------
/vimiv/utils/debug.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Various utility functions for debugging and profiling."""
4 |
5 | import contextlib
6 | import cProfile
7 | import functools
8 | import pstats
9 | import time
10 | from typing import Any, Iterator
11 |
12 | from vimiv.utils.customtypes import FuncT
13 |
14 |
15 | def timed(function: FuncT) -> FuncT:
16 | """Decorator to time a function and log evaluation time."""
17 |
18 | @functools.wraps(function)
19 | def inner(*args: Any, **kwargs: Any) -> Any:
20 | """Wrap decorated function and add timing."""
21 | start = time.time()
22 | return_value = function(*args, **kwargs)
23 | elapsed_in_ms = (time.time() - start) * 1000
24 | print(f"{function.__qualname__}: took {elapsed_in_ms:.3f} ms")
25 | return return_value
26 |
27 | # Mypy seems to disapprove the *args, **kwargs, but we just wrap the function
28 | return inner # type: ignore
29 |
30 |
31 | @contextlib.contextmanager
32 | def profile(amount: int = 15) -> Iterator[None]:
33 | """Contextmanager to profile code sections.
34 |
35 | Starts a cProfile.Profile upon entry, disables it on exit and prints profiling
36 | information.
37 |
38 | Usage:
39 | with profile(amount=10):
40 | # your code to profile here
41 | ...
42 | # This is no longer profiled
43 |
44 | Args:
45 | amount: Number of lines to restrict the output to.
46 | """
47 | cprofile = cProfile.Profile()
48 | cprofile.enable()
49 | yield
50 | cprofile.disable()
51 | stats = pstats.Stats(cprofile)
52 | stats.sort_stats("cumulative").print_stats(amount)
53 | stats.sort_stats("time").print_stats(amount)
54 |
--------------------------------------------------------------------------------
/vimiv/utils/xdg.py:
--------------------------------------------------------------------------------
1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4
2 |
3 | """Functions to help with XDG_USER_* settings."""
4 |
5 | import os
6 |
7 | from vimiv.qt.core import QStandardPaths
8 |
9 | import vimiv
10 |
11 |
12 | basedir = None
13 |
14 |
15 | def _standardpath(
16 | location: QStandardPaths.StandardLocation, name: str, *paths: str
17 | ) -> str:
18 | """Return absolute path to a standard storage directory.
19 |
20 | Args:
21 | location: Location ID according to QStandardPaths.
22 | name: Fallback name to use in case there is a base directory.
23 | paths: Any additional paths passed to os.path.join.
24 | """
25 | if basedir is not None:
26 | return os.path.join(basedir, name, *paths)
27 | return os.path.join(QStandardPaths.writableLocation(location), *paths)
28 |
29 |
30 | def makedirs(*paths: str) -> None:
31 | for path in paths:
32 | os.makedirs(path, mode=0o700, exist_ok=True)
33 |
34 |
35 | def user_data_dir(*paths: str) -> str:
36 | return _standardpath(
37 | QStandardPaths.StandardLocation.GenericDataLocation, "data", *paths
38 | )
39 |
40 |
41 | def user_config_dir(*paths: str) -> str:
42 | return _standardpath(
43 | QStandardPaths.StandardLocation.GenericConfigLocation, "config", *paths
44 | )
45 |
46 |
47 | def user_cache_dir(*paths: str) -> str:
48 | return _standardpath(
49 | QStandardPaths.StandardLocation.GenericCacheLocation, "cache", *paths
50 | )
51 |
52 |
53 | def vimiv_data_dir(*paths: str) -> str:
54 | return user_data_dir(vimiv.__name__, *paths)
55 |
56 |
57 | def vimiv_cache_dir(*paths: str) -> str:
58 | return user_cache_dir(vimiv.__name__, *paths)
59 |
60 |
61 | def vimiv_config_dir(*paths: str) -> str:
62 | return user_config_dir(vimiv.__name__, *paths)
63 |
--------------------------------------------------------------------------------