├── .coveragerc ├── .gitignore ├── .landscape.yaml ├── .noserc ├── .pycodestyle ├── .pydocstylerc ├── .pylintrc ├── .spelling_dict.txt ├── LICENSE ├── Makefile ├── c-lib ├── definitions.h ├── enhance.c ├── enhance.h └── math_func_eval.h ├── config ├── keys.conf └── vimivrc ├── contributing.md ├── icons ├── vimiv.svg ├── vimiv_128x128.png ├── vimiv_16x16.png ├── vimiv_256x256.png ├── vimiv_32x32.png ├── vimiv_512x512.png ├── vimiv_64x64.png ├── vimiv_banner.svg ├── vimiv_banner_400.png └── vimiv_banner_800.png ├── man ├── vimiv.1 └── vimivrc.5 ├── org.karlch.vimiv.gtk.metainfo.xml ├── readme.md ├── scripts ├── generate_manpages.py ├── install_icons.sh ├── remove_icons.sh ├── run_tests.sh └── uninstall_pythonpkg.sh ├── setup.py ├── tests ├── animation_test.py ├── app_test.py ├── commandline_test.py ├── completions_test.py ├── configparser_test.py ├── eventhandler_test.py ├── fail_arguments_test.py ├── fileactions_test.py ├── helpers_test.py ├── image_test.py ├── imageactions_test.py ├── information_test.py ├── invalid_mode_test.py ├── library_test.py ├── log_test.py ├── main_window_test.py ├── manipulate_test.py ├── mark_test.py ├── modeswitch_test.py ├── opening_test.py ├── search_test.py ├── settings_test.py ├── slideshow_test.py ├── statusbar_test.py ├── tags_test.py ├── thumbnail_manager_test.py ├── thumbnail_test.py ├── transform_test.py ├── trash_test.py ├── vimiv_testcase.py └── window_test.py ├── vimiv.desktop ├── vimiv.py └── vimiv ├── __init__.py ├── app.py ├── commandline.py ├── commands.py ├── completions.py ├── config_parser.py ├── eventhandler.py ├── exceptions.py ├── fileactions.py ├── helpers.py ├── image.py ├── image_enhance.py ├── imageactions.py ├── information.py ├── library.py ├── log.py ├── main_window.py ├── manipulate.py ├── mark.py ├── settings.py ├── slideshow.py ├── statusbar.py ├── tags.py ├── thumbnail.py ├── thumbnail_manager.py ├── transform.py ├── trash_manager.py ├── vimiv └── window.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | include = *vimiv* 3 | omit = 4 | */__init__* 5 | 6 | [report] 7 | exclude_lines = 8 | pragma: no cover 9 | if not running_tests: 10 | precision = 2 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | vimiv.egg-info 3 | usr 4 | install_log.txt 5 | TODO 6 | tests/htmlcov 7 | *__pycache__* 8 | *.coverage 9 | coverage.xml 10 | # These are the images used for testing 11 | tests/vimiv/ 12 | # I symlink this to the file in ../build/lib.linux-*/vimiv/ so I can run vimiv 13 | # via ./vimiv.py without having to re-install 14 | vimiv/*.so 15 | -------------------------------------------------------------------------------- /.landscape.yaml: -------------------------------------------------------------------------------- 1 | max-line-length: 80 2 | python-targets: 3 | - 3 4 | ignore-paths: 5 | - vimiv/__init__.py 6 | - vimiv/vimiv 7 | - vimiv.py 8 | -------------------------------------------------------------------------------- /.noserc: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity=1 3 | with-coverage=1 4 | cover-erase=1 5 | cover-html=1 6 | cover-html-dir=htmlcov 7 | cover-xml=1 8 | cover-xml-file=../coverage.xml 9 | where=tests 10 | -------------------------------------------------------------------------------- /.pycodestyle: -------------------------------------------------------------------------------- 1 | [pycodestyle] 2 | ignore = E402,W503 3 | max-line-length = 80 4 | -------------------------------------------------------------------------------- /.pydocstylerc: -------------------------------------------------------------------------------- 1 | [pydocstyle] 2 | # Disabled: 3 | # D102-105: Check for docstrings is handled by pylint. 4 | # D203: No blank lines before class docstrings. 5 | # D213: Multi-line docstring summary should start at the second line, we use 6 | # D212 Muli-line docstring summary should start at the first line 7 | ignore = D102,D103,D104,D105,D203,D213 8 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | # vim: ft=dosini fileencoding=utf-8: 2 | 3 | [MESSAGES CONTROL] 4 | enable=all 5 | disable=no-self-use, 6 | fixme, 7 | global-statement, 8 | locally-disabled, 9 | too-many-ancestors, 10 | too-few-public-methods, 11 | too-many-public-methods, 12 | too-many-instance-attributes, 13 | blacklisted-name, 14 | catching-non-exception, 15 | file-ignored, 16 | wrong-import-position, 17 | suppressed-message, 18 | too-many-return-statements, 19 | unused-argument, 20 | arguments-differ, 21 | 22 | [BASIC] 23 | function-rgx=[a-z_][a-z0-9_]{2,50}$ 24 | const-rgx=[A-Za-z_][A-Za-z0-9_]{0,30}$ 25 | method-rgx=[a-z_][A-Za-z0-9_]{2,50}$ 26 | attr-rgx=[a-z_][a-z0-9_]{0,30}$ 27 | argument-rgx=[a-z_][a-z0-9_]{0,30}$ 28 | variable-rgx=[a-z_][a-z0-9_]{0,30}$ 29 | docstring-min-length=3 30 | no-docstring-rgx=(^_|^main$) 31 | 32 | [FORMAT] 33 | max-line-length=80 34 | max-module-lines=1000 35 | ignore-long-lines=( 8 | 9 | #include "enhance.h" 10 | #include "math_func_eval.h" 11 | 12 | /***************************** 13 | * Generate python functions * 14 | *****************************/ 15 | 16 | static PyObject * 17 | enhance_bc(PyObject *self, PyObject *args) 18 | { 19 | /* Receive arguments from python */ 20 | PyObject *py_data; 21 | U_SHORT has_alpha; 22 | float brightness; 23 | float contrast; 24 | if (!PyArg_ParseTuple(args, "Oiff", 25 | &py_data, &has_alpha, &brightness, &contrast)) 26 | return NULL; 27 | 28 | /* Convert python bytes to U_CHAR* for pixel data */ 29 | if (!PyBytes_Check(py_data)) { 30 | PyErr_SetString(PyExc_TypeError, "Expected bytes"); 31 | return NULL; 32 | } 33 | U_CHAR* data = (U_CHAR*) PyBytes_AsString(py_data); 34 | const int size = PyBytes_Size(py_data); 35 | 36 | /* Run the C function to enhance brightness and contrast */ 37 | char *updated_data = PyMem_Malloc(size); 38 | enhance_bc_c(data, size, has_alpha, brightness, contrast, updated_data); 39 | 40 | /* Return python bytes of updated data and free memory */ 41 | PyObject *py_updated_data = PyBytes_FromStringAndSize(updated_data, size); 42 | PyMem_Free(updated_data); 43 | return py_updated_data; 44 | } 45 | 46 | /***************************** 47 | * Initialize python module * 48 | *****************************/ 49 | 50 | static PyMethodDef EnhanceMethods[] = { 51 | {"enhance_bc", enhance_bc, METH_VARARGS, "Enhance brightness and contrast"}, 52 | {NULL, NULL, 0, NULL} /* Sentinel */ 53 | }; 54 | 55 | static struct PyModuleDef enhance = { 56 | PyModuleDef_HEAD_INIT, 57 | "_image_enhance", /* Name */ 58 | NULL, /* Documentation */ 59 | -1, /* Keep state in global variables */ 60 | EnhanceMethods 61 | }; 62 | 63 | PyMODINIT_FUNC 64 | PyInit__image_enhance(void) 65 | { 66 | PyObject *m = PyModule_Create(&enhance); 67 | if (m == NULL) 68 | return NULL; 69 | return m; 70 | } 71 | 72 | /*************************************** 73 | * Actual C functions doing the math * 74 | ***************************************/ 75 | 76 | /* Make sure value stays between 0 and 255 */ 77 | static inline U_CHAR clamp(float value) 78 | { 79 | if (value < 0) 80 | return 0; 81 | else if (value > 1) 82 | return 255; 83 | return (U_CHAR) (value * 255); 84 | } 85 | 86 | /* Enhance brightness using the GIMP algorithm. */ 87 | static inline float enhance_brightness(float value, float factor) 88 | { 89 | if (factor < 0) 90 | return value * (1 + factor); 91 | return value + (1 - value) * factor; 92 | } 93 | 94 | /* Enhance contrast using the GIMP algorithm: 95 | value = (value - 0.5) * (tan ((factor + 1) * PI/4) ) + 0.5; */ 96 | static inline float enhance_contrast(float value, float factor) 97 | { 98 | U_CHAR tan_pos = (U_CHAR) (factor * 127 + 127); 99 | return (value - 0.5) * (TAN[tan_pos]) + 0.5; 100 | } 101 | 102 | /* Return the ARGB content of one pixel at index in data. */ 103 | static inline void set_pixel_content(U_CHAR* data, int index, U_CHAR* content) 104 | { 105 | for (U_SHORT i = 0; i < 4; i++) 106 | content[i] = data[index + i]; 107 | } 108 | 109 | /* Read pixel data of specific size and enhance brightness and contrast 110 | according to the two functions above. Change the values in updated_data which 111 | is of type char* so one pixel is equal to one byte allowing to create a 112 | python memoryview obect directly from memory. */ 113 | void enhance_bc_c(U_CHAR* data, const int size, U_SHORT has_alpha, 114 | float brightness, float contrast, char* updated_data) 115 | { 116 | for (int pixel = 0; pixel < size; pixel++) { 117 | /* Skip alpha channel */ 118 | if (has_alpha && pixel % 4 == ALPHA_CHANNEL) 119 | updated_data[pixel] = data[pixel]; 120 | else { 121 | float value = (float) data[pixel]; 122 | value /= 255; 123 | value = enhance_brightness(value, brightness); 124 | value = enhance_contrast(value, contrast); 125 | value = clamp(value); 126 | updated_data[pixel] = value; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /c-lib/enhance.h: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * C extension for vimiv 3 | * simple add-on to enhance brightness and contrast of an image on the pixel 4 | * scale. 5 | *******************************************************************************/ 6 | 7 | #include "definitions.h" 8 | 9 | /********************************** 10 | * Plain C function declarations * 11 | **********************************/ 12 | static inline U_CHAR clamp(float value); 13 | static inline float enhance_brightness(float value, float factor); 14 | static inline float enhance_contrast(float value, float factor); 15 | static void enhance_bc_c(U_CHAR* data, const int size, U_SHORT has_alpha, 16 | float brightness, float contrast, char* updated_data); 17 | -------------------------------------------------------------------------------- /c-lib/math_func_eval.h: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * C extension for vimiv 3 | * values of used math functions as array to greatly speed up computation 4 | *******************************************************************************/ 5 | 6 | /* Corresponds to tan(0) ... tan(pi/2) */ 7 | const float TAN[256] = { 8 | 0.0, 9 | 0.0061600635108754595, 10 | 0.012320594543743586, 11 | 0.018482060762539353, 12 | 0.024644930115173925, 13 | 0.030809670975751834, 14 | 0.0369767522870633, 15 | 0.04314664370344389, 16 | 0.04931981573409422, 17 | 0.05549673988695261, 18 | 0.06167788881321465, 19 | 0.06786373645259393, 20 | 0.07405475817941934, 21 | 0.08025143094966514, 22 | 0.08645423344901086, 23 | 0.09266364624202977, 24 | 0.09888015192260528, 25 | 0.10510423526567646, 26 | 0.1113363833804153, 27 | 0.11757708586493976, 28 | 0.12382683496266847, 29 | 0.13008612572042522, 30 | 0.13635545614840253, 31 | 0.14263532738209653, 32 | 0.14892624384632747, 33 | 0.15522871342146186, 34 | 0.16154324761195613, 35 | 0.16787036171734326, 36 | 0.1742105750057869, 37 | 0.1805644108903313, 38 | 0.18693239710797716, 39 | 0.19331506590171862, 40 | 0.19971295420567844, 41 | 0.20612660383348286, 42 | 0.2125565616700221, 43 | 0.2190033798667458, 44 | 0.22546761604064658, 45 | 0.23194983347709136, 46 | 0.23845060133666188, 47 | 0.24497049486617425, 48 | 0.25151009561404913, 49 | 0.2580699916502126, 50 | 0.2646507777907111, 51 | 0.2712530558272321, 52 | 0.27787743476172555, 53 | 0.28452453104633185, 54 | 0.29119496882882373, 55 | 0.2978893802037821, 56 | 0.3046084054697286, 57 | 0.3113526933924502, 58 | 0.3181229014747542, 59 | 0.3249196962329063, 60 | 0.3317437534800087, 61 | 0.33859575861658747, 62 | 0.34547640692866777, 63 | 0.3523864038936257, 64 | 0.3593264654941155, 65 | 0.36629731854038594, 66 | 0.37329970100130505, 67 | 0.38033436234443346, 68 | 0.38740206388549153, 69 | 0.3945035791475848, 70 | 0.4016396942305648, 71 | 0.4088112081909162, 72 | 0.41601893343257995, 73 | 0.423263696109136, 74 | 0.4305463365377879, 75 | 0.43786770962560984, 76 | 0.4452286853085361, 77 | 0.4526301490035913, 78 | 0.46007300207488366, 79 | 0.46755816231390296, 80 | 0.47508656443469105, 81 | 0.4826591605844736, 82 | 0.49027692087037017, 83 | 0.4979408339028262, 84 | 0.5056519073564383, 85 | 0.5134111685488736, 86 | 0.5212196650386156, 87 | 0.5290784652423032, 88 | 0.5369886590724612, 89 | 0.54495135859646, 90 | 0.5529676987175792, 91 | 0.561038837879088, 92 | 0.5691659587923035, 93 | 0.5773502691896257, 94 | 0.5855930026036005, 95 | 0.5938954191731078, 96 | 0.6022588064778285, 97 | 0.6106844804021931, 98 | 0.6191737860300802, 99 | 0.627728098571587, 100 | 0.6363488243232656, 101 | 0.645037401663282, 102 | 0.6537953020830297, 103 | 0.6626240312568045, 104 | 0.6715251301512302, 105 | 0.6805001761762068, 106 | 0.689550784379245, 107 | 0.698678608685144, 108 | 0.7078853431830762, 109 | 0.7171727234632397, 110 | 0.7265425280053609, 111 | 0.7359965796214442, 112 | 0.7455367469552964, 113 | 0.7551649460414834, 114 | 0.7648831419265244, 115 | 0.7746933503552735, 116 | 0.7845976395256096, 117 | 0.7945981319147116, 118 | 0.8046970061803917, 119 | 0.8148964991411393, 120 | 0.8251989078387428, 121 | 0.8356065916875675, 122 | 0.8461219747147974, 123 | 0.8567475478962048, 124 | 0.867485871592263, 125 | 0.8783395780897018, 126 | 0.8893113742539016, 127 | 0.9004040442978399, 128 | 0.9116204526736446, 129 | 0.9229635470931613, 130 | 0.9344363616843397, 131 | 0.9460420202906452, 132 | 0.9577837399211475, 133 | 0.9696648343594084, 134 | 0.9816887179397932, 135 | 0.9938589095003659, 136 | 1.0061790365221168, 137 | 1.0186528394648717, 138 | 1.0312841763109122, 139 | 1.0440770273280353, 140 | 1.0570355000645506, 141 | 1.0701638345895277, 142 | 1.0834664089924915, 143 | 1.096947745157704, 144 | 1.1106125148291928, 145 | 1.124465545983781, 146 | 1.138511829530552, 147 | 1.1527565263564559, 148 | 1.1672049747391287, 149 | 1.1818626981494842, 150 | 1.1967354134682304, 151 | 1.2118290396421805, 152 | 1.2271497068081045, 153 | 1.2427037659138829, 154 | 1.258497798868894, 155 | 1.2745386292579577, 156 | 1.290833333655699, 157 | 1.3073892535809886, 158 | 1.3242140081341474, 159 | 1.3413155073628606, 160 | 1.3587019664063444, 161 | 1.3763819204711734, 162 | 1.3943642406964145, 163 | 1.4126581509703273, 164 | 1.4312732457659159, 165 | 1.4502195090681118, 166 | 1.4695073344713765, 167 | 1.4891475465330626, 168 | 1.5091514234750762, 169 | 1.5295307213342493, 170 | 1.5502976996704645, 171 | 1.571465148951071, 172 | 1.5930464197405343, 173 | 1.6150554538357325, 174 | 1.63750681749994, 175 | 1.6604157369624346, 176 | 1.6837981363660277, 177 | 1.707670678361776, 178 | 1.7320508075688767, 179 | 1.7569567971385187, 180 | 1.7824077986834748, 181 | 1.8084238958607535, 182 | 1.8350261619230246, 183 | 1.8622367215860702, 184 | 1.890078817594717, 185 | 1.9185768824089033, 186 | 1.947756615475353, 187 | 1.9776450665993264, 188 | 2.008270725985793, 189 | 2.039663621580917, 190 | 2.0718554244138967, 191 | 2.104879562716969, 192 | 2.1387713456890376, 193 | 2.1735680978672924, 194 | 2.209309305182994, 195 | 2.246036773904215, 196 | 2.283794803811933, 197 | 2.3226303771190784, 198 | 2.362593364827953, 199 | 2.4037367524333115, 200 | 2.4461168871206596, 201 | 2.489793748886635, 202 | 2.5348312483266398, 203 | 2.581297554200899, 204 | 2.629265454311996, 205 | 2.678812753714218, 206 | 2.7300227148393543, 207 | 2.7829845447784765, 208 | 2.8377939357213924, 209 | 2.894553665444584, 210 | 2.953374265778564, 211 | 3.0143747682057294, 212 | 3.0776835371752527, 213 | 3.1434392034154084, 214 | 3.2117917115286727, 215 | 3.2829034985358527, 216 | 3.3569508228722795, 217 | 3.434125266730965, 218 | 3.5146354387177947, 219 | 3.598708908686593, 220 | 3.686594412550786, 221 | 3.7785643720677498, 222 | 3.8749177833717194, 223 | 3.975983538786189, 224 | 4.08212425968398, 225 | 4.193740734535314, 226 | 4.311277076638924, 227 | 4.435226741474583, 228 | 4.566139575601334, 229 | 4.704630109478456, 230 | 4.851387358071642, 231 | 5.007186459072304, 232 | 5.172902563674985, 233 | 5.349527505509771, 234 | 5.53818991831876, 235 | 5.740179664562731, 236 | 5.95697769260651, 237 | 6.190292784023414, 238 | 6.4421071202522775, 239 | 6.714733240918031, 240 | 7.010885860844025, 241 | 7.333773273521587, 242 | 7.687214869855916, 243 | 8.075793912535037, 244 | 8.505058554935575, 245 | 8.981789866329589, 246 | 9.514364454222587, 247 | 10.11325307006721, 248 | 10.791718657261582, 249 | 11.566813562574485, 250 | 12.460836998996488, 251 | 13.503521240015477, 252 | 14.73541028349576, 253 | 16.21326571388328, 254 | 18.019076472546086, 255 | 20.275825955868456, 256 | 23.176773768852335, 257 | 27.04401923231791, 258 | 32.45734109874216, 259 | 40.57629684185185, 260 | 54.1065205253986, 261 | 81.16491427824798, 262 | 162.33598862000602, 263 | 1.633123935319537e+16 264 | }; 265 | -------------------------------------------------------------------------------- /config/keys.conf: -------------------------------------------------------------------------------- 1 | # Keybinding file for the vimiv image viewer 2 | # Modifiers: Shift (through Shift+), Ctrl (through ^), Alt (through Alt+) 3 | # Please refer to vimivrc(5) for further information 4 | 5 | [IMAGE] ######################################################################## 6 | a: autorotate 7 | Shift+w: center 8 | Escape: clear_status 9 | colon: command 10 | Shift+y: copy_abspath 11 | y: copy_basename 12 | x: delete 13 | g: first 14 | e: fit_horiz 15 | Shift+e: fit_vert 16 | underscore: flip 0 17 | bar: flip 1 18 | Shift+o: focus_library 19 | f: fullscreen 20 | Shift+g: last 21 | o: library 22 | c: manipulate 23 | m: mark 24 | Shift+m: mark_toggle 25 | u: move_up 26 | n: next 27 | p: prev 28 | ^q: q 29 | q: q 30 | less: rotate 1 31 | greater: rotate 3 32 | h: scroll h 33 | Left: scroll h 34 | Shift+h: scroll H 35 | j: scroll j 36 | Down: scroll j 37 | Shift+j: scroll J 38 | k: scroll k 39 | Up: scroll k 40 | Shift+k: scroll K 41 | l: scroll l 42 | Right: scroll l 43 | Shift+l: scroll L 44 | slash: search 45 | Shift+n: search_next 46 | Shift+p: search_prev 47 | space: set play_animations! 48 | r: set rescale_svg! 49 | b: set display_bar! 50 | s: slideshow 51 | comma: set slideshow_delay -0.2 52 | period: set slideshow_delay +0.2 53 | t: thumbnail 54 | plus: zoom_in 55 | minus: zoom_out 56 | w: zoom_to 57 | 58 | Button2: library 59 | Button1: next 60 | Button3: prev 61 | 62 | [THUMBNAIL] #################################################################### 63 | a: autorotate 64 | Escape: clear_status 65 | colon: command 66 | Shift+y: copy_abspath 67 | y: copy_basename 68 | x: delete 69 | g: first 70 | underscore: flip 0 71 | bar: flip 1 72 | Shift+o: focus_library 73 | f: fullscreen 74 | Shift+g: last 75 | o: library 76 | m: mark 77 | Shift+m: mark_toggle 78 | u: move_up 79 | ^q: q 80 | q: q 81 | less: rotate 1 82 | greater: rotate 3 83 | h: scroll h 84 | Left: scroll h 85 | Shift+h: scroll H 86 | j: scroll j 87 | Down: scroll j 88 | Shift+j: scroll J 89 | k: scroll k 90 | Up: scroll k 91 | Shift+k: scroll K 92 | l: scroll l 93 | Right: scroll l 94 | Shift+l: scroll L 95 | slash: search 96 | Shift+n: search_next 97 | Shift+p: search_prev 98 | r: set rescale_svg! 99 | b: set display_bar! 100 | comma: set slideshow_delay -0.2 101 | period: set slideshow_delay +0.2 102 | t: thumbnail 103 | plus: zoom_in 104 | minus: zoom_out 105 | 106 | [LIBRARY] ###################################################################### 107 | a: autorotate 108 | Shift+w: center 109 | Escape: clear_status 110 | colon: command 111 | Shift+y: copy_abspath 112 | y: copy_basename 113 | x: delete 114 | g: first_lib 115 | e: fit_horiz 116 | Shift+e: fit_vert 117 | underscore: flip 0 118 | bar: flip 1 119 | f: fullscreen 120 | Shift+l: set library_width +20 121 | Shift+g: last_lib 122 | o: library 123 | m: mark 124 | Shift+m: mark_toggle 125 | u: move_up 126 | n: next 127 | p: prev 128 | ^q: q 129 | q: q 130 | less: rotate 1 131 | greater: rotate 3 132 | h: scroll_lib h 133 | Left: scroll_lib h 134 | j: scroll_lib j 135 | Down: scroll_lib j 136 | k: scroll_lib k 137 | Up: scroll_lib k 138 | l: scroll_lib l 139 | Right: scroll_lib l 140 | slash: search 141 | Shift+n: search_next 142 | Shift+p: search_prev 143 | r: set rescale_svg! 144 | ^h: set show_hidden! 145 | b: set display_bar! 146 | Shift+h: set library_width -20 147 | comma: set slideshow_delay -0.2 148 | period: set slideshow_delay +0.2 149 | t: thumbnail 150 | Shift+o: unfocus_library 151 | plus: zoom_in 152 | minus: zoom_out 153 | w: zoom_to 154 | 155 | Button3: move_up 156 | 157 | [MANIPULATE] ################################################################### 158 | Space: accept_changes 159 | Return: accept_changes 160 | a: autorotate 161 | Shift+w: center 162 | colon: command 163 | Shift+y: copy_abspath 164 | y: copy_basename 165 | Escape: discard_changes 166 | w: zoom_to 167 | e: fit_horiz 168 | Shift+e: fit_vert 169 | underscore: flip 0 170 | bar: flip 1 171 | b: focus_slider bri 172 | c: focus_slider con 173 | s: focus_slider sat 174 | f: fullscreen 175 | m: mark 176 | Shift+m: mark_toggle 177 | less: rotate 1 178 | greater: rotate 3 179 | h: slider -1 180 | l: slider +1 181 | Shift+h: slider -10 182 | Shift+l: slider +10 183 | 184 | [COMMAND] ###################################################################### 185 | Tab: complete 186 | Shift+Tab: complete_inverse 187 | Escape: discard_command 188 | ^n: history_down 189 | Down: history_down 190 | ^p: history_up 191 | Up: history_up 192 | ^q: q 193 | 194 | # vim:ft=dosini 195 | -------------------------------------------------------------------------------- /config/vimivrc: -------------------------------------------------------------------------------- 1 | # Configuration file for the vimiv image viewer 2 | # Please refer to vimivrc(5) for further information 3 | 4 | [GENERAL] ###################################################################### 5 | start_fullscreen: no 6 | start_slideshow: no 7 | slideshow_delay: 2 8 | shuffle: no 9 | display_bar: yes 10 | default_thumbsize: (128, 128) 11 | geometry: 800x600 12 | recursive: no 13 | rescale_svg: yes 14 | overzoom: 1 15 | search_case_sensitive: yes 16 | incsearch: yes 17 | copy_to_primary: no 18 | commandline_padding: 6 19 | thumb_padding: 10 20 | completion_height: 200 21 | play_animations: yes 22 | 23 | [LIBRARY] ###################################################################### 24 | start_show_library: no 25 | library_width: 300 26 | expand_lib: yes 27 | border_width: 0 28 | markup: 29 | show_hidden: no 30 | desktop_start_dir: ~ 31 | file_check_amount: 30 32 | tilde_in_statusbar: yes 33 | 34 | [EDIT] ######################################################################### 35 | autosave_images: yes 36 | 37 | [ALIASES] ###################################################################### 38 | 39 | # vim:ft=config 40 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | ### Contributing to vimiv 2 | 3 | --- 4 | > :construction: **NOTE:** The future of vimiv is in the 5 | > [Qt port](https://github.com/karlch/vimiv-qt) as discussed in 6 | > [this issue](https://github.com/karlch/vimiv/issues/61). New features will only be 7 | > implemented there and many improvements have already been made. Sticking with this 8 | > deprecated version is only recommended if you require a more stable software. In case 9 | > you miss anything in the Qt port, please 10 | > [open an issue](https://github.com/karlch/vimiv-qt/issues). Check the 11 | > [roadmap](https://karlch.github.io/vimiv-qt/roadmap.html) for more details. 12 | --- 13 | 14 | Therefore, if you wish to implement a new feature, please consider doing this in the 15 | [Qt port](https://github.com/karlch/vimiv-qt). You can find some general information 16 | [on its website](https://karlch.github.io/vimiv-qt/) 17 | as well as some helpful 18 | [starting points for contributing there](https://karlch.github.io/vimiv-qt/documentation/contributing.html). 19 | 20 | ### Reporting bugs 21 | If possible, please reproduce the bug running with --debug and include the 22 | content of the created logfile in $XDG_DATA_HOME/vimiv/vimiv.log. 23 | 24 | Vimiv tries to maintain backward compatibility with the latest Ubuntu LTS 25 | version. Supporting older systems is not planned and things will almost 26 | certainly break on them. 27 | -------------------------------------------------------------------------------- /icons/vimiv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 26 | 55 | 59 | 63 | 67 | 71 | 76 | 80 | 84 | 85 | 87 | 88 | 90 | image/svg+xml 91 | 93 | 94 | 95 | 96 | 97 | 103 | 110 | 111 | 116 | 124 | 125 | 131 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /icons/vimiv_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlch/vimiv/acbb83e003805e5304131be1f73d7f66528606d6/icons/vimiv_128x128.png -------------------------------------------------------------------------------- /icons/vimiv_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlch/vimiv/acbb83e003805e5304131be1f73d7f66528606d6/icons/vimiv_16x16.png -------------------------------------------------------------------------------- /icons/vimiv_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlch/vimiv/acbb83e003805e5304131be1f73d7f66528606d6/icons/vimiv_256x256.png -------------------------------------------------------------------------------- /icons/vimiv_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlch/vimiv/acbb83e003805e5304131be1f73d7f66528606d6/icons/vimiv_32x32.png -------------------------------------------------------------------------------- /icons/vimiv_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlch/vimiv/acbb83e003805e5304131be1f73d7f66528606d6/icons/vimiv_512x512.png -------------------------------------------------------------------------------- /icons/vimiv_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlch/vimiv/acbb83e003805e5304131be1f73d7f66528606d6/icons/vimiv_64x64.png -------------------------------------------------------------------------------- /icons/vimiv_banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 29 | 34 | 35 | 36 | 59 | 64 | 65 | 67 | 68 | 70 | image/svg+xml 71 | 73 | 74 | 75 | 76 | 77 | 83 | vimiv 101 | 102 | 108 | 114 | an image viewer with vim-like keybindings 140 | 149 | 157 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /icons/vimiv_banner_400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlch/vimiv/acbb83e003805e5304131be1f73d7f66528606d6/icons/vimiv_banner_400.png -------------------------------------------------------------------------------- /icons/vimiv_banner_800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlch/vimiv/acbb83e003805e5304131be1f73d7f66528606d6/icons/vimiv_banner_800.png -------------------------------------------------------------------------------- /man/vimiv.1: -------------------------------------------------------------------------------- 1 | .TH VIMIV 1 "May 2017" Linux vimiv 2 | .SH NAME 3 | .PP 4 | vimiv \- an image viewer with vim\-like keybindings 5 | .SH SYNOPSIS 6 | .PP 7 | \fB\fCvimiv\fR 8 | [\fB\fC\-bBfFhlLrRsSv\fR] 9 | [\fB\fC\-\-start\-from\-desktop\fR] 10 | [\fB\fC\-\-slideshow\fR] 11 | [\fB\fC\-\-slideshow\-delay\fR \fISLIDESHOW\-DELAY\fP] 12 | [\fB\fC\-g\fR, \fB\fC\-\-geometry\fR \fIGEOMETRY\fP] 13 | [\fB\fC\-\-temp\-basedir\fR] 14 | [\fB\fC\-\-config\fR \fIFILE\fP] 15 | [\fB\fC\-\-debug\fR] 16 | [\fIFILE\fP] 17 | \&... 18 | .SH DESCRIPTION 19 | .PP 20 | Vimiv is an image viewer with vim\-like keybindings. It is written in 21 | python3 using the Gtk3 toolkit. Some of the features are: 22 | .RS 23 | .IP \(bu 2 24 | Thumbnail mode 25 | .IP \(bu 2 26 | Simple library browser 27 | .IP \(bu 2 28 | Basic image editing 29 | .IP \(bu 2 30 | Command line with tab completion 31 | .RE 32 | .PP 33 | The complete documentation is not included in this manpage but is available at: 34 | .PP 35 | \[la]http://karlch.github.io/vimiv/documentation\[ra] 36 | .SH OPTIONS 37 | .TP 38 | \fB\fC\-b\fR, \fB\fC\-\-bar\fR 39 | display the statusbar 40 | .TP 41 | \fB\fC\-f\fR, \fB\fC\-\-fullscreen\fR 42 | start in fullscreen 43 | .TP 44 | \fB\fC\-h\fR, \fB\fC\-\-help\fR 45 | display a simple help text 46 | .TP 47 | \fB\fC\-l\fR, \fB\fC\-\-library\fR 48 | display the library 49 | .TP 50 | \fB\fC\-r\fR, \fB\fC\-\-recursive\fR 51 | search the directory recursively for images 52 | .TP 53 | \fB\fC\-s\fR, \fB\fC\-\-shuffle\fR 54 | shuffle the filelist 55 | .TP 56 | \fB\fC\-v\fR, \fB\fC\-\-version\fR 57 | show version information and exit 58 | .TP 59 | \fB\fC\-\-start\-from\-desktop\fR 60 | start using the desktop_start_dir as path 61 | .TP 62 | \fB\fC\-\-slideshow\fR 63 | start in slideshow mode 64 | .TP 65 | \fB\fC\-\-slideshow\-delay\fR \fISLIDESHOW_DELAY\fP 66 | set the slideshow delay 67 | .TP 68 | \fB\fC\-g\fR, \fB\fC\-\-geometry\fR \fIGEOMETRY\fP 69 | set the starting geometry 70 | .TP 71 | \fB\fC\-\-temp\-basedir\fR 72 | use a temporary basedir 73 | .TP 74 | \fB\fC\-\-config\fR \fIFILE\fP 75 | use FILE as local configuration file instead of 76 | $XDG_CONFIG_HOME/vimiv/vimivrc and ~/.vimiv/vimivrc. 77 | .TP 78 | \fB\fC\-\-debug\fR 79 | run in debug mode 80 | .PP 81 | All capitals negate the setting, so e.g. \-B means do not display the statusbar. 82 | For the long version prepend no\-, e.g. \-\-no\-bar. 83 | .SH BUGS 84 | .PP 85 | Probably. Please contact me under or, even 86 | better, open an issue on the github homepage. 87 | .SH SEE ALSO 88 | .PP 89 | .BR vimivrc (5) 90 | .SH THANKS TO 91 | .PP 92 | James Campos, author of Pim \[la]https://github.com/Narrat/Pim\[ra] upon which vimiv is 93 | built. 94 | .PP 95 | Bert Muennich, author of sxiv \[la]https://github.com/muennich/sxiv\[ra] which inspired 96 | many of the features of vimiv. 97 | .PP 98 | Anyone who has contributed or reported bugs. 99 | .SH RESOURCES 100 | .PP 101 | \fBWebsite\fP 102 | \[la]https://karlch.github.io/vimiv/\[ra] 103 | .PP 104 | \fBGithub\fP 105 | \[la]https://github.com/karlch/vimiv/\[ra] 106 | -------------------------------------------------------------------------------- /org.karlch.vimiv.gtk.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.karlch.vimiv.gtk 5 | CC0-1.0 6 | MIT 7 | Vimiv 8 | An image viewer with vim-like keybindings 9 | 10 |

11 | Vimiv is an image viewer with vim-like keybindings. It is written in python3 12 | using the Gtk3 toolkit. Some of the features are: 13 |

14 |
    15 |
  • Thumbnail mode
  • 16 |
  • Simple library browser
  • 17 |
  • Basic image editing
  • 18 |
  • Command line with tab completion
  • 19 |
20 |
21 | 22 | 23 | http://karlch.github.io/vimiv/images/screenshots/library.png 24 | 25 | 26 | http://karlch.github.io/vimiv/images/screenshots/thumbnail.png 27 | 28 | 29 | http://karlch.github.io/vimiv/images/screenshots/commandline.png 30 | 31 | 32 | http://karlch.github.io/vimiv 33 | ankursinha AT fedoraproject.org 34 |
35 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![vimiv banner](https://raw.githubusercontent.com/karlch/vimiv/master/icons/vimiv_banner_400.png) 2 | 3 | [![License badge](https://img.shields.io/github/license/karlch/vimiv.svg)](https://raw.githubusercontent.com/karlch/vimiv/master/LICENSE) 4 | [![Coverage](https://codecov.io/gh/karlch/vimiv/branch/master/graph/badge.svg)](https://codecov.io/gh/karlch/vimiv) 5 | [![Code Health](https://landscape.io/github/karlch/vimiv/master/landscape.svg?style=flat)](https://landscape.io/github/karlch/vimiv/master) 6 | [![Version badge](https://img.shields.io/github/tag/karlch/vimiv.svg)](https://github.com/karlch/vimiv/releases) 7 | 8 | --- 9 | > :construction: **NOTE:** The future of vimiv is in the 10 | > [Qt port](https://github.com/karlch/vimiv-qt) as discussed in 11 | > [this issue](https://github.com/karlch/vimiv/issues/61). New features will only be 12 | > implemented there and many improvements have already been made. Sticking with this 13 | > deprecated version is only recommended if you require a more stable software. In case 14 | > you miss anything in the Qt port, please 15 | > [open an issue](https://github.com/karlch/vimiv-qt/issues). Check the 16 | > [roadmap](https://karlch.github.io/vimiv-qt/roadmap.html) and 17 | > [migrating](https://karlch.github.io/vimiv-qt/documentation/migrating.html) 18 | > for more details. 19 | --- 20 | 21 | #### Version 0.9.2 (unreleased) 22 | 23 | [Releases](https://github.com/karlch/vimiv/releases "releases") 24 | | 25 | [Website](http://karlch.github.io/vimiv/ "website") 26 | | 27 | [Documentation](http://karlch.github.io/vimiv/documentation "website") 28 | | 29 | [Changelog](http://karlch.github.io/vimiv/changelog "changelog") 30 | 31 | Vimiv is an image viewer with vim-like keybindings. It is written in python3 32 | using the Gtk3 toolkit. Some of the features are: 33 | * Thumbnail mode 34 | * Simple library browser 35 | * Basic image editing 36 | * Command line with tab completion 37 | 38 | For much more information please check out the 39 | [documentation](http://karlch.github.io/vimiv/documentation "documentation"). 40 | If you are new to vimiv, this is a good place to 41 | [get started](http://karlch.github.io/vimiv/docs/usage "usage"). 42 | For a quick overview check out the 43 | [keybinding cheatsheet](http://karlch.github.io/vimiv/docs/keybindings_commands#keybinding-cheatsheet). 44 | 45 | ## Screenshots 46 | 47 | #### Open image and library 48 | 49 | Library 50 | 51 | #### Thumbnail mode 52 | 53 | Thumbnail 54 | 55 | ## Installation 56 | Install the dependencies listed below first. To use the 57 | development version clone this repository, 58 | for the latest stable check out the 59 | releases 60 | page. To install a simple `# make install` should suffice. To remove vimiv the 61 | standard `# make uninstall` works. You may need to update your icon cache after 62 | installation. 63 | 64 | For Arch Linux users the latest release is available from 65 | [community] 66 | and there is the AUR package 67 | vimiv-git 68 | for the development branch. 69 | 70 | Fedora Linux users can install stable releases from the 72 | official repositories using the provided package managers. 73 | 74 | ## Dependencies 75 | * python3 76 | * python-gobject 77 | * gtk3 78 | * python-setuptools (for installation) 79 | * python-dev (on debian-based systems for installation) 80 | * libgexiv2 (optional for EXIF support; needed for saving without deleting EXIF 81 | tags and for the autorotate command) 82 | 83 | ## Thanks to 84 | * James Campos, author of [Pim](https://github.com/Narrat/Pim) which was the 85 | starting point for vimiv 86 | * Bert Muennich, author of [sxiv](https://github.com/muennich/sxiv) which 87 | inspired many of the features of vimiv. 88 | * Anyone who has contributed or reported bugs 89 | -------------------------------------------------------------------------------- /scripts/generate_manpages.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Generates vimiv's man pages from the markdown pages of the website. 3 | 4 | Note: This scripts uses the directory of the website which is defined just after 5 | the imports. Adapt this to your own usage. 6 | """ 7 | 8 | import os 9 | import re 10 | import shutil 11 | import sys 12 | 13 | website_directory = os.path.expanduser("~/Coding/git/vimiv/vimiv-website/") 14 | 15 | 16 | def get_markdown_files(source="_pages/docs/", target="man/"): 17 | """Copy markdown files from source to target.""" 18 | os.makedirs(target, exist_ok=True) 19 | source_include = os.path.join(source, "include") 20 | for f in ["manpage.md", "manpage_5.md"]: 21 | shutil.copyfile(os.path.join(source, f), os.path.join(target, f)) 22 | if os.path.isdir(os.path.join(target, "include")): 23 | shutil.rmtree(os.path.join(target, "include")) 24 | shutil.copytree(source_include, os.path.join(target, "include"), ) 25 | 26 | 27 | def delete_jekyll_header(source): 28 | """Delete the --- whatever --- jekyll header from source.""" 29 | with open(source) as f: 30 | lines = f.readlines() 31 | 32 | amount = 0 33 | while amount != 2: 34 | line = lines[0] 35 | if "---" in line: 36 | amount += 1 37 | del lines[0] 38 | 39 | with open(source, "w") as f: 40 | for line in lines: 41 | f.write(line) 42 | 43 | 44 | def strip_html(source): 45 | """Remove all html tags from source.""" 46 | clean_html = re.compile('<.*?>') 47 | 48 | with open(source) as f: 49 | content = f.read() 50 | cleaned_content = re.sub(clean_html, "", content) 51 | 52 | with open(source, "w") as f: 53 | f.write(cleaned_content) 54 | 55 | 56 | def escape_markdown_characters(source): 57 | """Escape special markdown characters like "^" in source.""" 58 | with open(source) as f: 59 | content = f.read() 60 | cleaned_content = content.replace("^", "\^") 61 | 62 | with open(source, "w") as f: 63 | f.write(cleaned_content) 64 | 65 | 66 | def generate_main_page(source="man/manpage.md", target="man/vimiv.1"): 67 | """Generate the main vimiv man page from source markdown to target.""" 68 | delete_jekyll_header(source) 69 | escape_markdown_characters(source) 70 | cmd = "md2man-roff %s > %s" % (source, target) 71 | os.system(cmd) 72 | 73 | 74 | def replace_include_lines(source): 75 | """Replace jekyll include lines with the actual markdown content.""" 76 | with open(source) as f: 77 | lines = f.readlines() 78 | 79 | workcopy = list(lines) 80 | for i, line in enumerate(workcopy): 81 | dirname = os.path.dirname(source) 82 | if "include_relative" in line: 83 | basename = line.split()[2] 84 | filename = os.path.join(dirname, basename) 85 | with open(filename) as f: 86 | content = f.read() 87 | lines[i] = content 88 | 89 | with open(source, "w") as f: 90 | for line in lines: 91 | f.write(line) 92 | 93 | 94 | def convert_tables(source): 95 | """Convert markdown tables from source into a nicer format for man.""" 96 | with open(source) as f: 97 | lines = f.readlines() 98 | 99 | workcopy = list(lines) 100 | for i, line in enumerate(workcopy): 101 | # Table 102 | if line.startswith("|"): 103 | # Separator 104 | if line[2] == "-" or "|Setting|" in line or "|Name|" in line: 105 | lines[i] = "" 106 | # Content of a table line 107 | else: 108 | content = line.split("|") 109 | try: 110 | setting = content[1] 111 | value = content[2] 112 | description = content[4] 113 | new = "`%s`, `%s`\n %s\n\n" % (setting, value, description) 114 | # Table is shorter, use different format 115 | except IndexError: 116 | name = content[1] 117 | description = content[2] 118 | new = "`%s`\n %s\n\n" % (name, description) 119 | lines[i] = new 120 | 121 | with open(source, "w") as f: 122 | # pylint: disable=redefined-outer-name 123 | # We only use line in the loops anyway 124 | for line in lines: 125 | f.write(line) 126 | 127 | 128 | def generate_config_page(source="man/manpage_5.md", target="man/vimiv.5"): 129 | """Generate the vimivrc man page from source markdown to target.""" 130 | delete_jekyll_header(source) 131 | replace_include_lines(source) 132 | convert_tables(source) 133 | strip_html(source) 134 | escape_markdown_characters(source) 135 | cmd = "md2man-roff %s > %s" % (source, target) 136 | os.system(cmd) 137 | 138 | 139 | if __name__ == "__main__": 140 | # Move to website directory 141 | try: 142 | working_directory = os.getcwd() 143 | os.chdir(website_directory) 144 | except FileNotFoundError as e: 145 | print("Website directory not found.") 146 | print(e) 147 | sys.exit(1) 148 | get_markdown_files() 149 | generate_main_page(target=os.path.join(working_directory, "man", "vimiv.1")) 150 | generate_config_page(target=os.path.join(working_directory, "man", 151 | "vimivrc.5")) 152 | -------------------------------------------------------------------------------- /scripts/install_icons.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copy all vimiv icons to the correct icon directory 4 | 5 | DESTDIR=$1 6 | 7 | for i in 16 32 64 128 256 512; do 8 | install -Dm644 icons/vimiv_${i}x${i}.png ${DESTDIR}/usr/share/icons/hicolor/${i}x${i}/apps/vimiv.png 9 | done 10 | 11 | install -Dm644 icons/vimiv.svg ${DESTDIR}/usr/share/icons/hicolor/scalable/apps/vimiv.svg 12 | -------------------------------------------------------------------------------- /scripts/remove_icons.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Remove all vimiv icons from the system directory 4 | 5 | DESTDIR=$1 6 | 7 | for i in 16 32 64 128 256 512; do 8 | rm ${DESTDIR}/usr/share/icons/hicolor/${i}x${i}/apps/vimiv.png 9 | done 10 | 11 | rm ${DESTDIR}/usr/share/icons/hicolor/scalable/apps/vimiv.svg 12 | -------------------------------------------------------------------------------- /scripts/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Run tests for vimiv in Xvfb 3 | 4 | # Nicely formatted info message 5 | function print_info() { 6 | printf "\e[1;34m:: \e[1;37m%s\n\e[0;0m" "$1" 7 | } 8 | 9 | # Print fail message and exit 10 | function fail() { 11 | printf "\e[1;31merror: \e[1;37m%s\n\e[0;0m" "$1" 12 | exit 1 13 | } 14 | 15 | hash nosetests3 2>/dev/null || fail "nosetests3 command is not available" 16 | hash Xvfb 2>/dev/null || fail "Xvfb command is not available" 17 | 18 | # Receive current testimages 19 | test -d tests || fail "test module not found" 20 | 21 | if [[ -d "tests/vimiv/testimages" ]]; then 22 | print_info "Updating testimages" 23 | git --git-dir=tests/vimiv/.git/ --work-tree=tests/vimiv pull 24 | else 25 | print_info "Receiving testimages" 26 | git clone --branch=testimages https://github.com/karlch/vimiv tests/vimiv 27 | fi 28 | 29 | # Start Xvfb if possible 30 | if [[ ! -f /tmp/.X42-lock ]]; then 31 | print_info "Running tests in Xvfb" 32 | Xvfb :42 -screen 0 800x600x24 1>/dev/null 2>&1 & 33 | xvfb_pid="$!" 34 | 35 | # We need this so all directories are generated before running tests. 36 | if ! DISPLAY=":42" python3 tests/vimiv_testcase.py 1>/dev/null 2>&1; then 37 | kill "$xvfb_pid" 38 | fail "Main test failed." 39 | fi 40 | 41 | DISPLAY=":42" nosetests3 -c .noserc 42 | kill "$xvfb_pid" 43 | else 44 | fail "Close the running Xserver running on DISPLAY :42 to start Xvfb." 45 | fi 46 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf-8 3 | 4 | from setuptools import setup, Extension 5 | 6 | # C extensions 7 | enhance_module = Extension("vimiv._image_enhance", sources = ["c-lib/enhance.c"]) 8 | 9 | setup( 10 | name="vimiv", 11 | version="0.9.2.dev0", 12 | packages=['vimiv'], 13 | ext_modules = [enhance_module], 14 | scripts=['vimiv/vimiv'], 15 | install_requires=['PyGObject'], 16 | description="An image viewer with vim-like keybindings", 17 | license="MIT", 18 | url="https://github.com/karlch/vimiv", 19 | ) 20 | -------------------------------------------------------------------------------- /tests/animation_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Test animations in image mode for vimiv's testsuite.""" 3 | 4 | from unittest import main 5 | 6 | from vimiv_testcase import VimivTestCase, compare_pixbufs, refresh_gui 7 | 8 | 9 | class AnimationTest(VimivTestCase): 10 | """Test animations in image mode.""" 11 | 12 | @classmethod 13 | def setUpClass(cls): 14 | cls.init_test(cls, ["vimiv/testimages/animation/animation.gif"]) 15 | cls.image = cls.vimiv["image"] 16 | 17 | def test_toggle_animation(self): 18 | """Pause and play an animated gif.""" 19 | self._update_gif(True) 20 | # Frames should be updated 21 | first_pb = self.image.get_pixbuf_original() 22 | refresh_gui(0.1) 23 | second_pb = self.image.get_pixbuf_original() 24 | self.assertFalse(compare_pixbufs(first_pb, second_pb)) 25 | # Frames should no longer be updated 26 | self._update_gif(False) 27 | first_pb = self.image.get_pixbuf_original() 28 | refresh_gui(0.1) 29 | second_pb = self.image.get_pixbuf_original() 30 | self.assertTrue(compare_pixbufs(first_pb, second_pb)) 31 | # Back to standard state 32 | self._update_gif(True) 33 | 34 | def test_fail_transform_animation(self): 35 | """Fail transforming an animation.""" 36 | self.vimiv["transform"].rotate(3) 37 | self.check_statusbar("ERROR: Filetype not supported for rotate") 38 | self.vimiv["transform"].flip(True) 39 | self.check_statusbar("ERROR: Filetype not supported for flip") 40 | 41 | def test_zoom_animation(self): 42 | """Zoom an animation.""" 43 | start = self.image.get_zoom_percent() 44 | self.image.zoom_delta() 45 | self.assertGreater(self.image.get_zoom_percent(), start) 46 | self.image.zoom_delta(zoom_in=False) 47 | self.assertEqual(self.image.get_zoom_percent(), start) 48 | 49 | def test_overzoom(self): 50 | """Test overzoom at opening and fit afterwards for animations.""" 51 | # Overzoom is respected 52 | self.assertEqual(self.image.get_zoom_percent(), 100) 53 | # But not for a direct call to fit 54 | self.image.zoom_to(0, "fit") 55 | self.assertGreater(self.image.get_zoom_percent(), 100) 56 | self.image.move_pos(forward=False) 57 | 58 | def _update_gif(self, assertion): 59 | self.run_command("set play_animations %s" % (str(assertion))) 60 | refresh_gui(0.1) 61 | self.assertEqual(self.settings["play_animations"].get_value(), 62 | assertion) 63 | 64 | 65 | if __name__ == "__main__": 66 | main() 67 | -------------------------------------------------------------------------------- /tests/app_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Tests for the main file app.py for vimiv's test suite.""" 3 | 4 | import os 5 | import sys 6 | from unittest import main 7 | 8 | from gi.repository import GLib 9 | from vimiv.app import Vimiv 10 | 11 | from vimiv_testcase import VimivTestCase, refresh_gui 12 | 13 | 14 | class AppTest(VimivTestCase): 15 | """App Tests.""" 16 | 17 | @classmethod 18 | def setUpClass(cls): 19 | cls.init_test(cls) 20 | 21 | def test_handle_local_options(self): 22 | """Handle commandline arguments.""" 23 | # We need to catch information from standard output 24 | 25 | # Get version should print information to standard output and return 0 26 | option_version = GLib.VariantDict.new() 27 | bool_true = GLib.Variant("b", True) 28 | option_version.insert_value("version", bool_true) 29 | returncode = self.vimiv.do_handle_local_options(option_version) 30 | if not hasattr(sys.stdout, "getvalue"): 31 | self.fail("Need to run test in buffered mode.") 32 | # In pylint we do not run in buffered mode but this is checked with 33 | # hasattr just above. 34 | # pylint:disable=no-member 35 | output = sys.stdout.getvalue().strip() 36 | self.assertIn("vimiv", output) 37 | self.assertEqual(returncode, 0) 38 | # Set some different options and test if they were handled correctly 39 | options = GLib.VariantDict.new() 40 | options.insert_value("shuffle", bool_true) 41 | double_22 = GLib.Variant("d", 2.2) 42 | options.insert_value("slideshow-delay", double_22) 43 | string_geom = GLib.Variant("s", "400x400") 44 | options.insert_value("geometry", string_geom) 45 | returncode = self.vimiv.do_handle_local_options(options) 46 | self.assertEqual(returncode, -1) 47 | self.assertTrue(self.settings["shuffle"].get_value()) 48 | self.assertFalse(self.settings["start_slideshow"].get_value()) 49 | self.assertAlmostEqual(self.settings["slideshow_delay"].get_value(), 50 | 2.2) 51 | self.assertEqual(self.settings["geometry"].get_value(), (400, 400)) 52 | 53 | def test_temp_basedir(self): 54 | """Using a temporary basedir.""" 55 | # XDG_*_HOME directories should be in tmp 56 | self.assertIn("/tmp/vimiv-", os.getenv("XDG_CACHE_HOME")) 57 | self.assertIn("/tmp/vimiv-", os.getenv("XDG_CONFIG_HOME")) 58 | self.assertIn("/tmp/vimiv-", os.getenv("XDG_DATA_HOME")) 59 | # Thumbnail, Tag and Trash directory should contain tmp 60 | self.assertIn("/tmp/", self.vimiv["thumbnail"].get_cache_directory()) 61 | self.assertIn("/tmp/", self.vimiv["tags"].directory) 62 | trash_dir = self.vimiv["transform"].trash_manager.get_files_directory() 63 | info_dir = self.vimiv["transform"].trash_manager.get_info_directory() 64 | self.assertIn("/tmp/", trash_dir) 65 | self.assertIn("/tmp/", info_dir) 66 | # Create a tag in tmp as a simple test 67 | self.vimiv["tags"].write(["image1.py", "image2.py"], "tmptag") 68 | self.assertIn("tmptag", os.listdir(self.vimiv["tags"].directory)) 69 | 70 | @classmethod 71 | def tearDownClass(cls): 72 | cls.vimiv.quit_wrapper() 73 | os.chdir(cls.working_directory) 74 | 75 | 76 | class QuitTest(VimivTestCase): 77 | """Quit Tests.""" 78 | 79 | def setUp(self): 80 | """Recreate vimiv for every run.""" 81 | self.vimiv = Vimiv() 82 | self.init_test() 83 | 84 | def test_quit_with_running_threads(self): 85 | """Quit vimiv with external threads running.""" 86 | self.run_command("!sleep 0.2") 87 | self.vimiv.quit_wrapper() 88 | self.check_statusbar( 89 | "WARNING: Running external processes: sleep 0.2. Add ! to force.") 90 | p = self.vimiv["commandline"].running_processes[0] 91 | # Kill the subprocess 92 | self.assertFalse(p.poll()) 93 | self.vimiv.quit_wrapper(force=True) 94 | refresh_gui() 95 | self.assertEqual(p.poll(), -9) 96 | 97 | def test_clean_quit(self): 98 | """Quit vimiv without errors.""" 99 | self.vimiv.quit_wrapper() 100 | 101 | 102 | if __name__ == "__main__": 103 | main(buffer=True) 104 | -------------------------------------------------------------------------------- /tests/eventhandler_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Tests eventhandler.py for vimiv's test suite.""" 3 | 4 | from unittest import main 5 | 6 | from gi import require_version 7 | require_version("Gtk", "3.0") 8 | from gi.repository import Gdk, Gtk 9 | 10 | from vimiv_testcase import VimivTestCase, refresh_gui 11 | 12 | 13 | class KeyHandlerTest(VimivTestCase): 14 | """KeyHandler Tests.""" 15 | 16 | @classmethod 17 | def setUpClass(cls): 18 | cls.init_test(cls, ["vimiv/testimages/"]) 19 | 20 | def test_key_press(self): 21 | """Press key.""" 22 | self.vimiv["library"].file_select(None, Gtk.TreePath(1), None, True) 23 | image_before = self.vimiv.get_path() 24 | event = Gdk.Event().new(Gdk.EventType.KEY_PRESS) 25 | event.keyval = Gdk.keyval_from_name("n") 26 | self.vimiv["main_window"].emit("key_press_event", event) 27 | image_after = self.vimiv.get_path() 28 | self.assertNotEqual(image_before, image_after) 29 | event.keyval = Gdk.keyval_from_name("O") 30 | self.vimiv["main_window"].emit("key_press_event", event) 31 | self.assertTrue(self.vimiv["library"].is_focus()) 32 | 33 | def test_button_click(self): 34 | """Click mouse button.""" 35 | self.vimiv["library"].file_select(None, Gtk.TreePath(1), None, True) 36 | image_before = self.vimiv.get_path() 37 | event = Gdk.Event().new(Gdk.EventType.BUTTON_PRESS) 38 | event.button = 1 39 | self.vimiv["window"].emit("button_press_event", event) 40 | image_after = self.vimiv.get_path() 41 | self.assertNotEqual(image_before, image_after) 42 | # Double click should not work 43 | event = Gdk.Event().new(Gdk.EventType.DOUBLE_BUTTON_PRESS) 44 | event.button = 1 45 | self.vimiv["window"].emit("button_press_event", event) 46 | self.assertEqual(image_after, self.vimiv.get_path()) 47 | # Focus library via mouse click 48 | event = Gdk.Event().new(Gdk.EventType.BUTTON_PRESS) 49 | event.button = 2 50 | self.vimiv["window"].emit("button_press_event", event) 51 | self.assertTrue(self.vimiv["library"].is_focus()) 52 | 53 | def test_add_number(self): 54 | """Add number to the numstr and clear it.""" 55 | self.assertFalse(self.vimiv["eventhandler"].get_num_str()) 56 | # Add a number 57 | self.vimiv["eventhandler"].num_append("2") 58 | self.assertEqual(self.vimiv["eventhandler"].get_num_str(), "2") 59 | # Add another number, should change the timer_id 60 | self.vimiv["eventhandler"].num_append("3") 61 | self.assertEqual(self.vimiv["eventhandler"].get_num_str(), "23") 62 | # Clear manually, GLib timeout should definitely work as well if the 63 | # code runs without errors 64 | self.vimiv["eventhandler"].num_clear() 65 | self.assertFalse(self.vimiv["eventhandler"].get_num_str()) 66 | 67 | def test_receive_number(self): 68 | """Get a number from numstr and clear it.""" 69 | # Integer 70 | self.vimiv["eventhandler"].num_append("3") 71 | num = self.vimiv["eventhandler"].num_receive() 72 | self.assertEqual(num, 3) 73 | self.assertFalse(self.vimiv["eventhandler"].get_num_str()) 74 | # Float 75 | self.vimiv["eventhandler"].num_append("03") 76 | num = self.vimiv["eventhandler"].num_receive(to_float=True) 77 | self.assertEqual(num, 0.3) 78 | self.assertFalse(self.vimiv["eventhandler"].get_num_str()) 79 | # Empty should give default 80 | num = self.vimiv["eventhandler"].num_receive() 81 | self.assertEqual(num, 1) 82 | num = self.vimiv["eventhandler"].num_receive(5) 83 | self.assertEqual(num, 5) 84 | 85 | def test_add_number_via_keypress(self): 86 | """Add a number to the numstr by keypress.""" 87 | self.assertFalse(self.vimiv["eventhandler"].get_num_str()) 88 | event = Gdk.Event().new(Gdk.EventType.KEY_PRESS) 89 | event.keyval = Gdk.keyval_from_name("2") 90 | self.vimiv["library"].emit("key_press_event", event) 91 | self.assertEqual(self.vimiv["eventhandler"].get_num_str(), "2") 92 | # Clear as it might interfere 93 | self.vimiv["eventhandler"].num_clear() 94 | 95 | def test_key_press_modifier(self): 96 | """Press key with modifier.""" 97 | before = self.settings["show_hidden"].get_value() 98 | event = Gdk.Event().new(Gdk.EventType.KEY_PRESS) 99 | event.keyval = Gdk.keyval_from_name("h") 100 | event.state = Gdk.ModifierType.CONTROL_MASK 101 | self.vimiv["library"].emit("key_press_event", event) 102 | after = self.settings["show_hidden"].get_value() 103 | self.assertNotEqual(before, after) 104 | 105 | def test_touch(self): 106 | """Touch event.""" 107 | self.vimiv["library"].file_select(None, Gtk.TreePath(1), None, True) 108 | image_before = self.vimiv.get_path() 109 | event = Gdk.Event().new(Gdk.EventType.TOUCH_BEGIN) 110 | # Twice to check for exception 111 | self.vimiv["window"].emit("touch-event", event) 112 | self.vimiv["window"].emit("touch-event", event) 113 | image_after = self.vimiv.get_path() 114 | self.assertEqual(image_before, image_after) # Touch only disables 115 | self.vimiv["library"].toggle() 116 | self.assertTrue(self.vimiv["library"].is_focus()) 117 | refresh_gui() 118 | # Test again to see if it was re-activated properly 119 | self.test_button_click() 120 | 121 | 122 | if __name__ == "__main__": 123 | main() 124 | -------------------------------------------------------------------------------- /tests/fail_arguments_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Fail commands because of arguments test for vimiv's test suite.""" 3 | 4 | from unittest import main 5 | 6 | from vimiv_testcase import VimivTestCase 7 | 8 | 9 | class FailingArgTest(VimivTestCase): 10 | """Failing Argument Tests.""" 11 | 12 | @classmethod 13 | def setUpClass(cls): 14 | cls.init_test(cls) 15 | cls.cmdline = cls.vimiv["commandline"] 16 | 17 | def test_args(self): 18 | """Fail commands because of wrong number of arguments.""" 19 | # 0 Arguments allowed 20 | for cmd in ["accept_changes", "autorotate", "center", "copy_abspath", 21 | "copy_basename", "delete", "first", "first_lib", "fit", 22 | "fit_horiz", "fit_vert", "focus_library", "fullscreen", 23 | "last", "last_lib", "library", "manipulate", "mark", 24 | "mark_all", "mark_between", "move_up", "next", "next!", 25 | "prev", "prev!", "q", "q!", "reload_lib", "slideshow", 26 | "thumbnail", "unfocus_library", "version", "w", "wq"]: 27 | self.fail_arguments(cmd, 1, too_many=True) 28 | # 1 Argument optional 29 | for cmd in ["zoom_in", "zoom_out", "zoom_to"]: 30 | self.fail_arguments(cmd, 2, too_many=True) 31 | # 1 Argument required 32 | for cmd in ["flip", "rotate"]: 33 | self.fail_arguments(cmd, 2, too_many=True) 34 | self.fail_arguments(cmd, 0, too_many=False) 35 | # 1 Argument required, 1 argument optional 36 | for cmd in ["edit", "set"]: 37 | self.fail_arguments(cmd, 3, too_many=True) 38 | self.fail_arguments(cmd, 0, too_many=False) 39 | # 1 Argument required, any amount possible 40 | for cmd in ["format", "tag_write", "tag_load", "tag_remove", 41 | "undelete"]: 42 | self.fail_arguments(cmd, 0, too_many=False) 43 | self.allow_arbitary_n_arguments(cmd, 1) 44 | # 2 Arguments required, any amount possible 45 | for cmd in ["alias"]: 46 | self.fail_arguments(cmd, 1, too_many=False) 47 | self.allow_arbitary_n_arguments(cmd, 1) 48 | 49 | def fail_arguments(self, command, n_args, too_many=True): 50 | """Fail a command because of too many or too few arguments. 51 | 52 | Check for the correct error message. 53 | 54 | args: 55 | command: Command to fail. 56 | n_args: Amount of arguments to try. 57 | too_many: If True, n_args are too many for command. Otherwise too 58 | few. 59 | """ 60 | text = ":" + command + " arg" * n_args 61 | self.cmdline.set_text(text) 62 | self.cmdline.emit("activate") 63 | expected = "ERROR: Too many arguments for command" \ 64 | if too_many \ 65 | else "ERROR: Missing positional arguments for command" 66 | self.assertIn(expected, self.vimiv["statusbar"].get_message()) 67 | 68 | def allow_arbitary_n_arguments(self, command, min_n): 69 | """Fail if the command does not allow an arbitrary amount of arguments. 70 | 71 | Args: 72 | min_n: Minimum amount of arguments needed. 73 | """ 74 | for i in range(min_n, min_n * 100, 33): 75 | command = command + " a" * i 76 | self.run_command(command) 77 | not_expected = "ERROR: Too many arguments for command" 78 | self.assertNotIn(not_expected, 79 | self.vimiv["statusbar"].get_message()) 80 | 81 | 82 | if __name__ == "__main__": 83 | main() 84 | -------------------------------------------------------------------------------- /tests/fileactions_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Test fileactions.py for vimiv's test suite.""" 3 | 4 | import os 5 | import shutil 6 | from unittest import main 7 | 8 | import vimiv.fileactions as fileactions 9 | from gi import require_version 10 | require_version("Gtk", "3.0") 11 | from gi.repository import Gdk, Gtk 12 | 13 | from vimiv_testcase import VimivTestCase 14 | 15 | 16 | class FormatTest(VimivTestCase): 17 | """Test formatting files.""" 18 | 19 | @classmethod 20 | def setUpClass(cls): 21 | cls.init_test(cls, ["vimiv/"]) 22 | 23 | def test_format_files(self): 24 | """Format files according to a formatstring.""" 25 | shutil.copytree("testimages/", "testimages_to_format/") 26 | self.run_command("./testimages_to_format/arch-logo.png") 27 | self.vimiv["library"].toggle() 28 | fileactions.format_files(self.vimiv, "formatted_") 29 | files = [fil for fil in os.listdir() if "formatted_" in fil] 30 | files = sorted(files) 31 | expected_files = ["formatted_001.png", "formatted_002.jpg", 32 | "formatted_003", "formatted_004.bmp", 33 | "formatted_005.svg", "formatted_006.tiff"] 34 | for fil in expected_files: 35 | self.assertIn(fil, files) 36 | # Should not work without a path 37 | self.vimiv.populate([]) 38 | fileactions.format_files(self.vimiv, "formatted_") 39 | self.check_statusbar("INFO: No files in path") 40 | 41 | def test_format_files_with_exif(self): 42 | """Format files according to a formatstring with EXIF data.""" 43 | os.mkdir("testimages_to_format") 44 | shutil.copyfile("testimages/arch_001.jpg", 45 | "testimages_to_format/arch_001.jpg") 46 | self.run_command("./testimages_to_format/arch_001.jpg") 47 | self.vimiv["library"].toggle() 48 | fileactions.format_files(self.vimiv, "formatted_%Y_") 49 | self.assertIn("formatted_2016_001.jpg", os.listdir()) 50 | 51 | def test_fail_format_files_with_exif(self): 52 | """Run format with exif on a file that has no exif data.""" 53 | os.mkdir("testimages_to_format") 54 | shutil.copyfile("testimages/arch-logo.png", 55 | "testimages_to_format/arch-logo.png") 56 | self.run_command("./testimages_to_format/arch-logo.png") 57 | self.vimiv["library"].toggle() 58 | fileactions.format_files(self.vimiv, "formatted_%Y_") 59 | message = self.vimiv["statusbar"].get_message() 60 | self.assertIn("No exif data for", message) 61 | 62 | def tearDown(self): 63 | # Should not work in library 64 | if os.path.basename(os.getcwd()) != "vimiv": 65 | self.vimiv["library"].move_up() 66 | fileactions.format_files(self.vimiv, "formatted_") 67 | self.check_statusbar( 68 | "INFO: Format only works on opened image files") 69 | # Remove generated paths 70 | if os.path.isdir("testimages_to_format"): 71 | shutil.rmtree("testimages_to_format") 72 | 73 | 74 | class ClipboardTest(VimivTestCase): 75 | """Test copying to clipboard.""" 76 | 77 | @classmethod 78 | def setUpClass(cls): 79 | cls.compare_result = False # Used for the clipboard comparison 80 | cls.init_test(cls, ["vimiv/"]) 81 | 82 | def test_clipboard(self): 83 | """Copy image name to clipboard.""" 84 | def compare_text(clipboard, text, expected_text): 85 | self.compare_result = False 86 | self.compare_result = text == expected_text 87 | name = self.vimiv.get_pos(True) 88 | basename = os.path.basename(name) 89 | abspath = os.path.abspath(name) 90 | clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) 91 | primary = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY) 92 | # Copy basename and abspath to clipboard 93 | self.vimiv["clipboard"].copy_name(False) 94 | # Check if the info message is displayed correctly 95 | self.check_statusbar("INFO: Copied " + basename + " to clipboard") 96 | clipboard.request_text(compare_text, basename) 97 | self.assertTrue(self.compare_result) 98 | self.vimiv["clipboard"].copy_name(True) 99 | clipboard.request_text(compare_text, abspath) 100 | self.assertTrue(self.compare_result) 101 | # Toggle to primary and copy basename 102 | self.run_command("set copy_to_primary!") 103 | self.vimiv["clipboard"].copy_name(False) 104 | primary.request_text(compare_text, basename) 105 | self.assertTrue(self.compare_result) 106 | # Toggle back to clipboard and copy basename 107 | self.run_command("set copy_to_primary!") 108 | self.vimiv["clipboard"].copy_name(False) 109 | clipboard.request_text(compare_text, basename) 110 | self.assertTrue(self.compare_result) 111 | 112 | 113 | class FileActionsTest(VimivTestCase): 114 | """Fileactions Tests.""" 115 | 116 | @classmethod 117 | def setUpClass(cls): 118 | cls.compare_result = False # Used for the clipboard comparison 119 | cls.init_test(cls, ["vimiv/"]) 120 | 121 | def test_is_image(self): 122 | """Check whether file is an image.""" 123 | self.assertTrue(fileactions.is_image("testimages/arch_001.jpg")) 124 | self.assertFalse(fileactions.is_image("testimages/not_an_image.jpg")) 125 | 126 | 127 | if __name__ == "__main__": 128 | main() 129 | -------------------------------------------------------------------------------- /tests/helpers_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Test helpers.py for vimiv's test suite.""" 3 | 4 | import os 5 | import shutil 6 | from unittest import TestCase, main 7 | 8 | from vimiv import helpers 9 | 10 | 11 | class HelpersTest(TestCase): 12 | """Helpers Tests.""" 13 | 14 | def setUp(self): 15 | os.mkdir("tmp_testdir") 16 | open("tmp_testdir/foo", "a").close() 17 | open("tmp_testdir/.foo", "a").close() 18 | 19 | def test_listdir_wrapper(self): 20 | """Check the listdir_wrapper (hidden/no_hidden).""" 21 | no_hidden_files = helpers.listdir_wrapper("tmp_testdir", False) 22 | hidden_files = helpers.listdir_wrapper("tmp_testdir", True) 23 | self.assertEqual(len(no_hidden_files), 1) 24 | self.assertEqual(len(hidden_files), 2) 25 | self.assertEqual(hidden_files, sorted(hidden_files)) 26 | 27 | def test_read_file(self): 28 | """Check if a file is read correctly into a list of its lines.""" 29 | helpers.read_file("tmp_testdir/bar") 30 | self.assertTrue(os.path.exists("tmp_testdir/bar")) 31 | with open("tmp_testdir/baz", "w") as created_file: 32 | created_file.write("vimiv\nis\ngreat") 33 | created_file_content = helpers.read_file("tmp_testdir/baz") 34 | self.assertEqual(created_file_content, ["vimiv", "is", "great"]) 35 | 36 | def test_sizeof_fmt(self): 37 | """Format filesize in human-readable format.""" 38 | readable_size = helpers.sizeof_fmt(100) 39 | self.assertEqual(readable_size, "100B") 40 | readable_size = helpers.sizeof_fmt(10240) 41 | self.assertEqual(readable_size, "10.0K") 42 | huge_size = 1024**8 * 12 43 | readable_size = helpers.sizeof_fmt(huge_size) 44 | self.assertEqual(readable_size, "12.0Y") 45 | 46 | def test_error_message(self): 47 | """Error message popup.""" 48 | # Not much can happen here, if all attributes are set correctly it will 49 | # also run 50 | helpers.error_message("Test error", True) 51 | 52 | def test_read_info_from_man(self): 53 | """Read command information from the vimiv man page.""" 54 | infodict = helpers.read_info_from_man() 55 | # Check if some keys exist 56 | self.assertIn("set", infodict) 57 | self.assertIn("center", infodict) 58 | # Check if the first sentence is added correctly 59 | # Simple 60 | center_info = infodict["center"] 61 | self.assertEqual(center_info, "Scroll to the center of the image") 62 | # On two lines 63 | autorot_info = infodict["autorotate"] 64 | self.assertEqual( 65 | autorot_info, 66 | "Rotate all images in the current filelist according to exif data") 67 | # Containing extra periods 68 | # TODO reactivate if this case re-appears 69 | # More than one sentence 70 | flip_info = infodict["flip"] 71 | self.assertEqual(flip_info, "Flip the current image") 72 | 73 | def test_expansion(self): 74 | """Expand % and * in a command.""" 75 | filename = "first" 76 | filelist = ["first", "second.txt"] 77 | command1 = "echo %" 78 | result1 = helpers.expand_filenames(filename, filelist, command1) 79 | self.assertEqual(result1, "echo first") 80 | command2 = "echo * > ~/test.txt" 81 | result2 = helpers.expand_filenames(filename, filelist, command2) 82 | self.assertEqual(result2, "echo first second.txt > ~/test.txt") 83 | 84 | def tearDown(self): 85 | shutil.rmtree("tmp_testdir") 86 | 87 | 88 | if __name__ == "__main__": 89 | main() 90 | -------------------------------------------------------------------------------- /tests/imageactions_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Test imageactions.py for vimiv's test suite.""" 3 | 4 | import os 5 | import shutil 6 | import time 7 | from unittest import TestCase, main 8 | 9 | import vimiv.imageactions as imageactions 10 | from gi import require_version 11 | require_version('GdkPixbuf', '2.0') 12 | from gi.repository import GdkPixbuf 13 | 14 | from vimiv_testcase import compare_files 15 | 16 | 17 | class ImageActionsTest(TestCase): 18 | """Imageactions Tests.""" 19 | 20 | def setUp(self): 21 | self.working_directory = os.getcwd() 22 | os.chdir("vimiv/testimages/") 23 | self.orig = os.path.abspath("arch_001.jpg") 24 | self.filename = os.path.abspath("image_to_edit.jpg") 25 | self.filename_2 = os.path.abspath("image_to_edit_2.jpg") 26 | self._waiting = False # Used to wait for autorotate 27 | shutil.copyfile(self.orig, self.filename) 28 | shutil.copyfile(self.orig, self.filename_2) 29 | 30 | def test_rotate(self): 31 | """Rotate image file.""" 32 | def do_rotate_test(rotate_int): 33 | """Run the rotation test. 34 | 35 | Args: 36 | rotate_int: Number defining the rotation. 37 | """ 38 | pb = GdkPixbuf.Pixbuf.new_from_file(self.filename) 39 | orientation_before = pb.get_width() < pb.get_height() 40 | imageactions.rotate_file(self.filename, rotate_int) 41 | pb = GdkPixbuf.Pixbuf.new_from_file(self.filename) 42 | orientation_after = pb.get_width() < pb.get_height() 43 | if rotate_int in [1, 3]: 44 | self.assertNotEqual(orientation_before, orientation_after) 45 | elif rotate_int == 2: 46 | self.assertEqual(orientation_before, orientation_after) 47 | # Rotate counterclockwise 48 | do_rotate_test(1) 49 | # Rotate clockwise 50 | do_rotate_test(3) 51 | # Images are now equal again 52 | self.assertTrue(compare_files(self.orig, self.filename)) 53 | # Rotate 180 54 | do_rotate_test(2) 55 | # Images are not equal 56 | self.assertFalse(compare_files(self.orig, self.filename)) 57 | 58 | def test_flip(self): 59 | """Flipping of files.""" 60 | # Images equal before the flip 61 | self.assertTrue(compare_files(self.orig, self.filename)) 62 | # Images differ after the flip 63 | imageactions.flip_file(self.filename, False) 64 | self.assertFalse(compare_files(self.orig, self.filename)) 65 | # Images equal after flipping again 66 | imageactions.flip_file(self.filename, False) 67 | self.assertTrue(compare_files(self.orig, self.filename)) 68 | # Same for horizontal flip 69 | # Images equal before the flip 70 | self.assertTrue(compare_files(self.orig, self.filename)) 71 | # Images differ after the flip 72 | imageactions.flip_file(self.filename, True) 73 | self.assertFalse(compare_files(self.orig, self.filename)) 74 | # Images equal after flipping again 75 | imageactions.flip_file(self.filename, True) 76 | self.assertTrue(compare_files(self.orig, self.filename)) 77 | 78 | def test_autorotate(self): 79 | """Autorotate files.""" 80 | pb = GdkPixbuf.Pixbuf.new_from_file(self.filename) 81 | orientation_before = pb.get_width() < pb.get_height() 82 | autorotate = imageactions.Autorotate([self.filename]) 83 | autorotate.connect("completed", self._on_autorotate_completed) 84 | autorotate.run() 85 | # Wait for it to complete 86 | self._waiting = True 87 | while self._waiting: 88 | time.sleep(0.05) 89 | pb = GdkPixbuf.Pixbuf.new_from_file(self.filename) 90 | orientation_after = pb.get_width() < pb.get_height() 91 | self.assertNotEqual(orientation_before, orientation_after) 92 | 93 | def _on_autorotate_completed(self, autorotate, amount): 94 | self._waiting = False 95 | 96 | def tearDown(self): 97 | os.chdir(self.working_directory) 98 | os.remove(self.filename) 99 | os.remove(self.filename_2) 100 | 101 | 102 | if __name__ == "__main__": 103 | main() 104 | -------------------------------------------------------------------------------- /tests/information_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Test the Information class for vimiv's test suite.""" 3 | 4 | from unittest import TestCase, main 5 | 6 | from vimiv.information import Information 7 | 8 | 9 | class InformationTest(TestCase): 10 | """Information Tests. 11 | 12 | The pop-up tests simply run through the pop-ups not checking for anything. 13 | If all attributes are set correctly, not much can go wrong. 14 | """ 15 | 16 | @classmethod 17 | def setUpClass(cls): 18 | cls.information = Information() 19 | 20 | def test_print_version(self): 21 | """Print version information to screen.""" 22 | version = self.information.get_version() 23 | self.assertIn("0.9.2.dev0", version) 24 | 25 | def test_show_version_info(self): 26 | """Show the version info pop-up.""" 27 | self.information.show_version_info(True) 28 | 29 | def test_show_licence(self): 30 | """Show the licence pop-up.""" 31 | self.information.show_licence(None, True) 32 | 33 | 34 | if __name__ == "__main__": 35 | main() 36 | -------------------------------------------------------------------------------- /tests/invalid_mode_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Fail functions because called from wrong mode test for vimiv's test suite.""" 3 | 4 | from unittest import main 5 | 6 | from vimiv_testcase import VimivTestCase 7 | 8 | 9 | class FailingModeTest(VimivTestCase): 10 | """Failing Mode Tests.""" 11 | 12 | @classmethod 13 | def setUpClass(cls): 14 | cls.init_test(cls) 15 | cls.cmdline = cls.vimiv["commandline"] 16 | 17 | def test_fail_focus_slider(self): 18 | """Fail focus slider because not in manipulate.""" 19 | self.vimiv["manipulate"].focus_slider("bri") 20 | self.check_statusbar( 21 | "ERROR: Focusing a slider only makes sense in manipulate") 22 | 23 | def test_fail_button_clicked(self): 24 | """Fail exiting manipulate via button_clicked.""" 25 | self.vimiv["manipulate"].finish(False) 26 | self.check_statusbar( 27 | "ERROR: Finishing manipulate only makes sense in manipulate") 28 | 29 | 30 | if __name__ == "__main__": 31 | main() 32 | -------------------------------------------------------------------------------- /tests/log_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Tests for log for vimiv's test suite.""" 3 | 4 | import os 5 | import sys 6 | from unittest import main 7 | 8 | from gi import require_version 9 | require_version("Gtk", "3.0") 10 | from gi.repository import Gdk 11 | from vimiv.helpers import get_user_data_dir 12 | 13 | from vimiv_testcase import VimivTestCase 14 | 15 | 16 | class LogTest(VimivTestCase): 17 | """Log Tests.""" 18 | 19 | @classmethod 20 | def setUpClass(cls): 21 | cls.init_test(cls, debug=True) 22 | cls.logfile = os.path.join(get_user_data_dir(), 23 | "vimiv", "vimiv.log") 24 | 25 | def test_creation(self): 26 | """Creation and header of the log file.""" 27 | self.assertTrue(os.path.isfile(self.logfile)) 28 | content = self.read_log() 29 | self.assertIn(self.logfile.replace(os.getenv("HOME"), "~"), content) 30 | self.assertIn("[Version]", content) 31 | self.assertIn("[Python]", content) 32 | self.assertIn("[GTK]", content) 33 | self.assertIn("#" * 80 + "\n", content) # Proper separator in file 34 | 35 | def test_write(self): 36 | """Write message to log.""" 37 | self.vimiv["log"].write_message("Title", "Message") 38 | last_line = self.read_log(string=False)[-1] 39 | self.assertEqual(last_line, "%-15s %s\n" % ("[Title]", "Message")) 40 | # Message containing time 41 | self.vimiv["log"].write_message("Date", "time") 42 | last_line = self.read_log(string=False)[-1] 43 | self.assertNotIn("time", last_line) 44 | 45 | def test_write_to_stderr(self): 46 | """Write to stderr and therefore to log.""" 47 | sys.stderr.write("Traceback\nA great catastrophe") 48 | lines = self.read_log(string=False) 49 | self.assertIn("[stderr]", lines[-3]) 50 | self.assertIn("Traceback", lines[-2]) 51 | self.assertIn("A great catastrophe", lines[-1]) 52 | 53 | def test_debug_mode_command(self): 54 | """Run a command in debug mode and therefore log it.""" 55 | self.run_command("!:") 56 | last_line = self.read_log(string=False)[-1] 57 | self.assertEqual(last_line, "%-15s %s\n" % ("[commandline]", ":!:")) 58 | 59 | def test_debug_mode_statusbar(self): 60 | """Statusbar message in debug mode and therefore log it.""" 61 | self.vimiv["statusbar"].message("Useful", "info") 62 | last_line = self.read_log(string=False)[-1] 63 | self.assertEqual(last_line, "%-15s %s\n" % ("[info]", "Useful")) 64 | 65 | def test_debug_mode_eventhandler(self): 66 | """Run keybinding and mouse click in debug mode and therefore log it.""" 67 | # Keybinding 68 | event = Gdk.Event().new(Gdk.EventType.KEY_PRESS) 69 | event.keyval = Gdk.keyval_from_name("j") 70 | self.vimiv["library"].emit("key_press_event", event) 71 | last_line = self.read_log(string=False)[-1] 72 | self.assertEqual(last_line, "%-15s %s\n" % ("[key]", "j: scroll_lib j")) 73 | # Mouse click 74 | event = Gdk.Event().new(Gdk.EventType.BUTTON_PRESS) 75 | event.button = 1 76 | self.vimiv["window"].emit("button_press_event", event) 77 | last_line = self.read_log(string=False)[-1] 78 | self.assertEqual(last_line, "%-15s %s\n" % ("[key]", "Button1: next")) 79 | 80 | def test_debug_mode_numstr(self): 81 | """Add and clear num_str in debug mode and therefore log it.""" 82 | self.vimiv.debug = True 83 | self.vimiv["eventhandler"].num_append("3") 84 | last_line = self.read_log(string=False)[-1] 85 | self.assertEqual(last_line, "%-15s %s\n" % ("[number]", "3->3")) 86 | self.vimiv["eventhandler"].num_clear() 87 | last_line = self.read_log(string=False)[-1] 88 | self.assertEqual(last_line, "%-15s %s\n" % ("[number]", "cleared")) 89 | 90 | def read_log(self, string=True): 91 | """Read log to string and return the string. 92 | 93 | Args: 94 | string: If True, return content as string, else return lines as 95 | list. 96 | """ 97 | with open(self.logfile) as f: 98 | if string: 99 | content = f.read() 100 | else: 101 | content = f.readlines() 102 | return content 103 | 104 | 105 | if __name__ == "__main__": 106 | main() 107 | -------------------------------------------------------------------------------- /tests/main_window_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Test the MainWindow for vimiv's testsuite.""" 3 | 4 | from unittest import main 5 | 6 | from vimiv_testcase import VimivTestCase, refresh_gui 7 | 8 | 9 | class MainWindowTest(VimivTestCase): 10 | """MainWindow Test.""" 11 | 12 | @classmethod 13 | def setUpClass(cls): 14 | cls.init_test(cls, ["vimiv/testimages/arch_001.jpg"]) 15 | cls.window = cls.vimiv["main_window"] 16 | 17 | def test_scroll(self): 18 | """Scroll an image.""" 19 | def there_and_back(there, back): 20 | """Scroll in direction and back to the beginning. 21 | 22 | Args: 23 | there: Direction to scroll in. 24 | back: Direction to scroll back in. 25 | Return: 26 | Position after scrolling. 27 | """ 28 | if there in "lL": 29 | adj = self.window.get_hadjustment() 30 | else: 31 | adj = self.window.get_vadjustment() 32 | self.window.scroll(there) 33 | new_pos = adj.get_value() 34 | self.assertGreater(adj.get_value(), 0) 35 | self.window.scroll(back) 36 | self.assertFalse(adj.get_value()) 37 | return new_pos 38 | refresh_gui() 39 | # First zoom so something can happen 40 | size = self.window.get_allocation() 41 | w_scale = \ 42 | size.width / self.vimiv["image"].get_pixbuf_original().get_width() 43 | h_scale = \ 44 | size.height / self.vimiv["image"].get_pixbuf_original().get_height() 45 | needed_scale = max(w_scale, h_scale) * 1.5 46 | self.vimiv["image"].zoom_to(needed_scale) 47 | refresh_gui() 48 | # Adjustments should be at 0 49 | h_adj = self.window.get_hadjustment() 50 | v_adj = self.window.get_vadjustment() 51 | self.assertFalse(h_adj.get_value()) 52 | self.assertFalse(v_adj.get_value()) 53 | # Right and back left 54 | there_and_back("l", "h") 55 | # Down and back up 56 | there_and_back("j", "k") 57 | # Far right and back 58 | right_end = there_and_back("L", "H") 59 | self.assertEqual( 60 | right_end, 61 | h_adj.get_upper() - h_adj.get_lower() - size.width) 62 | # Bottom and back 63 | bottom = there_and_back("J", "K") 64 | self.assertEqual( 65 | bottom, 66 | v_adj.get_upper() - v_adj.get_lower() - size.height) 67 | # Center 68 | self.window.center_window() 69 | h_adj = self.window.get_hadjustment() 70 | v_adj = self.window.get_vadjustment() 71 | h_middle = \ 72 | (h_adj.get_upper() - h_adj.get_lower() - size.width) / 2 73 | v_middle = \ 74 | (v_adj.get_upper() - v_adj.get_lower() - size.height) / 2 75 | self.assertEqual(h_adj.get_value(), h_middle) 76 | self.assertEqual(v_adj.get_value(), v_middle) 77 | 78 | def test_scroll_error(self): 79 | """Scroll image or thumbnail with invalid argument.""" 80 | # Error message 81 | self.window.scroll("m") 82 | self.check_statusbar("ERROR: Invalid scroll direction m") 83 | 84 | 85 | if __name__ == "__main__": 86 | main() 87 | -------------------------------------------------------------------------------- /tests/manipulate_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Test manipulate.py for vimiv's test suite.""" 3 | 4 | import os 5 | import shutil 6 | from unittest import main 7 | 8 | from vimiv_testcase import (VimivTestCase, compare_files, compare_pixbufs, 9 | refresh_gui) 10 | 11 | 12 | class ManipulateTest(VimivTestCase): 13 | """Manipulate Tests.""" 14 | 15 | @classmethod 16 | def setUpClass(cls): 17 | if os.path.isdir("vimiv/testimages_man"): 18 | shutil.rmtree("vimiv/testimages_man") 19 | shutil.copytree("vimiv/testimages", "vimiv/testimages_man") 20 | cls.init_test(cls, ["vimiv/testimages_man/arch-logo.png"]) 21 | cls.manipulate = cls.vimiv["manipulate"] 22 | # Wait for image as this is used in manipulate 23 | while not cls.vimiv["image"].get_pixbuf(): 24 | refresh_gui(0.1) 25 | 26 | def setUp(self): 27 | """Set up by opening manipulate. Test half of toggling.""" 28 | self.manipulate.toggle() 29 | refresh_gui() 30 | self.assertTrue(self.manipulate.is_visible()) 31 | self.assertTrue(self.manipulate.sliders["bri"].is_focus()) 32 | 33 | def test_manipulate_image(self): 34 | """Test manipulate image.""" 35 | # Copy image before manipulation 36 | tmpfile = "tmp.jpg" 37 | if os.path.exists(tmpfile): 38 | os.remove(tmpfile) 39 | shutil.copyfile(self.vimiv.get_path(), tmpfile) 40 | # Leaving with False should not change the image 41 | self.manipulate.cmd_edit("bri", "20") 42 | self.manipulate.finish(False) 43 | self.assertTrue(compare_files(tmpfile, self.vimiv.get_path())) 44 | # Image is different to copied backup after manipulations 45 | self.manipulate.toggle() 46 | self.manipulate.cmd_edit("bri", "20") 47 | self.manipulate.finish(True) 48 | self.assertFalse(compare_files(tmpfile, self.vimiv.get_path())) 49 | self.manipulate.toggle() # Re-open to keep state equal 50 | 51 | def test_write_image(self): 52 | """Write an image to disk from manipulate.""" 53 | # Copy image before manipulation 54 | tmpfile = "tmp.jpg" 55 | if os.path.exists(tmpfile): 56 | os.remove(tmpfile) 57 | shutil.copyfile(self.vimiv.get_path(), tmpfile) 58 | # Image is different to copied backup after manipulations 59 | self.manipulate.cmd_edit("bri", "20") 60 | self.vimiv["transform"].write() 61 | self.assertFalse(compare_files(tmpfile, self.vimiv.get_path())) 62 | self.manipulate.toggle() # Re-open to keep state equal 63 | 64 | def test_focus_sliders(self): 65 | """Focusing sliders in manipulate.""" 66 | self.assertTrue(self.manipulate.sliders["bri"].is_focus()) 67 | self.manipulate.focus_slider("con") 68 | self.assertTrue(self.manipulate.sliders["con"].is_focus()) 69 | self.manipulate.focus_slider("sat") 70 | self.assertTrue(self.manipulate.sliders["sat"].is_focus()) 71 | self.manipulate.focus_slider("bri") 72 | self.assertTrue(self.manipulate.sliders["bri"].is_focus()) 73 | # Slider does not exist 74 | self.manipulate.focus_slider("val") 75 | self.check_statusbar("ERROR: No slider called val") 76 | 77 | def test_change_slider_value(self): 78 | """Change slider value in manipulate.""" 79 | # Change value 80 | self.manipulate.focus_slider("bri") 81 | self.manipulate.change_slider("-1") 82 | received_value = self.manipulate.sliders["bri"].get_value() 83 | self.assertEqual(received_value, -1) 84 | # Change value with a numstr 85 | self.vimiv["eventhandler"].set_num_str(5) 86 | self.manipulate.change_slider("2") 87 | received_value = self.manipulate.sliders["bri"].get_value() 88 | self.assertEqual(received_value, -1 + 2 * 5) 89 | # Not an integer 90 | self.manipulate.change_slider("hi") 91 | self.check_statusbar("ERROR: Could not convert 'hi' to int") 92 | received_value = self.manipulate.sliders["bri"].get_value() 93 | self.assertEqual(received_value, -1 + 2 * 5) 94 | 95 | def test_cmd_edit(self): 96 | """Test manipulating from command line commands.""" 97 | pb_1 = self.vimiv["image"].get_pixbuf() 98 | # Just call the function 99 | self.manipulate.cmd_edit("sat", "20") 100 | self.assertEqual(self.manipulate.sliders["sat"].get_value(), 20) 101 | self.assertTrue(self.manipulate.sliders["sat"].is_focus()) 102 | pb_2 = self.vimiv["image"].get_pixbuf() 103 | self.assertFalse(compare_pixbufs(pb_1, pb_2)) 104 | # Set contrast via command line 105 | self.run_command("edit con 35") 106 | self.assertEqual(self.manipulate.sliders["con"].get_value(), 35) 107 | self.assertTrue(self.manipulate.sliders["con"].is_focus()) 108 | pb_3 = self.vimiv["image"].get_pixbuf() 109 | self.assertFalse(compare_pixbufs(pb_2, pb_3)) 110 | # No argument means 0 111 | self.run_command("edit con") 112 | self.assertEqual(self.manipulate.sliders["con"].get_value(), 0) 113 | pb_4 = self.vimiv["image"].get_pixbuf() 114 | self.assertFalse(compare_pixbufs(pb_3, pb_4)) 115 | self.assertTrue(compare_pixbufs(pb_2, pb_4)) # We reset contrast 116 | # Close via command line 117 | self.run_command("discard_changes") 118 | self.assertFalse(self.manipulate.sliders["sat"].is_focus()) 119 | # Error: not a valid integer for manipulation 120 | self.run_command("edit bri value") 121 | self.assertFalse(self.manipulate.sliders["bri"].is_focus()) 122 | self.check_statusbar("ERROR: Argument must be of type integer") 123 | 124 | def test_check_for_edit(self): 125 | """Check if an image was edited.""" 126 | self.assertEqual(0, self.manipulate.check_for_edit(False)) 127 | self.manipulate.cmd_edit("bri", "10") 128 | self.assertEqual(1, self.manipulate.check_for_edit(False)) 129 | self.assertEqual(0, self.manipulate.check_for_edit(True)) 130 | 131 | def test_quit_with_edited_image(self): 132 | """Quit vimiv with an edited image.""" 133 | self.manipulate.cmd_edit("bri", "10") 134 | self.vimiv.quit_wrapper() 135 | self.check_statusbar("WARNING: Image has been edited, add ! to force") 136 | 137 | def tearDown(self): 138 | """Tear down by closing manipulate. Test other half of toggling.""" 139 | self.manipulate.finish(False) 140 | self.assertFalse(self.manipulate.sliders["bri"].is_focus()) 141 | 142 | @classmethod 143 | def tearDownClass(cls): 144 | cls.vimiv.quit() 145 | os.chdir(cls.working_directory) 146 | if os.path.isdir("./vimiv/testimages_man"): 147 | shutil.rmtree("vimiv/testimages_man") 148 | 149 | 150 | if __name__ == "__main__": 151 | main() 152 | -------------------------------------------------------------------------------- /tests/mark_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Mark tests for vimiv's test suite.""" 3 | 4 | import os 5 | from unittest import main 6 | 7 | from vimiv_testcase import VimivTestCase 8 | 9 | 10 | class MarkTest(VimivTestCase): 11 | """Mark Tests.""" 12 | 13 | @classmethod 14 | def setUpClass(cls): 15 | cls.init_test(cls, ["vimiv/testimages"]) 16 | # Move away from directory 17 | cls.vimiv["library"].scroll("j") 18 | 19 | def test_mark(self): 20 | """Mark images.""" 21 | # Mark 22 | self.vimiv["mark"].mark() 23 | expected_marked = [os.path.abspath("arch-logo.png")] 24 | self.assertEqual(expected_marked, self.vimiv["mark"].marked) 25 | # Remove mark 26 | self.vimiv["mark"].mark() 27 | self.assertEqual([], self.vimiv["mark"].marked) 28 | 29 | def test_toggle_mark(self): 30 | """Toggle marks.""" 31 | self.vimiv["mark"].mark() 32 | self.vimiv["mark"].toggle_mark() 33 | self.assertEqual([], self.vimiv["mark"].marked) 34 | self.vimiv["mark"].toggle_mark() 35 | expected_marked = [os.path.abspath("arch-logo.png")] 36 | self.assertEqual(expected_marked, self.vimiv["mark"].marked) 37 | 38 | def test_mark_all(self): 39 | """Mark all.""" 40 | self.vimiv["mark"].mark_all() 41 | expected_marked = ["arch-logo.png", "arch_001.jpg", "symlink_to_image", 42 | "vimiv.bmp", "vimiv.svg", "vimiv.tiff"] 43 | expected_marked = [os.path.abspath(image) for image in expected_marked] 44 | self.assertEqual(expected_marked, self.vimiv["mark"].marked) 45 | 46 | def test_mark_between(self): 47 | """Mark between.""" 48 | self.vimiv["mark"].mark() 49 | self.vimiv["eventhandler"].set_num_str(3) 50 | self.vimiv["library"].scroll("j") 51 | self.vimiv["mark"].mark() 52 | self.vimiv["mark"].mark_between() 53 | expected_marked = ["arch-logo.png", "arch_001.jpg", "symlink_to_image"] 54 | expected_marked = [os.path.abspath(image) for image in expected_marked] 55 | self.assertEqual(expected_marked, self.vimiv["mark"].marked) 56 | 57 | def test_fail_mark(self): 58 | """Fail marking.""" 59 | # Directory 60 | while not os.path.isdir(self.vimiv.get_pos(True)): 61 | self.vimiv["library"].scroll("j") 62 | self.vimiv["mark"].mark() 63 | self.check_statusbar("WARNING: Marking directories is not supported") 64 | # Too less images for mark between 65 | self.vimiv["mark"].mark_between() 66 | self.check_statusbar("ERROR: Not enough marks") 67 | 68 | def tearDown(self): 69 | self.vimiv["mark"].marked = [] 70 | self.vimiv["library"].move_pos(False) 71 | self.vimiv["library"].scroll("j") 72 | 73 | 74 | if __name__ == "__main__": 75 | main() 76 | -------------------------------------------------------------------------------- /tests/modeswitch_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Switching between modes tests for vimiv's test suite.""" 3 | 4 | import os 5 | from unittest import main 6 | 7 | from gi import require_version 8 | require_version("Gtk", "3.0") 9 | from gi.repository import Gtk 10 | 11 | from vimiv_testcase import VimivTestCase 12 | 13 | 14 | class ModeSwitchTestImage(VimivTestCase): 15 | """Mode Switching Tests starting from image mode.""" 16 | 17 | @classmethod 18 | def setUpClass(cls): 19 | cls.init_test(cls, ["vimiv/testimages/arch-logo.png"]) 20 | 21 | def setUp(self): 22 | self.test_directory = os.getcwd() 23 | 24 | def test_image_library(self): 25 | """Switch between image and library mode.""" 26 | self.assertTrue(self.vimiv["main_window"].is_focus()) 27 | self.assertTrue(self.vimiv["main_window"].is_visible()) 28 | self.assertFalse(self.vimiv["library"].is_visible()) 29 | 30 | self.vimiv["library"].toggle() 31 | self.assertFalse(self.vimiv["main_window"].is_focus()) 32 | self.assertTrue(self.vimiv["library"].is_visible()) 33 | self.assertTrue(self.vimiv["library"].is_focus()) 34 | 35 | self.vimiv["library"].toggle() 36 | self.assertTrue(self.vimiv["main_window"].is_focus()) 37 | self.assertTrue(self.vimiv["main_window"].is_visible()) 38 | self.assertFalse(self.vimiv["library"].is_visible()) 39 | 40 | def test_image_thumbnail(self): 41 | """Switch between image and thumbnail mode.""" 42 | self.assertTrue(self.vimiv["main_window"].is_focus()) 43 | self.assertTrue(self.vimiv["main_window"].is_visible()) 44 | self.assertEqual(type(self.vimiv["main_window"].get_child()), 45 | Gtk.Viewport) # We do not know the exact viewport 46 | 47 | self.vimiv["thumbnail"].toggle() 48 | self.assertTrue(self.vimiv["thumbnail"].is_focus()) 49 | self.assertEqual(self.vimiv["main_window"].get_child(), 50 | self.vimiv["thumbnail"]) 51 | self.assertEqual(self.vimiv["thumbnail"].last_focused, "im") 52 | # Quick insert of thumbnail <-> manipulate as it would be to expensive 53 | # to write an extra thumbnail class for this 54 | self.vimiv["manipulate"].toggle() 55 | self.assertTrue(self.vimiv["thumbnail"].is_focus()) 56 | self.assertFalse(self.vimiv["manipulate"].is_visible()) 57 | self.check_statusbar( 58 | "WARNING: Manipulate not supported in thumbnail mode") 59 | 60 | self.vimiv["thumbnail"].toggle() 61 | self.assertTrue(self.vimiv["main_window"].is_focus()) 62 | self.assertEqual(type(self.vimiv["main_window"].get_child()), 63 | Gtk.Viewport) 64 | 65 | def test_image_manipulate(self): 66 | """Switch between image and manipulate mode.""" 67 | self.assertTrue(self.vimiv["main_window"].is_focus()) 68 | self.assertTrue(self.vimiv["main_window"].is_visible()) 69 | self.assertFalse(self.vimiv["manipulate"].is_visible()) 70 | 71 | self.vimiv["manipulate"].toggle() 72 | self.assertTrue(self.vimiv["manipulate"].is_visible()) 73 | self.assertTrue(self.vimiv["manipulate"].sliders["bri"].is_focus()) 74 | 75 | self.vimiv["manipulate"].toggle() 76 | self.assertTrue(self.vimiv["main_window"].is_focus()) 77 | self.assertTrue(self.vimiv["main_window"].is_visible()) 78 | self.assertFalse(self.vimiv["manipulate"].is_visible()) 79 | 80 | def tearDown(self): 81 | os.chdir(self.test_directory) 82 | 83 | 84 | class ModeSwitchTestLibrary(VimivTestCase): 85 | """Mode Switching Tests starting from library mode.""" 86 | 87 | @classmethod 88 | def setUpClass(cls): 89 | cls.init_test(cls, ["vimiv/testimages/"]) 90 | 91 | def setUp(self): 92 | self.test_directory = os.getcwd() 93 | 94 | def test_library_image(self): 95 | """Switch between library and image mode.""" 96 | self.assertTrue(self.vimiv["library"].is_focus()) 97 | path = Gtk.TreePath( 98 | [self.vimiv["library"].files.index("arch-logo.png")]) 99 | self.vimiv["library"].file_select(None, path, None, True) 100 | self.assertTrue(self.vimiv["main_window"].is_focus()) 101 | self.assertTrue(self.vimiv["main_window"].is_visible()) 102 | self.assertFalse(self.vimiv["library"].is_visible()) 103 | 104 | self.vimiv["library"].toggle() 105 | self.assertTrue(self.vimiv["library"].is_focus()) 106 | self.assertTrue(self.vimiv["library"].is_visible()) 107 | 108 | def test_library_thumbnail(self): 109 | """Switch between library and thumbnail mode.""" 110 | self.assertTrue(self.vimiv["library"].is_focus()) 111 | self.vimiv["thumbnail"].toggle() 112 | self.assertTrue(self.vimiv["thumbnail"].is_focus()) 113 | self.assertTrue(self.vimiv["main_window"].is_visible()) 114 | self.assertEqual(self.vimiv["main_window"].get_child(), 115 | self.vimiv["thumbnail"]) 116 | self.assertEqual(self.vimiv["thumbnail"].last_focused, "lib") 117 | 118 | self.vimiv["thumbnail"].toggle() 119 | self.assertTrue(self.vimiv["library"].is_focus()) 120 | self.assertNotEqual(self.vimiv["main_window"].get_child(), 121 | self.vimiv["thumbnail"]) 122 | 123 | def test_library_manipulate(self): 124 | """Switch between library and manipulate mode should fail.""" 125 | self.assertTrue(self.vimiv["library"].is_focus()) 126 | self.vimiv["manipulate"].toggle() 127 | self.assertTrue(self.vimiv["library"].is_focus()) 128 | self.assertFalse(self.vimiv["manipulate"].is_visible()) 129 | self.check_statusbar("WARNING: Manipulate not supported in library") 130 | 131 | def tearDown(self): 132 | os.chdir(self.test_directory) 133 | self.vimiv["library"].reload(os.getcwd()) 134 | 135 | 136 | if __name__ == "__main__": 137 | main() 138 | -------------------------------------------------------------------------------- /tests/opening_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Test the opening of different file-types with vimiv.""" 3 | 4 | import os 5 | from unittest import main 6 | 7 | from vimiv_testcase import VimivTestCase 8 | 9 | 10 | class OpeningTest(VimivTestCase): 11 | """Open with different file-types Test.""" 12 | 13 | @classmethod 14 | def setUpClass(cls): 15 | cls.init_test(cls) 16 | 17 | def test_opening_with_directory(self): 18 | """Opening with a directory.""" 19 | expected_dir = os.path.abspath("vimiv/testimages") 20 | self.init_test(["vimiv/testimages"]) 21 | self.assertEqual(expected_dir, os.getcwd()) 22 | expected_files = ["animation", "arch-logo.png", "arch_001.jpg", 23 | "directory", "symlink_to_image", "vimiv.bmp", 24 | "vimiv.svg", "vimiv.tiff"] 25 | self.assertEqual(self.vimiv["library"].files, expected_files) 26 | self.assertTrue(self.vimiv["library"].is_focus()) 27 | self.assertTrue(self.vimiv["library"].grid.is_visible()) 28 | 29 | def test_opening_with_image(self): 30 | """Open with an image.""" 31 | expected_dir = os.path.abspath("vimiv/testimages") 32 | self.init_test(["vimiv/testimages/arch_001.jpg"]) 33 | # Check moving and image population 34 | self.assertEqual(expected_dir, os.getcwd()) 35 | expected_images = ["arch_001.jpg", "symlink_to_image", "vimiv.bmp", 36 | "vimiv.svg", "vimiv.tiff", "arch-logo.png"] 37 | for image in [os.path.abspath(im) for im in expected_images]: 38 | self.assertIn(image, self.vimiv.get_paths()) 39 | 40 | def test_opening_with_symlink(self): 41 | """Open with a symlink to an image.""" 42 | expected_dir = os.path.abspath("vimiv/testimages") 43 | self.init_test(["vimiv/testimages/symlink_to_image"]) 44 | # Check moving and image population 45 | self.assertEqual(expected_dir, os.getcwd()) 46 | expected_images = ["symlink_to_image", "vimiv.bmp", "vimiv.svg", 47 | "vimiv.tiff", "arch-logo.png", "arch_001.jpg"] 48 | expected_images = [os.path.abspath(image) for image in expected_images] 49 | for image in [os.path.abspath(im) for im in expected_images]: 50 | self.assertIn(image, self.vimiv.get_paths()) 51 | 52 | def test_opening_with_whitespace(self): 53 | """Open an image with whitespace and symlink in directory.""" 54 | expected_dir = os.path.abspath("vimiv/testimages/directory/") 55 | self.init_test(["vimiv/testimages/directory/symlink with spaces .jpg"]) 56 | # Check moving and image population 57 | self.assertEqual(expected_dir, os.getcwd()) 58 | expected_images = ["symlink with spaces .jpg"] 59 | expected_images = [os.path.abspath(image) for image in expected_images] 60 | self.assertEqual(expected_images, self.vimiv.get_paths()) 61 | 62 | def test_opening_recursively(self): 63 | """Open all images recursively.""" 64 | # Need to backup because we init in the wrong directory here 65 | working_dir = self.working_directory 66 | os.chdir("vimiv/testimages") 67 | self.init_test(["."], to_set=["recursive"], values=["true"]) 68 | self.assertEqual(8, len(self.vimiv.get_paths())) 69 | self.settings.reset() 70 | self.working_directory = working_dir 71 | 72 | def tearDown(self): 73 | self.vimiv.quit() 74 | os.chdir(self.working_directory) 75 | 76 | 77 | if __name__ == "__main__": 78 | main() 79 | -------------------------------------------------------------------------------- /tests/search_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Test Search independently of the command line.""" 3 | 4 | from unittest import TestCase, main 5 | 6 | from vimiv.commandline import Search 7 | from vimiv.settings import settings 8 | 9 | 10 | class SearchTest(TestCase): 11 | """Search Tests.""" 12 | 13 | @classmethod 14 | def setUpClass(cls): 15 | settings.override("incsearch", "false") 16 | settings.override("search_case_sensitive", "true") 17 | cls.search = Search(None) 18 | cls.filelist = [] 19 | 20 | def setUp(self): 21 | self.filelist = ["foo_bar", "foo_baz", "zab"] 22 | self.search.init(self.filelist, "foo_bar", "none") 23 | 24 | def test_search(self): 25 | """Search for strings in filelist with independent search class.""" 26 | self.search.run("foo") 27 | self.assertIn("foo_bar", self.search.results) 28 | self.assertIn("foo_baz", self.search.results) 29 | self.assertNotIn("zab", self.search.results) 30 | self.search.run("z") 31 | self.assertEqual(["foo_baz", "zab"], self.search.results) 32 | self.search.run("wololo") 33 | self.assertFalse(self.search.results) 34 | 35 | 36 | if __name__ == "__main__": 37 | main() 38 | -------------------------------------------------------------------------------- /tests/slideshow_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Test slideshow.py for vimiv's test suite.""" 3 | 4 | from unittest import main 5 | 6 | from gi import require_version 7 | require_version("Gtk", "3.0") 8 | from gi.repository import Gtk 9 | 10 | from vimiv_testcase import VimivTestCase 11 | 12 | 13 | def refresh_gui(vimiv=None): 14 | """Refresh the GUI as the Gtk.main() loop is not running when testing. 15 | 16 | Args: 17 | vimiv: Vimiv class to receive slideshow index. 18 | """ 19 | current_pos = vimiv.get_index() 20 | while current_pos == vimiv.get_index(): 21 | Gtk.main_iteration_do(False) 22 | 23 | 24 | class SlideshowTest(VimivTestCase): 25 | """Slideshow Tests.""" 26 | 27 | @classmethod 28 | def setUpClass(cls): 29 | cls.init_test(cls, ["vimiv/testimages/arch_001.jpg", 30 | "vimiv/testimages/vimiv.tiff"]) 31 | cls.slideshow = cls.vimiv["slideshow"] 32 | 33 | def test_toggle(self): 34 | """Toggle slideshow.""" 35 | self.assertFalse(self.slideshow.running) 36 | self.slideshow.toggle() 37 | self.assertTrue(self.slideshow.running) 38 | self.slideshow.toggle() 39 | self.assertFalse(self.slideshow.running) 40 | # Throw some errors 41 | self.vimiv["thumbnail"].toggled = True 42 | self.slideshow.toggle() 43 | self.assertFalse(self.slideshow.running) 44 | self.vimiv["thumbnail"].toggled = False 45 | paths_before = self.vimiv.get_paths() 46 | self.vimiv.populate([]) 47 | self.slideshow.toggle() 48 | self.assertFalse(self.slideshow.running) 49 | self.vimiv.paths = paths_before 50 | 51 | def test_set_delay(self): 52 | """Set slideshow delay.""" 53 | self.assertEqual(self._get_delay(), 2.0) 54 | self.run_command("set slideshow_delay 3.0") 55 | self.assertEqual(self._get_delay(), 3.0) 56 | self.run_command("set slideshow_delay -0.2") 57 | self.assertAlmostEqual(self._get_delay(), 2.8) 58 | self.run_command("set slideshow_delay +0.4") 59 | self.assertAlmostEqual(self._get_delay(), 3.2) 60 | # Shrink with num_str 61 | self.run_command("2set slideshow_delay -0.2") 62 | self.assertAlmostEqual(self._get_delay(), 2.8) 63 | # Set via toggle 64 | self.vimiv["eventhandler"].set_num_str(2) 65 | self.slideshow.toggle() 66 | self.assertEqual(self._get_delay(), 2.0) 67 | # Set to a too small value 68 | self.run_command("set slideshow_delay 0.1") 69 | self.assertEqual(self._get_delay(), 0.5) 70 | # Fail because of invalid argument 71 | self.run_command("set slideshow_delay value") 72 | self.check_statusbar("ERROR: Could not convert 'value' to float") 73 | 74 | def test_running(self): 75 | """Check if slideshow runs correctly.""" 76 | self.assertEqual(self.vimiv.get_index(), 0) 77 | self.slideshow.toggle() 78 | # Set delay when running 79 | self.run_command("set slideshow_delay 0.5") 80 | self.assertEqual(self._get_delay(), 0.5) 81 | # Paths are updated 82 | refresh_gui(self.vimiv) 83 | self.assertEqual(self.vimiv.get_index(), 1) 84 | refresh_gui(self.vimiv) 85 | self.assertEqual(self.vimiv.get_index(), 0) 86 | # Info message should be displayed for a while 87 | self.check_statusbar("INFO: Back at beginning of slideshow") 88 | while not self.vimiv.get_index(): 89 | self.check_statusbar("INFO: Back at beginning of slideshow") 90 | Gtk.main_iteration_do(False) 91 | 92 | def test_get_formatted_delay(self): 93 | """Read out the formatted delay from the slideshow.""" 94 | output = self.slideshow.get_formatted_delay() 95 | self.assertIn("2.0s", output) 96 | 97 | def _get_delay(self): 98 | return self.settings["slideshow_delay"].get_value() 99 | 100 | def tearDown(self): 101 | if self.slideshow.running: 102 | self.slideshow.toggle() 103 | self.settings.override("slideshow_delay", "2.0") 104 | 105 | 106 | if __name__ == "__main__": 107 | main() 108 | -------------------------------------------------------------------------------- /tests/statusbar_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Statusbar tests for vimiv's test suite.""" 3 | 4 | from unittest import main 5 | 6 | from vimiv_testcase import VimivTestCase, refresh_gui 7 | 8 | 9 | class StatusbarTest(VimivTestCase): 10 | """Statusbar Tests.""" 11 | 12 | @classmethod 13 | def setUpClass(cls): 14 | cls.init_test(cls) 15 | cls.statusbar = cls.vimiv["statusbar"] 16 | 17 | def test_toggle_statusbar(self): 18 | """Toggle the statusbar.""" 19 | self.assertTrue(self.statusbar.is_visible()) 20 | self.assertTrue(self.settings["display_bar"].get_value()) 21 | # Hide 22 | self.run_command("set display_bar!") 23 | self.assertFalse(self.statusbar.is_visible()) 24 | self.assertFalse(self.settings["display_bar"].get_value()) 25 | # Show again 26 | self.run_command("set display_bar!") 27 | self.assertTrue(self.statusbar.is_visible()) 28 | self.assertTrue(self.settings["display_bar"].get_value()) 29 | 30 | def test_message(self): 31 | """Show a message.""" 32 | self.statusbar.message("Test error", "error") 33 | self.check_statusbar("ERROR: Test error") 34 | self.statusbar.message("Test warning", "warning") 35 | self.check_statusbar("WARNING: Test warning") 36 | self.statusbar.message("Test info", "info", timeout=0.01) 37 | self.check_statusbar("INFO: Test info") 38 | # Remove error message 39 | refresh_gui(0.015) 40 | self.assertNotEqual(self.statusbar.get_message(), 41 | "INFO: Test info") 42 | 43 | def test_hidden_message(self): 44 | """Show an error message with an initially hidden statusbar.""" 45 | # Hide 46 | self.run_command("set display_bar!") 47 | self.assertFalse(self.statusbar.is_visible()) 48 | # Send an error message 49 | self.statusbar.message("Test error") 50 | self.check_statusbar("ERROR: Test error") 51 | self.assertTrue(self.statusbar.is_visible()) 52 | # Remove error message 53 | self.statusbar.update_info() 54 | self.assertNotEqual(self.statusbar.get_message(), 55 | "ERROR: Test error") 56 | self.assertFalse(self.statusbar.is_visible()) 57 | # Show again 58 | self.run_command("set display_bar!") 59 | self.assertTrue(self.statusbar.is_visible()) 60 | 61 | def test_clear_status(self): 62 | """Clear num_str, search and error message.""" 63 | self.vimiv["eventhandler"].set_num_str(42) 64 | self.vimiv["commandline"].search.results = ["Something"] 65 | self.statusbar.message("Catastrophe", "error") 66 | self.check_statusbar("ERROR: Catastrophe") 67 | self.statusbar.clear_status() 68 | self.assertFalse(self.vimiv["eventhandler"].get_num_str()) 69 | self.assertFalse(self.vimiv["commandline"].search.results) 70 | self.assertNotEqual(self.vimiv["statusbar"].get_message(), 71 | "Error: Catastrophe") 72 | 73 | 74 | if __name__ == "__main__": 75 | main() 76 | -------------------------------------------------------------------------------- /tests/tags_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Tag tests for vimiv's test suite.""" 3 | 4 | import os 5 | from unittest import main 6 | 7 | from gi import require_version 8 | require_version('Gtk', '3.0') 9 | from vimiv.helpers import read_file, get_user_data_dir 10 | 11 | from vimiv_testcase import VimivTestCase 12 | 13 | 14 | class TagsTest(VimivTestCase): 15 | """Tag Tests.""" 16 | 17 | @classmethod 18 | def setUpClass(cls): 19 | cls.init_test(cls) 20 | cls.tagdir = os.path.join(get_user_data_dir(), "vimiv", "Tags") 21 | 22 | def test_tag_create_remove(self): 23 | """Create and remove a tag.""" 24 | taglist = ["vimiv/testimages/arch-logo.png", 25 | "vimiv/testimages/arch_001.jpg"] 26 | taglist = [os.path.abspath(image) for image in taglist] 27 | self.vimiv["tags"].write(taglist, "arch_test_tag") 28 | created_file = os.path.join(self.tagdir, "arch_test_tag") 29 | file_content = read_file(created_file) 30 | self.assertEqual(file_content, taglist) 31 | self.vimiv["tags"].remove("arch_test_tag") 32 | self.assertFalse(os.path.isfile(created_file)) 33 | 34 | def test_tag_create_remove_with_whitespace(self): 35 | """Create and remove a tag with whitespace in the name.""" 36 | taglist = ["vimiv/testimages/arch-logo.png", 37 | "vimiv/testimages/arch_001.jpg"] 38 | taglist = [os.path.abspath(image) for image in taglist] 39 | self.vimiv["tags"].write(taglist, "arch test tag") 40 | created_file = os.path.join(self.tagdir, "arch test tag") 41 | file_content = read_file(created_file) 42 | self.assertEqual(file_content, taglist) 43 | self.vimiv["tags"].remove("arch test tag") 44 | self.assertFalse(os.path.isfile(created_file)) 45 | 46 | def test_tag_append(self): 47 | """Append to a tag.""" 48 | taglist = [os.path.abspath("vimiv/testimages/arch-logo.png")] 49 | self.vimiv["tags"].write(taglist, "arch_test_tag") 50 | taglist2 = [os.path.abspath("vimiv/testimages/arch_001.jpg")] 51 | self.vimiv["tags"].write(taglist2, "arch_test_tag") 52 | complete_taglist = taglist + taglist2 53 | created_file = os.path.join(self.tagdir, "arch_test_tag") 54 | file_content = read_file(created_file) 55 | self.assertEqual(file_content, complete_taglist) 56 | self.vimiv["tags"].remove("arch_test_tag") 57 | 58 | def test_tag_load(self): 59 | """Load a tag.""" 60 | taglist = ["vimiv/testimages/arch-logo.png", 61 | "vimiv/testimages/arch_001.jpg"] 62 | taglist = [os.path.abspath(image) for image in taglist] 63 | self.vimiv["tags"].write(taglist, "arch_test_tag") 64 | self.vimiv["tags"].load("arch_test_tag") 65 | self.assertEqual(self.vimiv.get_paths(), taglist) 66 | self.vimiv["tags"].remove("arch_test_tag") 67 | 68 | def test_tag_load_single(self): 69 | """Load a tag with only one file.""" 70 | taglist = ["vimiv/testimages/arch-logo.png"] 71 | taglist = [os.path.abspath(image) for image in taglist] 72 | self.vimiv["tags"].write(taglist, "arch_test_tag") 73 | self.vimiv["tags"].load("arch_test_tag") 74 | self.assertEqual(self.vimiv.get_paths(), taglist) 75 | self.vimiv["tags"].remove("arch_test_tag") 76 | 77 | def test_tag_errors(self): 78 | """Error messages with tags.""" 79 | unavailable_file = os.path.join(self.tagdir, "foo_is_real") 80 | self.vimiv["tags"].load("foo_is_real") 81 | self.check_statusbar("ERROR: Tagfile 'foo_is_real' has no valid images") 82 | os.remove(unavailable_file) 83 | self.vimiv["tags"].remove("foo_is_real") # Error message 84 | self.check_statusbar("ERROR: Tagfile 'foo_is_real' does not exist") 85 | 86 | def test_tag_commandline(self): 87 | """Tag commands from command line.""" 88 | # Set some marked images 89 | taglist = ["vimiv/testimages/arch-logo.png", 90 | "vimiv/testimages/arch_001.jpg"] 91 | taglist = [os.path.abspath(image) for image in taglist] 92 | self.vimiv["mark"].marked = list(taglist) 93 | # Write a tag 94 | self.run_command("tag_write new_test_tag") 95 | created_file = os.path.join(self.tagdir, "new_test_tag") 96 | file_content = read_file(created_file) 97 | self.assertEqual(taglist, file_content) 98 | # Load a tag 99 | self.run_command("tag_load new_test_tag") 100 | self.assertEqual(self.vimiv.get_paths(), taglist) 101 | # Delete a tag 102 | self.run_command("tag_remove new_test_tag") 103 | self.assertFalse(os.path.isfile(created_file)) 104 | 105 | def tearDown(self): 106 | os.chdir(self.working_directory) 107 | 108 | 109 | if __name__ == "__main__": 110 | main() 111 | -------------------------------------------------------------------------------- /tests/thumbnail_manager_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Test thumbnail_manager.py for vimiv's test suite. 3 | 4 | Most of the base class ThumbnailManager is indirectly tested in the thumbnail 5 | test. We therefore test the implementation of the ThumbnailStore directly. 6 | """ 7 | 8 | import hashlib 9 | import os 10 | import shutil 11 | import tempfile 12 | from unittest import TestCase, main 13 | 14 | from gi import require_version 15 | require_version('Gtk', '3.0') 16 | from vimiv.helpers import get_user_cache_dir 17 | from vimiv.thumbnail_manager import ThumbnailStore 18 | 19 | 20 | class ThumbnailManagerTest(TestCase): 21 | """Test thumbnail_manager.""" 22 | 23 | @classmethod 24 | def setUpClass(cls): 25 | cls.thumb_store = ThumbnailStore() 26 | cache_dir = get_user_cache_dir() 27 | cls.thumb_dir = os.path.join(cache_dir, "thumbnails/large") 28 | 29 | def test_get_thumbnail(self): 30 | """Get the filename of the thumbnail and create it.""" 31 | # Standard file for which the thumbnail does not yet exist 32 | new_dir = tempfile.TemporaryDirectory(prefix="vimivtests-") 33 | new_file = os.path.join(new_dir.name, "test.png") 34 | shutil.copyfile("vimiv/testimages/arch-logo.png", new_file) 35 | received_name = self.thumb_store.get_thumbnail(new_file) 36 | # Check name 37 | uri = "file://" + os.path.abspath(os.path.expanduser(new_file)) 38 | thumb_name = hashlib.md5(bytes(uri, "utf-8")).hexdigest() + ".png" 39 | expected_name = os.path.join(self.thumb_dir, thumb_name) 40 | self.assertEqual(received_name, expected_name) 41 | # File should exist and is a file 42 | self.assertTrue(os.path.isfile(received_name)) 43 | 44 | new_dir.cleanup() 45 | 46 | # Now the newly generated thumbnail file 47 | received_name = self.thumb_store.get_thumbnail(expected_name) 48 | self.assertEqual(received_name, expected_name) 49 | 50 | # A file that does not exist 51 | self.assertFalse(self.thumb_store.get_thumbnail("bla")) 52 | 53 | 54 | if __name__ == "__main__": 55 | main() 56 | -------------------------------------------------------------------------------- /tests/thumbnail_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Test thumbnail.py for vimiv's test suite.""" 3 | 4 | import os 5 | from unittest import main 6 | 7 | from gi import require_version 8 | from gi.repository import Gtk 9 | 10 | from vimiv_testcase import VimivTestCase, refresh_gui 11 | 12 | require_version("Gtk", "3.0") 13 | 14 | 15 | class ThumbnailTest(VimivTestCase): 16 | """Test thumbnail.""" 17 | 18 | @classmethod 19 | def setUpClass(cls): 20 | cls.init_test(cls, ["vimiv/testimages/arch-logo.png"]) 21 | cls.thumb = cls.vimiv["thumbnail"] 22 | 23 | def setUp(self): 24 | self.thumb.toggle() 25 | 26 | def test_toggle(self): 27 | """Toggle thumbnail mode.""" 28 | self.assertTrue(self.thumb.toggled) 29 | self.assertTrue(self.thumb.is_focus()) 30 | self.thumb.toggle() 31 | self.assertFalse(self.thumb.toggled) 32 | self.assertFalse(self.thumb.is_focus()) 33 | 34 | def test_iconview_clicked(self): 35 | """Select thumbnail.""" 36 | path = Gtk.TreePath([1]) 37 | self.thumb.emit("item-activated", path) 38 | self.assertFalse(self.thumb.toggled) 39 | self.assertFalse(self.thumb.is_focus()) 40 | expected_image = os.path.abspath("arch_001.jpg") 41 | received_image = self.vimiv.get_path() 42 | self.assertEqual(expected_image, received_image) 43 | 44 | def test_calculate_columns(self): 45 | """Calculate thumbnail columns.""" 46 | expected_columns = int(800 / (self.thumb.get_zoom_level()[0] + 30)) 47 | received_columns = self.thumb.get_columns() 48 | self.assertEqual(expected_columns, received_columns) 49 | 50 | def test_reload(self): 51 | """Reload and name thumbnails.""" 52 | path = self.vimiv.get_pos(True) 53 | expected_name, _ = os.path.splitext(path) 54 | expected_name = os.path.basename(expected_name) 55 | name = self._get_thumbnail_name() 56 | self.assertEqual(expected_name, name) 57 | self.vimiv["mark"].mark() 58 | self.thumb.reload(path) 59 | name = self._get_thumbnail_name() 60 | self.assertEqual(name, expected_name + " [*]") 61 | 62 | def test_move(self): 63 | """Move in thumbnail mode.""" 64 | start = self.vimiv.get_pos() 65 | # All items are in the same row 66 | # l moves 1 to the right 67 | self.thumb.move_direction("l") 68 | self.assertEqual(self.vimiv.get_paths()[start + 1], 69 | self.vimiv.get_pos(True)) 70 | # h moves 1 to the left 71 | self.thumb.move_direction("h") 72 | self.assertEqual(self.vimiv.get_paths()[start], 73 | self.vimiv.get_pos(True)) 74 | # L moves to the last element 75 | self.thumb.move_direction("L") 76 | self.assertEqual(self.vimiv.get_paths()[-1], self.vimiv.get_pos(True)) 77 | # H moves back to the first element 78 | self.thumb.move_direction("H") 79 | self.assertEqual(self.vimiv.get_paths()[0], 80 | self.vimiv.get_pos(True)) 81 | # L and H directly from the window implementation of scroll 82 | self.vimiv["main_window"].scroll("L") 83 | self.assertEqual(self.vimiv.get_paths()[-1], self.vimiv.get_pos(True)) 84 | self.vimiv["main_window"].scroll("H") 85 | self.assertEqual(self.vimiv.get_paths()[0], 86 | self.vimiv.get_pos(True)) 87 | # Get amount of rows for vertical scrolling 88 | last = len(self.vimiv.get_paths()) - 1 89 | rows = self.thumb.get_item_row(Gtk.TreePath(last)) + 1 90 | if rows > 1: 91 | self.fail("Implementation not done for more than one row.") 92 | for direction in "jkJK": 93 | self.thumb.move_direction(direction) 94 | self.assertEqual(self.vimiv.get_paths()[0], 95 | self.vimiv.get_pos(True)) 96 | 97 | def test_search(self): 98 | """Search in thumbnail mode.""" 99 | # Incsearch 100 | self.vimiv["commandline"].incsearch = True 101 | self.vimiv["commandline"].enter_search() 102 | self.vimiv["commandline"].set_text("/symlink") 103 | self.assertIn("symlink_to_image", 104 | self.vimiv.get_paths()[self.thumb.get_position()]) 105 | self.vimiv["commandline"].leave() 106 | # Normal search 107 | self.vimiv["commandline"].incsearch = False 108 | self.run_search("arch") 109 | self.assertIn("arch-logo", self._get_thumbnail_name()) 110 | # Moving 111 | self.vimiv["commandline"].search_move(forward=True) 112 | self.assertIn("arch_001", self._get_thumbnail_name()) 113 | self.vimiv["commandline"].search_move(forward=False) 114 | self.assertIn("arch-logo", self._get_thumbnail_name()) 115 | 116 | def test_zoom(self): 117 | """Zoom in thumbnail mode.""" 118 | # Zoom to the value of 128 119 | while self.thumb.get_zoom_level()[0] > 128: 120 | self._zoom_wrapper(False) 121 | while self.thumb.get_zoom_level()[0] < 128: 122 | self._zoom_wrapper(True) 123 | self.assertEqual(self.thumb.get_zoom_level(), (128, 128)) 124 | # Zoom in and check thumbnail size and pixbuf 125 | self._zoom_wrapper(True) 126 | self.assertEqual(self.thumb.get_zoom_level(), (256, 256)) 127 | # Zoom in twice should end at (512, 512) 128 | self._zoom_wrapper(True) 129 | refresh_gui() 130 | self._zoom_wrapper(True) 131 | self.assertEqual(self.thumb.get_zoom_level(), (512, 512)) 132 | # We check the pixbuf here as well 133 | scale = self._get_pixbuf_scale() 134 | # This might take a while, repeat until it works setting a limit 135 | count = 0 136 | while scale != 512: 137 | if count > 10: 138 | self.fail("Pixbuf not updated") 139 | scale = self._get_pixbuf_scale() 140 | refresh_gui(0.1) 141 | count += 1 142 | # Zoom out 143 | self._zoom_wrapper(False) 144 | self._zoom_wrapper(False) 145 | self.assertEqual(self.thumb.get_zoom_level(), (128, 128)) 146 | # Zoom directly with the window implementation of zoom 147 | self.vimiv["window"].zoom(True) 148 | self.assertEqual(self.thumb.get_zoom_level(), (256, 256)) 149 | self.vimiv["window"].zoom(False) 150 | self.assertEqual(self.thumb.get_zoom_level(), (128, 128)) 151 | 152 | def _get_thumbnail_name(self): 153 | model = self.thumb.get_model() 154 | index = self.thumb.get_position() 155 | return model.get_value(model.get_iter(index), 1) 156 | 157 | def _get_thumbnail_pixbuf(self, index): 158 | model = self.thumb.get_model() 159 | return model.get_value(model.get_iter(index), 0) 160 | 161 | def _get_pixbuf_scale(self): 162 | pixbuf = self._get_thumbnail_pixbuf(0) 163 | return max(pixbuf.get_width(), pixbuf.get_height()) 164 | 165 | def _zoom_wrapper(self, zoom_in): 166 | self.thumb.zoom(zoom_in) 167 | refresh_gui() 168 | 169 | def tearDown(self): 170 | if self.thumb.toggled: 171 | self.thumb.toggle() 172 | self.vimiv.index = 0 173 | 174 | 175 | if __name__ == "__main__": 176 | main() 177 | -------------------------------------------------------------------------------- /tests/trash_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Test vimiv's trash management.""" 3 | 4 | import os 5 | import tempfile 6 | import time 7 | from unittest import TestCase, main 8 | 9 | from gi import require_version 10 | require_version('Gtk', '3.0') 11 | from vimiv import trash_manager 12 | from vimiv.helpers import get_user_data_dir 13 | 14 | 15 | class TrashTest(TestCase): 16 | """TrashManager Tests.""" 17 | 18 | @classmethod 19 | def setUpClass(cls): 20 | # Run in tmp 21 | cls.tmpdir = tempfile.TemporaryDirectory(prefix="vimiv-tests-") 22 | os.environ["XDG_DATA_HOME"] = cls.tmpdir.name 23 | cls.trash_manager = trash_manager.TrashManager() 24 | cls.files_directory = os.path.join(get_user_data_dir(), 25 | "Trash/files") 26 | cls.info_directory = os.path.join(get_user_data_dir(), 27 | "Trash/info") 28 | cls.testfile = "" 29 | cls.basename = "" 30 | 31 | def setUp(self): 32 | _, self.testfile = tempfile.mkstemp() 33 | self.basename = os.path.basename(self.testfile) 34 | self._create_file() 35 | self.assertTrue(os.path.exists(self.testfile)) 36 | 37 | def test_move_to_trash(self): 38 | """Move a file to the trash directory.""" 39 | self.trash_manager.delete(self.testfile) 40 | self.assertFalse(os.path.exists(self.testfile)) # File should not exist 41 | # Trash file should exist and contain "something" 42 | expected_trashfile = os.path.join(self.files_directory, self.basename) 43 | self.assertTrue(os.path.exists(expected_trashfile)) 44 | with open(expected_trashfile) as f: 45 | content = f.read() 46 | self.assertEqual(content, "something\n") 47 | # Info file should exist and contain path, and date 48 | expected_infofile = os.path.join(self.info_directory, 49 | self.basename + ".trashinfo") 50 | self.assertTrue(os.path.exists(expected_infofile)) 51 | with open(expected_infofile) as f: 52 | lines = f.readlines() 53 | self.assertEqual(lines[0], "[Trash Info]\n") 54 | self.assertEqual(lines[1], "Path=%s\n" % (self.testfile)) 55 | self.assertIn("DeletionDate=%s" % (time.strftime("%Y%m")), lines[2]) 56 | 57 | def test_undelete_from_trash(self): 58 | """Undelete a file from trash.""" 59 | # First delete a file 60 | self.trash_manager.delete(self.testfile) 61 | self.assertFalse(os.path.exists(self.testfile)) 62 | # Now undelete it 63 | self.trash_manager.undelete(self.basename) 64 | self.assertTrue(os.path.exists(self.testfile)) 65 | 66 | def test_delete_file_with_same_name(self): 67 | """Delete a file with the same name more than twice.""" 68 | def run_one_round(suffix=""): 69 | """Test if self.testfile + suffix exists in trash.""" 70 | self.trash_manager.delete(self.testfile) 71 | self.assertFalse(os.path.exists(self.testfile)) 72 | expected_trashfile = \ 73 | os.path.join(self.files_directory, self.basename + suffix) 74 | self.assertTrue(os.path.exists(expected_trashfile)) 75 | self._create_file() 76 | run_one_round() 77 | run_one_round(".2") 78 | run_one_round(".3") 79 | 80 | def _create_file(self): 81 | with open(self.testfile, "w") as f: 82 | f.write("something\n") 83 | 84 | def tearDown(self): 85 | if os.path.exists(self.testfile): 86 | os.remove(self.testfile) 87 | 88 | 89 | if __name__ == "__main__": 90 | main() 91 | -------------------------------------------------------------------------------- /tests/vimiv_testcase.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Wrapper of TestCase which sets up the vimiv class and quits it when done.""" 3 | 4 | import os 5 | import time 6 | from unittest import TestCase, main 7 | 8 | from gi import require_version 9 | require_version("GdkPixbuf", "2.0") 10 | require_version("Gtk", "3.0") 11 | from gi.repository import GdkPixbuf, Gio, GLib, Gtk 12 | from vimiv.app import Vimiv 13 | from vimiv.settings import settings 14 | 15 | 16 | def refresh_gui(delay=0): 17 | """Refresh the GUI as the Gtk.main() loop is not running when testing. 18 | 19 | Args: 20 | delay: Time to wait before refreshing. 21 | """ 22 | time.sleep(delay) 23 | while Gtk.events_pending(): 24 | Gtk.main_iteration_do(False) 25 | 26 | 27 | def compare_pixbufs(pb1, pb2): 28 | """Compare to pixbufs.""" 29 | return pb1.get_pixels() == pb2.get_pixels() 30 | 31 | 32 | def compare_files(file1, file2): 33 | """Directly compare two image files using GdkPixbuf.""" 34 | pb1 = GdkPixbuf.Pixbuf.new_from_file(file1) 35 | pb2 = GdkPixbuf.Pixbuf.new_from_file(file2) 36 | return compare_pixbufs(pb1, pb2) 37 | 38 | 39 | class VimivTestCase(TestCase): 40 | """Wrapper Class of TestCase.""" 41 | 42 | @classmethod 43 | def setUpClass(cls): 44 | # Get set in init_test as setUpClass will be overridden 45 | cls.working_directory = "" 46 | cls.settings = settings 47 | cls.vimiv = Vimiv() 48 | cls.init_test(cls) 49 | 50 | def init_test(self, paths=None, to_set=None, values=None, debug=False): 51 | """Initialize a testable vimiv object saved as self.vimiv. 52 | 53 | Args: 54 | paths: Paths passed to vimiv. 55 | to_set: List of settings to be set. 56 | values: List of values for settings to be set. 57 | """ 58 | self.working_directory = os.getcwd() 59 | # Create vimiv class with settings, paths, ... 60 | self.vimiv = Vimiv(True) # True for running_tests 61 | self.settings = settings 62 | self.vimiv.debug = debug 63 | options = GLib.VariantDict.new() 64 | bool_true = GLib.Variant("b", True) 65 | options.insert_value("temp-basedir", bool_true) 66 | self.vimiv.do_handle_local_options(options) 67 | # Set the required settings 68 | if to_set: 69 | for i, setting in enumerate(to_set): 70 | self.settings.override(setting, values[i]) 71 | self.vimiv.register() 72 | self.vimiv.do_startup(self.vimiv) 73 | if paths: 74 | # Create a list of Gio.Files for vimiv.do_open 75 | files = [Gio.File.new_for_path(path) for path in paths] 76 | self.vimiv.do_open(files, len(paths), "") 77 | else: 78 | self.vimiv.activate_vimiv(self.vimiv) 79 | 80 | def run_command(self, command, external=False): 81 | """Run a command in the command line. 82 | 83 | Args: 84 | command: The command to run. 85 | external: If True, run an external command and wait for it to 86 | finish. 87 | """ 88 | self.vimiv["commandline"].enter(command) 89 | self.vimiv["commandline"].emit("activate") 90 | if external: 91 | while self.vimiv["commandline"].running_processes: 92 | self.vimiv["commandline"].running_processes[0].wait() 93 | time.sleep(0.001) 94 | 95 | def run_search(self, string): 96 | """Search for string from the command line.""" 97 | self.vimiv["commandline"].enter_search() 98 | self.vimiv["commandline"].set_text("/" + string) 99 | self.vimiv["commandline"].emit("activate") 100 | 101 | def check_statusbar(self, expected_text): 102 | """Check statusbar for text.""" 103 | statusbar_text = self.vimiv["statusbar"].get_message() 104 | self.assertEqual(expected_text, statusbar_text) 105 | 106 | @classmethod 107 | def tearDownClass(cls): 108 | cls.settings.reset() 109 | cls.vimiv.quit() 110 | os.chdir(cls.working_directory) 111 | 112 | 113 | if __name__ == "__main__": 114 | main() 115 | -------------------------------------------------------------------------------- /tests/window_test.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Tests window.py for vimiv's test suite.""" 3 | 4 | import os 5 | from unittest import main, skipUnless 6 | 7 | from gi import require_version 8 | require_version('Gtk', '3.0') 9 | from gi.repository import Gdk 10 | 11 | from vimiv_testcase import VimivTestCase, refresh_gui 12 | 13 | 14 | class WindowTest(VimivTestCase): 15 | """Window Tests.""" 16 | 17 | @classmethod 18 | def setUpClass(cls): 19 | cls.init_test(cls, ["vimiv/testimages/"]) 20 | 21 | def test_fullscreen(self): 22 | """Toggle fullscreen.""" 23 | # Start without fullscreen 24 | self.assertFalse(self._is_fullscreen()) 25 | # Fullscreen 26 | self.vimiv["window"].toggle_fullscreen() 27 | refresh_gui(0.05) 28 | # Still not reliable 29 | # self.assertTrue(self._is_fullscreen()) 30 | # Unfullscreen 31 | self.vimiv["window"].toggle_fullscreen() 32 | refresh_gui(0.05) 33 | # self.assertFalse(self.vimiv["window"].is_fullscreen) 34 | self.vimiv["window"].fullscreen() 35 | 36 | def _is_fullscreen(self): 37 | state = self.vimiv["window"].get_window().get_state() 38 | return True if state & Gdk.WindowState.FULLSCREEN else False 39 | 40 | @skipUnless(os.getenv("DISPLAY") == ":42", "Must run in Xvfb") 41 | def test_check_resize(self): 42 | """Resize window and check winsize.""" 43 | self.assertEqual(self.vimiv["window"].winsize, (800, 600)) 44 | self.vimiv["window"].resize(400, 300) 45 | refresh_gui() 46 | self.assertEqual(self.vimiv["window"].winsize, (400, 300)) 47 | 48 | 49 | if __name__ == "__main__": 50 | main() 51 | -------------------------------------------------------------------------------- /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 --start-from-desktop %F 9 | Categories=Graphics;GTK 10 | MimeType=image/bmp;image/gif;image/jpeg;image/jp2;image/jpeg2000;image/jpx;image/png;image/svg;image/tiff; 11 | -------------------------------------------------------------------------------- /vimiv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 3 | """Vimiv executable.""" 4 | import signal 5 | import sys 6 | from vimiv.app import Vimiv 7 | 8 | if __name__ == '__main__': 9 | signal.signal(signal.SIGINT, signal.SIG_DFL) # ^C 10 | Vimiv().run(sys.argv) 11 | -------------------------------------------------------------------------------- /vimiv/__init__.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """vimiv init: tests some imports and imports main.""" 3 | 4 | import sys 5 | 6 | __license__ = "MIT" 7 | __version__ = "0.9.2.dev0" 8 | __author__ = __maintainer__ = "Christian Karl" 9 | __email__ = "karlch@protonmail.com" 10 | 11 | try: 12 | from gi import require_version 13 | require_version("Gtk", "3.0") 14 | try: 15 | require_version("GExiv2", "0.10") 16 | # No EXIF support 17 | except ValueError: 18 | pass 19 | from gi.repository import GLib, Gtk, Gdk, GdkPixbuf, Pango 20 | 21 | except ImportError as import_error: 22 | message = import_error.msg + "\n" + "Are all dependencies installed?" 23 | print(message) 24 | sys.exit(1) 25 | -------------------------------------------------------------------------------- /vimiv/config_parser.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """All parsers for vimiv.""" 3 | 4 | import configparser 5 | import os 6 | import sys 7 | 8 | from vimiv.exceptions import SettingNotFoundError, StringConversionError 9 | from vimiv.helpers import error_message, get_user_config_dir 10 | from vimiv.settings import settings 11 | 12 | 13 | def get_aliases(config): 14 | """Add aliases from the configfile to the ALIASES section of settings. 15 | 16 | Args: 17 | config: configparser.ConfigParser of the configfile. 18 | 19 | Return: 20 | aliases: Dictionary of aliases. 21 | message: Error message filled with aliases that cannot be parsed. 22 | """ 23 | message = "" 24 | aliases = {} 25 | 26 | try: 27 | alias_section = config["ALIASES"] 28 | except KeyError: 29 | # Default to no aliases if the section does not exist in the configfile 30 | alias_section = dict() 31 | 32 | for alias in alias_section: 33 | try: 34 | aliases[alias] = alias_section[alias] 35 | except configparser.InterpolationError as e: 36 | message += "Parsing alias '%s' failed.\n" % alias \ 37 | + "If you meant to use % for current file, use %%.\n" \ 38 | + e.message + "\n" 39 | 40 | return aliases, message 41 | 42 | 43 | def parse_config(commandline_config=None, running_tests=False): 44 | """Check each configfile for settings and apply them. 45 | 46 | Args: 47 | commandline_config: Configfile given by command line flag to parse. 48 | running_tests: If True, running from testsuite. 49 | Return: 50 | Dictionary of modified settings. 51 | """ 52 | configfiles = [] 53 | # We do not want to parse user configuration files when running the test 54 | # suite 55 | if not running_tests: 56 | configfiles += [ 57 | "/etc/vimiv/vimivrc", 58 | os.path.join(get_user_config_dir(), "vimiv/vimivrc"), 59 | os.path.expanduser("~/.vimiv/vimivrc")] 60 | if commandline_config: 61 | configfiles.append(commandline_config) 62 | 63 | # Error message, gets filled with invalid sections in the user's configfile. 64 | # If any exist, a popup is displayed at startup. 65 | message = "" 66 | 67 | # Let ConfigParser parse the list of configuration files 68 | config = configparser.ConfigParser() 69 | try: 70 | config.read(configfiles) 71 | except UnicodeDecodeError as e: 72 | message += "Could not decode configfile.\n" + str(e) 73 | except configparser.MissingSectionHeaderError as e: 74 | message += "Invalid configfile.\n" + str(e) 75 | except configparser.ParsingError as e: 76 | message += str(e) 77 | 78 | # Override settings with settings in config 79 | for header in ["GENERAL", "LIBRARY", "EDIT"]: 80 | if header not in config: 81 | continue 82 | section = config[header] 83 | for setting in section: 84 | try: 85 | settings.override(setting, section[setting]) 86 | except StringConversionError as e: 87 | message += str(e) + "\n" 88 | except SettingNotFoundError: 89 | message += "Unknown setting %s\n" % (setting) 90 | 91 | # Receive aliases 92 | aliases, partial_message = get_aliases(config) 93 | settings.set_aliases(aliases) 94 | message += partial_message 95 | 96 | if message: 97 | error_message(message, running_tests=running_tests) 98 | return settings 99 | 100 | 101 | def parse_keys(keyfiles=None, running_tests=False): 102 | """Check for a keyfile and parse it. 103 | 104 | Args: 105 | keyfiles: List of keybinding files. If not None, use this list instead 106 | of the default files. 107 | running_tests: If True running from testsuite. Do not show error popup. 108 | Return: 109 | Dictionary of keybindings. 110 | """ 111 | if not keyfiles: 112 | keyfiles = \ 113 | ["/etc/vimiv/keys.conf", 114 | os.path.join(get_user_config_dir(), "vimiv/keys.conf"), 115 | os.path.expanduser("~/.vimiv/keys.conf")] 116 | # Read the list of files 117 | keys = configparser.ConfigParser() 118 | try: 119 | # No file for keybindings found 120 | if not keys.read(keyfiles): 121 | message = "Keyfile not found. Exiting." 122 | error_message(message, running_tests=running_tests) 123 | sys.exit(1) 124 | except configparser.DuplicateOptionError as e: 125 | message = e.message + ".\n Duplicate keybinding. Exiting." 126 | error_message(message, running_tests=running_tests) 127 | sys.exit(1) 128 | 129 | # Get the keybinding dictionaries checking for errors 130 | try: 131 | keys_image = keys["IMAGE"] 132 | keys_thumbnail = keys["THUMBNAIL"] 133 | keys_library = keys["LIBRARY"] 134 | keys_manipulate = keys["MANIPULATE"] 135 | keys_command = keys["COMMAND"] 136 | except KeyError as e: 137 | message = "Missing section " + str(e) + " in keys.conf.\n" \ 138 | "Refer to vimivrc(5) to fix your config." 139 | error_message(message, running_tests=running_tests) 140 | sys.exit(1) 141 | 142 | # Update the dictionaries of every window with the keybindings that apply 143 | # for more than one window 144 | def update_keybindings(sections, keydict): 145 | """Add keybindings from generic sections to keydict.""" 146 | for section in sections: 147 | if section in keys: 148 | print("Section", section, "is deprecated and will be removed in" 149 | " a future version.") 150 | keydict.update(keys[section]) 151 | update_keybindings(["GENERAL", "IM_THUMB", "IM_LIB"], keys_image) 152 | update_keybindings(["GENERAL", "IM_THUMB"], keys_thumbnail) 153 | update_keybindings(["GENERAL", "IM_LIB"], keys_library) 154 | 155 | # Generate one dictionary for all and return it 156 | keybindings = {"IMAGE": keys_image, 157 | "THUMBNAIL": keys_thumbnail, 158 | "LIBRARY": keys_library, 159 | "MANIPULATE": keys_manipulate, 160 | "COMMAND": keys_command} 161 | return keybindings 162 | -------------------------------------------------------------------------------- /vimiv/eventhandler.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Handles the keyboard for vimiv.""" 3 | 4 | from gi.repository import Gdk, GLib, GObject 5 | from vimiv.config_parser import parse_keys 6 | from vimiv.helpers import get_float, get_int 7 | 8 | 9 | class EventHandler(GObject.Object): 10 | """Handle keyboard/mouse/touch events for vimiv. 11 | 12 | Attributes: 13 | num_str: String containing repetition number for commands. 14 | 15 | _app: Main vimiv application to interact with. 16 | _keys: Keybindings from configfiles. 17 | _timer_id: ID of the current timer running to clear num_str. 18 | _timer_id_touch: ID of the current timer running to reconnect clicks 19 | after a touch by touchscreen. 20 | """ 21 | 22 | def __init__(self, app): 23 | """Initialize defaults.""" 24 | super(EventHandler, self).__init__() 25 | self._num_str = "" 26 | self._app = app 27 | self._timer_id = 0 28 | self._timer_id_touch = 0 29 | self._keys = parse_keys(running_tests=self._app.running_tests) 30 | 31 | def on_key_press(self, widget, event, widget_name): 32 | """Handle key press event.""" 33 | if event.type == Gdk.EventType.KEY_PRESS: 34 | keyval = event.keyval 35 | keyname = Gdk.keyval_name(keyval) 36 | keyname = self._check_modifiers(event, keyname) 37 | return self._run(keyname, widget_name) 38 | 39 | def on_click(self, widget, event, widget_name): 40 | """Handle click event.""" 41 | # Do not handle double clicks 42 | if not event.type == Gdk.EventType.BUTTON_PRESS: 43 | return 44 | button_name = "Button" + str(event.button) 45 | button_name = self._check_modifiers(event, button_name) 46 | return self._run(button_name, widget_name) 47 | 48 | def _run(self, keyname, widget_name): 49 | """Run the correct function per keypress. 50 | 51 | Args: 52 | keyname: Name of the key that was pressed/clicked/... 53 | widget_name: Name of widget to operate on: image, library... 54 | """ 55 | # Numbers for the num_str 56 | if widget_name != "COMMAND" and keyname.isdigit(): 57 | self.num_append(keyname) 58 | return True 59 | # Get the relevant keybindings for the window from the various 60 | # sections in the keys.conf file 61 | keys = self._keys[widget_name] 62 | # Get the command to which the pressed key is bound and run it 63 | if keyname in keys: 64 | keybinding = keys[keyname] 65 | # Write keybinding and key to log in debug mode 66 | if self._app.debug: 67 | self._app["log"].write_message("key", 68 | keyname + ": " + keybinding) 69 | self._app["commandline"].run_command(keybinding, keyname) 70 | return True # Deactivates default bindings 71 | # Activate default keybindings 72 | else: 73 | return False 74 | 75 | def on_touch(self, widget, event): 76 | """Clear mouse connection when touching screen. 77 | 78 | This stops calling the ButtonX bindings when using the touch screen. 79 | Reasoning: We do not want to e.g. move to the next image when trying to 80 | zoom in. 81 | """ 82 | try: 83 | self._app["window"].disconnect_by_func(self.on_click) 84 | # Was already disconnected 85 | except TypeError: 86 | pass 87 | if self._timer_id_touch: 88 | GLib.source_remove(self._timer_id_touch) 89 | self._timer_id_touch = GLib.timeout_add(5, self._reconnect_click) 90 | return True 91 | 92 | def _reconnect_click(self): 93 | """Reconnect the click signal after a touch event.""" 94 | self._app["window"].connect("button_press_event", 95 | self.on_click, "IMAGE") 96 | self._timer_id_touch = 0 97 | 98 | def _check_modifiers(self, event, keyname): 99 | """Update keyname according to modifiers in event.""" 100 | shiftkeys = ["space", "Return", "Tab", "Escape", "BackSpace", 101 | "Up", "Down", "Left", "Right"] 102 | # Check for Control (^), Mod1 (Alt) or Shift 103 | if event.get_state() & Gdk.ModifierType.CONTROL_MASK: 104 | keyname = "^" + keyname 105 | if event.get_state() & Gdk.ModifierType.MOD1_MASK: 106 | keyname = "Alt+" + keyname 107 | # Shift+ for all letters and for keys that don't support it 108 | if (event.get_state() & Gdk.ModifierType.SHIFT_MASK and 109 | (len(keyname) < 2 or keyname in shiftkeys 110 | or keyname.startswith("Button"))): 111 | keyname = "Shift+" + keyname.lower() 112 | if keyname == "ISO_Left_Tab": # Tab is named really weird under shift 113 | keyname = "Shift+Tab" 114 | return keyname 115 | 116 | def num_append(self, num, remove_by_timeout=True): 117 | """Add a new char to num_str. 118 | 119 | Args: 120 | num: The number to append to the string. 121 | remove_by_timeout: If True, add a timeout to clear the num_str. 122 | """ 123 | # Remove old timers if we have new numbers 124 | if self._timer_id: 125 | GLib.source_remove(self._timer_id) 126 | self._timer_id = GLib.timeout_add_seconds(1, self.num_clear) 127 | self._num_str += num 128 | self._convert_trailing_zeros() 129 | # Write number to log file in debug mode 130 | if self._app.debug: 131 | self._app["log"].write_message("number", num + "->" + self._num_str) 132 | self._app["statusbar"].update_info() 133 | 134 | def num_clear(self): 135 | """Clear num_str.""" 136 | # Remove any timers as we are clearing now anyway 137 | if self._timer_id: 138 | GLib.source_remove(self._timer_id) 139 | # Write number cleared to log file in debug mode 140 | if self._app.debug and self._num_str: 141 | self._app["log"].write_message("number", "cleared") 142 | self._timer_id = 0 143 | # Reset 144 | self._num_str = "" 145 | self._app["statusbar"].update_info() 146 | 147 | def num_receive(self, number=1, to_float=False): 148 | """Receive self._num_str and clear it. 149 | 150 | Args: 151 | number: Number to return if self._num_str is empty. 152 | to_float: If True, convert num_str to float. Else to int. 153 | Return: 154 | The received number or default. 155 | """ 156 | if self._num_str: 157 | number = get_float(self._num_str) \ 158 | if to_float else get_int(self._num_str) 159 | self.num_clear() 160 | return number 161 | 162 | def get_num_str(self): 163 | return self._num_str 164 | 165 | def set_num_str(self, number): 166 | self.num_clear() 167 | self.num_append(str(number)) 168 | 169 | def _convert_trailing_zeros(self): 170 | """If prefixed with zero add a decimal point to self._num_str.""" 171 | if self._num_str.startswith("0") and not self._num_str.startswith("0."): 172 | self._num_str = self._num_str.replace("0", "0.") 173 | -------------------------------------------------------------------------------- /vimiv/exceptions.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Contains custom exceptions used by vimiv.""" 3 | 4 | 5 | class NoSearchResultsError(Exception): 6 | """Raised when a search result is accessed although there are no results.""" 7 | 8 | 9 | class StringConversionError(ValueError): 10 | """Raised when a setting or argument could not be converted to its type.""" 11 | 12 | 13 | class SettingNotFoundError(Exception): 14 | """Raised when a setting does not exist.""" 15 | 16 | 17 | class NotABoolean(Exception): 18 | """Raised when a setting is not a boolean.""" 19 | 20 | 21 | class NotANumber(Exception): 22 | """Raised when a setting is not a number.""" 23 | 24 | 25 | class AliasError(Exception): 26 | """Raised when there are problems when adding an alias.""" 27 | 28 | 29 | class TrashUndeleteError(Exception): 30 | """Raised when there were problems calling :undelete.""" 31 | 32 | 33 | class NotTransformable(Exception): 34 | """Raised when an image is not transformable for transform.py.""" 35 | 36 | 37 | class ArgumentAmountError(Exception): 38 | """Raised if the amount of arguments is not compatible with a command.""" 39 | -------------------------------------------------------------------------------- /vimiv/helpers.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Wrappers around standard library functions used in vimiv.""" 3 | 4 | import gzip 5 | import os 6 | import re 7 | 8 | from gi.repository import Gtk 9 | from vimiv.exceptions import StringConversionError 10 | 11 | 12 | def listdir_wrapper(path, show_hidden=False): 13 | """Re-implementation of os.listdir which mustn't show hidden files. 14 | 15 | Args: 16 | path: Path of the directory in which os.listdir is called. 17 | show_hidden: If true, show hidden files. Else do not. 18 | Return: 19 | Sorted list of files in path. 20 | """ 21 | all_files = sorted(os.listdir(os.path.expanduser(path))) 22 | if show_hidden: 23 | return all_files 24 | return [fil for fil in all_files if not fil.startswith(".")] 25 | 26 | 27 | def read_file(filename): 28 | """Read the content of a file into a list or create file. 29 | 30 | Args: 31 | filename: The name of the file to be read. 32 | Return: 33 | Content of filename in a list. 34 | """ 35 | content = [] 36 | try: 37 | with open(filename, "r") as f: 38 | for line in f: 39 | content.append(line.rstrip("\n")) 40 | except FileNotFoundError: 41 | os.makedirs(os.path.dirname(filename), exist_ok=True) 42 | with open(filename, "w") as f: 43 | f.write("") 44 | return content 45 | 46 | 47 | def sizeof_fmt(num): 48 | """Print size of a byte number in human-readable format. 49 | 50 | Args: 51 | num: Filesize in bytes. 52 | 53 | Return: 54 | Filesize in human-readable format. 55 | """ 56 | for unit in ["B", "K", "M", "G", "T", "P", "E", "Z"]: 57 | if abs(num) < 1024.0: 58 | if abs(num) < 100: 59 | return "%3.1f%s" % (num, unit) 60 | return "%3.0f%s" % (num, unit) 61 | num /= 1024.0 62 | return "%.1f%s" % (num, "Y") 63 | 64 | 65 | def error_message(message, running_tests=False): 66 | """Show a GTK Error Pop Up with message. 67 | 68 | Args: 69 | message: The message to display. 70 | running_tests: If True running from testsuite. Do not show popup. 71 | """ 72 | # Always print the error message first 73 | print("\033[91mError:\033[0m", message) 74 | # Then display a Gtk Popup 75 | popup = Gtk.Dialog(title="vimiv - Error", transient_for=Gtk.Window()) 76 | popup.set_default_size(600, 1) 77 | popup.add_button("Close", Gtk.ResponseType.CLOSE) 78 | message_label = Gtk.Label() 79 | # Set up label so it actually follows the default width of 600 80 | message_label.set_hexpand(True) 81 | message_label.set_line_wrap(True) 82 | message_label.set_size_request(600, 1) 83 | message_label.set_text(message) 84 | box = popup.get_child() 85 | box.set_border_width(12) 86 | grid = Gtk.Grid() 87 | grid.set_column_spacing(12) 88 | box.pack_start(grid, False, False, 0) 89 | icon_size = Gtk.IconSize(5) 90 | error_icon = Gtk.Image.new_from_icon_name("dialog-error", icon_size) 91 | grid.attach(error_icon, 0, 0, 1, 1) 92 | grid.attach(message_label, 1, 0, 1, 1) 93 | popup.show_all() 94 | if not running_tests: 95 | popup.run() 96 | popup.destroy() 97 | 98 | 99 | def read_info_from_man(): 100 | """Read information on commands from the vimivrc.5 man page. 101 | 102 | Return: 103 | A dictionary containing command-names and information on them. 104 | """ 105 | infodict = {} 106 | # Man page might have been installed as .5.gz by a package manager 107 | if os.path.isfile("/usr/share/man/man5/vimivrc.5"): 108 | with open("/usr/share/man/man5/vimivrc.5") as f: 109 | man_text = f.read() 110 | elif os.path.isfile("/usr/share/man/man5/vimivrc.5.gz"): 111 | with gzip.open("/usr/share/man/man5/vimivrc.5.gz") as f: 112 | man_text = f.read().decode() 113 | else: 114 | return {} 115 | # Loop over lines receiving command name and info 116 | lines = man_text.split("\n") 117 | for i, line in enumerate(man_text.split("\n")): 118 | if line.startswith("\\fB\\fC"): 119 | name = line.replace("\\fB\\fC", "").replace("\\fR", "") 120 | info = lines[i + 1].rstrip(".") # Remove trailing periods 121 | infodict[name] = info 122 | return infodict 123 | 124 | 125 | def expand_filenames(filename, filelist, command): 126 | """Expand % to filename and * to filelist in command.""" 127 | # Escape spaces for the shell 128 | filename = filename.replace(" ", "\\\\\\\\ ") 129 | filelist = [f.replace(" ", "\\\\\\\\ ") for f in filelist] 130 | # Substitute % and * with escaping 131 | command = re.sub(r'(? 1: 95 | _cpu_count -= 1 96 | self._thread_pool = Pool(_cpu_count) 97 | 98 | def run(self): 99 | """Start autorotating the images in self._filelist.""" 100 | for filename in self._filelist: 101 | self._thread_pool.apply_async(self._rotate, (filename,), 102 | callback=self._on_rotated) 103 | 104 | def _rotate(self, filename): 105 | """Rotate filename using pixbuf.apply_embedded_orientation().""" 106 | if not edit_supported(filename): 107 | return 108 | exif = GExiv2.Metadata(filename) 109 | if not exif.get_supports_exif(): 110 | return 111 | orientation = exif.get_orientation() 112 | if orientation not in [GExiv2.Orientation.NORMAL, 113 | GExiv2.Orientation.UNSPECIFIED]: 114 | pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) 115 | pixbuf = pixbuf.apply_embedded_orientation() 116 | save_pixbuf(pixbuf, filename, update_orientation_tag=True) 117 | self._rotated_count += 1 118 | 119 | def _on_rotated(self, thread_pool_result): 120 | self._processed_count += 1 121 | if self._processed_count == len(self._filelist): 122 | self.emit("completed", self._rotated_count) 123 | 124 | 125 | GObject.signal_new("completed", Autorotate, GObject.SIGNAL_RUN_LAST, None, 126 | (GObject.TYPE_PYOBJECT,)) 127 | -------------------------------------------------------------------------------- /vimiv/information.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Information pop-up for vimiv. 3 | 4 | Shows the current version, the icon, gives a link to the GitHub site and the 5 | possibility to display the licence. 6 | """ 7 | 8 | from gi.repository import GLib, Gtk 9 | 10 | 11 | class Information(): 12 | """Class containing information on vimiv.""" 13 | 14 | def get_version(self): 15 | """Return current version. 16 | 17 | Return: 18 | String of the current version. 19 | """ 20 | import vimiv 21 | return "vimiv version %s" % vimiv.__version__ 22 | 23 | def show_version_info(self, running_tests=False): 24 | """Show information about current version in a Gtk pop-up. 25 | 26 | Args: 27 | running_tests: If True running from testsuite. Do not show pop-up. 28 | """ 29 | # Popup with buttons 30 | popup = Gtk.Dialog(title="vimiv - Info", transient_for=Gtk.Window()) 31 | licence_button = popup.add_button(" Licence", Gtk.ResponseType.HELP) 32 | popup.add_button("Close", Gtk.ResponseType.CLOSE) 33 | # Different widgets 34 | licence_button.connect("clicked", self.show_licence) 35 | licence_icon = Gtk.Image.new_from_icon_name( 36 | "text-x-generic-symbolic", 0) 37 | licence_button.set_image(licence_icon) 38 | licence_button.set_always_show_image(True) 39 | 40 | version_label = Gtk.Label() 41 | version_label.set_markup( 42 | '' + self.get_version() + "") 43 | 44 | info_label = Gtk.Label() 45 | info_label.set_text("vimiv - an image viewer with vim-like keybindings") 46 | info_label.set_hexpand(True) 47 | 48 | website_label = Gtk.Label() 49 | website_label.set_halign(Gtk.Align.END) 50 | website_label.set_markup('Website') 52 | github_label = Gtk.Label() 53 | github_label.set_halign(Gtk.Align.START) 54 | github_label.set_markup('GitHub') 56 | 57 | icon_theme = Gtk.IconTheme.get_default() 58 | # Icon not available in theme when icon cache was not updated 59 | try: 60 | vimiv_pixbuf = icon_theme.load_icon("vimiv", 128, 0) 61 | vimiv_image = Gtk.Image.new_from_pixbuf(vimiv_pixbuf) 62 | load_image = True 63 | except GLib.Error: 64 | load_image = False 65 | 66 | # Layout 67 | box = popup.get_child() 68 | box.set_border_width(12) 69 | grid = Gtk.Grid() 70 | grid.set_column_spacing(12) 71 | grid.set_row_spacing(12) 72 | box.pack_start(grid, False, False, 12) 73 | grid.attach(version_label, 0, 0, 2, 1) 74 | if load_image: 75 | grid.attach(vimiv_image, 0, 1, 2, 1) 76 | grid.attach(info_label, 0, 2, 2, 1) 77 | grid.attach(website_label, 0, 3, 1, 1) 78 | grid.attach(github_label, 1, 3, 1, 1) 79 | popup.show_all() 80 | if not running_tests: 81 | popup.run() 82 | popup.destroy() 83 | 84 | def show_licence(self, button, running_tests=False): 85 | """Show the licence in a new pop-up window. 86 | 87 | Args: 88 | button: The Gtk.Button() that called this function. 89 | running_tests: If True running from testsuite. Do not show pop-up. 90 | """ 91 | popup = Gtk.Dialog(title="vimiv - Licence", transient_for=Gtk.Window()) 92 | popup.add_button("Close", Gtk.ResponseType.CLOSE) 93 | 94 | label = Gtk.Label() 95 | title = 'MIT Licence\n\n' 96 | 97 | # Get license text from file 98 | with open("/usr/share/licenses/vimiv/LICENSE") as license_file: 99 | license_text = license_file.read() 100 | label_markup = title + license_text 101 | label.set_markup(label_markup) 102 | 103 | box = popup.get_child() 104 | box.pack_start(label, False, False, 0) 105 | box.set_border_width(12) 106 | popup.show_all() 107 | if not running_tests: 108 | popup.run() 109 | popup.destroy() 110 | -------------------------------------------------------------------------------- /vimiv/log.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Class to handle log file for vimiv.""" 3 | 4 | import os 5 | import sys 6 | import time 7 | 8 | from gi.repository import Gtk 9 | 10 | from vimiv.helpers import get_user_data_dir 11 | 12 | 13 | class Log(): 14 | """Log file handler. 15 | 16 | Attributes: 17 | _filename: Name of the log file. 18 | _terminal: sys.stderr to interact with 19 | """ 20 | 21 | def __init__(self, app): 22 | """Create the necessary objects and settings. 23 | 24 | Args: 25 | app: The main vimiv application to interact with. 26 | """ 27 | datadir = os.path.join(get_user_data_dir(), "vimiv") 28 | os.makedirs(datadir, exist_ok=True) 29 | self._filename = os.path.join(datadir, "vimiv.log") 30 | self._terminal = sys.stderr 31 | # Redirect stderr in debug mode so it is written to the log file as well 32 | if app.debug: 33 | sys.stderr = self 34 | # Create a new log file at startup 35 | with open(self._filename, "w") as f: 36 | f.write("Vimiv log written to " 37 | + self._filename.replace(os.getenv("HOME"), "~") 38 | + "\n") 39 | self._write_separator() 40 | # Header containing version and Gtk version 41 | self.write_message("Version", app["information"].get_version()) 42 | self.write_message("Python", sys.version.split()[0]) 43 | gtk_version = str(Gtk.get_major_version()) + "." \ 44 | + str(Gtk.get_minor_version()) + "." \ 45 | + str(Gtk.get_micro_version()) 46 | self.write_message("GTK", gtk_version) 47 | self._write_separator() 48 | # Start time 49 | self.write_message("Started", "time") 50 | 51 | def write_message(self, header, message): 52 | """Write information to the log file. 53 | 54 | Args: 55 | header: Title of the message, gets surrounded in []. 56 | message: Log message. 57 | """ 58 | if "time" in message: 59 | now = [str(t) for t in time.localtime()[3:6]] 60 | formatted_time = ":".join(now) 61 | message = message.replace("time", formatted_time) 62 | message = message.replace("\n", "\n" + " " * 16) 63 | formatted_message = "%-15s %s\n" % ("[" + header + "]", message) 64 | with open(self._filename, "a") as f: 65 | f.write(formatted_message) 66 | 67 | def write(self, message): 68 | """Write stderr message to log file and terminal.""" 69 | print(message, end="") 70 | if "Traceback" in message: 71 | self.write_message("stderr", "") 72 | with open(self._filename, "a") as f: 73 | f.write(message) 74 | 75 | def flush(self): 76 | self._terminal.flush() 77 | 78 | def fileno(self): 79 | return self._terminal.fileno() 80 | 81 | def _write_separator(self): 82 | """Write a neat 80 * # separator to the log file.""" 83 | with open(self._filename, "a") as f: 84 | f.write("#" * 80 + "\n") 85 | -------------------------------------------------------------------------------- /vimiv/main_window.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Gtk.ScrolledWindow class which is usually the main window of vimiv. 3 | 4 | The ScrolledWindow can either include a Gtk.Image in IMAGE mode or a 5 | Gtk.IconView in THUMBNAIL mode. 6 | """ 7 | 8 | import os 9 | 10 | from gi.repository import Gtk 11 | from vimiv.helpers import listdir_wrapper 12 | from vimiv.image import Image 13 | from vimiv.thumbnail import Thumbnail 14 | 15 | 16 | class MainWindow(Gtk.ScrolledWindow): 17 | """Main window of vimiv containing either an Image or an IconView. 18 | 19 | Attributes: 20 | image: Vimiv Image class which may be displayed. 21 | thumbnail: Vimiv Thumbnail class which may be displayed. 22 | 23 | _app: The main vimiv class to interact with. 24 | """ 25 | 26 | def __init__(self, app): 27 | """Initialize image and thumbnail attributes and configure self.""" 28 | super(MainWindow, self).__init__() 29 | self._app = app 30 | self.image = Image(app) 31 | self.thumbnail = Thumbnail(app) 32 | 33 | self.set_hexpand(True) 34 | self.set_vexpand(True) 35 | # Image is default 36 | self.add(self.image) 37 | self.connect("key_press_event", self._app["eventhandler"].on_key_press, 38 | "IMAGE") 39 | 40 | # Connect signals 41 | self._app.connect("widget-layout-changed", self._on_widgets_changed) 42 | self._app.connect("paths-changed", self._on_paths_changed) 43 | 44 | def switch_to_child(self, new_child): 45 | """Switch the widget displayed in the main window. 46 | 47 | Args: 48 | new_child: The child to switch to. 49 | """ 50 | self.remove(self.get_child()) 51 | self.add(new_child) 52 | self.show_all() 53 | 54 | def center_window(self): 55 | """Center the widget in the current window.""" 56 | h_adj = self.get_hadjustment() 57 | size = self.get_allocation() 58 | h_middle = (h_adj.get_upper() - h_adj.get_lower() - size.width) / 2 59 | h_adj.set_value(h_middle) 60 | v_adj = self.get_vadjustment() 61 | v_middle = (v_adj.get_upper() - v_adj.get_lower() - size.height) / 2 62 | v_adj.set_value(v_middle) 63 | self.set_hadjustment(h_adj) 64 | self.set_vadjustment(v_adj) 65 | 66 | def scroll(self, direction): 67 | """Scroll the correct object. 68 | 69 | Args: 70 | direction: Scroll direction to emit. 71 | """ 72 | if direction not in "hjklHJKL": 73 | self._app["statusbar"].message( 74 | "Invalid scroll direction " + direction, "error") 75 | elif self.thumbnail.toggled: 76 | self.thumbnail.move_direction(direction) 77 | else: 78 | self._scroll(direction) 79 | return True # Deactivates default bindings (here for Arrows) 80 | 81 | def _scroll(self, direction): 82 | """Scroll the widget. 83 | 84 | Args: 85 | direction: Direction to scroll in. 86 | """ 87 | steps = self._app["eventhandler"].num_receive() 88 | scale = self.image.get_scroll_scale() 89 | h_adj = self.get_hadjustment() 90 | size = self.get_allocation() 91 | h_size = h_adj.get_upper() - h_adj.get_lower() - size.width 92 | h_step = h_size / scale * steps 93 | v_adj = self.get_vadjustment() 94 | v_size = v_adj.get_upper() - v_adj.get_lower() - size.height 95 | v_step = v_size / scale * steps 96 | # To the ends 97 | if direction == "H": 98 | h_adj.set_value(0) 99 | elif direction == "J": 100 | v_adj.set_value(v_size) 101 | elif direction == "K": 102 | v_adj.set_value(0) 103 | elif direction == "L": 104 | h_adj.set_value(h_size) 105 | # By step 106 | elif direction == "h": 107 | h_adj.set_value(h_adj.get_value() - h_step) 108 | elif direction == "j": 109 | v_adj.set_value(v_adj.get_value() + v_step) 110 | elif direction == "k": 111 | v_adj.set_value(v_adj.get_value() - v_step) 112 | elif direction == "l": 113 | h_adj.set_value(h_adj.get_value() + h_step) 114 | self.set_hadjustment(h_adj) 115 | self.set_vadjustment(v_adj) 116 | 117 | def _on_widgets_changed(self, app, widget): 118 | """Recalculate thumbnails or rezoom image when the layout changed.""" 119 | if self.thumbnail.toggled: 120 | self.thumbnail.calculate_columns() 121 | elif self._app.get_paths() and self.image.fit_image != "user": 122 | self.image.zoom_to(0, self.image.fit_image) 123 | 124 | def _on_paths_changed(self, app, transform): 125 | """Reload paths image and/or thumbnail when paths have changed.""" 126 | if self._app.get_paths(): 127 | # Get all files in directory again 128 | focused_path = self._app.get_pos(True) 129 | decremented_index = max(0, self._app.get_pos() - 1) 130 | directory = os.path.dirname(focused_path) 131 | files = [os.path.join(directory, fil) 132 | for fil in listdir_wrapper(directory)] 133 | self._app.populate(files) 134 | # Reload thumbnail 135 | if self.thumbnail.toggled: 136 | self.thumbnail.on_paths_changed() 137 | # Refocus the path 138 | if focused_path in self._app.get_paths(): 139 | index = self._app.get_paths().index(focused_path) 140 | # Stay as close as possible 141 | else: 142 | index = min(decremented_index, len(self._app.get_paths()) - 1) 143 | if self.thumbnail.toggled: 144 | self.thumbnail.move_to_pos(index) 145 | else: 146 | self._app["eventhandler"].set_num_str(index + 1) 147 | self.image.move_pos() 148 | self._app["statusbar"].update_info() 149 | # We need to check again as populate was called 150 | if not self._app.get_paths(): 151 | self.hide() 152 | -------------------------------------------------------------------------------- /vimiv/mark.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Handle marking of images for vimiv.""" 3 | 4 | import os 5 | 6 | from gi.repository import GObject 7 | 8 | 9 | class Mark(GObject.Object): 10 | """Handle marking of images. 11 | 12 | Attributes: 13 | marked: List of currently marked images. 14 | 15 | _app: The main vimiv application to interact with. 16 | _marked_bak: List of last marked images to be able to toggle marks. 17 | 18 | Signals: 19 | marks-changed: Emitted when new images were marked so other widgets can 20 | update their display. 21 | """ 22 | 23 | def __init__(self, app): 24 | super(Mark, self).__init__() 25 | self._app = app 26 | self.marked = [] 27 | self._marked_bak = [] 28 | 29 | def mark(self): 30 | """Mark the current image.""" 31 | # Check which image 32 | current = os.path.abspath(os.path.basename(self._app.get_pos(True))) 33 | # Toggle the mark 34 | if os.path.isfile(current): 35 | if current in self.marked: 36 | self.marked.remove(current) 37 | else: 38 | self.marked.append(current) 39 | self.emit("marks-changed", [current]) 40 | else: 41 | self._app["statusbar"].message( 42 | "Marking directories is not supported", "warning") 43 | 44 | def toggle_mark(self): 45 | """Toggle mark status. 46 | 47 | If images are marked all marks are removed, otherwise the last marked 48 | images are re-marked. 49 | """ 50 | if self.marked: 51 | self._marked_bak = self.marked 52 | self.marked = [] 53 | else: 54 | self.marked, self._marked_bak = self._marked_bak, self.marked 55 | to_reload = self.marked + self._marked_bak 56 | self.emit("marks-changed", to_reload) 57 | 58 | def mark_all(self): 59 | """Mark all images.""" 60 | # Get the correct filelist 61 | if self._app["library"].is_focus(): 62 | files = [] 63 | for fil in self._app["library"].files: 64 | files.append(os.path.abspath(fil)) 65 | elif self._app.get_paths(): 66 | files = self._app.get_paths() 67 | else: 68 | self._app["statusbar"].message("No image to mark", "error") 69 | # Add all to the marks 70 | for fil in files: 71 | if os.path.isfile(fil) and fil not in self.marked: 72 | self.marked.append(fil) 73 | self.emit("marks-changed", files) 74 | 75 | def mark_between(self): 76 | """Mark all images between the two last marks.""" 77 | # Check if there are enough marks 78 | if len(self.marked) < 2: 79 | self._app["statusbar"].message("Not enough marks", "error") 80 | return 81 | start = self.marked[-2] 82 | end = self.marked[-1] 83 | # Get the correct filelist 84 | if self._app["library"].is_focus(): 85 | files = [] 86 | for fil in self._app["library"].files: 87 | if not os.path.isdir(fil): 88 | files.append(os.path.abspath(fil)) 89 | elif self._app.get_paths(): 90 | files = self._app.get_paths() 91 | else: 92 | self._app["statusbar"].message("No image to mark", "error") 93 | # Find the images to mark 94 | for i, image in enumerate(files): 95 | if image == start: 96 | start = i 97 | elif image == end: 98 | end = i 99 | for i in range(start + 1, end): 100 | self.marked.insert(-1, files[i]) 101 | self.emit("marks-changed", self.marked) 102 | 103 | 104 | GObject.signal_new("marks-changed", Mark, GObject.SIGNAL_RUN_LAST, None, 105 | (GObject.TYPE_PYOBJECT,)) 106 | -------------------------------------------------------------------------------- /vimiv/slideshow.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Slideshow for vimiv.""" 3 | 4 | from gi.repository import GLib, GObject 5 | from vimiv.settings import settings 6 | 7 | 8 | class Slideshow(GObject.Object): 9 | """Handle everything related to slideshow for vimiv. 10 | 11 | Attributes: 12 | running: If True the slideshow is running. 13 | 14 | _app: The main application class to interact with. 15 | _start_index: Index of the image when slideshow was started. Saved to 16 | display a message when slideshow is back at the beginning. 17 | _timer_id: ID of the currently running GLib.Timeout. 18 | """ 19 | 20 | def __init__(self, app): 21 | """Create the necessary objects and settings. 22 | 23 | Args: 24 | app: The main vimiv application to interact with. 25 | """ 26 | super(Slideshow, self).__init__() 27 | self._app = app 28 | 29 | self._start_index = 0 30 | self.running = False 31 | self._timer_id = GLib.Timeout 32 | settings.connect("changed", self._on_settings_changed) 33 | 34 | def toggle(self): 35 | """Toggle the slideshow or update the delay.""" 36 | if not self._app.get_paths(): 37 | message = "No valid paths, starting slideshow failed" 38 | self._app["statusbar"].message(message, "error") 39 | return 40 | if self._app["thumbnail"].toggled: 41 | message = "Slideshow makes no sense in thumbnail mode" 42 | self._app["statusbar"].message(message, "warning") 43 | return 44 | # Delay changed via vimiv["eventhandler"].num_str? 45 | number = self._app["eventhandler"].get_num_str() 46 | if number: 47 | settings.override("slideshow_delay", number) 48 | self._app["eventhandler"].num_clear() 49 | # If the delay wasn't changed in any way just toggle the slideshow 50 | else: 51 | self.running = not self.running 52 | if self.running: 53 | delay = 1000 * settings["slideshow_delay"].get_value() 54 | self._start_index = self._app.get_index() 55 | self._timer_id = GLib.timeout_add(delay, self._next) 56 | else: 57 | self._app["statusbar"].lock = False 58 | GLib.source_remove(self._timer_id) 59 | self._app["statusbar"].update_info() 60 | 61 | def _next(self): 62 | """Command to run in the GLib.timeout moving to the next image.""" 63 | self._app["image"].move_index() 64 | # Info if slideshow returns to beginning 65 | if self._app.get_index() == self._start_index: 66 | message = "Back at beginning of slideshow" 67 | self._app["statusbar"].lock = True 68 | self._app["statusbar"].message(message, "info") 69 | else: 70 | self._app["statusbar"].lock = False 71 | return True # So we continue running 72 | 73 | def get_formatted_delay(self): 74 | """Return the delay formatted neatly for the statusbar.""" 75 | delay = settings["slideshow_delay"].get_value() 76 | return "[slideshow - {0:.1f}s]".format(delay) 77 | 78 | def _on_settings_changed(self, new_settings, setting): 79 | if setting == "slideshow_delay": 80 | delay = settings["slideshow_delay"].get_value() 81 | # Set a minimum 82 | if delay < 0.5: 83 | settings.override("slideshow_delay", "0.5") 84 | self._app["statusbar"].message( 85 | "Delays shorter than 0.5 s are not allowed", "warning") 86 | return 87 | # If slideshow was running reload it 88 | if self.running: 89 | GLib.source_remove(self._timer_id) 90 | self._timer_id = GLib.timeout_add(1000 * delay, self._next) 91 | self._app["statusbar"].update_info() 92 | -------------------------------------------------------------------------------- /vimiv/tags.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Deal with the handling of tags for vimiv.""" 3 | 4 | import os 5 | 6 | from gi.repository import Gtk 7 | from vimiv.helpers import error_message, read_file, get_user_data_dir 8 | 9 | 10 | class TagHandler(object): 11 | """Handle tags. 12 | 13 | Attributes: 14 | directory: Directory in which tags are stored. 15 | last: Last tag that was loaded. 16 | 17 | _app: The main vimiv application to interact with. 18 | """ 19 | 20 | def __init__(self, app): 21 | """Create the necessary objects. 22 | 23 | Args: 24 | app: The main vimiv application to interact with. 25 | """ 26 | self._app = app 27 | # Create tags directory 28 | self.directory = os.path.join(get_user_data_dir(), "vimiv", "Tags") 29 | os.makedirs(self.directory, exist_ok=True) 30 | # If there are tags in the old location, move them to the new one 31 | # TODO remove this in a new version, assigned for 0.10 32 | old_directory = os.path.expanduser("~/.vimiv/Tags") 33 | if os.path.isdir(old_directory): 34 | old_tags = os.listdir(old_directory) 35 | for f in old_tags: 36 | old_path = os.path.join(old_directory, f) 37 | new_path = os.path.join(self.directory, f) 38 | os.rename(old_path, new_path) 39 | message = "Moved tags from the old directory %s " \ 40 | "to the new location %s." % (old_directory, self.directory) 41 | error_message(message, running_tests=self._app.running_tests) 42 | os.rmdir(old_directory) 43 | self.last = "" 44 | 45 | def write(self, imagelist, tagname): 46 | """Add a list of images to a tag. 47 | 48 | Args: 49 | imagelist: List of images to write to the tag. 50 | tagname: Name of tag to operate on. 51 | """ 52 | # Backup required for tests 53 | imagelist = imagelist if imagelist else self._app["mark"].marked 54 | tagfile_name = os.path.join(self.directory, tagname) 55 | tagged_images = self._read(tagname) 56 | with open(tagfile_name, "a") as tagfile: 57 | for image in imagelist: 58 | if image not in tagged_images: 59 | tagfile.write(image + "\n") 60 | 61 | def remove(self, tagname): 62 | """Remove a tag showing an error if the tag doesn't exist. 63 | 64 | Args: 65 | tagname: Name of tag to operate on. 66 | """ 67 | tagfile_name = os.path.join(self.directory, tagname) 68 | if os.path.isfile(tagfile_name): 69 | os.remove(tagfile_name) 70 | else: 71 | err = "Tagfile '%s' does not exist" % (tagname) 72 | self._app["statusbar"].message(err, "error") 73 | 74 | def load(self, tagname): 75 | """Load all images in a tag as current filelist. 76 | 77 | Args: 78 | tagname: Name of tag to operate on. 79 | """ 80 | os.chdir(self.directory) 81 | # Read file and get all tagged images as list 82 | tagged_images = self._read(tagname) 83 | # Populate filelist 84 | self._app.populate(tagged_images, expand_single=False) 85 | if self._app.get_paths(): 86 | self._app["main_window"].show() 87 | self._app["library"].set_hexpand(False) 88 | self._app["image"].load() 89 | # Focus in library if it is open 90 | if self._app["library"].grid.is_visible(): 91 | self._app["library"].reload(self.directory) 92 | tag_pos = self._app["library"].files.index(tagname) 93 | self._app["library"].set_cursor(Gtk.TreePath(tag_pos), 94 | None, False) 95 | # Remember last tag selected 96 | self.last = tagname 97 | else: 98 | message = "Tagfile '%s' has no valid images" % (tagname) 99 | self._app["statusbar"].message(message, "error") 100 | self.last = "" 101 | 102 | def _read(self, tagname): 103 | """Read a tag and return the images in it. 104 | 105 | Args: 106 | tagname: Name of tag to operate on. 107 | 108 | Return: 109 | List of images in the tag called tagname. 110 | """ 111 | tagfile_name = os.path.join(self.directory, tagname) 112 | tagged_images = read_file(tagfile_name) 113 | return tagged_images 114 | -------------------------------------------------------------------------------- /vimiv/trash_manager.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Provides class to handle a shared trash directory. 3 | 4 | The TrashManager deletes and undeletes images from the user's Trash directory in 5 | $XDG_DATA_HOME/Trash according to the freedesktop.org trash specification. 6 | """ 7 | 8 | import configparser 9 | import os 10 | import shutil 11 | import tempfile 12 | import time 13 | 14 | from vimiv.exceptions import TrashUndeleteError 15 | from vimiv.helpers import get_user_data_dir 16 | 17 | 18 | class TrashManager(): 19 | """Provides mechanism to delete and undelete images. 20 | 21 | Attributes: 22 | files_directory: Directory to which the "deleted" files are moved. 23 | info_directory: Directory in which information on the "deleted" files is 24 | stored. 25 | """ 26 | 27 | def __init__(self): 28 | """Create a new TrashManager.""" 29 | self.files_directory = os.path.join(get_user_data_dir(), "Trash/files") 30 | self.info_directory = os.path.join(get_user_data_dir(), "Trash/info") 31 | os.makedirs(self.files_directory, exist_ok=True) 32 | os.makedirs(self.info_directory, exist_ok=True) 33 | 34 | def delete(self, filename): 35 | """Move a file to the trash directory. 36 | 37 | Args: 38 | filename: The original name of the file. 39 | """ 40 | trash_filename = self._get_trash_filename(filename) 41 | self._create_info_file(trash_filename, filename) 42 | shutil.move(filename, trash_filename) 43 | 44 | def undelete(self, basename): 45 | """Undelete a file from the trash directory. 46 | 47 | Args: 48 | basename: The basename of the file in the trash directory. 49 | """ 50 | info_filename = os.path.join(self.info_directory, 51 | basename + ".trashinfo") 52 | trash_filename = os.path.join(self.files_directory, basename) 53 | if not os.path.exists(info_filename) \ 54 | or not os.path.exists(trash_filename): 55 | raise TrashUndeleteError("file does not exist") 56 | if os.path.isdir(trash_filename): 57 | raise TrashUndeleteError("directories are not supported") 58 | info = configparser.ConfigParser() 59 | info.read(info_filename) 60 | content = info["Trash Info"] 61 | original_filename = content["Path"] 62 | # Directory of the file is not accessible 63 | if not os.path.isdir(os.path.dirname(original_filename)): 64 | raise TrashUndeleteError("original directory is not accessible") 65 | shutil.move(trash_filename, original_filename) 66 | os.remove(info_filename) 67 | 68 | def _get_trash_filename(self, filename): 69 | """Return the name of the file in self.files_directory. 70 | 71 | Args: 72 | filename: The original name of the file. 73 | """ 74 | path = os.path.join(self.files_directory, os.path.basename(filename)) 75 | # Ensure that we do not overwrite any files 76 | extension = 2 77 | original_path = path 78 | while os.path.exists(path): 79 | path = original_path + "." + str(extension) 80 | extension += 1 81 | return path 82 | 83 | def _create_info_file(self, trash_filename, original_filename): 84 | """Create file with information as specified by the standard. 85 | 86 | Args: 87 | trash_filename: The name of the file in self.files_directory. 88 | original_filename: The original name of the file. 89 | """ 90 | # Note: we cannot use configparser here as it writes keys in lowercase 91 | info_path = os.path.join( 92 | self.info_directory, 93 | os.path.basename(trash_filename) + ".trashinfo") 94 | # Write to temporary file and use shutil.move to make sure the operation 95 | # is an atomic operation as specified by the standard 96 | fd, temp_path = tempfile.mkstemp(dir=self.info_directory) 97 | os.close(fd) 98 | temp_file = open(temp_path, "w") 99 | temp_file.write("[Trash Info]\n") 100 | temp_file.write("Path=%s\n" % (original_filename)) 101 | temp_file.write("DeletionDate=%s\n" % (time.strftime("%Y%m%dT%H%M%S"))) 102 | # Make sure that all data is on disk 103 | temp_file.flush() 104 | os.fsync(temp_file.fileno()) 105 | temp_file.close() 106 | shutil.move(temp_path, info_path) 107 | 108 | def get_files_directory(self): 109 | return self.files_directory 110 | 111 | def get_info_directory(self): 112 | return self.info_directory 113 | -------------------------------------------------------------------------------- /vimiv/vimiv: -------------------------------------------------------------------------------- 1 | ../vimiv.py -------------------------------------------------------------------------------- /vimiv/window.py: -------------------------------------------------------------------------------- 1 | # vim: ft=python fileencoding=utf-8 sw=4 et sts=4 2 | """Window class for vimiv.""" 3 | 4 | from gi.repository import Gdk, Gtk 5 | from vimiv.settings import settings 6 | 7 | 8 | class Window(Gtk.ApplicationWindow): 9 | """"Vimiv Window containing the general structure. 10 | 11 | Additionally handles events like fullscreen and resize. 12 | 13 | Attributes: 14 | winsize: The windowsize as tuple. 15 | 16 | _app: The main vimiv application to interact with. 17 | """ 18 | 19 | def __init__(self, app): 20 | """Create the necessary objects and settings. 21 | 22 | Args: 23 | app: The main vimiv application to interact with. 24 | """ 25 | super(Window, self).__init__() 26 | 27 | self._app = app 28 | 29 | self.connect("destroy", self._app.quit_wrapper) 30 | self.connect("button_press_event", self._app["eventhandler"].on_click, 31 | "IMAGE") 32 | self.connect("touch-event", self._app["eventhandler"].on_touch) 33 | self.add_events(Gdk.EventMask.KEY_PRESS_MASK | 34 | Gdk.EventMask.POINTER_MOTION_MASK) 35 | 36 | # Set initial window size 37 | self.winsize = settings["geometry"].get_value() 38 | self.resize(self.winsize[0], self.winsize[1]) 39 | 40 | if settings["start_fullscreen"].get_value(): 41 | self.fullscreen() 42 | 43 | # Auto resize 44 | self.connect("check-resize", self._auto_resize) 45 | # Focus changes with mouse 46 | for widget in [self._app["library"], 47 | self._app["thumbnail"], 48 | self._app["manipulate"].sliders["bri"], 49 | self._app["manipulate"].sliders["con"], 50 | self._app["manipulate"].sliders["sat"], 51 | self._app["image"]]: 52 | widget.connect("button-release-event", self._on_click) 53 | 54 | def toggle_fullscreen(self): 55 | """Toggle fullscreen.""" 56 | if self.get_window().get_state() & Gdk.WindowState.FULLSCREEN: 57 | self.unfullscreen() 58 | else: 59 | self.fullscreen() 60 | 61 | def zoom(self, zoom_in=True, step=1): 62 | """Zoom image or thumbnails. 63 | 64 | Args: 65 | zoom_in: If True, zoom in. 66 | """ 67 | if self._app["thumbnail"].toggled: 68 | self._app["thumbnail"].zoom(zoom_in) 69 | else: 70 | self._app["image"].zoom_delta(zoom_in=zoom_in, step=step) 71 | 72 | def _auto_resize(self, window): 73 | """Automatically resize widgets when window is resized. 74 | 75 | Args: 76 | window: The window which emitted the resize event. 77 | """ 78 | if self.get_size() != self.winsize: 79 | self.winsize = self.get_size() 80 | self._app.emit("widget-layout-changed", self) 81 | 82 | def _on_click(self, widget, event_button): 83 | """Update statusbar with the currently focused widget after mouse click. 84 | 85 | Args: 86 | widget: The widget that emitted the signal. 87 | event_button: Mouse button that was pressed. 88 | """ 89 | self._app["statusbar"].update_info() 90 | --------------------------------------------------------------------------------